summaryrefslogtreecommitdiff
path: root/ui/windows
diff options
context:
space:
mode:
Diffstat (limited to 'ui/windows')
-rw-r--r--ui/windows/__pycache__/__init__.cpython-312.pycbin157 -> 148 bytes
-rw-r--r--ui/windows/__pycache__/devices_window.cpython-312.pycbin22041 -> 24793 bytes
-rw-r--r--ui/windows/__pycache__/devices_window.cpython-314.pycbin23231 -> 26153 bytes
-rw-r--r--ui/windows/__pycache__/plot_window.cpython-312.pycbin44685 -> 44632 bytes
-rw-r--r--ui/windows/__pycache__/plot_window.cpython-314.pycbin46111 -> 46224 bytes
-rw-r--r--ui/windows/__pycache__/settings_window.cpython-312.pycbin15935 -> 15906 bytes
-rw-r--r--ui/windows/__pycache__/settings_window.cpython-314.pycbin16392 -> 13698 bytes
-rw-r--r--ui/windows/__pycache__/signals_window.cpython-312.pycbin54087 -> 56462 bytes
-rw-r--r--ui/windows/__pycache__/signals_window.cpython-314.pycbin53420 -> 59737 bytes
-rw-r--r--ui/windows/devices_window.py96
-rw-r--r--ui/windows/plot_window.py4
-rw-r--r--ui/windows/settings_window.py44
-rw-r--r--ui/windows/signals_window.py61
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
index 1454fd8..66495c5 100644
--- a/ui/windows/__pycache__/__init__.cpython-312.pyc
+++ b/ui/windows/__pycache__/__init__.cpython-312.pyc
Binary files 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
--- a/ui/windows/__pycache__/devices_window.cpython-312.pyc
+++ b/ui/windows/__pycache__/devices_window.cpython-312.pyc
Binary files 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
--- a/ui/windows/__pycache__/devices_window.cpython-314.pyc
+++ b/ui/windows/__pycache__/devices_window.cpython-314.pyc
Binary files 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
--- a/ui/windows/__pycache__/plot_window.cpython-312.pyc
+++ b/ui/windows/__pycache__/plot_window.cpython-312.pyc
Binary files 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
--- a/ui/windows/__pycache__/plot_window.cpython-314.pyc
+++ b/ui/windows/__pycache__/plot_window.cpython-314.pyc
Binary files 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
--- a/ui/windows/__pycache__/settings_window.cpython-312.pyc
+++ b/ui/windows/__pycache__/settings_window.cpython-312.pyc
Binary files 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
--- a/ui/windows/__pycache__/settings_window.cpython-314.pyc
+++ b/ui/windows/__pycache__/settings_window.cpython-314.pyc
Binary files 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
--- a/ui/windows/__pycache__/signals_window.cpython-312.pyc
+++ b/ui/windows/__pycache__/signals_window.cpython-312.pyc
Binary files 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
--- a/ui/windows/__pycache__/signals_window.cpython-314.pyc
+++ b/ui/windows/__pycache__/signals_window.cpython-314.pyc
Binary files 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()