summaryrefslogtreecommitdiff
path: root/ui/strip_chart.py
diff options
context:
space:
mode:
authorChristian Kolset <ckolset@colostate.edu>2026-08-03 15:32:56 -0600
committerChristian Kolset <ckolset@colostate.edu>2026-08-03 15:32:56 -0600
commit9bcad5d3af258eb72bc9c8ac87e3f1713e1b8b80 (patch)
treebd2b07ae968e3b422c536fab21f57a5b660d4b98 /ui/strip_chart.py
parent68a0c5ba06f471d1d63aedfb61d708faa4b76565 (diff)
Fix acquisition pipeline stutter with many channels
Batches per-tick device readings into one signal emit instead of one per channel, evaluates derived channels once per tick instead of once per raw sample, and makes ChannelBuffer.window() cost scale with the window size instead of total buffer history. Strip chart X-range now scrolls on an independent ~30fps timer instead of only snapping when new data lands, which was the main visible cause of the stutter. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'ui/strip_chart.py')
-rw-r--r--ui/strip_chart.py54
1 files changed, 45 insertions, 9 deletions
diff --git a/ui/strip_chart.py b/ui/strip_chart.py
index 5a1d1e9..a212e5c 100644
--- a/ui/strip_chart.py
+++ b/ui/strip_chart.py
@@ -14,7 +14,7 @@ from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QDoubleSpinBox, QPushButton, QSizePolicy,
)
-from PyQt6.QtCore import Qt, pyqtSlot
+from PyQt6.QtCore import Qt, pyqtSlot, QTimer
from PyQt6.QtGui import QFont
try:
@@ -52,6 +52,22 @@ _STYLES = {
}
+def _windowed(times_dq, values_dq, seconds: float):
+ """Last `seconds` of a (times, values) deque pair, walking in from the
+ newest sample only — same trick as ChannelBuffer.window(), needed here
+ for derived-channel buffers which aren't wrapped in ChannelBuffer."""
+ if not times_dq:
+ return [], []
+ cutoff = times_dq[-1] - seconds
+ out_t, out_v = [], []
+ for t, v in zip(reversed(times_dq), reversed(values_dq)):
+ if t < cutoff:
+ break
+ out_t.append(t); out_v.append(v)
+ out_t.reverse(); out_v.reverse()
+ return out_t, out_v
+
+
class StripChartWidget(QWidget):
def __init__(self, engine: AcquisitionEngine, registry: DeviceRegistry,
processor: SignalProcessor):
@@ -63,9 +79,18 @@ class StripChartWidget(QWidget):
self._cfg: Optional[LayoutConfig] = None
# (dev_id, ch_id) → [{curve, plot, spec}]
self._curves: Dict[Tuple, List[dict]] = {}
+ # Time-axis plots, scrolled continuously by _scroll_timer rather than
+ # only when a new sample lands — decouples the visible scroll rate
+ # from the (much slower) poll rate so it doesn't look stepped.
+ self._time_plots: List = []
self._build()
self.apply_layout(build_default_layout(registry, processor))
+ self._scroll_timer = QTimer(self)
+ self._scroll_timer.setInterval(33) # ~30 fps
+ self._scroll_timer.timeout.connect(self._on_scroll_tick)
+ self._scroll_timer.start()
+
def _build(self):
lay = QVBoxLayout(self); lay.setContentsMargins(6,6,6,4); lay.setSpacing(4)
ctrl = QHBoxLayout()
@@ -89,7 +114,7 @@ class StripChartWidget(QWidget):
self._cfg = cfg
self._win.setValue(cfg.time_window_s)
if not _HAS_PG: return
- self._gw.clear(); self._curves.clear()
+ self._gw.clear(); self._curves.clear(); self._time_plots.clear()
if not cfg.panes: return
mf = QFont("IBM Plex Mono", 8)
@@ -130,6 +155,7 @@ class StripChartWidget(QWidget):
plot.setXLink(ref_plot)
if spec.x_source == "time":
ref_plot = plot
+ self._time_plots.append(plot)
if cfg.show_legend:
t = _current_theme
@@ -182,7 +208,8 @@ class StripChartWidget(QWidget):
if device_id == "derived":
bufs = self.processor.get_derived_buffer(channel_id)
if bufs is None or len(bufs[0]) < 2: return
- ts_l = list(bufs[0]); vs_l = list(bufs[1])
+ ts_l, vs_l = _windowed(bufs[0], bufs[1], window)
+ if not ts_l: return
else:
buf = self.engine.get_buffer(device_id, channel_id)
if buf is None or len(buf) < 2: return
@@ -193,13 +220,11 @@ class StripChartWidget(QWidget):
spec = e["pane"]
# Determine X axis data
if spec.x_source == "time":
- cutoff = ts_l[-1] - window if ts_l else 0
- idx = next((i for i,t in enumerate(ts_l) if t >= cutoff), 0)
- x_arr = np.array(ts_l[idx:], dtype=np.float64)
- y_arr = np.array(vs_l[idx:], dtype=np.float64)
+ x_arr = np.array(ts_l, dtype=np.float64)
+ y_arr = np.array(vs_l, dtype=np.float64)
e["curve"].setData(x_arr, y_arr)
- if x_arr.size:
- e["plot"].setXRange(x_arr[-1]-window, x_arr[-1], padding=0)
+ # X-range scrolling is handled by _scroll_timer, continuously —
+ # not here, so the window doesn't jump only on data arrival.
else:
# X = another channel's processed values
parts = spec.x_source.split("/")
@@ -227,3 +252,14 @@ class StripChartWidget(QWidget):
def _on_pause(self, c: bool):
self._paused = c
self._pause.setText("▶ Resume" if c else "⏸ Pause")
+
+ def _on_scroll_tick(self):
+ """Scroll time-axis plots at a steady UI rate, independent of the
+ (much slower) poll rate — keeps the window sliding smoothly instead
+ of jumping once per data tick."""
+ if self._paused or not _HAS_PG or not self._cfg or not self.engine.is_running():
+ return
+ now = self.engine.elapsed_now()
+ window = self._cfg.time_window_s
+ for plot in self._time_plots:
+ plot.setXRange(now - window, now, padding=0)