summaryrefslogtreecommitdiff
path: root/ui/main_window.py
diff options
context:
space:
mode:
authorChristian Kolset <christian.kolset@gmail.com>2026-04-20 16:55:57 -0600
committerChristian Kolset <christian.kolset@gmail.com>2026-04-20 16:55:57 -0600
commitd5acb04b88373d33b038bb59945fb5ab8b4f543b (patch)
tree21c37f2b54258ef20edc1b9b7226327d5f5b1a3a /ui/main_window.py
parent425ba78ee1f760978b23a09fe8acbbc9b8b5dae4 (diff)
V8
Diffstat (limited to 'ui/main_window.py')
-rw-r--r--ui/main_window.py383
1 files changed, 238 insertions, 145 deletions
diff --git a/ui/main_window.py b/ui/main_window.py
index d329571..e69ab1a 100644
--- a/ui/main_window.py
+++ b/ui/main_window.py
@@ -1,214 +1,307 @@
"""
-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 │
- └──────────────────────────────────────────────────────────┘
+ui/main_window.py
+
+Toolbar sections (left→right):
+ [▶ RUN] [⬤ LOG] | [⚙ DEVICES] | [⚗ SIGNALS] | [📐 PLOT] | [⚙ SETTINGS]
+ spacer | timer
+
+Central:
+ Left: ControlPanel (output widgets only)
+ Center: StripChartWidget
+
+All secondary features live in floating windows.
"""
from PyQt6.QtWidgets import (
QMainWindow, QWidget, QHBoxLayout, QVBoxLayout,
QSplitter, QStatusBar, QLabel, QPushButton,
- QToolBar, QSizePolicy, QMessageBox,
+ QToolBar, QSizePolicy, QApplication,
)
from PyQt6.QtCore import Qt, QTimer, pyqtSlot
+import os
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 core.signal_processor import SignalProcessor
-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
+from ui.control_panel import ControlPanel
+from ui.strip_chart import StripChartWidget
+from ui.windows.devices_window import DevicesWindow
+from ui.windows.signals_window import SignalsWindow
+from ui.windows.plot_window import PlotWindow, build_default_layout
+from ui.windows.settings_window import SettingsWindow
+
+
+_DARK_QSS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "style_dark.qss")
+_LIGHT_QSS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "style_light.qss")
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
- self.setWindowTitle("LabDAQ — Data Acquisition System")
- self.setMinimumSize(1360, 820)
+ self.setWindowTitle("LabDAQ")
+ self.setMinimumSize(1000, 640)
- self.registry = DeviceRegistry()
- self.engine = AcquisitionEngine(poll_interval_ms=100)
+ self.registry = DeviceRegistry()
+ self.engine = AcquisitionEngine(poll_interval_ms=100)
+ self.processor = SignalProcessor()
+ self._settings = dict(SettingsWindow._defaults)
self._elapsed = 0
- self._running = False
+ self._win_devices = None
+ self._win_signals = None
+ self._win_plot = None
+ self._win_settings = None
self._init_demo_devices()
self._build_ui()
self._connect_signals()
+ # ── Demo ──────────────────────────────────────────────────────────────
+
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]:
+ for dev in [
+ AnalogInputDevice(device_id="ai_0", num_channels=4, simulate=True, backend="nidaqmx"),
+ AnalogInputDevice(device_id="ard_0", num_channels=4, simulate=True, backend="arduino"),
+ DigitalIODevice (device_id="dio_0", num_inputs=4, num_outputs=4, simulate=True),
+ SerialDevice (device_id="ser_0", num_channels=3, simulate=True),
+ ]:
+ dev.connect()
self.registry.add_instance(dev)
self.engine.add_device(dev)
+ # ── UI ────────────────────────────────────────────────────────────────
+
def _build_ui(self):
- tb = QToolBar("Main")
- tb.setObjectName("mainToolbar")
- tb.setMovable(False)
+ # ── Toolbar ──────────────────────────────────────────────────────
+ tb = QToolBar(); tb.setObjectName("mainToolbar"); tb.setMovable(False)
self.addToolBar(tb)
+ def _sep():
+ s = QFrame(); s.setFrameShape(QFrame.Shape.VLine)
+ s.setObjectName("toolbarSep"); return s
+
+ # RUN / LOG
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._run_btn.setObjectName("runButton"); self._run_btn.setCheckable(True)
+ self._run_btn.clicked.connect(self._toggle_run); tb.addWidget(self._run_btn)
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)
+ 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)
+ tb.addWidget(_sep())
+
+ # DEVICES
+ dev_btn = QPushButton("⊞ Devices")
+ dev_btn.setObjectName("toolbarSectionBtn")
+ dev_btn.setCheckable(True)
+ dev_btn.clicked.connect(lambda c: self._toggle_win("devices", c, dev_btn))
+ tb.addWidget(dev_btn); self._btn_devices = dev_btn
+
+ tb.addWidget(_sep())
+
+ # SIGNALS
+ sig_btn = QPushButton("⚗ Signals")
+ sig_btn.setObjectName("toolbarSectionBtn")
+ sig_btn.setCheckable(True)
+ sig_btn.clicked.connect(lambda c: self._toggle_win("signals", c, sig_btn))
+ tb.addWidget(sig_btn); self._btn_signals = sig_btn
+
+ tb.addWidget(_sep())
- spacer = QWidget()
- spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
+ # PLOT
+ plot_btn = QPushButton("📐 Plot")
+ plot_btn.setObjectName("toolbarSectionBtn")
+ plot_btn.setCheckable(True)
+ plot_btn.clicked.connect(lambda c: self._toggle_win("plot", c, plot_btn))
+ tb.addWidget(plot_btn); self._btn_plot = plot_btn
+
+ tb.addWidget(_sep())
+
+ # SETTINGS
+ set_btn = QPushButton("⚙ Settings")
+ set_btn.setObjectName("toolbarSectionBtn")
+ set_btn.setCheckable(True)
+ set_btn.clicked.connect(lambda c: self._toggle_win("settings", c, set_btn))
+ tb.addWidget(set_btn); self._btn_settings = set_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")
+ 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)
+ # ── Central ───────────────────────────────────────────────────────
+ 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)
+ 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._ctrl = ControlPanel(self.registry)
+ self._ctrl.setMinimumWidth(200); self._ctrl.setMaximumWidth(320)
+ hsplit.addWidget(self._ctrl)
- self._chart = StripChartWidget(self.engine, self.registry)
+ self._chart = StripChartWidget(self.engine, self.registry, self.processor)
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])
+ hsplit.setSizes([240, 1000])
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)
+ # ── Status bar ─────────────────────────────────────────────────
+ sb = QStatusBar(); self.setStatusBar(sb)
+ self._status = QLabel("Ready"); sb.addWidget(self._status)
+ self._log_lbl = QLabel(""); sb.addPermanentWidget(self._log_lbl)
- self._clock = QTimer(self)
- self._clock.setInterval(1000)
+ 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):
+ self.engine.new_data.connect(self.processor.on_raw_data)
+ self.processor.processed_data.connect(self._chart.on_new_data)
+ self.engine.log_started.connect(lambda p: self._log_lbl.setText(f"● {p}"))
+ self.engine.log_stopped.connect(lambda p: self._log_lbl.setText(f"✓ {p}"))
+
+ # ── Window management ─────────────────────────────────────────────────
+
+ def _toggle_win(self, name: str, checked: bool, btn: QPushButton):
+ creators = {
+ "devices": self._open_devices,
+ "signals": self._open_signals,
+ "plot": self._open_plot,
+ "settings": self._open_settings,
+ }
+ wins = {
+ "devices": "_win_devices",
+ "signals": "_win_signals",
+ "plot": "_win_plot",
+ "settings": "_win_settings",
+ }
if checked:
+ creators[name]()
+ else:
+ win = getattr(self, wins[name], None)
+ if win: win.hide()
+
+ def _open_devices(self):
+ if self._win_devices is None:
+ self._win_devices = DevicesWindow(self.registry, self.engine, self)
+ self._win_devices.device_added.connect(self._on_device_added)
+ self._win_devices.device_removed.connect(self._on_device_removed)
+ self._win_devices.device_reconfigured.connect(self._on_device_reconfigured)
+ self._win_devices.closed.connect(lambda: self._btn_devices.setChecked(False))
+ self._show_win(self._win_devices, "right")
+
+ def _open_signals(self):
+ if self._win_signals is None:
+ self._win_signals = SignalsWindow(self.registry, self.processor, self)
+ self._win_signals.derived_changed.connect(self._on_derived_changed)
+ self._win_signals.visibility_changed.connect(self._chart.refresh)
+ self._win_signals.closed.connect(lambda: self._btn_signals.setChecked(False))
+ self._show_win(self._win_signals, "right")
+
+ def _open_plot(self):
+ if self._win_plot is None:
+ self._win_plot = PlotWindow(self.registry, self.processor,
+ self._chart._cfg, self)
+ self._win_plot.layout_applied.connect(self._chart.apply_layout)
+ self._win_plot.closed.connect(lambda: self._btn_plot.setChecked(False))
+ else:
+ self._win_plot.cfg = self._chart._cfg
+ self._win_plot._populate()
+ self._show_win(self._win_plot, "below")
+
+ def _open_settings(self):
+ if self._win_settings is None:
+ self._win_settings = SettingsWindow(self.registry, self.engine,
+ self._settings, self)
+ self._win_settings.theme_changed.connect(self._apply_theme)
+ self._win_settings.settings_changed.connect(self._on_settings)
+ self._win_settings.closed.connect(lambda: self._btn_settings.setChecked(False))
+ self._show_win(self._win_settings, "right")
+
+ def _show_win(self, win: QWidget, position: str = "right"):
+ geo = self.geometry()
+ if not win.isVisible():
+ if position == "right":
+ win.move(geo.right() + 8, geo.top() + 40)
+ elif position == "below":
+ win.move(geo.left(), geo.bottom() + 8)
+ win.show(); win.raise_(); win.activateWindow()
+
+ # ── Device/signal events ──────────────────────────────────────────────
+
+ def _on_device_added(self):
+ self._chart.refresh()
+ if self._win_plot: self._win_plot.refresh_channels()
+ self._status.setText("Device added.")
+
+ def _on_device_removed(self, dev_id: str):
+ self._chart.refresh()
+ self._status.setText(f"Device '{dev_id}' removed.")
+
+ def _on_device_reconfigured(self, dev_id: str):
+ """Called after Configure dialog closes — rebuild chart curves."""
+ self._chart.refresh()
+ if self._win_plot:
+ self._win_plot.refresh_channels()
+ self._status.setText(f"Device '{dev_id}' reconfigured.")
+
+ def _on_derived_changed(self):
+ self._chart.refresh()
+ if self._win_plot: self._win_plot.refresh_channels()
+
+ # ── Run / Log ─────────────────────────────────────────────────────────
+
+ def _toggle_run(self, c: bool):
+ if c:
self.engine.start()
- self._run_btn.setText("⏹ STOP")
- self._log_btn.setEnabled(True)
- self._clock.start()
- self._status_lbl.setText("Acquiring…")
- self._running = True
+ self._run_btn.setText("⏹ STOP"); self._log_btn.setEnabled(True)
+ self._clock.start(); self._status.setText("Acquiring…")
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()
+ if self._log_btn.isChecked(): self._log_btn.setChecked(False)
+ self._log_btn.setEnabled(False); self._clock.stop()
+ self._status.setText("Stopped")
+
+ def _toggle_log(self, c: bool):
+ if c:
+ os.makedirs(self._settings.get("log_dir","logs"), exist_ok=True)
+ p = self.engine.start_logging(
+ os.path.join(self._settings.get("log_dir","logs"), ""))
self._log_btn.setText("⏹ LOGGING")
- self._status_lbl.setText(f"Logging → {path}")
+ self._status.setText(f"Logging → {p}")
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)
+ self.engine.stop_logging(); self._log_btn.setText("⬤ LOG")
+
+ # ── Settings ──────────────────────────────────────────────────────────
+
+ def _apply_theme(self, theme: str):
+ qss_file = _DARK_QSS if theme == "dark" else _LIGHT_QSS
+ if os.path.exists(qss_file):
+ with open(qss_file) as f:
+ QApplication.instance().setStyleSheet(f.read())
+ # Update pyqtgraph plot colours independently of QSS
+ self._chart.set_theme(theme)
+
+ def _on_settings(self, cfg: dict):
+ self._settings.update(cfg)
+ self.engine._interval = cfg.get("poll_ms", 100) / 1000.0
def _tick(self):
self._elapsed += 1
- h = self._elapsed // 3600
- m = (self._elapsed % 3600) // 60
- s = self._elapsed % 60
+ 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()
+ for w in (self._win_devices,self._win_signals,self._win_plot,self._win_settings):
+ if w: w.close()
+ self.engine.stop(); event.accept()
+
+
+from PyQt6.QtWidgets import QFrame