diff options
| author | Christian Kolset <christian.kolset@gmail.com> | 2026-04-13 15:22:56 -0600 |
|---|---|---|
| committer | Christian Kolset <christian.kolset@gmail.com> | 2026-04-13 15:22:56 -0600 |
| commit | 2ed9d37da7b27d25173535550fb92702225ac14e (patch) | |
| tree | d6ff360a656e577629d3cefdaa1ae02af7255864 /ui/main_window.py | |
| parent | 8d6acf3a8ea4b37f86b321dbf430be5be01b1267 (diff) | |
Removed QtPy5 snippet and fixed directories
Diffstat (limited to 'ui/main_window.py')
| -rw-r--r-- | ui/main_window.py | 214 |
1 files changed, 214 insertions, 0 deletions
diff --git a/ui/main_window.py b/ui/main_window.py new file mode 100644 index 0000000..d329571 --- /dev/null +++ b/ui/main_window.py @@ -0,0 +1,214 @@ +""" +ui/main_window.py — Main application window. + +Layout: + ┌──────────────────────────────────────────────────────────┐ + │ TOOLBAR [▶ RUN] [⬤ LOG] [+ Device] 00:00:00 │ + ├────────────┬───────────────────────────┬─────────────────┤ + │ DEVICES │ STRIP CHART (pyqtgraph) │ CHANNELS │ + │ (left) │ │ (readouts) │ + │ │ ├─────────────────┤ + │ │ │ ALARMS │ + └────────────┴───────────────────────────┴─────────────────┘ + │ status bar │ + └──────────────────────────────────────────────────────────┘ +""" + +from PyQt6.QtWidgets import ( + QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, + QSplitter, QStatusBar, QLabel, QPushButton, + QToolBar, QSizePolicy, QMessageBox, +) +from PyQt6.QtCore import Qt, QTimer, pyqtSlot + +from devices.analog_input import AnalogInputDevice +from devices.digital_io import DigitalIODevice +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 +from ui.add_device_dialog import AddDeviceDialog + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("LabDAQ — Data Acquisition System") + self.setMinimumSize(1360, 820) + + self.registry = DeviceRegistry() + self.engine = AcquisitionEngine(poll_interval_ms=100) + + self._elapsed = 0 + self._running = False + + self._init_demo_devices() + self._build_ui() + self._connect_signals() + + def _init_demo_devices(self): + ai = AnalogInputDevice(device_id="ai_0", num_channels=4, simulate=True, backend="nidaqmx") + ai.connect() + ard = AnalogInputDevice(device_id="ard_0", num_channels=4, simulate=True, backend="arduino") + ard.connect() + dio = DigitalIODevice(device_id="dio_0", num_inputs=4, num_outputs=4, simulate=True, backend="nidaqmx") + dio.connect() + ser = SerialDevice(device_id="ser_0", num_channels=3, simulate=True) + ser.connect() + for dev in [ai, ard, dio, ser]: + self.registry.add_instance(dev) + self.engine.add_device(dev) + + def _build_ui(self): + tb = QToolBar("Main") + tb.setObjectName("mainToolbar") + tb.setMovable(False) + self.addToolBar(tb) + + self._run_btn = QPushButton("▶ RUN") + self._run_btn.setObjectName("runButton") + self._run_btn.setCheckable(True) + self._run_btn.clicked.connect(self._toggle_run) + tb.addWidget(self._run_btn) + tb.addSeparator() + + 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) + tb.addWidget(self._log_btn) + tb.addSeparator() + + add_btn = QPushButton("+ Device") + add_btn.setObjectName("addDeviceButton") + add_btn.clicked.connect(self._add_device) + tb.addWidget(add_btn) + + spacer = QWidget() + spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) + tb.addWidget(spacer) + + self._time_lbl = QLabel("00:00:00") + self._time_lbl.setObjectName("timeLabel") + tb.addWidget(self._time_lbl) + + central = QWidget() + self.setCentralWidget(central) + root = QHBoxLayout(central) + root.setContentsMargins(0, 0, 0, 0) + root.setSpacing(0) + + hsplit = QSplitter(Qt.Orientation.Horizontal) + hsplit.setHandleWidth(3) + + self._dev_panel = DevicePanel(self.registry, self.engine) + self._dev_panel.setMinimumWidth(210) + self._dev_panel.setMaximumWidth(300) + hsplit.addWidget(self._dev_panel) + + self._chart = StripChartWidget(self.engine, self.registry) + hsplit.addWidget(self._chart) + + right_widget = QWidget() + right_widget.setMinimumWidth(230) + right_widget.setMaximumWidth(320) + right_lay = QVBoxLayout(right_widget) + right_lay.setContentsMargins(0, 0, 0, 0) + right_lay.setSpacing(0) + vsplit = QSplitter(Qt.Orientation.Vertical) + vsplit.setHandleWidth(3) + self._readout_panel = ReadoutPanel(self.registry) + self._alarm_panel = AlarmPanel() + vsplit.addWidget(self._readout_panel) + vsplit.addWidget(self._alarm_panel) + vsplit.setSizes([500, 300]) + right_lay.addWidget(vsplit) + hsplit.addWidget(right_widget) + + hsplit.setSizes([230, 880, 260]) + root.addWidget(hsplit) + + sb = QStatusBar() + self.setStatusBar(sb) + self._status_lbl = QLabel("Ready — simulation mode active") + sb.addWidget(self._status_lbl) + self._log_lbl = QLabel("") + sb.addPermanentWidget(self._log_lbl) + + self._clock = QTimer(self) + self._clock.setInterval(1000) + self._clock.timeout.connect(self._tick) + + 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._chart.on_new_data) + self.engine.log_started.connect(lambda p: self._log_lbl.setText(f"● LOG {p}")) + self.engine.log_stopped.connect(lambda p: self._log_lbl.setText(f"✓ Saved {p}")) + self._dev_panel.config_requested.connect(self._open_config) + + 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…") + self._running = True + else: + self.engine.stop() + self._run_btn.setText("▶ RUN") + if self._log_btn.isChecked(): + self._log_btn.setChecked(False) + self._log_btn.setEnabled(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("⏹ LOGGING") + self._status_lbl.setText(f"Logging → {path}") + else: + self.engine.stop_logging() + self._log_btn.setText("⬤ LOG") + + def _add_device(self): + dlg = AddDeviceDialog(self.registry, self) + if dlg.exec(): + dev = dlg.created_device + if dev: + dev.connect() + self.registry.add_instance(dev) + self.engine.add_device(dev) + self._dev_panel.refresh() + self._readout_panel.refresh() + self._chart.refresh() + self._status_lbl.setText(f"Added device: {dev.info.device_id}") + + def _open_config(self, device_id: str): + dev = self.registry.get_instance(device_id) + if dev: + DeviceConfigDialog(dev, self).exec() + + @pyqtSlot(str, str, float, str) + def _on_alarm(self, dev_id: str, ch_id: str, value: float, kind: str): + self._alarm_panel.add_alarm(dev_id, ch_id, value, kind) + + def _tick(self): + self._elapsed += 1 + h = self._elapsed // 3600 + m = (self._elapsed % 3600) // 60 + s = self._elapsed % 60 + self._time_lbl.setText(f"{h:02d}:{m:02d}:{s:02d}") + + def closeEvent(self, event): + self.engine.stop() + event.accept() |
