summaryrefslogtreecommitdiff
path: root/daq_system/ui
diff options
context:
space:
mode:
Diffstat (limited to 'daq_system/ui')
-rw-r--r--daq_system/ui/__pycache__/alarm_panel.cpython-312.pycbin5462 -> 0 bytes
-rw-r--r--daq_system/ui/__pycache__/config_dialog.cpython-312.pycbin9345 -> 0 bytes
-rw-r--r--daq_system/ui/__pycache__/device_panel.cpython-312.pycbin7943 -> 0 bytes
-rw-r--r--daq_system/ui/__pycache__/main_window.cpython-312.pycbin13127 -> 0 bytes
-rw-r--r--daq_system/ui/__pycache__/readout_panel.cpython-312.pycbin8378 -> 0 bytes
-rw-r--r--daq_system/ui/__pycache__/strip_chart.cpython-312.pycbin8328 -> 0 bytes
-rw-r--r--daq_system/ui/add_device_dialog.py7
-rw-r--r--daq_system/ui/alarm_panel.py85
-rw-r--r--daq_system/ui/config_dialog.py174
-rw-r--r--daq_system/ui/device_panel.py127
-rw-r--r--daq_system/ui/main_window.py228
-rw-r--r--daq_system/ui/readout_panel.py128
-rw-r--r--daq_system/ui/strip_chart.py140
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
deleted file mode 100644
index 7f2776c..0000000
--- a/daq_system/ui/__pycache__/alarm_panel.cpython-312.pyc
+++ /dev/null
Binary files differ
diff --git a/daq_system/ui/__pycache__/config_dialog.cpython-312.pyc b/daq_system/ui/__pycache__/config_dialog.cpython-312.pyc
deleted file mode 100644
index d62887f..0000000
--- a/daq_system/ui/__pycache__/config_dialog.cpython-312.pyc
+++ /dev/null
Binary files differ
diff --git a/daq_system/ui/__pycache__/device_panel.cpython-312.pyc b/daq_system/ui/__pycache__/device_panel.cpython-312.pyc
deleted file mode 100644
index 0616690..0000000
--- a/daq_system/ui/__pycache__/device_panel.cpython-312.pyc
+++ /dev/null
Binary files differ
diff --git a/daq_system/ui/__pycache__/main_window.cpython-312.pyc b/daq_system/ui/__pycache__/main_window.cpython-312.pyc
deleted file mode 100644
index d6af385..0000000
--- a/daq_system/ui/__pycache__/main_window.cpython-312.pyc
+++ /dev/null
Binary files differ
diff --git a/daq_system/ui/__pycache__/readout_panel.cpython-312.pyc b/daq_system/ui/__pycache__/readout_panel.cpython-312.pyc
deleted file mode 100644
index 47d94da..0000000
--- a/daq_system/ui/__pycache__/readout_panel.cpython-312.pyc
+++ /dev/null
Binary files differ
diff --git a/daq_system/ui/__pycache__/strip_chart.cpython-312.pyc b/daq_system/ui/__pycache__/strip_chart.cpython-312.pyc
deleted file mode 100644
index 95dc075..0000000
--- a/daq_system/ui/__pycache__/strip_chart.cpython-312.pyc
+++ /dev/null
Binary files differ
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)