From 2b9943ba0d28449a590acfa8a41555b174b6ba84 Mon Sep 17 00:00:00 2001 From: Christian Kolset Date: Mon, 27 Apr 2026 16:53:14 -0600 Subject: Improved arduino interface. Now allowing the user to configure pins on the fly. --- ui/__pycache__/__init__.cpython-312.pyc | Bin 149 -> 140 bytes ui/__pycache__/__init__.cpython-314.pyc | Bin 151 -> 142 bytes ui/__pycache__/add_device_dialog.cpython-312.pyc | Bin 29935 -> 27249 bytes ui/__pycache__/add_device_dialog.cpython-314.pyc | Bin 32389 -> 29249 bytes ui/__pycache__/config_dialog.cpython-312.pyc | Bin 6931 -> 4644 bytes ui/__pycache__/config_dialog.cpython-314.pyc | Bin 0 -> 4746 bytes ui/__pycache__/control_editor.cpython-312.pyc | Bin 22602 -> 22558 bytes ui/__pycache__/control_editor.cpython-314.pyc | Bin 22435 -> 24973 bytes ui/__pycache__/control_panel.cpython-312.pyc | Bin 42708 -> 42676 bytes ui/__pycache__/control_panel.cpython-314.pyc | Bin 44201 -> 47116 bytes ui/__pycache__/main_window.cpython-312.pyc | Bin 28788 -> 29520 bytes ui/__pycache__/main_window.cpython-314.pyc | Bin 27190 -> 31743 bytes ui/__pycache__/profile_manager_ui.cpython-312.pyc | Bin 8371 -> 8357 bytes ui/__pycache__/strip_chart.cpython-312.pyc | Bin 14901 -> 15321 bytes ui/__pycache__/strip_chart.cpython-314.pyc | Bin 16625 -> 17230 bytes ui/add_device_dialog.py | 263 ++++++++------------- ui/config_dialog.py | 46 +--- ui/main_window.py | 24 +- ui/strip_chart.py | 5 + ui/windows/__pycache__/__init__.cpython-312.pyc | Bin 157 -> 148 bytes .../__pycache__/devices_window.cpython-312.pyc | Bin 22041 -> 24793 bytes .../__pycache__/devices_window.cpython-314.pyc | Bin 23231 -> 26153 bytes ui/windows/__pycache__/plot_window.cpython-312.pyc | Bin 44685 -> 44632 bytes ui/windows/__pycache__/plot_window.cpython-314.pyc | Bin 46111 -> 46224 bytes .../__pycache__/settings_window.cpython-312.pyc | Bin 15935 -> 15906 bytes .../__pycache__/settings_window.cpython-314.pyc | Bin 16392 -> 13698 bytes .../__pycache__/signals_window.cpython-312.pyc | Bin 54087 -> 56462 bytes .../__pycache__/signals_window.cpython-314.pyc | Bin 53420 -> 59737 bytes ui/windows/devices_window.py | 96 +++++--- ui/windows/plot_window.py | 4 + ui/windows/settings_window.py | 44 +--- ui/windows/signals_window.py | 61 ++++- 32 files changed, 250 insertions(+), 293 deletions(-) create mode 100644 ui/__pycache__/config_dialog.cpython-314.pyc (limited to 'ui') diff --git a/ui/__pycache__/__init__.cpython-312.pyc b/ui/__pycache__/__init__.cpython-312.pyc index 42ac581..8dc1c2f 100644 Binary files a/ui/__pycache__/__init__.cpython-312.pyc and b/ui/__pycache__/__init__.cpython-312.pyc differ diff --git a/ui/__pycache__/__init__.cpython-314.pyc b/ui/__pycache__/__init__.cpython-314.pyc index ef40b13..15d71d0 100644 Binary files a/ui/__pycache__/__init__.cpython-314.pyc and b/ui/__pycache__/__init__.cpython-314.pyc differ diff --git a/ui/__pycache__/add_device_dialog.cpython-312.pyc b/ui/__pycache__/add_device_dialog.cpython-312.pyc index 990c229..1e970d6 100644 Binary files a/ui/__pycache__/add_device_dialog.cpython-312.pyc and b/ui/__pycache__/add_device_dialog.cpython-312.pyc differ diff --git a/ui/__pycache__/add_device_dialog.cpython-314.pyc b/ui/__pycache__/add_device_dialog.cpython-314.pyc index f5852ba..687bc1e 100644 Binary files a/ui/__pycache__/add_device_dialog.cpython-314.pyc and b/ui/__pycache__/add_device_dialog.cpython-314.pyc differ diff --git a/ui/__pycache__/config_dialog.cpython-312.pyc b/ui/__pycache__/config_dialog.cpython-312.pyc index 5a051dc..91921e5 100644 Binary files a/ui/__pycache__/config_dialog.cpython-312.pyc and b/ui/__pycache__/config_dialog.cpython-312.pyc differ diff --git a/ui/__pycache__/config_dialog.cpython-314.pyc b/ui/__pycache__/config_dialog.cpython-314.pyc new file mode 100644 index 0000000..91d6b9b Binary files /dev/null and b/ui/__pycache__/config_dialog.cpython-314.pyc differ diff --git a/ui/__pycache__/control_editor.cpython-312.pyc b/ui/__pycache__/control_editor.cpython-312.pyc index e76411d..3a53377 100644 Binary files a/ui/__pycache__/control_editor.cpython-312.pyc and b/ui/__pycache__/control_editor.cpython-312.pyc differ diff --git a/ui/__pycache__/control_editor.cpython-314.pyc b/ui/__pycache__/control_editor.cpython-314.pyc index aaea0ee..d3904f7 100644 Binary files a/ui/__pycache__/control_editor.cpython-314.pyc and b/ui/__pycache__/control_editor.cpython-314.pyc differ diff --git a/ui/__pycache__/control_panel.cpython-312.pyc b/ui/__pycache__/control_panel.cpython-312.pyc index 45f1eb9..1c101a8 100644 Binary files a/ui/__pycache__/control_panel.cpython-312.pyc and b/ui/__pycache__/control_panel.cpython-312.pyc differ diff --git a/ui/__pycache__/control_panel.cpython-314.pyc b/ui/__pycache__/control_panel.cpython-314.pyc index 79f9084..a5b859f 100644 Binary files a/ui/__pycache__/control_panel.cpython-314.pyc and b/ui/__pycache__/control_panel.cpython-314.pyc differ diff --git a/ui/__pycache__/main_window.cpython-312.pyc b/ui/__pycache__/main_window.cpython-312.pyc index 3aefd67..ffd6e12 100644 Binary files a/ui/__pycache__/main_window.cpython-312.pyc and b/ui/__pycache__/main_window.cpython-312.pyc differ diff --git a/ui/__pycache__/main_window.cpython-314.pyc b/ui/__pycache__/main_window.cpython-314.pyc index acef085..709972c 100644 Binary files a/ui/__pycache__/main_window.cpython-314.pyc and b/ui/__pycache__/main_window.cpython-314.pyc differ diff --git a/ui/__pycache__/profile_manager_ui.cpython-312.pyc b/ui/__pycache__/profile_manager_ui.cpython-312.pyc index 93491f7..97cbe3a 100644 Binary files a/ui/__pycache__/profile_manager_ui.cpython-312.pyc and b/ui/__pycache__/profile_manager_ui.cpython-312.pyc differ diff --git a/ui/__pycache__/strip_chart.cpython-312.pyc b/ui/__pycache__/strip_chart.cpython-312.pyc index a3e25c0..33e713b 100644 Binary files a/ui/__pycache__/strip_chart.cpython-312.pyc and b/ui/__pycache__/strip_chart.cpython-312.pyc differ diff --git a/ui/__pycache__/strip_chart.cpython-314.pyc b/ui/__pycache__/strip_chart.cpython-314.pyc index e56015f..ef745d3 100644 Binary files a/ui/__pycache__/strip_chart.cpython-314.pyc and b/ui/__pycache__/strip_chart.cpython-314.pyc differ diff --git a/ui/add_device_dialog.py b/ui/add_device_dialog.py index 4df71a3..5258995 100644 --- a/ui/add_device_dialog.py +++ b/ui/add_device_dialog.py @@ -1,12 +1,9 @@ """ ui/add_device_dialog.py -Add Device dialog — type-aware, shows the right fields for each device. - -Each device type gets its own config panel so the user sees exactly -the fields they need and nothing irrelevant. - -Serial port scanning populates a clickable list that sets the port field. +Add Device dialog — user selects physical device type (Arduino / NI-DAQ / Serial). +Backend is determined by device type; Arduino and NI-DAQ combine analog input +and digital I/O into one physical device entry. """ import os @@ -21,26 +18,22 @@ from PyQt6.QtCore import Qt, QThread, pyqtSignal from PyQt6.QtGui import QFont from devices.device_registry import DeviceRegistry -from devices.analog_input import AnalogInputDevice -from devices.digital_io import DigitalIODevice +from devices.arduino_device import ArduinoDevice +from devices.nidaqmx_device import NidaqmxDevice from devices.serial_device import SerialDevice, _FORMAT_LABELS -# ── Port scanner thread ─────────────────────────────────────────────────────── +# ── Background scan threads ─────────────────────────────────────────────────── class PortScanThread(QThread): ports_found = pyqtSignal(list) - def run(self): from api_layers.arduino_layer import ArduinoLayer self.ports_found.emit(ArduinoLayer.list_ports()) -# ── Background scan threads ─────────────────────────────────────────────────── - class NIScanThread(QThread): devices_found = pyqtSignal(list) - def run(self): try: import nidaqmx # type: ignore @@ -50,14 +43,14 @@ class NIScanThread(QThread): self.devices_found.emit(devs) -# ── Reusable port scanner widget ───────────────────────────────────────────── +# ── Reusable scanner widgets ────────────────────────────────────────────────── class PortScanGroup(QGroupBox): """Scan-and-click widget that fills a target QLineEdit with the chosen port.""" def __init__(self, target_edit: QLineEdit): super().__init__("Available Serial Ports") - self._target = target_edit + self._target = target_edit self._scanner = None lay = QVBoxLayout(self) @@ -121,7 +114,7 @@ class NIScanGroup(QGroupBox): def __init__(self, target_edit: QLineEdit): super().__init__("Available NI Devices") - self._target = target_edit + self._target = target_edit self._scanner = None lay = QVBoxLayout(self) @@ -182,159 +175,111 @@ class NIScanGroup(QGroupBox): # ── Per-type config panels ──────────────────────────────────────────────────── -class AnalogInputPanel(QWidget): - """Config fields for AnalogInputDevice.""" +class ArduinoPanel(QWidget): + """Config fields for ArduinoDevice (analog + digital I/O combined).""" def __init__(self): super().__init__() lay = QFormLayout(self) lay.setContentsMargins(0, 4, 0, 4) - # Backend - self.backend_cb = QComboBox() - self.backend_cb.addItems(["nidaqmx", "arduino"]) - lay.addRow("Backend:", self.backend_cb) - - # Channel count - self.ch_spin = QSpinBox() - self.ch_spin.setRange(1, 16) - self.ch_spin.setValue(4) - lay.addRow("Channels:", self.ch_spin) - - # Voltage range (NI) - self.ni_device_edit = QLineEdit("Dev1") - lay.addRow("NI Device:", self.ni_device_edit) + self.port_edit = QLineEdit("") + self.port_edit.setPlaceholderText("e.g. COM3 or /dev/ttyUSB0") + lay.addRow("Port:", self.port_edit) - self.min_v_spin = QDoubleSpinBox() - self.min_v_spin.setRange(-100.0, 0.0) - self.min_v_spin.setValue(-10.0) - self.min_v_spin.setSuffix(" V") - lay.addRow("Min Voltage:", self.min_v_spin) + self.baud_cb = QComboBox() + self.baud_cb.addItems(["9600", "57600", "115200", "230400"]) + self.baud_cb.setCurrentText("115200") + lay.addRow("Baud Rate:", self.baud_cb) - self.max_v_spin = QDoubleSpinBox() - self.max_v_spin.setRange(0.0, 100.0) - self.max_v_spin.setValue(10.0) - self.max_v_spin.setSuffix(" V") - lay.addRow("Max Voltage:", self.max_v_spin) + self.analog_pins_edit = QLineEdit("0, 1, 2, 3") + self.analog_pins_edit.setPlaceholderText("e.g. 0, 1, 2, 3 (indices into A0–A5)") + lay.addRow("Analog Input Pins:", self.analog_pins_edit) - self.ard_port_edit = QLineEdit("") - self.ard_port_edit.setPlaceholderText("e.g. COM3 or /dev/ttyUSB0") - lay.addRow("Arduino Port:", self.ard_port_edit) + self.di_pins_edit = QLineEdit("2, 3") + self.di_pins_edit.setPlaceholderText("e.g. 2, 3, 8") + lay.addRow("Digital Input Pins:", self.di_pins_edit) - self.ard_baud_cb = QComboBox() - self.ard_baud_cb.addItems(["9600", "57600", "115200", "230400"]) - self.ard_baud_cb.setCurrentText("115200") - lay.addRow("Arduino Baud:", self.ard_baud_cb) + self.do_pins_edit = QLineEdit("5, 6, 7, 9") + self.do_pins_edit.setPlaceholderText("e.g. 5, 6, 7, 9") + lay.addRow("Digital Output Pins:", self.do_pins_edit) - # Simulate self.sim_chk = QCheckBox("Simulation mode") self.sim_chk.setChecked(False) lay.addRow(self.sim_chk) - self._ni_scan_grp = NIScanGroup(self.ni_device_edit) - lay.addRow(self._ni_scan_grp) - - self._ard_scan_grp = PortScanGroup(self.ard_port_edit) - lay.addRow(self._ard_scan_grp) - - self._ni_widgets = [self.ni_device_edit, self.min_v_spin, self.max_v_spin, self._ni_scan_grp] - self._ard_widgets = [self.ard_port_edit, self.ard_baud_cb, self._ard_scan_grp] - self.backend_cb.currentTextChanged.connect(self._on_backend_changed) - self._on_backend_changed(self.backend_cb.currentText()) - - def _on_backend_changed(self, backend: str): - ni = backend == "nidaqmx" - lay = self.layout() - for w in self._ni_widgets: - w.setVisible(ni) - lbl = lay.labelForField(w) - if lbl: - lbl.setVisible(ni) - for w in self._ard_widgets: - w.setVisible(not ni) - lbl = lay.labelForField(w) - if lbl: - lbl.setVisible(not ni) - - def build_device(self, device_id: str) -> AnalogInputDevice: - return AnalogInputDevice( + lay.addRow(PortScanGroup(self.port_edit)) + + def build_device(self, device_id: str) -> ArduinoDevice: + from devices.arduino_device import _parse_pin_edit, _parse_analog_pin_edit + analog = _parse_analog_pin_edit(self.analog_pins_edit.text()) or None + di = _parse_pin_edit(self.di_pins_edit.text()) or None + do = _parse_pin_edit(self.do_pins_edit.text()) or None + return ArduinoDevice( device_id=device_id, - num_channels=self.ch_spin.value(), + analog_pins=analog, + di_pins=di, + do_pins=do, simulate=self.sim_chk.isChecked(), - backend=self.backend_cb.currentText(), - ni_device=self.ni_device_edit.text().strip() or "Dev1", - ni_min_v=self.min_v_spin.value(), - ni_max_v=self.max_v_spin.value(), - ard_port=self.ard_port_edit.text().strip() or "COM3", - ard_baud=int(self.ard_baud_cb.currentText()), + port=self.port_edit.text().strip() or "COM3", + baud=int(self.baud_cb.currentText()), ) -class DigitalIOPanel(QWidget): - """Config fields for DigitalIODevice.""" +class NidaqmxPanel(QWidget): + """Config fields for NidaqmxDevice (analog + digital I/O combined).""" def __init__(self): super().__init__() lay = QFormLayout(self) lay.setContentsMargins(0, 4, 0, 4) - self.backend_cb = QComboBox() - self.backend_cb.addItems(["nidaqmx", "arduino"]) - lay.addRow("Backend:", self.backend_cb) + self.ni_device_edit = QLineEdit("Dev1") + lay.addRow("NI Device:", self.ni_device_edit) + + self.analog_spin = QSpinBox() + self.analog_spin.setRange(0, 16) + self.analog_spin.setValue(4) + lay.addRow("Analog Inputs:", self.analog_spin) - self.in_spin = QSpinBox() - self.in_spin.setRange(0, 32) - self.in_spin.setValue(4) - lay.addRow("Digital Inputs:", self.in_spin) + self.min_v_spin = QDoubleSpinBox() + self.min_v_spin.setRange(-100.0, 0.0) + self.min_v_spin.setValue(-10.0) + self.min_v_spin.setSuffix(" V") + lay.addRow("Min Voltage:", self.min_v_spin) - self.out_spin = QSpinBox() - self.out_spin.setRange(0, 32) - self.out_spin.setValue(4) - lay.addRow("Digital Outputs:", self.out_spin) + self.max_v_spin = QDoubleSpinBox() + self.max_v_spin.setRange(0.0, 100.0) + self.max_v_spin.setValue(10.0) + self.max_v_spin.setSuffix(" V") + lay.addRow("Max Voltage:", self.max_v_spin) - self.ni_device_edit = QLineEdit("Dev1") - lay.addRow("NI Device:", self.ni_device_edit) + self.di_spin = QSpinBox() + self.di_spin.setRange(0, 32) + self.di_spin.setValue(2) + lay.addRow("Digital Inputs:", self.di_spin) - self.ard_port_edit = QLineEdit("") - self.ard_port_edit.setPlaceholderText("e.g. COM3 or /dev/ttyUSB0") - lay.addRow("Arduino Port:", self.ard_port_edit) + self.do_spin = QSpinBox() + self.do_spin.setRange(0, 32) + self.do_spin.setValue(4) + lay.addRow("Digital Outputs:", self.do_spin) self.sim_chk = QCheckBox("Simulation mode") self.sim_chk.setChecked(False) lay.addRow(self.sim_chk) - self._ni_scan_grp = NIScanGroup(self.ni_device_edit) - lay.addRow(self._ni_scan_grp) - - self._ard_scan_grp = PortScanGroup(self.ard_port_edit) - lay.addRow(self._ard_scan_grp) - - self.backend_cb.currentTextChanged.connect(self._on_backend_changed) - self._on_backend_changed(self.backend_cb.currentText()) - - def _on_backend_changed(self, backend: str): - ni = backend == "nidaqmx" - lay = self.layout() - for w, show in [ - (self.ni_device_edit, ni), - (self._ni_scan_grp, ni), - (self.ard_port_edit, not ni), - (self._ard_scan_grp, not ni), - ]: - w.setVisible(show) - lbl = lay.labelForField(w) - if lbl: - lbl.setVisible(show) - - def build_device(self, device_id: str) -> DigitalIODevice: - return DigitalIODevice( + lay.addRow(NIScanGroup(self.ni_device_edit)) + + def build_device(self, device_id: str) -> NidaqmxDevice: + return NidaqmxDevice( device_id=device_id, - num_inputs=self.in_spin.value(), - num_outputs=self.out_spin.value(), + num_analog=self.analog_spin.value(), + min_v=self.min_v_spin.value(), + max_v=self.max_v_spin.value(), + num_di=self.di_spin.value(), + num_do=self.do_spin.value(), simulate=self.sim_chk.isChecked(), - backend=self.backend_cb.currentText(), ni_device=self.ni_device_edit.text().strip() or "Dev1", - ard_port=self.ard_port_edit.text().strip() or "COM3", ) @@ -367,7 +312,7 @@ class SerialPanel(QWidget): self.sim_chk.setChecked(False) form.addRow(self.sim_chk) - note = QLabel("Protocol-specific settings (queries, registers, motors) available in device config dialog.") + note = QLabel("Protocol-specific settings available in device config dialog.") note.setObjectName("traceSource") note.setWordWrap(True) @@ -389,9 +334,9 @@ class SerialPanel(QWidget): # ── Main dialog ─────────────────────────────────────────────────────────────── _PANELS = { - "Analog Input": AnalogInputPanel, - "Digital I/O": DigitalIOPanel, - "Serial / UART": SerialPanel, + "Arduino": (ArduinoPanel, "ard"), + "NI-DAQ": (NidaqmxPanel, "ni"), + "Serial / UART":(SerialPanel, "ser"), } @@ -401,23 +346,23 @@ class AddDeviceDialog(QDialog): self.registry = registry self.created_device = None self.setWindowTitle("Add Device") - self.setMinimumSize(480, 460) - self.resize(500, 600) + self.setMinimumSize(480, 480) + self.resize(500, 620) self._build() def _build(self): root = QVBoxLayout(self) root.setSpacing(10) - # Header hdr = QLabel("Add New Device") hdr.setObjectName("devWindowTitle") root.addWidget(hdr) - div = QFrame(); div.setFrameShape(QFrame.Shape.HLine) - div.setObjectName("devWindowDivider"); root.addWidget(div) + div = QFrame() + div.setFrameShape(QFrame.Shape.HLine) + div.setObjectName("devWindowDivider") + root.addWidget(div) - # Device type selector type_row = QFormLayout() self._type_cb = QComboBox() self._type_cb.addItems(list(_PANELS.keys())) @@ -425,26 +370,28 @@ class AddDeviceDialog(QDialog): type_row.addRow("Device Type:", self._type_cb) self._id_edit = QLineEdit() - self._id_edit.setPlaceholderText("Leave blank for auto (e.g. ai_1, dio_0)") + self._id_edit.setPlaceholderText("Leave blank for auto") type_row.addRow("Device ID:", self._id_edit) root.addLayout(type_row) - div2 = QFrame(); div2.setFrameShape(QFrame.Shape.HLine) - div2.setObjectName("devWindowDivider"); root.addWidget(div2) + div2 = QFrame() + div2.setFrameShape(QFrame.Shape.HLine) + div2.setObjectName("devWindowDivider") + root.addWidget(div2) - # Stacked type-specific panels self._stack = QStackedWidget() self._panels = {} - for name, cls in _PANELS.items(): + for name, (cls, _prefix) in _PANELS.items(): panel = cls() self._panels[name] = panel self._stack.addWidget(panel) root.addWidget(self._stack, 1) - div3 = QFrame(); div3.setFrameShape(QFrame.Shape.HLine) - div3.setObjectName("devWindowDivider"); root.addWidget(div3) + div3 = QFrame() + div3.setFrameShape(QFrame.Shape.HLine) + div3.setObjectName("devWindowDivider") + root.addWidget(div3) - # Buttons btn_row = QHBoxLayout() btn_row.addStretch() cancel = QPushButton("Cancel") @@ -457,16 +404,12 @@ class AddDeviceDialog(QDialog): btn_row.addWidget(add) root.addLayout(btn_row) + self._on_type_changed(0) + def _on_type_changed(self, idx: int): self._stack.setCurrentIndex(idx) - # Auto-suggest a device ID based on type type_name = self._type_cb.currentText() - prefixes = { - "Analog Input": "ai", - "Digital I/O": "dio", - "Serial / UART": "ser", - } - prefix = prefixes.get(type_name, "dev") + _, prefix = _PANELS.get(type_name, (None, "dev")) existing = {d.info.device_id for d in self.registry.all_instances()} for i in range(100): candidate = f"{prefix}_{i}" @@ -479,14 +422,8 @@ class AddDeviceDialog(QDialog): panel = self._panels[type_name] dev_id = self._id_edit.text().strip() - # Auto-generate ID if blank if not dev_id: - prefixes = { - "Analog Input": "ai", - "Digital I/O": "dio", - "Serial / UART": "ser", - } - prefix = prefixes.get(type_name, "dev") + _, prefix = _PANELS.get(type_name, (None, "dev")) existing = {d.info.device_id for d in self.registry.all_instances()} for i in range(100): candidate = f"{prefix}_{i}" diff --git a/ui/config_dialog.py b/ui/config_dialog.py index 4976b36..bafac24 100644 --- a/ui/config_dialog.py +++ b/ui/config_dialog.py @@ -4,8 +4,8 @@ ui/config_dialog.py — Device configuration dialog (tabbed). from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, - QWidget, QFormLayout, QGroupBox, QScrollArea, - QLabel, QLineEdit, QDoubleSpinBox, QCheckBox, + QWidget, QFormLayout, QScrollArea, + QLabel, QLineEdit, QPushButton, ) from PyQt6.QtCore import Qt @@ -37,10 +37,7 @@ class DeviceConfigDialog(QDialog): scroll.setWidget(cfg_widget) tabs.addTab(scroll, "Hardware / Backend") - # ── Tab 2: Channel settings ─────────────────────────────────── - tabs.addTab(self._channel_tab(), "Channels") - - # ── Tab 3: Device info ──────────────────────────────────────── + # ── Tab 2: Device info ──────────────────────────────────────── tabs.addTab(self._info_tab(), "Info") layout.addWidget(tabs) @@ -53,43 +50,6 @@ class DeviceConfigDialog(QDialog): btn_row.addWidget(close_btn) layout.addLayout(btn_row) - def _channel_tab(self): - w = QScrollArea() - w.setWidgetResizable(True) - container = QWidget() - layout = QVBoxLayout(container) - - for ch in self.device.info.channels: - grp = QGroupBox(f"{ch.channel_id} — {ch.name}") - form = QFormLayout(grp) - - name_e = QLineEdit(ch.name) - unit_e = QLineEdit(ch.unit) - en_chk = QCheckBox() - en_chk.setChecked(ch.enabled) - - form.addRow("Name:", name_e) - form.addRow("Unit:", unit_e) - form.addRow("Enabled:", en_chk) - - apply = QPushButton("Apply") - apply.setObjectName("applyButton") - - def _make_apply(c, ne, ue, ec): - def _do(): - c.name = ne.text() - c.unit = ue.text() - c.enabled = ec.isChecked() - return _do - - apply.clicked.connect(_make_apply(ch, name_e, unit_e, en_chk)) - form.addRow(apply) - layout.addWidget(grp) - - layout.addStretch() - w.setWidget(container) - return w - def _info_tab(self): w = QWidget() form = QFormLayout(w) diff --git a/ui/main_window.py b/ui/main_window.py index 483ca49..0bb62f6 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -27,8 +27,8 @@ class _WheelBlocker(QObject): return True return super().eventFilter(obj, event) -from devices.analog_input import AnalogInputDevice -from devices.digital_io import DigitalIODevice +from devices.arduino_device import ArduinoDevice +from devices.nidaqmx_device import NidaqmxDevice from devices.serial_device import SerialDevice from devices.device_registry import DeviceRegistry from core.acquisition import AcquisitionEngine @@ -75,15 +75,10 @@ class MainWindow(QMainWindow): # ── Demo ────────────────────────────────────────────────────────────── def _init_demo_devices(self): - # NOTE: if you have a real Arduino, configure ard_0 and dio_0 to use - # the same port — port_registry ensures only one serial connection - # is opened, and both devices share it automatically. for dev in [ - AnalogInputDevice(device_id="ai_0", num_channels=4, simulate=True, backend="nidaqmx"), - AnalogInputDevice(device_id="ard_0", num_channels=1, simulate=True, backend="arduino"), - DigitalIODevice (device_id="dio_0", num_inputs=2, num_outputs=4, - simulate=True, backend="arduino"), - SerialDevice (device_id="ser_0", num_channels=3, simulate=True), + NidaqmxDevice(device_id="ni_0", num_analog=4, num_di=2, num_do=4, simulate=True), + ArduinoDevice(device_id="ard_0", num_analog=4, num_di=2, num_do=4, simulate=True), + SerialDevice (device_id="ser_0", num_channels=3, simulate=True), ]: dev.connect() self.registry.add_instance(dev) @@ -217,6 +212,7 @@ class MainWindow(QMainWindow): self._win_devices.device_added.connect(self._on_device_added) self._win_devices.device_removed.connect(self._on_device_removed) self._win_devices.device_reconfigured.connect(self._on_device_reconfigured) + self._win_devices.channel_visibility_changed.connect(self._on_channel_visibility_changed) self._win_devices.closed.connect(lambda: self._btn_devices.setChecked(False)) self._show_win(self._win_devices, "right") @@ -225,6 +221,7 @@ class MainWindow(QMainWindow): self._win_signals = SignalsWindow(self.registry, self.processor, self) self._win_signals.derived_changed.connect(self._on_derived_changed) self._win_signals.visibility_changed.connect(self._chart.refresh) + self._win_signals.channel_toggled.connect(self._on_channel_visibility_changed) self._win_signals.closed.connect(lambda: self._btn_signals.setChecked(False)) self._show_win(self._win_signals, "right") @@ -341,6 +338,13 @@ class MainWindow(QMainWindow): if self._win_plot: self._win_plot.refresh_channels() self._status.setText(f"Device '{dev_id}' reconfigured.") + def _on_channel_visibility_changed(self, dev_id: str, ch_id: str, enabled: bool): + self._chart.on_channel_enabled_changed(dev_id, ch_id, enabled) + if self._win_signals: + self._win_signals.on_channel_enabled_changed(dev_id, ch_id, enabled) + if self._win_plot: + self._win_plot.refresh_channels() + def _on_derived_changed(self): self._chart.refresh() if self._win_plot: self._win_plot.refresh_channels() diff --git a/ui/strip_chart.py b/ui/strip_chart.py index 7bc2a18..523102d 100644 --- a/ui/strip_chart.py +++ b/ui/strip_chart.py @@ -158,6 +158,11 @@ class StripChartWidget(QWidget): def refresh(self): self.apply_layout(self._cfg or build_default_layout(self.registry, self.processor)) + def on_channel_enabled_changed(self, dev_id: str, ch_id: str, enabled: bool): + key = (dev_id, ch_id) + for e in self._curves.get(key, []): + e["curve"].setVisible(enabled) + @pyqtSlot(str, str, float, float) def on_new_data(self, device_id: str, channel_id: str, ts: float, val: float): if self._paused or not _HAS_PG or not self._cfg: return diff --git a/ui/windows/__pycache__/__init__.cpython-312.pyc b/ui/windows/__pycache__/__init__.cpython-312.pyc index 1454fd8..66495c5 100644 Binary files a/ui/windows/__pycache__/__init__.cpython-312.pyc and b/ui/windows/__pycache__/__init__.cpython-312.pyc differ diff --git a/ui/windows/__pycache__/devices_window.cpython-312.pyc b/ui/windows/__pycache__/devices_window.cpython-312.pyc index fb52b79..dc62b48 100644 Binary files a/ui/windows/__pycache__/devices_window.cpython-312.pyc and b/ui/windows/__pycache__/devices_window.cpython-312.pyc differ diff --git a/ui/windows/__pycache__/devices_window.cpython-314.pyc b/ui/windows/__pycache__/devices_window.cpython-314.pyc index e1a4868..9f819dd 100644 Binary files a/ui/windows/__pycache__/devices_window.cpython-314.pyc and b/ui/windows/__pycache__/devices_window.cpython-314.pyc differ diff --git a/ui/windows/__pycache__/plot_window.cpython-312.pyc b/ui/windows/__pycache__/plot_window.cpython-312.pyc index 7499763..d3a1583 100644 Binary files a/ui/windows/__pycache__/plot_window.cpython-312.pyc and b/ui/windows/__pycache__/plot_window.cpython-312.pyc differ diff --git a/ui/windows/__pycache__/plot_window.cpython-314.pyc b/ui/windows/__pycache__/plot_window.cpython-314.pyc index 30b0a23..9782293 100644 Binary files a/ui/windows/__pycache__/plot_window.cpython-314.pyc and b/ui/windows/__pycache__/plot_window.cpython-314.pyc differ diff --git a/ui/windows/__pycache__/settings_window.cpython-312.pyc b/ui/windows/__pycache__/settings_window.cpython-312.pyc index 47d5611..d596487 100644 Binary files a/ui/windows/__pycache__/settings_window.cpython-312.pyc and b/ui/windows/__pycache__/settings_window.cpython-312.pyc differ diff --git a/ui/windows/__pycache__/settings_window.cpython-314.pyc b/ui/windows/__pycache__/settings_window.cpython-314.pyc index cc9691b..f732521 100644 Binary files a/ui/windows/__pycache__/settings_window.cpython-314.pyc and b/ui/windows/__pycache__/settings_window.cpython-314.pyc differ diff --git a/ui/windows/__pycache__/signals_window.cpython-312.pyc b/ui/windows/__pycache__/signals_window.cpython-312.pyc index ec57533..9872798 100644 Binary files a/ui/windows/__pycache__/signals_window.cpython-312.pyc and b/ui/windows/__pycache__/signals_window.cpython-312.pyc differ diff --git a/ui/windows/__pycache__/signals_window.cpython-314.pyc b/ui/windows/__pycache__/signals_window.cpython-314.pyc index 224e31e..2027f59 100644 Binary files a/ui/windows/__pycache__/signals_window.cpython-314.pyc and b/ui/windows/__pycache__/signals_window.cpython-314.pyc differ diff --git a/ui/windows/devices_window.py b/ui/windows/devices_window.py index fbaf9fa..8f6a48b 100644 --- a/ui/windows/devices_window.py +++ b/ui/windows/devices_window.py @@ -118,6 +118,17 @@ class DeviceRow(QFrame): class ChannelsTab(QWidget): """Shows all channels across all devices in an editable table.""" + channel_visibility_changed = pyqtSignal(str, str, bool) # dev_id, ch_id, enabled + + # Col indices + _C_DEVICE = 0 + _C_CH_ID = 1 + _C_ENABLED = 2 + _C_NAME = 3 + _C_UNIT = 4 + _C_MIN = 5 + _C_MAX = 6 + def __init__(self, registry: DeviceRegistry): super().__init__() self.registry = registry @@ -129,18 +140,23 @@ class ChannelsTab(QWidget): self._table = QTableWidget() self._table.setObjectName("channelTable") - self._table.setColumnCount(6) + self._table.setColumnCount(7) self._table.setHorizontalHeaderLabels( - ["Device", "Channel ID", "Name", "Unit", "Min", "Max"] + ["Device", "Channel ID", "On", "Name", "Unit", "Min", "Max"] ) - self._table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) - self._table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) + hdr = self._table.horizontalHeader() + hdr.setSectionResizeMode(self._C_NAME, QHeaderView.ResizeMode.Stretch) + hdr.setSectionResizeMode(self._C_DEVICE, QHeaderView.ResizeMode.ResizeToContents) + hdr.setSectionResizeMode(self._C_CH_ID, QHeaderView.ResizeMode.ResizeToContents) + hdr.setSectionResizeMode(self._C_ENABLED, QHeaderView.ResizeMode.ResizeToContents) self._table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self._table.setAlternatingRowColors(True) self._table.verticalHeader().setVisible(False) + self._table.cellChanged.connect(self._on_cell_changed) lay.addWidget(self._table) def refresh(self): + self._table.blockSignals(True) self._table.setRowCount(0) row = 0 for dev in self.registry.all_instances(): @@ -150,35 +166,57 @@ class ChannelsTab(QWidget): dev_item = QTableWidgetItem(f"{dev.info.icon} {dev.info.device_id}") dev_item.setFlags(dev_item.flags() & ~Qt.ItemFlag.ItemIsEditable) dev_item.setForeground(QColor("#64748b")) - self._table.setItem(row, 0, dev_item) + self._table.setItem(row, self._C_DEVICE, dev_item) ch_item = QTableWidgetItem(ch.channel_id) ch_item.setFlags(ch_item.flags() & ~Qt.ItemFlag.ItemIsEditable) ch_item.setForeground(QColor(ch.color)) - self._table.setItem(row, 1, ch_item) + self._table.setItem(row, self._C_CH_ID, ch_item) + + # Enabled checkbox — centred in cell + chk_container = QWidget() + chk_lay = QHBoxLayout(chk_container) + chk_lay.setContentsMargins(0, 0, 0, 0) + chk_lay.setAlignment(Qt.AlignmentFlag.AlignCenter) + chk = QCheckBox() + chk.setChecked(ch.enabled) + dev_id_cap = dev.info.device_id + ch_id_cap = ch.channel_id + chk.toggled.connect( + lambda checked, d=dev_id_cap, c=ch_id_cap: self._on_enabled(d, c, checked) + ) + chk_lay.addWidget(chk) + self._table.setCellWidget(row, self._C_ENABLED, chk_container) name_item = QTableWidgetItem(ch.name) - self._table.setItem(row, 2, name_item) + name_item.setData(Qt.ItemDataRole.UserRole, (dev.info.device_id, ch.channel_id)) + self._table.setItem(row, self._C_NAME, name_item) unit_item = QTableWidgetItem(ch.unit) - self._table.setItem(row, 3, unit_item) + self._table.setItem(row, self._C_UNIT, unit_item) min_item = QTableWidgetItem(str(ch.min_value)) max_item = QTableWidgetItem(str(ch.max_value)) - self._table.setItem(row, 4, min_item) - self._table.setItem(row, 5, max_item) + self._table.setItem(row, self._C_MIN, min_item) + self._table.setItem(row, self._C_MAX, max_item) - # Store references for saving - name_item.setData(Qt.ItemDataRole.UserRole, (dev.info.device_id, ch.channel_id)) row += 1 - self._table.cellChanged.connect(self._on_cell_changed) + self._table.blockSignals(False) + + def _on_enabled(self, dev_id: str, ch_id: str, checked: bool): + dev = self.registry.get_instance(dev_id) + if dev: + ch = dev.get_channel(ch_id) + if ch: + ch.enabled = checked + self.channel_visibility_changed.emit(dev_id, ch_id, checked) def _on_cell_changed(self, row: int, col: int): - item = self._table.item(row, 2) # name col - if item is None: + name_item = self._table.item(row, self._C_NAME) + if name_item is None: return - ref = item.data(Qt.ItemDataRole.UserRole) + ref = name_item.data(Qt.ItemDataRole.UserRole) if ref is None: return dev_id, ch_id = ref @@ -188,27 +226,28 @@ class ChannelsTab(QWidget): ch = dev.get_channel(ch_id) if not ch: return - if col == 2: - ch.name = self._table.item(row, 2).text() - elif col == 3: - ch.unit = self._table.item(row, 3).text() - elif col == 4: + if col == self._C_NAME: + ch.name = self._table.item(row, self._C_NAME).text() + elif col == self._C_UNIT: + ch.unit = self._table.item(row, self._C_UNIT).text() + elif col == self._C_MIN: try: - ch.min_value = float(self._table.item(row, 4).text()) + ch.min_value = float(self._table.item(row, self._C_MIN).text()) except ValueError: pass - elif col == 5: + elif col == self._C_MAX: try: - ch.max_value = float(self._table.item(row, 5).text()) + ch.max_value = float(self._table.item(row, self._C_MAX).text()) except ValueError: pass class DevicesWindow(QWidget): - device_added = pyqtSignal() - device_removed = pyqtSignal(str) - device_reconfigured = pyqtSignal(str) - closed = pyqtSignal() + device_added = pyqtSignal() + device_removed = pyqtSignal(str) + device_reconfigured = pyqtSignal(str) + channel_visibility_changed = pyqtSignal(str, str, bool) # dev_id, ch_id, enabled + closed = pyqtSignal() def __init__(self, registry: DeviceRegistry, engine: AcquisitionEngine, parent=None): super().__init__(parent, Qt.WindowType.Window | Qt.WindowType.Tool) @@ -265,6 +304,7 @@ class DevicesWindow(QWidget): # Tab 2: Channels self._ch_tab = ChannelsTab(self.registry) + self._ch_tab.channel_visibility_changed.connect(self.channel_visibility_changed) tabs.addTab(self._ch_tab, " Channels ") tabs.currentChanged.connect(lambda i: self._ch_tab.refresh() if i == 1 else None) diff --git a/ui/windows/plot_window.py b/ui/windows/plot_window.py index ea1ba1c..9a13781 100644 --- a/ui/windows/plot_window.py +++ b/ui/windows/plot_window.py @@ -207,6 +207,8 @@ class PaneBlock(QFrame): self._x_cb.addItem("⏱ Time (elapsed s)", userData="time") for dev in self.registry.all_instances(): for ch in dev.info.channels: + if not ch.enabled: + continue self._x_cb.addItem(f"{dev.info.device_id}/{ch.channel_id} ({ch.name})", userData=f"{dev.info.device_id}/{ch.channel_id}") for dc in self.processor.get_derived(): @@ -271,6 +273,8 @@ class PaneBlock(QFrame): 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"{dev.info.device_id} / {ch.channel_id} ({ch.name})", userData=(dev.info.device_id, ch.channel_id, ch.name, ch.color)) for dc in self.processor.get_derived(): diff --git a/ui/windows/settings_window.py b/ui/windows/settings_window.py index 88093ff..9d7b7bc 100644 --- a/ui/windows/settings_window.py +++ b/ui/windows/settings_window.py @@ -7,14 +7,13 @@ Tabs: General — theme (dark/light/system), font sizes, polling rate Acquisition — sample rate, buffer size, CSV log directory Display — default time window, legend, grid defaults - Controls — configure output widget assignments per channel """ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, - QTabWidget, QFrame, QFormLayout, QGroupBox, QComboBox, + QTabWidget, QFrame, QFormLayout, QComboBox, QSpinBox, QDoubleSpinBox, QCheckBox, QLineEdit, - QFileDialog, QScrollArea, QSizePolicy, + QFileDialog, QScrollArea, ) from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QCloseEvent, QFont @@ -73,7 +72,6 @@ class SettingsWindow(QWidget): tabs.addTab(self._general_tab(), " General ") tabs.addTab(self._acquisition_tab(), " Acquisition ") tabs.addTab(self._display_tab(), " Display ") - tabs.addTab(self._controls_tab(), " Controls ") # Bottom bar btm = QWidget(); btm.setObjectName("cfgBottomBar") @@ -159,44 +157,6 @@ class SettingsWindow(QWidget): root = QVBoxLayout(w); root.setContentsMargins(0,0,0,0); root.addWidget(scroll) return w - def _controls_tab(self): - """Output channel assignments — which device channel each control widget drives.""" - w = QWidget() - scroll = QScrollArea(); scroll.setWidgetResizable(True) - scroll.setObjectName("deviceScroll") - cont = QWidget(); lay = QVBoxLayout(cont) - lay.setContentsMargins(14,12,14,12); lay.setSpacing(8) - - info = QLabel( - "Configure which physical output channels are driven by each control widget.\n" - "Add output widget mappings below. Changes take effect on next app start." - ) - info.setObjectName("traceSource"); info.setWordWrap(True) - lay.addWidget(info) - - grp = QGroupBox("Output Assignments") - g_lay = QFormLayout(grp); g_lay.setSpacing(6) - - # Collect digital output channels - out_channels = ["— none —"] - for dev in self.registry.all_instances(): - for ch in dev.info.channels: - if ch.channel_id.startswith("do") or "out" in ch.channel_id.lower(): - out_channels.append(f"{dev.info.device_id} / {ch.channel_id} ({ch.name})") - - self._out_combos = {} - for label in ["Pump Power", "Heater", "Motor Enable", "PWM Ch 1"]: - cb = QComboBox(); cb.setObjectName("channelPickerCb") - cb.addItems(out_channels) - g_lay.addRow(f"{label}:", cb) - self._out_combos[label] = cb - - lay.addWidget(grp) - lay.addStretch() - scroll.setWidget(cont) - root = QVBoxLayout(w); root.setContentsMargins(0,0,0,0); root.addWidget(scroll) - return w - # ── Actions ─────────────────────────────────────────────────────────── def _browse_log(self): diff --git a/ui/windows/signals_window.py b/ui/windows/signals_window.py index ff863af..0bf9dba 100644 --- a/ui/windows/signals_window.py +++ b/ui/windows/signals_window.py @@ -40,6 +40,8 @@ def _channel_combo(registry: DeviceRegistry, cb = QComboBox(); cb.setObjectName("channelPickerCb") for dev in registry.all_instances(): for ch in dev.info.channels: + if not ch.enabled: + continue label = f"{dev.info.device_id} / {ch.channel_id} ({ch.name})" if show_unit and ch.unit: label += f" [{ch.unit}]" @@ -255,7 +257,8 @@ class PipelineTab(QWidget): super().__init__() self.registry = registry self.processor = processor - self._shown: Set[Tuple[str,str]] = set() # (dev_id, ch_id) + self._shown: Set[Tuple[str,str]] = set() + self._blocks: dict[Tuple[str,str], ChannelPipelineBlock] = {} self._build() def _build(self): @@ -285,10 +288,11 @@ class PipelineTab(QWidget): scroll.setWidget(self._cont) lay.addWidget(scroll, 1) - # Populate with all device channels on startup + # Populate with enabled device channels on startup for dev in self.registry.all_instances(): for ch in dev.info.channels: - self._add_block(dev.info.device_id, ch.channel_id, ch.name, ch.unit) + if ch.enabled: + self._add_block(dev.info.device_id, ch.channel_id, ch.name, ch.unit) self._refresh_avail_cb() @@ -301,9 +305,29 @@ class PipelineTab(QWidget): block = ChannelPipelineBlock(dev_id, ch_id, ch_name, unit, self.processor) block.changed.connect(self.changed) block.removed.connect(self._on_remove_block) + self._blocks[(dev_id, ch_id)] = block # Insert before the trailing stretch self._inner.insertWidget(self._inner.count() - 1, block) + def on_channel_enabled_changed(self, dev_id: str, ch_id: str, enabled: bool): + key = (dev_id, ch_id) + if enabled: + if key not in self._shown: + dev = self.registry.get_instance(dev_id) + if dev: + ch = dev.get_channel(ch_id) + if ch: + self._add_block(dev_id, ch_id, ch.name, ch.unit) + else: + block = self._blocks.get(key) + if block: + block.setVisible(True) + else: + block = self._blocks.get(key) + if block: + block.setVisible(False) + self._refresh_avail_cb() + def _on_add_channel(self): data = self._avail_cb.currentData() if not data: @@ -319,7 +343,9 @@ class PipelineTab(QWidget): self._refresh_avail_cb() def _on_remove_block(self, block: ChannelPipelineBlock): - self._shown.discard((block.dev_id, block.ch_id)) + key = (block.dev_id, block.ch_id) + self._shown.discard(key) + self._blocks.pop(key, None) self._inner.removeWidget(block) block.deleteLater() self._refresh_avail_cb() @@ -328,6 +354,8 @@ class PipelineTab(QWidget): self._avail_cb.clear() for dev in self.registry.all_instances(): for ch in dev.info.channels: + if not ch.enabled: + continue if (dev.info.device_id, ch.channel_id) not in self._shown: label = f"{dev.info.device_id} / {ch.channel_id} ({ch.name})" if ch.unit: @@ -520,7 +548,8 @@ class DerivedBlock(QFrame): # ══════════════════════════════════════════════════════════════════════════════ class SignalsTab(QWidget): - changed = pyqtSignal() + changed = pyqtSignal() + channel_toggled = pyqtSignal(str, str, bool) # dev_id, ch_id, enabled def __init__(self, registry: DeviceRegistry, processor: SignalProcessor): super().__init__() @@ -547,13 +576,19 @@ class SignalsTab(QWidget): # Physical channels — flat list across all devices for dev in self.registry.all_instances(): for ch in dev.info.channels: + dev_id_cap = dev.info.device_id + ch_id_cap = ch.channel_id self._lay.addWidget(self._make_row( color = ch.color, name = f"{ch.channel_id} — {ch.name}", unit = ch.unit, - source = dev.info.device_id, + source = dev_id_cap, enabled = ch.enabled, - on_toggle = lambda v, c=ch: (setattr(c,"enabled",v), self.changed.emit()), + on_toggle = lambda v, c=ch, d=dev_id_cap, ci=ch_id_cap: ( + setattr(c, "enabled", v), + self.changed.emit(), + self.channel_toggled.emit(d, ci, v), + ), )) # Thin divider before derived (only if both sections have entries) @@ -609,6 +644,7 @@ class SignalsWindow(QWidget): pipeline_changed = pyqtSignal() derived_changed = pyqtSignal() visibility_changed = pyqtSignal() + channel_toggled = pyqtSignal(str, str, bool) # dev_id, ch_id, enabled closed = pyqtSignal() def __init__(self, registry: DeviceRegistry, @@ -649,6 +685,7 @@ class SignalsWindow(QWidget): # Tab 3: Flat signals list self._vis_tab = SignalsTab(self.registry, self.processor) self._vis_tab.changed.connect(self.visibility_changed) + self._vis_tab.channel_toggled.connect(self._on_vis_tab_toggled) tabs.addTab(self._vis_tab, " Signals ") # Keep Signals tab in sync when derived channels are added/removed @@ -720,5 +757,15 @@ class SignalsWindow(QWidget): self._color_idx = len(self._derived_blocks) self._vis_tab.refresh() + def _on_vis_tab_toggled(self, dev_id: str, ch_id: str, enabled: bool): + """Signals tab toggled a channel — propagate to pipeline + chart without re-refreshing vis_tab.""" + self._pip_tab.on_channel_enabled_changed(dev_id, ch_id, enabled) + self.channel_toggled.emit(dev_id, ch_id, enabled) + + def on_channel_enabled_changed(self, dev_id: str, ch_id: str, enabled: bool): + """Called from outside (Devices window) — update pipeline block and sync vis_tab checkboxes.""" + self._pip_tab.on_channel_enabled_changed(dev_id, ch_id, enabled) + self._vis_tab.refresh() + def closeEvent(self, e: QCloseEvent): self.closed.emit(); e.accept() -- cgit v1.2.3