diff options
| author | Christian Kolset <christian.kolset@gmail.com> | 2026-09-19 17:38:07 -0600 |
|---|---|---|
| committer | Christian Kolset <christian.kolset@gmail.com> | 2026-09-19 17:38:07 -0600 |
| commit | f3327d56d48d63b21ffc03a55fa5e7db753d1707 (patch) | |
| tree | e19d4c7846dd4a434f8a18f82290597731b4a12c /ui/windows/config_window.py | |
| parent | 00671a90bb51bfe485d98a928a456d55adbe1faf (diff) | |
Redesign Signal/Channel naming and fix pin-remap breaking references
Arduino analog Signal ids used to be the pin name itself, so fixing a
wrong pin silently changed the id and orphaned any plot trace, control,
or filter pipeline pointing at it. Analog Signal ids are now positional
(ai0, ai1, ...) and decoupled from the underlying pin, matching how
digital I/O already worked.
More broadly, formalizes a Device -> Signal -> Channel -> Plot naming
model (SignalConfig for physical device channels, Channel for derived/
computed ones) and adds core/reference_registry.py to propagate any
Signal or Channel id rename through plot traces, controls, and filter
pipelines instead of leaving them dangling. Signal ids are now editable
in the Signals tab with the same propagation.
.labui profile schema bumped to v2.0 with a transparent migration for
old files, including remapping old pin-name Signal ids to the new
positional ones. Removed six confirmed-dead files (two legacy device
classes, four superseded top-level ui/ duplicates).
Diffstat (limited to 'ui/windows/config_window.py')
| -rw-r--r-- | ui/windows/config_window.py | 69 |
1 files changed, 58 insertions, 11 deletions
diff --git a/ui/windows/config_window.py b/ui/windows/config_window.py index 604b6ed..62789f1 100644 --- a/ui/windows/config_window.py +++ b/ui/windows/config_window.py @@ -28,9 +28,9 @@ from PyQt6.QtGui import QCloseEvent, QColor, QShowEvent from devices.device_registry import DeviceRegistry from core.acquisition import AcquisitionEngine -from core.signal_processor import SignalProcessor, DerivedChannel +from core.signal_processor import SignalProcessor, Channel -from ui.windows.devices_window import DeviceRow, ChannelsTab +from ui.windows.devices_window import DeviceRow, SignalsTab from ui.windows.channels_window import PipelineTab, _slug_id, _dedupe_name from ui.windows.plot_window import ( PlotWindow, LayoutConfig, LayoutCanvas, PaneBlock, @@ -43,8 +43,8 @@ from ui.namespace_panel import NamespacePanel # Enhanced signals table — adds Dir badge and pipeline ⚙ button # ══════════════════════════════════════════════════════════════════════════════ -class _SignalsTab(ChannelsTab): - """ChannelsTab with a Dir (READ/WRITE) badge. Operations (filters/calibration/math) +class _SignalsTab(SignalsTab): + """SignalsTab with a Dir (READ/WRITE) badge. Operations (filters/calibration/math) are configured per Channel in the Channels pane, not here.""" _C_DEVICE = 0 @@ -58,6 +58,7 @@ class _SignalsTab(ChannelsTab): _C_MKCH = 8 # "+ Channel" button channel_created = pyqtSignal() + channel_renamed = pyqtSignal(str, str, str, str) # old_dev, old_id, new_dev, new_id def __init__(self, registry: DeviceRegistry, processor: SignalProcessor): self._processor = processor @@ -91,7 +92,7 @@ class _SignalsTab(ChannelsTab): self._table.setRowCount(0) row = 0 for dev in self.registry.all_instances(): - for ch in dev.info.channels: + for ch in dev.info.signals: self._table.insertRow(row) dev_item = QTableWidgetItem(f"{dev.info.icon} {dev.info.device_id}") @@ -99,8 +100,8 @@ class _SignalsTab(ChannelsTab): dev_item.setForeground(QColor("#64748b")) 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 = QTableWidgetItem(ch.signal_id) + ch_item.setData(Qt.ItemDataRole.UserRole, (dev.info.device_id, ch.signal_id)) ch_item.setForeground(QColor(ch.color)) self._table.setItem(row, self._C_CH_ID, ch_item) @@ -122,7 +123,7 @@ class _SignalsTab(ChannelsTab): chk.setEnabled(False) else: dev_id_cap = dev.info.device_id - ch_id_cap = ch.channel_id + ch_id_cap = ch.signal_id chk.toggled.connect( lambda checked, d=dev_id_cap, c=ch_id_cap: self._on_enabled(d, c, checked) @@ -131,7 +132,7 @@ class _SignalsTab(ChannelsTab): self._table.setCellWidget(row, self._C_ENABLED, chk_w) name_item = QTableWidgetItem(ch.name) - name_item.setData(Qt.ItemDataRole.UserRole, (dev.info.device_id, ch.channel_id)) + name_item.setData(Qt.ItemDataRole.UserRole, (dev.info.device_id, ch.signal_id)) self._table.setItem(row, self._C_NAME, name_item) self._table.setItem(row, self._C_UNIT, QTableWidgetItem(ch.unit)) @@ -148,7 +149,7 @@ class _SignalsTab(ChannelsTab): mkch_btn.setFixedSize(24, 22) mkch_btn.setToolTip("Create a Channel from this Signal") d_cap, c_cap, n_cap, u_cap, col_cap = ( - dev.info.device_id, ch.channel_id, ch.name, ch.unit, ch.color + dev.info.device_id, ch.signal_id, ch.name, ch.unit, ch.color ) mkch_btn.clicked.connect( lambda _=False, d=d_cap, c=c_cap, n=n_cap, u=u_cap, col=col_cap: @@ -161,6 +162,49 @@ class _SignalsTab(ChannelsTab): self._table.blockSignals(False) + def _on_cell_changed(self, row: int, col: int): + if col == self._C_CH_ID: + self._on_id_edited(row) + return + super()._on_cell_changed(row, col) + + def _on_id_edited(self, row: int): + """Signal IDs are stable/backend-facing by default (see + devices/base_device.py::SignalConfig) but user-editable here, same + as a derived Channel's ID — renaming propagates through + reference_registry.propagate_rename() (wired via channel_renamed) + so plot traces / controls / pipelines pointing at the old id follow + it instead of going dangling (the bug that motivated this feature: + an Arduino analog pin remap used to change the id outright).""" + id_item = self._table.item(row, self._C_CH_ID) + if id_item is None: + return + ref = id_item.data(Qt.ItemDataRole.UserRole) + if ref is None: + return + dev_id, old_id = ref + new_id = id_item.text().strip() + if not new_id or new_id == old_id: + self._table.blockSignals(True) + id_item.setText(old_id) + self._table.blockSignals(False) + return + dev = self.registry.get_instance(dev_id) + if not dev or any(s.signal_id == new_id for s in dev.info.signals if s.signal_id != old_id): + self._table.blockSignals(True) + id_item.setText(old_id) + self._table.blockSignals(False) + return + ch = dev.get_signal(old_id) + if not ch: + return + ch.signal_id = new_id + id_item.setData(Qt.ItemDataRole.UserRole, (dev_id, new_id)) + name_item = self._table.item(row, self._C_NAME) + if name_item is not None: + name_item.setData(Qt.ItemDataRole.UserRole, (dev_id, new_id)) + self.channel_renamed.emit(dev_id, old_id, dev_id, new_id) + def _on_create_channel(self, dev_id: str, ch_id: str, ch_name: str, unit: str, color: str): """Create a passthrough Channel (kind="none") from this Signal, named after it — the "1 Signal -> 1 Channel" quick path. No-ops if @@ -173,7 +217,7 @@ class _SignalsTab(ChannelsTab): existing_ids = {d.channel_id for d in existing} name = _dedupe_name(ch_name, existing_names) channel_id = _slug_id(name, existing_ids) - dc = DerivedChannel(channel_id=channel_id, name=name, kind="none", + dc = Channel(channel_id=channel_id, name=name, kind="none", sources=[(dev_id, ch_id)], source_names=[ch_name], unit=unit, color=color or "#00d4ff") err = self._processor.add_derived(dc) @@ -275,6 +319,7 @@ class ConfigWindow(QWidget): channel_unit_changed = pyqtSignal(str, str, str) pipeline_changed = pyqtSignal() derived_changed = pyqtSignal() + channel_renamed = pyqtSignal(str, str, str, str) # old_dev, old_id, new_dev, new_id layout_applied = pyqtSignal(object) closed = pyqtSignal() @@ -360,6 +405,7 @@ class ConfigWindow(QWidget): self._signals_tab.channel_visibility_changed.connect(self._on_sig_visibility) self._signals_tab.channel_name_changed.connect(self._on_sig_name) self._signals_tab.channel_unit_changed.connect(self._on_sig_unit) + self._signals_tab.channel_renamed.connect(self.channel_renamed) sig_l.addWidget(self._signals_tab, 1) splitter.addWidget(sig_w) @@ -468,6 +514,7 @@ class ConfigWindow(QWidget): self._pipeline_tab = PipelineTab(self.registry, self.processor) self._pipeline_tab.changed.connect(self.pipeline_changed) self._pipeline_tab.derived_changed.connect(self.derived_changed) + self._pipeline_tab.channel_renamed.connect(self.channel_renamed) self._pipeline_tab.refresh_derived() # load derived channels already in processor # (e.g. window built fresh after profile load) inner_splitters = self._pipeline_tab.findChildren(QSplitter) |
