diff options
Diffstat (limited to 'daq_system/ui')
| -rw-r--r-- | daq_system/ui/__pycache__/alarm_panel.cpython-312.pyc | bin | 5462 -> 0 bytes | |||
| -rw-r--r-- | daq_system/ui/__pycache__/config_dialog.cpython-312.pyc | bin | 9345 -> 0 bytes | |||
| -rw-r--r-- | daq_system/ui/__pycache__/device_panel.cpython-312.pyc | bin | 7943 -> 0 bytes | |||
| -rw-r--r-- | daq_system/ui/__pycache__/main_window.cpython-312.pyc | bin | 13127 -> 0 bytes | |||
| -rw-r--r-- | daq_system/ui/__pycache__/readout_panel.cpython-312.pyc | bin | 8378 -> 0 bytes | |||
| -rw-r--r-- | daq_system/ui/__pycache__/strip_chart.cpython-312.pyc | bin | 8328 -> 0 bytes | |||
| -rw-r--r-- | daq_system/ui/add_device_dialog.py | 7 | ||||
| -rw-r--r-- | daq_system/ui/alarm_panel.py | 85 | ||||
| -rw-r--r-- | daq_system/ui/config_dialog.py | 174 | ||||
| -rw-r--r-- | daq_system/ui/device_panel.py | 127 | ||||
| -rw-r--r-- | daq_system/ui/main_window.py | 228 | ||||
| -rw-r--r-- | daq_system/ui/readout_panel.py | 128 | ||||
| -rw-r--r-- | daq_system/ui/strip_chart.py | 140 |
13 files changed, 0 insertions, 889 deletions
diff --git a/daq_system/ui/__pycache__/alarm_panel.cpython-312.pyc b/daq_system/ui/__pycache__/alarm_panel.cpython-312.pyc Binary files differdeleted file mode 100644 index 7f2776c..0000000 --- a/daq_system/ui/__pycache__/alarm_panel.cpython-312.pyc +++ /dev/null diff --git a/daq_system/ui/__pycache__/config_dialog.cpython-312.pyc b/daq_system/ui/__pycache__/config_dialog.cpython-312.pyc Binary files differdeleted file mode 100644 index d62887f..0000000 --- a/daq_system/ui/__pycache__/config_dialog.cpython-312.pyc +++ /dev/null diff --git a/daq_system/ui/__pycache__/device_panel.cpython-312.pyc b/daq_system/ui/__pycache__/device_panel.cpython-312.pyc Binary files differdeleted file mode 100644 index 0616690..0000000 --- a/daq_system/ui/__pycache__/device_panel.cpython-312.pyc +++ /dev/null diff --git a/daq_system/ui/__pycache__/main_window.cpython-312.pyc b/daq_system/ui/__pycache__/main_window.cpython-312.pyc Binary files differdeleted file mode 100644 index d6af385..0000000 --- a/daq_system/ui/__pycache__/main_window.cpython-312.pyc +++ /dev/null diff --git a/daq_system/ui/__pycache__/readout_panel.cpython-312.pyc b/daq_system/ui/__pycache__/readout_panel.cpython-312.pyc Binary files differdeleted file mode 100644 index 47d94da..0000000 --- a/daq_system/ui/__pycache__/readout_panel.cpython-312.pyc +++ /dev/null diff --git a/daq_system/ui/__pycache__/strip_chart.cpython-312.pyc b/daq_system/ui/__pycache__/strip_chart.cpython-312.pyc Binary files differdeleted file mode 100644 index 95dc075..0000000 --- a/daq_system/ui/__pycache__/strip_chart.cpython-312.pyc +++ /dev/null diff --git a/daq_system/ui/add_device_dialog.py b/daq_system/ui/add_device_dialog.py deleted file mode 100644 index 8851c99..0000000 --- a/daq_system/ui/add_device_dialog.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -ui/add_device_dialog.py - -Re-exported from config_dialog for import convenience. -""" - -from ui.config_dialog import AddDeviceDialog # noqa: F401 diff --git a/daq_system/ui/alarm_panel.py b/daq_system/ui/alarm_panel.py deleted file mode 100644 index 590d785..0000000 --- a/daq_system/ui/alarm_panel.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -ui/alarm_panel.py — Alarm event log panel. -""" - -from datetime import datetime -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QScrollArea, QFrame, QPushButton -) -from PyQt6.QtCore import Qt - - -class AlarmEntry(QFrame): - def __init__(self, device_id, channel_id, value, kind, timestamp): - super().__init__() - self.setObjectName("alarmEntry") - color = "#f85149" if kind == "high" else "#d29922" - self.setStyleSheet(f"border-left: 3px solid {color};") - layout = QHBoxLayout(self) - layout.setContentsMargins(8, 4, 8, 4) - icon = "▲" if kind == "high" else "▼" - msg = QLabel(f"{icon} {device_id} / {channel_id} = {value:.3f} [{kind.upper()}]") - msg.setObjectName("alarmMsg") - ts_lbl = QLabel(timestamp) - ts_lbl.setObjectName("alarmTs") - layout.addWidget(msg) - layout.addStretch() - layout.addWidget(ts_lbl) - - -class AlarmPanel(QWidget): - def __init__(self): - super().__init__() - self._build_ui() - - def _build_ui(self): - layout = QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - hdr_row = QHBoxLayout() - hdr = QLabel(" ALARMS") - hdr.setObjectName("panelHeader") - hdr_row.addWidget(hdr) - hdr_row.addStretch() - clr = QPushButton("Clear") - clr.setObjectName("clearAlarmsBtn") - clr.clicked.connect(self.clear_alarms) - hdr_row.addWidget(clr) - hdr_widget = QWidget() - hdr_widget.setLayout(hdr_row) - hdr_widget.setObjectName("alarmHeaderWidget") - layout.addWidget(hdr_widget) - - scroll = QScrollArea() - scroll.setWidgetResizable(True) - self._container = QWidget() - self._inner = QVBoxLayout(self._container) - self._inner.setContentsMargins(4, 4, 4, 4) - self._inner.setSpacing(3) - self._inner.addStretch() - scroll.setWidget(self._container) - layout.addWidget(scroll) - - self._scroll = scroll - self._count = 0 - - def add_alarm(self, device_id, channel_id, value, kind): - ts = datetime.now().strftime("%H:%M:%S") - entry = AlarmEntry(device_id, channel_id, value, kind, ts) - self._inner.insertWidget(0, entry) - self._count += 1 - # Keep last 200 - if self._count > 200: - item = self._inner.takeAt(self._inner.count() - 2) - if item and item.widget(): - item.widget().deleteLater() - self._count -= 1 - - def clear_alarms(self): - while self._inner.count() > 1: - item = self._inner.takeAt(0) - if item and item.widget(): - item.widget().deleteLater() - self._count = 0 diff --git a/daq_system/ui/config_dialog.py b/daq_system/ui/config_dialog.py deleted file mode 100644 index a7b0cdf..0000000 --- a/daq_system/ui/config_dialog.py +++ /dev/null @@ -1,174 +0,0 @@ -""" -ui/config_dialog.py — Per-device configuration dialog. -""" - -from PyQt6.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QTabWidget, QWidget, QFormLayout, - QLineEdit, QDoubleSpinBox, QCheckBox, QScrollArea -) -from PyQt6.QtCore import Qt - - -class DeviceConfigDialog(QDialog): - def __init__(self, device, parent=None): - super().__init__(parent) - self.device = device - self.setWindowTitle(f"Configure: {device.info.name} [{device.info.device_id}]") - self.setMinimumSize(500, 400) - self._build() - - def _build(self): - layout = QVBoxLayout(self) - - tabs = QTabWidget() - - # Tab 1: Device-specific config widget - dev_tab = QScrollArea() - dev_tab.setWidgetResizable(True) - dev_tab.setWidget(self.device.get_config_widget()) - tabs.addTab(dev_tab, "Device Settings") - - # Tab 2: Channel config - ch_tab = self._build_channel_tab() - tabs.addTab(ch_tab, "Channels") - - layout.addWidget(tabs) - - # Buttons - btn_row = QHBoxLayout() - btn_row.addStretch() - ok = QPushButton("Close") - ok.setDefault(True) - ok.clicked.connect(self.accept) - btn_row.addWidget(ok) - layout.addLayout(btn_row) - - def _build_channel_tab(self): - w = QWidget() - layout = QVBoxLayout(w) - for ch in self.device.info.channels: - grp_layout = QFormLayout() - name_edit = QLineEdit(ch.name) - unit_edit = QLineEdit(ch.unit) - en_check = QCheckBox() - en_check.setChecked(ch.enabled) - - lo_spin = QDoubleSpinBox() - lo_spin.setRange(-1e9, 1e9) - lo_spin.setValue(ch.alarm_low or 0.0) - - hi_spin = QDoubleSpinBox() - hi_spin.setRange(-1e9, 1e9) - hi_spin.setValue(ch.alarm_high or 100.0) - - grp_layout.addRow(f"[{ch.channel_id}] Name:", name_edit) - grp_layout.addRow("Unit:", unit_edit) - grp_layout.addRow("Enabled:", en_check) - grp_layout.addRow("Alarm Low:", lo_spin) - grp_layout.addRow("Alarm High:", hi_spin) - - def make_apply(c, ne, ue, ec, ls, hs): - def apply(): - c.name = ne.text() - c.unit = ue.text() - c.enabled = ec.isChecked() - c.alarm_low = ls.value() - c.alarm_high = hs.value() - return apply - - apply_btn = QPushButton("Apply") - apply_btn.clicked.connect(make_apply(ch, name_edit, unit_edit, en_check, lo_spin, hi_spin)) - grp_layout.addRow(apply_btn) - layout.addLayout(grp_layout) - - layout.addStretch() - return w - - -# ------------------------------------------------------------------ # -# Add Device Dialog # -# ------------------------------------------------------------------ # - -""" -ui/add_device_dialog.py — Dialog for adding a new device to the system. -""" - -from PyQt6.QtWidgets import ( - QDialog, QVBoxLayout, QFormLayout, QComboBox, - QLineEdit, QCheckBox, QSpinBox, QPushButton, QHBoxLayout, QLabel -) - -from devices.device_registry import DeviceRegistry -from devices.analog_input import AnalogInputDevice -from devices.digital_io import DigitalIODevice -from devices.temperature import TemperatureDevice -from devices.serial_device import SerialDevice - - -DEVICE_CONSTRUCTORS = { - "Analog Input": AnalogInputDevice, - "Digital I/O": DigitalIODevice, - "Temperature": TemperatureDevice, - "Serial / UART": SerialDevice, -} - - -class AddDeviceDialog(QDialog): - def __init__(self, registry: DeviceRegistry, parent=None): - super().__init__(parent) - self.registry = registry - self.created_device = None - self.setWindowTitle("Add Device") - self.setMinimumWidth(340) - self._build() - - def _build(self): - layout = QVBoxLayout(self) - form = QFormLayout() - - self.type_cb = QComboBox() - self.type_cb.addItems(list(DEVICE_CONSTRUCTORS.keys())) - form.addRow("Device Type:", self.type_cb) - - self.id_edit = QLineEdit() - self.id_edit.setPlaceholderText("e.g. ai_1") - form.addRow("Device ID:", self.id_edit) - - self.ch_spin = QSpinBox() - self.ch_spin.setRange(1, 16) - self.ch_spin.setValue(4) - form.addRow("# Channels:", self.ch_spin) - - self.sim_check = QCheckBox("Simulation Mode") - self.sim_check.setChecked(True) - form.addRow(self.sim_check) - - layout.addLayout(form) - - btns = QHBoxLayout() - btns.addStretch() - cancel = QPushButton("Cancel") - cancel.clicked.connect(self.reject) - add = QPushButton("Add") - add.setDefault(True) - add.clicked.connect(self._on_add) - btns.addWidget(cancel) - btns.addWidget(add) - layout.addLayout(btns) - - def _on_add(self): - dev_type = self.type_cb.currentText() - dev_id = self.id_edit.text().strip() or f"dev_{len(self.registry)}" - cls = DEVICE_CONSTRUCTORS[dev_type] - try: - dev = cls( - device_id=dev_id, - num_channels=self.ch_spin.value(), - simulate=self.sim_check.isChecked() - ) - self.created_device = dev - self.accept() - except Exception as e: - from PyQt6.QtWidgets import QMessageBox - QMessageBox.critical(self, "Error", str(e)) diff --git a/daq_system/ui/device_panel.py b/daq_system/ui/device_panel.py deleted file mode 100644 index 9b6c7e4..0000000 --- a/daq_system/ui/device_panel.py +++ /dev/null @@ -1,127 +0,0 @@ -""" -ui/device_panel.py - -Left sidebar showing all registered devices with status indicators. -""" - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QLabel, QPushButton, - QHBoxLayout, QScrollArea, QFrame -) -from PyQt6.QtCore import Qt, pyqtSignal - -from devices.base_device import DeviceStatus -from devices.device_registry import DeviceRegistry -from core.acquisition import AcquisitionEngine - - -class DeviceCard(QFrame): - config_clicked = pyqtSignal(str) - toggle_clicked = pyqtSignal(str, bool) - - STATUS_COLORS = { - DeviceStatus.CONNECTED: "#3fb950", - DeviceStatus.SIMULATED: "#58a6ff", - DeviceStatus.DISCONNECTED: "#8b949e", - DeviceStatus.CONNECTING: "#d29922", - DeviceStatus.ERROR: "#f85149", - } - - def __init__(self, device, parent=None): - super().__init__(parent) - self.device = device - self.setObjectName("deviceCard") - self._build() - - def _build(self): - layout = QVBoxLayout(self) - layout.setContentsMargins(8, 8, 8, 8) - layout.setSpacing(4) - - # Header row - hdr = QHBoxLayout() - icon = QLabel(self.device.info.icon) - icon.setObjectName("deviceIcon") - hdr.addWidget(icon) - - name = QLabel(self.device.info.name) - name.setObjectName("deviceName") - hdr.addWidget(name) - hdr.addStretch() - - color = self.STATUS_COLORS.get(self.device.status, "#8b949e") - self.status_dot = QLabel("●") - self.status_dot.setStyleSheet(f"color: {color}; font-size: 10px;") - hdr.addWidget(self.status_dot) - - layout.addLayout(hdr) - - # ID + type - sub = QLabel(f"{self.device.info.device_id} · {self.device.info.device_type}") - sub.setObjectName("deviceSub") - layout.addWidget(sub) - - # Channel count - ch_count = sum(1 for ch in self.device.info.channels if ch.enabled) - ch_lbl = QLabel(f"{ch_count} channel{'s' if ch_count != 1 else ''}") - ch_lbl.setObjectName("deviceChannelCount") - layout.addWidget(ch_lbl) - - # Config button - cfg_btn = QPushButton("Configure") - cfg_btn.setObjectName("configButton") - cfg_btn.clicked.connect(lambda: self.config_clicked.emit(self.device.info.device_id)) - layout.addWidget(cfg_btn) - - def refresh_status(self): - color = self.STATUS_COLORS.get(self.device.status, "#8b949e") - self.status_dot.setStyleSheet(f"color: {color}; font-size: 10px;") - - -class DevicePanel(QWidget): - config_requested = pyqtSignal(str) - - def __init__(self, registry: DeviceRegistry, engine: AcquisitionEngine): - super().__init__() - self.registry = registry - self.engine = engine - self._cards = {} - self._build_ui() - self.refresh() - - def _build_ui(self): - layout = QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - header = QLabel(" DEVICES") - header.setObjectName("panelHeader") - layout.addWidget(header) - - scroll = QScrollArea() - scroll.setWidgetResizable(True) - scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) - scroll.setObjectName("deviceScroll") - - self._container = QWidget() - self._inner = QVBoxLayout(self._container) - self._inner.setContentsMargins(8, 8, 8, 8) - self._inner.setSpacing(8) - self._inner.addStretch() - - scroll.setWidget(self._container) - layout.addWidget(scroll) - - def refresh(self): - # Remove old cards - for card in self._cards.values(): - self._inner.removeWidget(card) - card.deleteLater() - self._cards.clear() - - for dev in self.registry.all_instances(): - card = DeviceCard(dev) - card.config_clicked.connect(self.config_requested.emit) - # Insert before the stretch - self._inner.insertWidget(self._inner.count() - 1, card) - self._cards[dev.info.device_id] = card diff --git a/daq_system/ui/main_window.py b/daq_system/ui/main_window.py deleted file mode 100644 index 8b602d1..0000000 --- a/daq_system/ui/main_window.py +++ /dev/null @@ -1,228 +0,0 @@ -""" -ui/main_window.py - -Main application window. Hosts the left sidebar (device list), -center strip chart, right panel (numeric readouts + alarms). -""" - -from PyQt6.QtWidgets import ( - QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, - QSplitter, QStatusBar, QLabel, QPushButton, - QToolBar, QMessageBox, QFileDialog -) -from PyQt6.QtCore import Qt, QTimer, pyqtSlot -from PyQt6.QtGui import QAction, QIcon, QFont - -from devices.analog_input import AnalogInputDevice -from devices.digital_io import DigitalIODevice -from devices.temperature import TemperatureDevice -from devices.serial_device import SerialDevice -from devices.device_registry import DeviceRegistry -from core.acquisition import AcquisitionEngine - -from ui.device_panel import DevicePanel -from ui.strip_chart import StripChartWidget -from ui.readout_panel import ReadoutPanel -from ui.alarm_panel import AlarmPanel -from ui.config_dialog import DeviceConfigDialog - - -class MainWindow(QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("LabDAQ — Data Acquisition System") - self.setMinimumSize(1280, 780) - - self.registry = DeviceRegistry() - self.engine = AcquisitionEngine(poll_interval_ms=100) - self._running = False - - self._init_default_devices() - self._build_ui() - self._connect_signals() - - # ------------------------------------------------------------------ # - # Default demo configuration # - # ------------------------------------------------------------------ # - - def _init_default_devices(self): - ai = AnalogInputDevice(device_id="ai_0", num_channels=4, simulate=True) - ai.connect() - temp = TemperatureDevice(device_id="temp_0", num_channels=3, simulate=True) - temp.connect() - dio = DigitalIODevice(device_id="dio_0", num_inputs=4, num_outputs=4, simulate=True) - dio.connect() - - for dev in [ai, temp, dio]: - self.registry.add_instance(dev) - self.engine.add_device(dev) - - # ------------------------------------------------------------------ # - # UI construction # - # ------------------------------------------------------------------ # - - def _build_ui(self): - # ---- Toolbar ---- - toolbar = QToolBar("Main") - toolbar.setMovable(False) - toolbar.setObjectName("mainToolbar") - self.addToolBar(toolbar) - - self.run_btn = QPushButton("▶ RUN") - self.run_btn.setObjectName("runButton") - self.run_btn.setCheckable(True) - self.run_btn.clicked.connect(self._toggle_run) - - self.log_btn = QPushButton("⬤ LOG") - self.log_btn.setObjectName("logButton") - self.log_btn.setCheckable(True) - self.log_btn.setEnabled(False) - self.log_btn.clicked.connect(self._toggle_log) - - add_dev_btn = QPushButton("+ Device") - add_dev_btn.setObjectName("addDeviceButton") - add_dev_btn.clicked.connect(self._add_device) - - toolbar.addWidget(self.run_btn) - toolbar.addSeparator() - toolbar.addWidget(self.log_btn) - toolbar.addSeparator() - toolbar.addWidget(add_dev_btn) - - # Spacer - spacer = QWidget() - spacer.setSizePolicy( - spacer.sizePolicy().horizontalPolicy(), - spacer.sizePolicy().verticalPolicy() - ) - from PyQt6.QtWidgets import QSizePolicy - spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) - toolbar.addWidget(spacer) - - self.time_label = QLabel("00:00:00") - self.time_label.setObjectName("timeLabel") - toolbar.addWidget(self.time_label) - - # ---- Central layout ---- - central = QWidget() - self.setCentralWidget(central) - root = QHBoxLayout(central) - root.setContentsMargins(0, 0, 0, 0) - root.setSpacing(0) - - splitter = QSplitter(Qt.Orientation.Horizontal) - splitter.setHandleWidth(4) - - # Left: device panel - self.device_panel = DevicePanel(self.registry, self.engine) - self.device_panel.setMinimumWidth(200) - splitter.addWidget(self.device_panel) - - # Center: strip chart - self.strip_chart = StripChartWidget(self.engine, self.registry) - splitter.addWidget(self.strip_chart) - - # Right: readouts + alarms - right_panel = QWidget() - right_layout = QVBoxLayout(right_panel) - right_layout.setContentsMargins(0, 0, 0, 0) - right_layout.setSpacing(0) - - self.readout_panel = ReadoutPanel(self.registry) - self.alarm_panel = AlarmPanel() - - right_splitter = QSplitter(Qt.Orientation.Vertical) - right_splitter.addWidget(self.readout_panel) - right_splitter.addWidget(self.alarm_panel) - right_layout.addWidget(right_splitter) - - right_panel.setMinimumWidth(220) - splitter.addWidget(right_panel) - - splitter.setSizes([220, 820, 240]) - root.addWidget(splitter) - - # ---- Status bar ---- - self.status_bar = QStatusBar() - self.setStatusBar(self.status_bar) - self._status_lbl = QLabel("Ready") - self.status_bar.addWidget(self._status_lbl) - - # Clock timer - self._elapsed = 0 - self._clock = QTimer() - self._clock.setInterval(1000) - self._clock.timeout.connect(self._tick_clock) - - def _connect_signals(self): - self.engine.alarm_triggered.connect(self._on_alarm) - self.engine.new_data.connect(self.readout_panel.on_new_data) - self.engine.new_data.connect(self.strip_chart.on_new_data) - self.device_panel.config_requested.connect(self._open_config) - - # ------------------------------------------------------------------ # - # Actions # - # ------------------------------------------------------------------ # - - def _toggle_run(self, checked: bool): - if checked: - self.engine.start() - self.run_btn.setText("⏹ STOP") - self.log_btn.setEnabled(True) - self._clock.start() - self._status_lbl.setText("Acquiring data…") - self._running = True - else: - self.engine.stop() - self.run_btn.setText("▶ RUN") - self.log_btn.setEnabled(False) - if self.log_btn.isChecked(): - self.log_btn.setChecked(False) - self._toggle_log(False) - self._clock.stop() - self._status_lbl.setText("Stopped") - self._running = False - - def _toggle_log(self, checked: bool): - if checked: - path = self.engine.start_logging() - self.log_btn.setText(f"⏹ LOGGING") - self._status_lbl.setText(f"Logging → {path}") - else: - self.engine.stop_logging() - self.log_btn.setText("⬤ LOG") - self._status_lbl.setText("Log saved.") - - def _add_device(self): - from ui.add_device_dialog import AddDeviceDialog - dlg = AddDeviceDialog(self.registry, self) - if dlg.exec(): - dev = dlg.created_device - if dev: - self.registry.add_instance(dev) - self.engine.add_device(dev) - dev.connect() - self.device_panel.refresh() - self.readout_panel.refresh() - self.strip_chart.refresh() - - def _open_config(self, device_id: str): - dev = self.registry.get_instance(device_id) - if dev: - dlg = DeviceConfigDialog(dev, self) - dlg.exec() - - @pyqtSlot(str, str, float, str) - def _on_alarm(self, device_id: str, channel_id: str, value: float, kind: str): - self.alarm_panel.add_alarm(device_id, channel_id, value, kind) - - def _tick_clock(self): - self._elapsed += 1 - h = self._elapsed // 3600 - m = (self._elapsed % 3600) // 60 - s = self._elapsed % 60 - self.time_label.setText(f"{h:02d}:{m:02d}:{s:02d}") - - def closeEvent(self, event): - self.engine.stop() - event.accept() diff --git a/daq_system/ui/readout_panel.py b/daq_system/ui/readout_panel.py deleted file mode 100644 index 6086c30..0000000 --- a/daq_system/ui/readout_panel.py +++ /dev/null @@ -1,128 +0,0 @@ -""" -ui/readout_panel.py - -Right-side numeric readout panel. Shows current value for every channel -with a colored bar indicator and alarm highlighting. -""" - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QScrollArea, QFrame, QProgressBar -) -from PyQt6.QtCore import Qt, pyqtSlot -from PyQt6.QtGui import QColor - -from devices.device_registry import DeviceRegistry - - -class ChannelReadout(QFrame): - def __init__(self, device_name: str, channel_config, parent=None): - super().__init__(parent) - self.ch = channel_config - self.device_name = device_name - self.setObjectName("channelReadout") - self._alarm_active = False - self._build() - - def _build(self): - layout = QVBoxLayout(self) - layout.setContentsMargins(8, 6, 8, 6) - layout.setSpacing(2) - - top = QHBoxLayout() - self.name_lbl = QLabel(f"{self.ch.name}") - self.name_lbl.setObjectName("readoutName") - top.addWidget(self.name_lbl) - top.addStretch() - self.value_lbl = QLabel("---") - self.value_lbl.setObjectName("readoutValue") - self.value_lbl.setStyleSheet(f"color: {self.ch.color};") - top.addWidget(self.value_lbl) - unit = QLabel(self.ch.unit) - unit.setObjectName("readoutUnit") - top.addWidget(unit) - layout.addLayout(top) - - # Mini bar - self.bar = QProgressBar() - self.bar.setRange(0, 1000) - self.bar.setValue(0) - self.bar.setTextVisible(False) - self.bar.setMaximumHeight(4) - self.bar.setObjectName("readoutBar") - self.bar.setStyleSheet( - f"QProgressBar::chunk {{ background: {self.ch.color}; border-radius: 2px; }}" - ) - layout.addWidget(self.bar) - - def update_value(self, value: float): - self.value_lbl.setText(f"{value:.3f}") - rng = self.ch.max_value - self.ch.min_value - if rng > 0: - norm = int(((value - self.ch.min_value) / rng) * 1000) - self.bar.setValue(max(0, min(1000, norm))) - - def set_alarm(self, active: bool): - if active != self._alarm_active: - self._alarm_active = active - self.setProperty("alarm", active) - self.style().unpolish(self) - self.style().polish(self) - - -class ReadoutPanel(QWidget): - def __init__(self, registry: DeviceRegistry): - super().__init__() - self.registry = registry - self._readouts: dict = {} # (device_id, channel_id) -> ChannelReadout - self._build_ui() - self.refresh() - - def _build_ui(self): - layout = QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - hdr = QLabel(" CHANNELS") - hdr.setObjectName("panelHeader") - layout.addWidget(hdr) - - scroll = QScrollArea() - scroll.setWidgetResizable(True) - scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) - - self._container = QWidget() - self._inner = QVBoxLayout(self._container) - self._inner.setContentsMargins(6, 6, 6, 6) - self._inner.setSpacing(4) - self._inner.addStretch() - - scroll.setWidget(self._container) - layout.addWidget(scroll) - - def refresh(self): - for w in self._readouts.values(): - self._inner.removeWidget(w) - w.deleteLater() - self._readouts.clear() - - for dev in self.registry.all_instances(): - for ch in dev.info.channels: - if not ch.enabled: - continue - ro = ChannelReadout(dev.info.name, ch) - self._inner.insertWidget(self._inner.count() - 1, ro) - self._readouts[(dev.info.device_id, ch.channel_id)] = ro - - @pyqtSlot(str, str, float, float) - def on_new_data(self, device_id: str, channel_id: str, timestamp: float, value: float): - ro = self._readouts.get((device_id, channel_id)) - if ro: - ro.update_value(value) - ch = ro.ch - alarm = False - if ch.alarm_low is not None and value < ch.alarm_low: - alarm = True - if ch.alarm_high is not None and value > ch.alarm_high: - alarm = True - ro.set_alarm(alarm) diff --git a/daq_system/ui/strip_chart.py b/daq_system/ui/strip_chart.py deleted file mode 100644 index 46d73fa..0000000 --- a/daq_system/ui/strip_chart.py +++ /dev/null @@ -1,140 +0,0 @@ -""" -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) |
