summaryrefslogtreecommitdiff
path: root/daq_system/ui/main_window.py
diff options
context:
space:
mode:
Diffstat (limited to 'daq_system/ui/main_window.py')
-rw-r--r--daq_system/ui/main_window.py228
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()