summaryrefslogtreecommitdiff
path: root/ui/windows/config_window.py
diff options
context:
space:
mode:
authorChristian Kolset <ckolset@colostate.edu>2026-08-10 16:12:05 -0600
committerChristian Kolset <ckolset@colostate.edu>2026-08-10 16:12:05 -0600
commit5d5bcaed8639a162fda85c8cd0d340c296a054b8 (patch)
treea7a1a62fb607c9c85127dab4aab17ccf8e22c906 /ui/windows/config_window.py
parent46231c00994cd681d598961f2056dbda31c3e483 (diff)
Unify Signals/Channels around auto-naming, plot-by-channel, and shared vars
Devices: auto-name from OS-reported port description (Device Manager / udev string) instead of a generic label; default Serial #Channels 1; consistent simulate-mode checkbox across backends; inline device rename. Channels: calibration/filters move out of Signals into a Channel "Operation" (adds Scale/Offset alongside the existing math kinds); single-physical-source Channels get an embedded Filters block; new Channels auto-name/auto-ID from their source and reject duplicate names/IDs on apply; "Create Channel" button on each Signal row; renamed "Sources" -> "Signals" in the Channels UI; Signals' READ/WRITE dir badge (was silently-broken IN/OUT) now reflects the real writable flag. Plot: default layout is one pane per Channel (not per device, not raw Signals) titled by channel name; "+ Add Channel" is a checkbox dialog instead of an inline dropdown; traces get up/down reorder; live unit changes (e.g. a Mark-10 gauge switching lb/kgF/N/ozF) propagate to an open pane's axis label without clobbering a manually-typed one. Control Panel: hardware assignment now picks from the same writable- Signals list instead of a separate device/channel combo with a hardcoded Arduino pin list; stale visual indicator when a control's target device/channel disconnects or disappears; documented the `vars` bridge (Control Panel On Action Script <-> Channel scripts) in both directions' help text and script templates. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'ui/windows/config_window.py')
-rw-r--r--ui/windows/config_window.py113
1 files changed, 56 insertions, 57 deletions
diff --git a/ui/windows/config_window.py b/ui/windows/config_window.py
index 05cc1fb..64c669d 100644
--- a/ui/windows/config_window.py
+++ b/ui/windows/config_window.py
@@ -5,8 +5,8 @@ Unified Configuration window — Devices | Channels | Plot in one tabbed window.
Channels tab layout (2 columns):
Left — signal table (Device / Signal ID / Dir / On / Name / Unit / Min / Max)
- ⚙ button per row opens that channel's filter/calibration pipeline dialog
- Right — derived / virtual channels
+ Right — derived / virtual channels — operations (filters/calibration/math/script)
+ are configured per Channel here, not on the Signals side.
"""
from __future__ import annotations
@@ -17,17 +17,17 @@ from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QToolButton,
QScrollArea, QFrame, QTabWidget, QTableWidget, QTableWidgetItem,
QHeaderView, QCheckBox, QMessageBox, QAbstractItemView, QSplitter,
- QDoubleSpinBox, QDialog, QDialogButtonBox,
+ QDoubleSpinBox,
)
from PyQt6.QtCore import Qt, pyqtSignal, QTimer
from PyQt6.QtGui import QCloseEvent, QColor
from devices.device_registry import DeviceRegistry
from core.acquisition import AcquisitionEngine
-from core.signal_processor import SignalProcessor
+from core.signal_processor import SignalProcessor, DerivedChannel
from ui.windows.devices_window import DeviceRow, ChannelsTab
-from ui.windows.channels_window import PipelineTab, ChannelPipelineBlock
+from ui.windows.channels_window import PipelineTab, _slug_id, _dedupe_name
from ui.windows.plot_window import (
PlotWindow, LayoutConfig, LayoutCanvas, PaneBlock,
build_default_layout,
@@ -35,37 +35,12 @@ from ui.windows.plot_window import (
# ══════════════════════════════════════════════════════════════════════════════
-# Pipeline dialog — per-channel filter/calibration popup
-# ══════════════════════════════════════════════════════════════════════════════
-
-class _PipelineDialog(QDialog):
- """Shows one ChannelPipelineBlock inside a dialog for a single signal."""
-
- def __init__(self, dev_id: str, ch_id: str, ch_name: str, unit: str,
- processor: SignalProcessor, parent=None):
- super().__init__(parent)
- self.setWindowTitle(f"Pipeline — {dev_id} / {ch_id}")
- self.setMinimumWidth(460)
- self.resize(500, 420)
-
- lay = QVBoxLayout(self)
- lay.setContentsMargins(0, 0, 0, 12)
-
- self._block = ChannelPipelineBlock(dev_id, ch_id, ch_name, unit, processor)
- self._block._toggle() # start expanded
- lay.addWidget(self._block, 1)
-
- btns = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
- btns.rejected.connect(self.accept)
- lay.addWidget(btns)
-
-
-# ══════════════════════════════════════════════════════════════════════════════
# Enhanced signals table — adds Dir badge and pipeline ⚙ button
# ══════════════════════════════════════════════════════════════════════════════
class _SignalsTab(ChannelsTab):
- """ChannelsTab with Dir (IN/OUT) badge and per-row ⚙ pipeline button."""
+ """ChannelsTab with a Dir (READ/WRITE) badge. Operations (filters/calibration/math)
+ are configured per Channel in the Channels pane, not here."""
_C_DEVICE = 0
_C_CH_ID = 1
@@ -75,7 +50,9 @@ class _SignalsTab(ChannelsTab):
_C_UNIT = 5
_C_MIN = 6
_C_MAX = 7
- _C_PIPE = 8 # ⚙ pipeline button
+ _C_MKCH = 8 # "+ Channel" button
+
+ channel_created = pyqtSignal()
def __init__(self, registry: DeviceRegistry, processor: SignalProcessor):
self._processor = processor
@@ -97,7 +74,7 @@ class _SignalsTab(ChannelsTab):
hdr.setSectionResizeMode(self._C_CH_ID, QHeaderView.ResizeMode.ResizeToContents)
hdr.setSectionResizeMode(self._C_DIR, QHeaderView.ResizeMode.ResizeToContents)
hdr.setSectionResizeMode(self._C_ENABLED, QHeaderView.ResizeMode.ResizeToContents)
- hdr.setSectionResizeMode(self._C_PIPE, QHeaderView.ResizeMode.ResizeToContents)
+ hdr.setSectionResizeMode(self._C_MKCH, QHeaderView.ResizeMode.ResizeToContents)
self._table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self._table.setAlternatingRowColors(True)
self._table.verticalHeader().setVisible(False)
@@ -122,8 +99,8 @@ class _SignalsTab(ChannelsTab):
ch_item.setForeground(QColor(ch.color))
self._table.setItem(row, self._C_CH_ID, ch_item)
- is_out = getattr(ch, "is_output", False)
- dir_item = QTableWidgetItem("OUT" if is_out else "IN")
+ is_out = getattr(ch, "writable", False)
+ dir_item = QTableWidgetItem("WRITE" if is_out else "READ")
dir_item.setFlags(dir_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
dir_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
dir_item.setForeground(QColor("#f59e0b") if is_out else QColor("#22c55e"))
@@ -156,33 +133,47 @@ class _SignalsTab(ChannelsTab):
self._table.setItem(row, self._C_MIN, QTableWidgetItem(str(ch.min_value)))
self._table.setItem(row, self._C_MAX, QTableWidgetItem(str(ch.max_value)))
- # Pipeline ⚙ button
- pipe_w = QWidget()
- pipe_l = QHBoxLayout(pipe_w)
- pipe_l.setContentsMargins(2, 0, 2, 0)
- pipe_l.setAlignment(Qt.AlignmentFlag.AlignCenter)
- pipe_btn = QToolButton()
- pipe_btn.setText("⚙")
- pipe_btn.setObjectName("traceRemoveBtn")
- pipe_btn.setFixedSize(22, 22)
- pipe_btn.setToolTip("Configure filters / calibration")
- d_cap, c_cap, n_cap, u_cap = (
- dev.info.device_id, ch.channel_id, ch.name, ch.unit
+ mkch_w = QWidget()
+ mkch_l = QHBoxLayout(mkch_w)
+ mkch_l.setContentsMargins(2, 0, 2, 0)
+ mkch_l.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ mkch_btn = QToolButton()
+ mkch_btn.setText("⊕")
+ mkch_btn.setObjectName("addTraceBtn")
+ mkch_btn.setFixedSize(24, 22)
+ mkch_btn.setToolTip("Create a Channel from this Signal")
+ d_cap, c_cap, n_cap, u_cap, col_cap = (
+ dev.info.device_id, ch.channel_id, ch.name, ch.unit, ch.color
)
- pipe_btn.clicked.connect(
- lambda _=False, d=d_cap, c=c_cap, n=n_cap, u=u_cap:
- self._open_pipeline(d, c, n, u)
+ mkch_btn.clicked.connect(
+ lambda _=False, d=d_cap, c=c_cap, n=n_cap, u=u_cap, col=col_cap:
+ self._on_create_channel(d, c, n, u, col)
)
- pipe_l.addWidget(pipe_btn)
- self._table.setCellWidget(row, self._C_PIPE, pipe_w)
+ mkch_l.addWidget(mkch_btn)
+ self._table.setCellWidget(row, self._C_MKCH, mkch_w)
row += 1
self._table.blockSignals(False)
- def _open_pipeline(self, dev_id: str, ch_id: str, ch_name: str, unit: str):
- dlg = _PipelineDialog(dev_id, ch_id, ch_name, unit, self._processor, self)
- dlg.exec()
+ def _on_create_channel(self, dev_id: str, ch_id: str, ch_name: str, unit: str, color: str):
+ """Create a passthrough Channel (kind="none") from this Signal,
+ named after it — the "1 Signal -> 1 Channel" quick path. No-ops if
+ a passthrough Channel for this exact Signal already exists."""
+ existing = self._processor.get_derived()
+ for dc in existing:
+ if dc.kind == "none" and dc.sources == [(dev_id, ch_id)]:
+ return
+ existing_names = {d.name for d in existing}
+ existing_ids = {d.channel_id for d in existing}
+ name = _dedupe_name(ch_name, existing_names)
+ channel_id = _slug_id(name, existing_ids)
+ dc = DerivedChannel(channel_id=channel_id, name=name, kind="none",
+ sources=[(dev_id, ch_id)], source_names=[ch_name],
+ unit=unit, color=color or "#00d4ff")
+ err = self._processor.add_derived(dc)
+ if err is None:
+ self.channel_created.emit()
# ══════════════════════════════════════════════════════════════════════════════
@@ -353,6 +344,7 @@ class ConfigWindow(QWidget):
row = DeviceRow(dev)
row.configure_clicked.connect(self._on_configure_device)
row.remove_clicked.connect(self._on_remove_device)
+ row.name_changed.connect(self._on_device_renamed)
self._dev_inner.insertWidget(self._dev_inner.count() - 1, row)
self._dev_rows[dev.info.device_id] = row
@@ -371,14 +363,20 @@ class ConfigWindow(QWidget):
self.engine.add_device(dev)
self.refresh_devices()
self._signals_tab.refresh()
+ self._pipeline_tab.on_device_added()
self.device_added.emit()
+ def _on_device_renamed(self, device_id: str):
+ self._signals_tab.refresh()
+ self.device_reconfigured.emit(device_id)
+
def _on_configure_device(self, device_id: str):
from ui.config_dialog import DeviceConfigDialog
dev = self.registry.get_instance(device_id)
if dev:
DeviceConfigDialog(dev, self).exec()
self._signals_tab.refresh()
+ self._pipeline_tab.on_device_reconfigured(device_id)
self.device_reconfigured.emit(device_id)
def _on_remove_device(self, device_id: str):
@@ -412,7 +410,7 @@ class ConfigWindow(QWidget):
sig_l = QVBoxLayout(sig_w); sig_l.setContentsMargins(0, 0, 0, 0); sig_l.setSpacing(0)
sig_bar = QWidget(); sig_bar.setObjectName("cfgGlobalBar")
- sb_lay = QHBoxLayout(sig_bar); sb_lay.setContentsMargins(10, 7, 10, 7)
+ sb_lay = QHBoxLayout(sig_bar); sb_lay.setContentsMargins(10, 7, 10, 7); sb_lay.setSpacing(6)
sb_lbl = QLabel("Signals"); sb_lbl.setObjectName("devWindowTitle")
sb_lay.addWidget(sb_lbl, 1)
sig_l.addWidget(sig_bar)
@@ -435,6 +433,7 @@ class ConfigWindow(QWidget):
if inner_splitters:
inner_splitters[0].widget(0).setVisible(False)
splitter.addWidget(self._pipeline_tab)
+ self._signals_tab.channel_created.connect(self._pipeline_tab.refresh_derived)
splitter.setStretchFactor(0, 2)
splitter.setStretchFactor(1, 1)