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/channels_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/channels_window.py')
| -rw-r--r-- | ui/windows/channels_window.py | 105 |
1 files changed, 55 insertions, 50 deletions
diff --git a/ui/windows/channels_window.py b/ui/windows/channels_window.py index 87d7840..f581851 100644 --- a/ui/windows/channels_window.py +++ b/ui/windows/channels_window.py @@ -15,7 +15,7 @@ Classes: PipelineTab — splitter content (physical + virtual sections) ChannelPipelineBlock — collapsible filter+math stack for one physical channel ChannelPickerDialog — checkbox dialog for selecting channels to add - DerivedBlock — editor for one virtual/derived channel + ChannelBlock — editor for one virtual/derived channel FilterRow — single filter stage row inside a pipeline block SignalsListTab — unused; kept for profile-load compatibility """ @@ -35,7 +35,7 @@ from PyQt6.QtGui import QCloseEvent, QFont, QFontMetrics from devices.device_registry import DeviceRegistry from core.signal_processor import ( - SignalProcessor, ChannelPipeline, DerivedChannel, + SignalProcessor, ChannelPipeline, Channel, FilterBase, FILTER_CLASSES, ) from ui.windows.plot_window import _DragHandle, _DragReorderContainer @@ -60,8 +60,8 @@ def _channel_display_name(registry: DeviceRegistry, else: for dev in registry.all_instances(): if dev.info.device_id == dev_id: - for ch in dev.info.channels: - if ch.channel_id == ch_id: + for ch in dev.info.signals: + if ch.signal_id == ch_id: return ch.name if ch.name else ch_id return ch_id @@ -72,16 +72,16 @@ def _channel_combo(registry: DeviceRegistry, show_unit: bool = False) -> QComboBox: cb = QComboBox(); cb.setObjectName("channelPickerCb") for dev in registry.all_instances(): - for ch in dev.info.channels: + for ch in dev.info.signals: if not ch.enabled: continue - label = f"{ch.name} ({dev.info.device_id}/{ch.channel_id})" + label = f"Signal: {ch.name} ({dev.info.device_id}/{ch.signal_id})" if show_unit and ch.unit: label += f" [{ch.unit}]" - cb.addItem(label, userData=(dev.info.device_id, ch.channel_id)) + cb.addItem(label, userData=(dev.info.device_id, ch.signal_id)) if include_derived and processor: for dc in processor.get_derived(): - label = f"[derived] {dc.channel_id} ({dc.name})" + label = f"Channel: {dc.channel_id} ({dc.name})" if show_unit and dc.unit: label += f" [{dc.unit}]" cb.addItem(label, userData=("derived", dc.channel_id)) @@ -110,17 +110,17 @@ class ChannelPickerDialog(QDialog): any_available = False for dev in registry.all_instances(): - for ch in dev.info.channels: + for ch in dev.info.signals: if not ch.enabled: continue - if (dev.info.device_id, ch.channel_id) in already_shown: + if (dev.info.device_id, ch.signal_id) in already_shown: continue any_available = True - label = f"{ch.name} ({dev.info.device_id}/{ch.channel_id})" + label = f"{ch.name} ({dev.info.device_id}/{ch.signal_id})" if ch.unit: label += f" [{ch.unit}]" chk = QCheckBox(label) - self._checks.append((chk, dev.info.device_id, ch.channel_id)) + self._checks.append((chk, dev.info.device_id, ch.signal_id)) cl.addWidget(chk) if not any_available: @@ -237,7 +237,7 @@ class ChannelPipelineBlock(QFrame): self._unit = unit outer = QVBoxLayout(self); outer.setContentsMargins(0, 0, 0, 0); outer.setSpacing(0) - # Header (omitted when embedded in another block, e.g. DerivedBlock's + # Header (omitted when embedded in another block, e.g. ChannelBlock's # Filters section — the parent already provides a header/toggle) if self._show_header: hdr = QWidget(); hdr.setObjectName("plotBlockHeader"); hdr.setFixedHeight(32) @@ -405,6 +405,7 @@ class ChannelPipelineBlock(QFrame): class PipelineTab(QWidget): changed = pyqtSignal() derived_changed = pyqtSignal() + channel_renamed = pyqtSignal(str, str, str, str) # old_dev, old_id, new_dev, new_id def __init__(self, registry: DeviceRegistry, processor: SignalProcessor): super().__init__() @@ -412,7 +413,7 @@ class PipelineTab(QWidget): self.processor = processor self._shown: Set[Tuple[str, str]] = set() self._blocks: dict[Tuple[str, str], ChannelPipelineBlock] = {} - self._derived_blocks: List[DerivedBlock] = [] + self._derived_blocks: List[ChannelBlock] = [] self._color_idx = 0 self._build() @@ -494,9 +495,9 @@ class PipelineTab(QWidget): QTimer.singleShot(0, _init_split) for dev in self.registry.all_instances(): - for ch in dev.info.channels: + for ch in dev.info.signals: if ch.enabled: - self._add_block(dev.info.device_id, ch.channel_id, ch.name, ch.unit) + self._add_block(dev.info.device_id, ch.signal_id, ch.name, ch.unit) # ── Physical channel block management ────────────────────────────────────── @@ -516,7 +517,7 @@ class PipelineTab(QWidget): if key not in self._shown: dev = self.registry.get_instance(dev_id) if dev: - ch = dev.get_channel(ch_id) + ch = dev.get_signal(ch_id) if ch: self._add_block(dev_id, ch_id, ch.name, ch.unit) else: @@ -541,7 +542,7 @@ class PipelineTab(QWidget): if dev is None: self.on_device_removed(dev_id) return - valid = {ch.channel_id for ch in dev.info.channels} + valid = {ch.signal_id for ch in dev.info.signals} keys = [(d, c) for (d, c) in list(self._shown) if d == dev_id and c not in valid] for key in keys: @@ -560,7 +561,7 @@ class PipelineTab(QWidget): else: dev = self.registry.get_instance(dev_id) if dev: - ch = dev.get_channel(ch_id) + ch = dev.get_signal(ch_id) if ch and ch.enabled: self._add_block(dev_id, ch_id, ch_name, ch.unit) self._refresh_src_cb() @@ -580,13 +581,13 @@ class PipelineTab(QWidget): self._src_cb.clear() self._src_cb.insertItem(0, "Signal: none (empty channel)", userData=None) for dev in self.registry.all_instances(): - for ch in dev.info.channels: + for ch in dev.info.signals: if not ch.enabled: continue - label = f"{ch.name} ({dev.info.device_id}/{ch.channel_id})" - self._src_cb.addItem(label, userData=(dev.info.device_id, ch.channel_id)) + label = f"Signal: {ch.name} ({dev.info.device_id}/{ch.signal_id})" + self._src_cb.addItem(label, userData=(dev.info.device_id, ch.signal_id)) for dc in self.processor.get_derived(): - label = f"[derived] {dc.channel_id} ({dc.name})" + label = f"Channel: {dc.channel_id} ({dc.name})" self._src_cb.addItem(label, userData=("derived", dc.channel_id)) if current: for i in range(self._src_cb.count()): @@ -607,7 +608,7 @@ class PipelineTab(QWidget): dev = self.registry.get_instance(dev_id) if not dev: continue - ch = dev.get_channel(ch_id) + ch = dev.get_signal(ch_id) if not ch: continue self._add_block(dev_id, ch_id, ch.name, ch.unit) @@ -635,23 +636,27 @@ class PipelineTab(QWidget): src_name = f"Channel {len(self._derived_blocks) + 1}" name = _dedupe_name(src_name, existing_names) channel_id = _slug_id(name, existing_ids) - dc = DerivedChannel(channel_id=channel_id, name=name, + dc = Channel(channel_id=channel_id, name=name, kind="expression", color=color) blk = self._make_derived_block(dc) self._virt_inner.insertWidget(self._virt_inner.count() - 1, blk) if src: blk._add_src(src) - def _make_derived_block(self, dc: DerivedChannel) -> DerivedBlock: - blk = DerivedBlock(dc, self.registry, self.processor) + def _make_derived_block(self, dc: Channel) -> ChannelBlock: + blk = ChannelBlock(dc, self.registry, self.processor) blk.removed.connect(self._rm_virtual) blk.applied.connect(lambda _: self.derived_changed.emit()) - blk.id_renamed.connect(self.refresh_derived) + blk.id_renamed.connect(self._on_block_id_renamed) blk._drag_handle.set_drag_source(self, blk) self._derived_blocks.append(blk) return blk - def _rm_virtual(self, blk: DerivedBlock): + def _on_block_id_renamed(self, old_id: str, new_id: str): + self.refresh_derived() + self.channel_renamed.emit("derived", old_id, "derived", new_id) + + def _rm_virtual(self, blk: ChannelBlock): self.processor.remove_derived(blk.dc.channel_id) self._derived_blocks.remove(blk) self._virt_inner.removeWidget(blk); blk.deleteLater() @@ -659,7 +664,7 @@ class PipelineTab(QWidget): # ── Drag-to-reorder — see _DragReorderContainer in ui/windows/plot_window.py - def _index_of(self, block: DerivedBlock) -> int: + def _index_of(self, block: ChannelBlock) -> int: return self._derived_blocks.index(block) def _drop_index(self, y: float) -> int: @@ -708,7 +713,7 @@ class PipelineTab(QWidget): self.derived_changed.emit() def refresh_derived(self): - """Rebuild DerivedBlock widgets from processor state (called after profile load).""" + """Rebuild ChannelBlock widgets from processor state (called after profile load).""" for blk in self._derived_blocks: self._virt_inner.removeWidget(blk); blk.deleteLater() self._derived_blocks.clear() @@ -723,7 +728,7 @@ class PipelineTab(QWidget): # ══════════════════════════════════════════════════════════════════════════════ -# Derived editor for one DerivedChannel +# Derived editor for one Channel # ══════════════════════════════════════════════════════════════════════════════ _BUILTIN = ["none", "scale_offset", "derivative", "second_derivative", "power", "rms", "difference", "sum"] @@ -806,13 +811,17 @@ def _sub_xrefs(code: str, names: list) -> str: return re.sub(r'\bx\[(\d+)\]', repl, code) -class DerivedBlock(QFrame): +class ChannelBlock(QFrame): removed = pyqtSignal(object) - applied = pyqtSignal(object) # DerivedChannel - id_renamed = pyqtSignal() # this Channel's ID changed — other blocks' - # source pickers may now be stale - - def __init__(self, dc: DerivedChannel, registry: DeviceRegistry, + applied = pyqtSignal(object) # Channel + id_renamed = pyqtSignal(str, str) # old_id, new_id — this Channel's ID + # changed; other blocks' source + # pickers, and everything outside + # this processor referencing it + # (plot traces, controls), may now + # be stale — see propagate_rename() + + def __init__(self, dc: Channel, registry: DeviceRegistry, processor: SignalProcessor): super().__init__() self.dc = dc; self.registry = registry; self.processor = processor @@ -1192,12 +1201,8 @@ class DerivedBlock(QFrame): self._st.setStyleSheet("color:#ef4444;") return renamed = channel_id != self._orig_id + old_id = self._orig_id if renamed: - # Other channels sourced from this one under the old ID would - # otherwise keep pointing at a reference that no longer exists — - # fix their sources up front, carry the ring buffer + latest - # value over so the rename doesn't reset their plotted history. - self.processor.rename_derived(self._orig_id, channel_id) self._orig_id = channel_id sources = [cb.currentData() for cb in self._src_combos if cb.currentData()] if self.dc.kind in ("derivative", "second_derivative"): @@ -1215,11 +1220,11 @@ class DerivedBlock(QFrame): elif self.dc.kind in ("function", "custom_script"): self.dc.script = self._code.toPlainText() err = self.processor.add_derived(self.dc) - # The rename above already committed regardless of whether this - # channel's own expression/script compiles — other blocks' source - # pickers need to refresh either way. + # Emitted regardless of whether this channel's own expression/script + # compiles — propagate_rename() (via id_renamed) still needs to run + # so other channels' sources / plot traces / controls don't dangle. if renamed: - self.id_renamed.emit() + self.id_renamed.emit(old_id, channel_id) if err: self._st.setText(f"⚠ {err[:100]}") self._st.setStyleSheet("color:#ef4444;") @@ -1261,12 +1266,12 @@ class SignalsListTab(QWidget): if it.widget(): it.widget().deleteLater() for dev in self.registry.all_instances(): - for ch in dev.info.channels: + for ch in dev.info.signals: dev_id_cap = dev.info.device_id - ch_id_cap = ch.channel_id + ch_id_cap = ch.signal_id self._lay.addWidget(self._make_row( color = ch.color, - name = f"{ch.channel_id} — {ch.name}", + name = f"{ch.signal_id} — {ch.name}", unit = ch.unit, source = dev_id_cap, enabled = ch.enabled, |
