summaryrefslogtreecommitdiff
path: root/ui/main_window.py
diff options
context:
space:
mode:
authorChristian Kolset <christian.kolset@gmail.com>2026-04-13 15:22:56 -0600
committerChristian Kolset <christian.kolset@gmail.com>2026-04-13 15:22:56 -0600
commit2ed9d37da7b27d25173535550fb92702225ac14e (patch)
treed6ff360a656e577629d3cefdaa1ae02af7255864 /ui/main_window.py
parent8d6acf3a8ea4b37f86b321dbf430be5be01b1267 (diff)
Removed QtPy5 snippet and fixed directories
Diffstat (limited to 'ui/main_window.py')
-rw-r--r--ui/main_window.py214
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()