diff options
| author | Christian Kolset <christian.kolset@gmail.com> | 2026-04-27 16:53:14 -0600 |
|---|---|---|
| committer | Christian Kolset <christian.kolset@gmail.com> | 2026-04-27 16:53:14 -0600 |
| commit | 2b9943ba0d28449a590acfa8a41555b174b6ba84 (patch) | |
| tree | e8bef8c9bc61dd7e2f9c8a9b177058a4b09546db /ui/windows | |
| parent | 67cfa0a514c7de4605ed7360e15a81aa781e510e (diff) | |
Improved arduino interface. Now allowing the user to configure pins on the fly.
Diffstat (limited to 'ui/windows')
| -rw-r--r-- | ui/windows/__pycache__/__init__.cpython-312.pyc | bin | 157 -> 148 bytes | |||
| -rw-r--r-- | ui/windows/__pycache__/devices_window.cpython-312.pyc | bin | 22041 -> 24793 bytes | |||
| -rw-r--r-- | ui/windows/__pycache__/devices_window.cpython-314.pyc | bin | 23231 -> 26153 bytes | |||
| -rw-r--r-- | ui/windows/__pycache__/plot_window.cpython-312.pyc | bin | 44685 -> 44632 bytes | |||
| -rw-r--r-- | ui/windows/__pycache__/plot_window.cpython-314.pyc | bin | 46111 -> 46224 bytes | |||
| -rw-r--r-- | ui/windows/__pycache__/settings_window.cpython-312.pyc | bin | 15935 -> 15906 bytes | |||
| -rw-r--r-- | ui/windows/__pycache__/settings_window.cpython-314.pyc | bin | 16392 -> 13698 bytes | |||
| -rw-r--r-- | ui/windows/__pycache__/signals_window.cpython-312.pyc | bin | 54087 -> 56462 bytes | |||
| -rw-r--r-- | ui/windows/__pycache__/signals_window.cpython-314.pyc | bin | 53420 -> 59737 bytes | |||
| -rw-r--r-- | ui/windows/devices_window.py | 96 | ||||
| -rw-r--r-- | ui/windows/plot_window.py | 4 | ||||
| -rw-r--r-- | ui/windows/settings_window.py | 44 | ||||
| -rw-r--r-- | ui/windows/signals_window.py | 61 |
13 files changed, 128 insertions, 77 deletions
diff --git a/ui/windows/__pycache__/__init__.cpython-312.pyc b/ui/windows/__pycache__/__init__.cpython-312.pyc Binary files differindex 1454fd8..66495c5 100644 --- a/ui/windows/__pycache__/__init__.cpython-312.pyc +++ b/ui/windows/__pycache__/__init__.cpython-312.pyc diff --git a/ui/windows/__pycache__/devices_window.cpython-312.pyc b/ui/windows/__pycache__/devices_window.cpython-312.pyc Binary files differindex fb52b79..dc62b48 100644 --- a/ui/windows/__pycache__/devices_window.cpython-312.pyc +++ b/ui/windows/__pycache__/devices_window.cpython-312.pyc diff --git a/ui/windows/__pycache__/devices_window.cpython-314.pyc b/ui/windows/__pycache__/devices_window.cpython-314.pyc Binary files differindex e1a4868..9f819dd 100644 --- a/ui/windows/__pycache__/devices_window.cpython-314.pyc +++ b/ui/windows/__pycache__/devices_window.cpython-314.pyc diff --git a/ui/windows/__pycache__/plot_window.cpython-312.pyc b/ui/windows/__pycache__/plot_window.cpython-312.pyc Binary files differindex 7499763..d3a1583 100644 --- a/ui/windows/__pycache__/plot_window.cpython-312.pyc +++ b/ui/windows/__pycache__/plot_window.cpython-312.pyc diff --git a/ui/windows/__pycache__/plot_window.cpython-314.pyc b/ui/windows/__pycache__/plot_window.cpython-314.pyc Binary files differindex 30b0a23..9782293 100644 --- a/ui/windows/__pycache__/plot_window.cpython-314.pyc +++ b/ui/windows/__pycache__/plot_window.cpython-314.pyc diff --git a/ui/windows/__pycache__/settings_window.cpython-312.pyc b/ui/windows/__pycache__/settings_window.cpython-312.pyc Binary files differindex 47d5611..d596487 100644 --- a/ui/windows/__pycache__/settings_window.cpython-312.pyc +++ b/ui/windows/__pycache__/settings_window.cpython-312.pyc diff --git a/ui/windows/__pycache__/settings_window.cpython-314.pyc b/ui/windows/__pycache__/settings_window.cpython-314.pyc Binary files differindex cc9691b..f732521 100644 --- a/ui/windows/__pycache__/settings_window.cpython-314.pyc +++ b/ui/windows/__pycache__/settings_window.cpython-314.pyc diff --git a/ui/windows/__pycache__/signals_window.cpython-312.pyc b/ui/windows/__pycache__/signals_window.cpython-312.pyc Binary files differindex ec57533..9872798 100644 --- a/ui/windows/__pycache__/signals_window.cpython-312.pyc +++ b/ui/windows/__pycache__/signals_window.cpython-312.pyc diff --git a/ui/windows/__pycache__/signals_window.cpython-314.pyc b/ui/windows/__pycache__/signals_window.cpython-314.pyc Binary files differindex 224e31e..2027f59 100644 --- a/ui/windows/__pycache__/signals_window.cpython-314.pyc +++ b/ui/windows/__pycache__/signals_window.cpython-314.pyc 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() |
