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