summaryrefslogtreecommitdiff
path: root/ui/windows/plot_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/plot_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/plot_window.py')
-rw-r--r--ui/windows/plot_window.py132
1 files changed, 96 insertions, 36 deletions
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,12 +304,77 @@ 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)