diff options
Diffstat (limited to 'daq_system/ui/main_window.py')
| -rw-r--r-- | daq_system/ui/main_window.py | 228 |
1 files changed, 228 insertions, 0 deletions
diff --git a/daq_system/ui/main_window.py b/daq_system/ui/main_window.py new file mode 100644 index 0000000..8b602d1 --- /dev/null +++ b/daq_system/ui/main_window.py @@ -0,0 +1,228 @@ +""" +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() |
