""" 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()