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 /core/signal_processor.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 'core/signal_processor.py')
| -rw-r--r-- | core/signal_processor.py | 60 |
1 files changed, 32 insertions, 28 deletions
diff --git a/core/signal_processor.py b/core/signal_processor.py index 4781e91..da615b6 100644 --- a/core/signal_processor.py +++ b/core/signal_processor.py @@ -230,7 +230,7 @@ def _make_src_names(sources: List[Tuple[str, str]], # ══════════════════════════════════════════════════════════════════════════════ @dataclass -class DerivedChannel: +class Channel: """ A virtual channel computed from one or more physical channels. @@ -393,7 +393,7 @@ class SignalProcessor(QObject): def __init__(self): super().__init__() self._pipelines: Dict[Tuple[str, str], ChannelPipeline] = {} - self._derived: List[DerivedChannel] = [] + self._derived: List[Channel] = [] self._lock = threading.Lock() # Latest raw values cache for derived evaluation @@ -421,7 +421,7 @@ class SignalProcessor(QObject): # ── Derived channel management ──────────────────────────────────────── - def add_derived(self, dc: DerivedChannel) -> Optional[str]: + def add_derived(self, dc: Channel) -> Optional[str]: """Add a derived channel. Returns compile error string or None.""" err = dc.compile() if err: @@ -437,36 +437,40 @@ class SignalProcessor(QObject): self._derived = [d for d in self._derived if d.channel_id != channel_id] self._derived_bufs.pop(channel_id, None) - def rename_derived(self, old_id: str, new_id: str): - """Propagate a Channel ID rename to every other derived channel's - sources — otherwise anything chained from it keeps a dangling - ("derived", old_id) reference (SignalProcessor._evaluate_derived - looks sources up by exact match) and silently stops updating. - Also carries the ring buffer and latest-value cache over under the - new key so the rename doesn't reset plotted history or blank the - Value column for a poll cycle. Does NOT touch source_names or - expression/script text — those are display labels / user-authored - code, out of scope for an automatic rewrite.""" - if old_id == new_id: + def rename_reference(self, old_ref: Tuple[str, str], new_ref: Tuple[str, str]): + """Propagate a (device_id, channel_id) reference rename to everything + this processor holds one of — otherwise anything reading it keeps a + dangling reference (_evaluate_derived/_process_one look sources and + pipelines up by exact match) and silently stops updating. Covers + both a physical Signal rename and a derived Channel rename with the + same code path — the renamed item's own identity (Channel. + channel_id, or a device's SignalConfig) is the caller's job; this + only fixes up *references to* old_ref held elsewhere. + Also carries the latest-value cache, and — when old_ref is itself a + derived Channel (device_id == "derived") — its ring buffer, over + under the new key so the rename doesn't reset plotted history or + blank the Value column for a poll cycle. Does NOT touch + source_names or expression/script text — those are display labels / + user-authored code, out of scope for an automatic rewrite.""" + if old_ref == new_ref: return with self._lock: + if old_ref in self._pipelines: + self._pipelines[new_ref] = self._pipelines.pop(old_ref) + for dc in self._derived: - if dc.channel_id == new_id: - continue - new_sources = [ - ("derived", new_id) if d == "derived" and c == old_id else (d, c) - for d, c in dc.sources - ] + new_sources = [new_ref if s == old_ref else s for s in dc.sources] if new_sources != dc.sources: dc.sources = new_sources - buf = self._derived_bufs.pop(old_id, None) - if buf is not None: - self._derived_bufs[new_id] = buf - - entry = self._latest.pop(("derived", old_id), None) + entry = self._latest.pop(old_ref, None) if entry is not None: - self._latest[("derived", new_id)] = entry + self._latest[new_ref] = entry + + if old_ref[0] == "derived": + buf = self._derived_bufs.pop(old_ref[1], None) + if buf is not None: + self._derived_bufs[new_ref[1]] = buf def reorder_derived(self, src_idx: int, dst_idx: int): """Move the derived channel at src_idx to dst_idx — list order is @@ -478,7 +482,7 @@ class SignalProcessor(QObject): dc = self._derived.pop(src_idx) self._derived.insert(dst_idx, dc) - def get_derived(self) -> List[DerivedChannel]: + def get_derived(self) -> List[Channel]: return list(self._derived) def get_derived_buffer(self, channel_id: str): @@ -575,7 +579,7 @@ class SignalProcessor(QObject): self.derived_error.emit(dc.channel_id, traceback.format_exc(limit=3)) - def _compute_derived(self, dc: DerivedChannel, inputs: List[float], + def _compute_derived(self, dc: Channel, inputs: List[float], t: float, latest: dict) -> Optional[float]: if dc.kind in ("expression", "function", "custom_script"): if dc._fn is None: |
