diff options
Diffstat (limited to 'daq_system/ui/strip_chart.py')
| -rw-r--r-- | daq_system/ui/strip_chart.py | 140 |
1 files changed, 140 insertions, 0 deletions
diff --git a/daq_system/ui/strip_chart.py b/daq_system/ui/strip_chart.py new file mode 100644 index 0000000..46d73fa --- /dev/null +++ b/daq_system/ui/strip_chart.py @@ -0,0 +1,140 @@ +""" +ui/strip_chart.py + +Live scrolling strip chart using pyqtgraph. +Each channel gets its own colored trace. Window duration is adjustable. +""" + +from typing import Dict, List +import numpy as np + +from PyQt6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, + QComboBox, QDoubleSpinBox, QCheckBox, QScrollArea, + QPushButton, QSizePolicy +) +from PyQt6.QtCore import Qt, pyqtSlot + +try: + import pyqtgraph as pg + pg.setConfigOptions(antialias=True, background="#0d1117", foreground="#c9d1d9") + HAS_PG = True +except ImportError: + HAS_PG = False + +from devices.device_registry import DeviceRegistry +from core.acquisition import AcquisitionEngine + + +class StripChartWidget(QWidget): + def __init__(self, engine: AcquisitionEngine, registry: DeviceRegistry): + super().__init__() + self.engine = engine + self.registry = registry + self._plots: Dict[str, Dict] = {} # device_id -> {channel_id -> curve} + self._window_s = 30.0 + self._paused = False + self._build_ui() + self.refresh() + + def _build_ui(self): + layout = QVBoxLayout(self) + layout.setContentsMargins(4, 4, 4, 4) + layout.setSpacing(4) + + # Control bar + ctrl = QHBoxLayout() + ctrl.addWidget(QLabel("Window:")) + + self.window_spin = QDoubleSpinBox() + self.window_spin.setRange(1, 600) + self.window_spin.setValue(self._window_s) + self.window_spin.setSuffix(" s") + self.window_spin.valueChanged.connect(lambda v: setattr(self, "_window_s", v)) + ctrl.addWidget(self.window_spin) + + ctrl.addStretch() + + self.pause_btn = QPushButton("⏸ Pause") + self.pause_btn.setCheckable(True) + self.pause_btn.setObjectName("pauseButton") + self.pause_btn.toggled.connect(lambda c: setattr(self, "_paused", c)) + self.pause_btn.toggled.connect(lambda c: self.pause_btn.setText("▶ Resume" if c else "⏸ Pause")) + ctrl.addWidget(self.pause_btn) + + layout.addLayout(ctrl) + + if HAS_PG: + self._build_pg_chart(layout) + else: + layout.addWidget(QLabel( + "pyqtgraph not installed.\nRun: pip install pyqtgraph\n\nData is still being acquired.", + alignment=Qt.AlignmentFlag.AlignCenter + )) + + def _build_pg_chart(self, parent_layout): + self.plot_widget = pg.GraphicsLayoutWidget() + self.plot_widget.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding + ) + parent_layout.addWidget(self.plot_widget) + + def refresh(self): + if not HAS_PG: + return + self.plot_widget.clear() + self._plots.clear() + + devices = self.registry.all_instances() + num = len(devices) + if num == 0: + return + + for row_idx, dev in enumerate(devices): + plot = self.plot_widget.addPlot(row=row_idx, col=0) + plot.setLabel("left", f"{dev.info.icon} {dev.info.name}") + plot.showGrid(x=True, y=True, alpha=0.15) + plot.getAxis("bottom").setStyle(showValues=(row_idx == num - 1)) + + if row_idx < num - 1: + plot.getAxis("bottom").setHeight(0) + else: + plot.setLabel("bottom", "Time (s)") + + self._plots[dev.info.device_id] = {} + for ch in dev.info.channels: + if not ch.enabled: + continue + color = ch.color + pen = pg.mkPen(color=color, width=1.5) + curve = plot.plot([], [], pen=pen, name=ch.name) + self._plots[dev.info.device_id][ch.channel_id] = { + "curve": curve, + "plot": plot, + } + + @pyqtSlot(str, str, float, float) + def on_new_data(self, device_id: str, channel_id: str, timestamp: float, value: float): + if self._paused or not HAS_PG: + return + dev_plots = self._plots.get(device_id, {}) + entry = dev_plots.get(channel_id) + if entry is None: + return + + buf = self.engine.get_buffer(device_id, channel_id) + if buf is None or len(buf) < 2: + return + + ts, vs = buf.all() + ts = np.array(ts, dtype=np.float64) + vs = np.array(vs, dtype=np.float64) + + t_max = ts[-1] + t_min = t_max - self._window_s + mask = ts >= t_min + ts = ts[mask] + vs = vs[mask] + + entry["curve"].setData(ts, vs) + entry["plot"].setXRange(t_min, t_max, padding=0) |
