From 5d5bcaed8639a162fda85c8cd0d340c296a054b8 Mon Sep 17 00:00:00 2001 From: Christian Kolset Date: Mon, 10 Aug 2026 16:12:05 -0600 Subject: 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 --- ui/windows/plot_window.py | 132 +++++++++++++++++++++++++++++++++------------- 1 file changed, 96 insertions(+), 36 deletions(-) (limited to 'ui/windows/plot_window.py') diff --git a/ui/windows/plot_window.py b/ui/windows/plot_window.py index f311e9e..119efab 100644 --- a/ui/windows/plot_window.py +++ b/ui/windows/plot_window.py @@ -26,7 +26,7 @@ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QScrollArea, QFrame, QLineEdit, QDoubleSpinBox, QSpinBox, QCheckBox, QComboBox, QColorDialog, QToolButton, - QFileDialog, QMessageBox, + QFileDialog, QMessageBox, QDialog, QDialogButtonBox, ) from PyQt6.QtCore import Qt, pyqtSignal, QPoint, QRect from PyQt6.QtGui import QColor, QCloseEvent, QPainter, QPen @@ -89,18 +89,12 @@ class LayoutConfig: def build_default_layout(registry: DeviceRegistry, processor: SignalProcessor) -> LayoutConfig: + """One pane per Channel (processor.get_derived()), titled by channel + name — raw Signals (physical device channels) are not auto-plotted. + Signals are just hardware endpoints; a Channel is what the operator has + explicitly chosen to process/observe, so that's what the default plot + shows. Use the "+ Add Channel" button on a Signal row to promote it.""" cfg = LayoutConfig() - for dev in registry.all_instances(): - enabled = [ch for ch in dev.info.channels if ch.enabled] - if not enabled: continue - pane = PaneSpec(title=dev.info.name, - y_label=enabled[0].unit if enabled else "") - for ch in enabled: - pane.traces.append(TraceSpec( - device_id=dev.info.device_id, channel_id=ch.channel_id, - label=ch.name, color=ch.color, - )) - if pane.traces: cfg.panes.append(pane) for dc in processor.get_derived(): pane = PaneSpec(title=dc.name, y_label=dc.unit) pane.traces.append(TraceSpec( @@ -281,8 +275,9 @@ class ColorSwatch(QPushButton): # ══════════════════════════════════════════════════════════════════════════════ class TraceRow(QFrame): - removed = pyqtSignal(object) - changed = pyqtSignal() + removed = pyqtSignal(object) + changed = pyqtSignal() + move_requested = pyqtSignal(object, int) # (self, direction: -1 up / +1 down) def __init__(self, tr: TraceSpec): super().__init__(); self.tr = tr; self.setObjectName("traceRow"); self._build() def _build(self): @@ -309,11 +304,76 @@ class TraceRow(QFrame): self._w.setValue(self.tr.width); self._w.setFixedWidth(56) self._w.valueChanged.connect(lambda v: setattr(self.tr,"width",v)) lay.addWidget(self._w) + up = QToolButton(); up.setText("▲"); up.setObjectName("traceRemoveBtn") + up.setFixedSize(18,18); up.setToolTip("Move up") + up.clicked.connect(lambda: self.move_requested.emit(self, -1)) + lay.addWidget(up) + dn = QToolButton(); dn.setText("▼"); dn.setObjectName("traceRemoveBtn") + dn.setFixedSize(18,18); dn.setToolTip("Move down") + dn.clicked.connect(lambda: self.move_requested.emit(self, 1)) + lay.addWidget(dn) rm = QToolButton(); rm.setText("✕"); rm.setObjectName("traceRemoveBtn") rm.setFixedSize(22,22); rm.clicked.connect(lambda: self.removed.emit(self)) lay.addWidget(rm) +# ══════════════════════════════════════════════════════════════════════════════ +# Trace channel picker — checkbox dialog, replaces the old inline dropdown +# ══════════════════════════════════════════════════════════════════════════════ + +class TraceChannelPickerDialog(QDialog): + """Checkbox list of enabled device + derived channels — same pattern as + channels_window.ChannelPickerDialog, so adding traces to a pane is + consistent with how channels are added elsewhere in the app.""" + + def __init__(self, registry: DeviceRegistry, processor: SignalProcessor, parent=None): + super().__init__(parent) + self.setWindowTitle("Add Channels to Pane") + self.setMinimumWidth(380) + self.resize(420, 320) + self._checks: List[Tuple] = [] # (QCheckBox, dev_id, ch_id, name, color) + + lay = QVBoxLayout(self) + scroll = QScrollArea(); scroll.setWidgetResizable(True) + scroll.setObjectName("deviceScroll") + cont = QWidget() + cl = QVBoxLayout(cont); cl.setContentsMargins(8, 8, 8, 8); cl.setSpacing(4) + + any_available = False + for dev in registry.all_instances(): + for ch in dev.info.channels: + if not ch.enabled: + continue + any_available = True + chk = QCheckBox(f"{ch.name} ({dev.info.device_id}/{ch.channel_id})") + self._checks.append((chk, dev.info.device_id, ch.channel_id, ch.name, ch.color)) + cl.addWidget(chk) + for dc in processor.get_derived(): + if not dc.enabled: + continue + any_available = True + chk = QCheckBox(f"{dc.name} ([virtual]/{dc.channel_id})") + self._checks.append((chk, "derived", dc.channel_id, dc.name, dc.color)) + cl.addWidget(chk) + + if not any_available: + cl.addWidget(QLabel("No channels available.")) + + cl.addStretch() + scroll.setWidget(cont) + lay.addWidget(scroll, 1) + + btns = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) + btns.accepted.connect(self.accept) + btns.rejected.connect(self.reject) + lay.addWidget(btns) + + def selected(self) -> List[Tuple[str, str, str, str]]: + return [(d, c, n, col) for chk, d, c, n, col in self._checks if chk.isChecked()] + + # ══════════════════════════════════════════════════════════════════════════════ # Pane block # ══════════════════════════════════════════════════════════════════════════════ @@ -411,30 +471,15 @@ class PaneBlock(QFrame): for tr in self.spec.traces: self._add_row(tr) pick_row = QHBoxLayout() - self._picker = self._make_picker(); pick_row.addWidget(self._picker,1) ab = QPushButton("+ Add Channel"); ab.setObjectName("addTraceBtn") - ab.clicked.connect(self._on_add); pick_row.addWidget(ab) + ab.clicked.connect(self._on_add); pick_row.addWidget(ab); pick_row.addStretch() self._tlay.addLayout(pick_row) outer.addWidget(tr_w) - def _make_picker(self): - cb = QComboBox(); cb.setObjectName("channelPickerCb") - for dev in self.registry.all_instances(): - for ch in dev.info.channels: - if not ch.enabled: - continue - cb.addItem(f"{ch.name} ({dev.info.device_id}/{ch.channel_id})", - userData=(dev.info.device_id, ch.channel_id, ch.name, ch.color)) - for dc in self.processor.get_derived(): - if not dc.enabled: - continue - cb.addItem(f"{dc.name} ([virtual]/{dc.channel_id})", - userData=("derived", dc.channel_id, dc.name, dc.color)) - return cb - def _add_row(self, tr: TraceSpec): row = TraceRow(tr); row.removed.connect(self._rm_row) row.changed.connect(self.changed) + row.move_requested.connect(self._move_row) self._rows.append(row) self._tlay.insertWidget(self._tlay.count()-1, row) @@ -443,12 +488,27 @@ class PaneBlock(QFrame): self._rows.remove(row); self._tlay.removeWidget(row); row.deleteLater() self.changed.emit() + def _move_row(self, row: TraceRow, direction: int): + if row not in self._rows: + return + idx = self._rows.index(row) + new_idx = idx + direction + if new_idx < 0 or new_idx >= len(self._rows): + return + self._rows[idx], self._rows[new_idx] = self._rows[new_idx], self._rows[idx] + self.spec.traces[idx], self.spec.traces[new_idx] = self.spec.traces[new_idx], self.spec.traces[idx] + self._tlay.removeWidget(row) + self._tlay.insertWidget(new_idx, row) + self.changed.emit() + def _on_add(self): - data = self._picker.currentData() - if not data: return - d,c,n,col = data - tr = TraceSpec(device_id=d,channel_id=c,label=n,color=col) - self.spec.traces.append(tr); self._add_row(tr); self.changed.emit() + dlg = TraceChannelPickerDialog(self.registry, self.processor, self) + if dlg.exec() != QDialog.DialogCode.Accepted: + return + for d, c, n, col in dlg.selected(): + tr = TraceSpec(device_id=d, channel_id=c, label=n, color=col) + self.spec.traces.append(tr); self._add_row(tr) + self.changed.emit() def _on_auto(self, v): self.spec.y_auto = v; self._ymin.setEnabled(not v); self._ymax.setEnabled(not v) -- cgit v1.2.3