summaryrefslogtreecommitdiff
path: root/core/signal_processor.py
diff options
context:
space:
mode:
authorChristian Kolset <christian.kolset@gmail.com>2026-09-19 17:38:07 -0600
committerChristian Kolset <christian.kolset@gmail.com>2026-09-19 17:38:07 -0600
commitf3327d56d48d63b21ffc03a55fa5e7db753d1707 (patch)
treee19d4c7846dd4a434f8a18f82290597731b4a12c /core/signal_processor.py
parent00671a90bb51bfe485d98a928a456d55adbe1faf (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.py60
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: