summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Kolset <christian.kolset@gmail.com>2026-06-09 15:40:35 -0600
committerChristian Kolset <christian.kolset@gmail.com>2026-06-09 15:40:35 -0600
commita35b9b121218625445bda40729a8222243199c77 (patch)
treea429f65f6dbe62c1253c8c31730b8420153678be
parent17b2f07545b89487c2d1bb936dc52a612d38d470 (diff)
Redesign Add Device dialog with streamlined two-section layout
- Unified device scanner at top (serial ports, NI devices, cameras) replaces per-panel scan groups; clicking pre-fills port/type - Port, baud rate, NI Device, and simulation toggle moved into Available Devices section; shown/hidden by device type - Device type, ID, and Protocol/Format grouped under Configuration - Panel order: Serial/UART β†’ Arduino β†’ NI-DAQ (Serial first) - Add button always enabled; validation on press shows error popup with focus jump to the offending field - CameraPanel loses sim_chk (sim state set by dialog via set_simulate) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--plugins/motion_capture/camera_panel.py92
-rw-r--r--ui/add_device_dialog.py485
2 files changed, 270 insertions, 307 deletions
diff --git a/plugins/motion_capture/camera_panel.py b/plugins/motion_capture/camera_panel.py
index 611c942..3fdd286 100644
--- a/plugins/motion_capture/camera_panel.py
+++ b/plugins/motion_capture/camera_panel.py
@@ -1,17 +1,15 @@
"""
motion_capture/camera_panel.py
-CameraPanel for AddDeviceDialog β€” scans for OpenCV cameras and builds
+CameraPanel for AddDeviceDialog β€” shows selected camera info and builds
a CameraDevice when the user clicks "Add Device".
+Camera discovery and simulation toggle are handled by AddDeviceDialog.
"""
from __future__ import annotations
-from PyQt6.QtCore import Qt, QThread, pyqtSignal
-from PyQt6.QtWidgets import (
- QCheckBox, QGroupBox, QHBoxLayout, QLabel, QListWidget,
- QListWidgetItem, QPushButton, QVBoxLayout, QWidget,
-)
+from PyQt6.QtCore import QThread, pyqtSignal
+from PyQt6.QtWidgets import QLabel, QVBoxLayout, QWidget
class CameraScanThread(QThread):
@@ -40,44 +38,17 @@ class CameraPanel(QWidget):
def __init__(self):
super().__init__()
- self._cameras: list = []
self._selected_index: int = 0
- self._scanner = None
+ self._simulate: bool = False
lay = QVBoxLayout(self)
lay.setContentsMargins(0, 4, 0, 4)
lay.setSpacing(8)
- self.sim_chk = QCheckBox("Simulation mode (Lissajous pattern, no camera needed)")
- self.sim_chk.setChecked(False)
- lay.addWidget(self.sim_chk)
-
- scan_grp = QGroupBox("Available Cameras")
- sg = QVBoxLayout(scan_grp)
- sg.setSpacing(4)
-
- top = QHBoxLayout()
- self._scan_btn = QPushButton("πŸ” Scan Cameras")
- self._scan_btn.setObjectName("addTraceBtn")
- self._scan_btn.clicked.connect(self._scan)
- self._status = QLabel("Click Scan to detect cameras")
- self._status.setObjectName("traceSource")
- top.addWidget(self._scan_btn)
- top.addWidget(self._status, 1)
- sg.addLayout(top)
-
- self._list = QListWidget()
- self._list.setMaximumHeight(120)
- self._list.setObjectName("portList")
- self._list.itemClicked.connect(self._on_select)
- self._list.setToolTip("Click a camera to select it")
- sg.addWidget(self._list)
-
- hint = QLabel("↑ Click a camera above to select it")
- hint.setObjectName("traceSource")
- sg.addWidget(hint)
-
- lay.addWidget(scan_grp)
+ self._selected_lbl = QLabel("No camera selected β€” scan above and click a camera")
+ self._selected_lbl.setObjectName("traceSource")
+ self._selected_lbl.setWordWrap(True)
+ lay.addWidget(self._selected_lbl)
note = QLabel(
"Requires OpenCV: pip install opencv-python\n"
@@ -88,45 +59,20 @@ class CameraPanel(QWidget):
lay.addWidget(note)
lay.addStretch()
- # ── Scan ─────────────────────────────────────────────────────────────
-
- def _scan(self):
- self._scan_btn.setEnabled(False)
- self._status.setText("Scanning…")
- self._list.clear()
- self._scanner = CameraScanThread()
- self._scanner.cameras_found.connect(self._on_found)
- self._scanner.start()
-
- def _on_found(self, cameras):
- self._scan_btn.setEnabled(True)
- self._cameras = cameras
- self._list.clear()
- if not cameras:
- self._status.setText("No cameras found")
- item = QListWidgetItem(" No cameras detected")
- item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsSelectable)
- self._list.addItem(item)
- else:
- self._status.setText(f"{len(cameras)} camera(s) found")
- for idx, label in cameras:
- item = QListWidgetItem(f" {label}")
- item.setData(Qt.ItemDataRole.UserRole, idx)
- self._list.addItem(item)
- self._list.setCurrentRow(0)
- self._selected_index = cameras[0][0]
-
- def _on_select(self, item: QListWidgetItem):
- idx = item.data(Qt.ItemDataRole.UserRole)
- if idx is not None:
- self._selected_index = idx
-
- # ── Build ─────────────────────────────────────────────────────────────
+ def is_valid(self) -> bool:
+ return True
+
+ def set_simulate(self, simulate: bool):
+ self._simulate = simulate
+
+ def set_camera_index(self, index: int):
+ self._selected_index = index
+ self._selected_lbl.setText(f"Selected: Camera {index}")
def build_device(self, device_id: str):
from device import CameraDevice
return CameraDevice(
device_id=device_id,
camera_index=self._selected_index,
- simulate=self.sim_chk.isChecked(),
+ simulate=self._simulate,
)
diff --git a/ui/add_device_dialog.py b/ui/add_device_dialog.py
index 5258995..f0cbde1 100644
--- a/ui/add_device_dialog.py
+++ b/ui/add_device_dialog.py
@@ -1,12 +1,9 @@
"""
ui/add_device_dialog.py
-Add Device dialog β€” user selects physical device type (Arduino / NI-DAQ / Serial).
-Backend is determined by device type; Arduino and NI-DAQ combine analog input
-and digital I/O into one physical device entry.
+Add Device dialog β€” unified device scanner + type/config form.
"""
-import os
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
QComboBox, QLineEdit, QSpinBox, QDoubleSpinBox,
@@ -15,7 +12,6 @@ from PyQt6.QtWidgets import (
QListWidgetItem, QFrame, QSizePolicy,
)
from PyQt6.QtCore import Qt, QThread, pyqtSignal
-from PyQt6.QtGui import QFont
from devices.device_registry import DeviceRegistry
from devices.arduino_device import ArduinoDevice
@@ -43,154 +39,132 @@ class NIScanThread(QThread):
self.devices_found.emit(devs)
-# ── Reusable scanner widgets ──────────────────────────────────────────────────
+# ── Device list scanner (embedded in Available Devices section) ───────────────
-class PortScanGroup(QGroupBox):
- """Scan-and-click widget that fills a target QLineEdit with the chosen port."""
+class _DeviceListScanner(QWidget):
+ """Scan button + result list. Emits device_selected(kind, value) on click."""
- def __init__(self, target_edit: QLineEdit):
- super().__init__("Available Serial Ports")
- self._target = target_edit
- self._scanner = None
+ device_selected = pyqtSignal(str, object) # kind = "serial"|"ni"|"camera"
+
+ def __init__(self):
+ super().__init__()
+ self._port_scanner = None
+ self._ni_scanner = None
+ self._cam_scanner = None
+ self._pending = 0
lay = QVBoxLayout(self)
+ lay.setContentsMargins(0, 0, 0, 0)
lay.setSpacing(4)
top = QHBoxLayout()
- self._scan_btn = QPushButton("πŸ” Scan Ports")
+ self._scan_btn = QPushButton("πŸ” Scan All")
self._scan_btn.setObjectName("addTraceBtn")
- self._scan_btn.clicked.connect(self._scan)
- self._status = QLabel("Click Scan to detect ports")
+ self._scan_btn.clicked.connect(self.scan_all)
+ self._status = QLabel("Click Scan to detect devices")
self._status.setObjectName("traceSource")
top.addWidget(self._scan_btn)
top.addWidget(self._status, 1)
lay.addLayout(top)
self._list = QListWidget()
- self._list.setMaximumHeight(100)
+ self._list.setFixedHeight(120)
self._list.setObjectName("portList")
self._list.itemClicked.connect(self._on_select)
- self._list.setToolTip("Click a port to select it")
+ self._list.setToolTip("Click a device to pre-fill configuration")
lay.addWidget(self._list)
- hint = QLabel("↑ Click a port above to fill the Port field")
+ hint = QLabel("↑ Click a device to pre-fill port and configuration")
hint.setObjectName("traceSource")
lay.addWidget(hint)
- def _scan(self):
+ def scan_all(self):
self._scan_btn.setEnabled(False)
self._status.setText("Scanning…")
self._list.clear()
- self._scanner = PortScanThread()
- self._scanner.ports_found.connect(self._on_found)
- self._scanner.start()
-
- def _on_found(self, ports):
- self._scan_btn.setEnabled(True)
- self._list.clear()
- if not ports:
- self._status.setText("No ports found")
- item = QListWidgetItem(" No serial ports detected")
- item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsSelectable)
- self._list.addItem(item)
- else:
- self._status.setText(f"{len(ports)} port(s) found")
- for device, desc in ports:
- label = f" {device}"
- if desc and desc != device:
- label += f" β€” {desc}"
- item = QListWidgetItem(label)
- item.setData(Qt.ItemDataRole.UserRole, device)
- self._list.addItem(item)
-
- def _on_select(self, item: QListWidgetItem):
- port = item.data(Qt.ItemDataRole.UserRole)
- if port:
- self._target.setText(port)
-
-
-class NIScanGroup(QGroupBox):
- """Scan-and-click widget that fills a target QLineEdit with the chosen NI device."""
-
- def __init__(self, target_edit: QLineEdit):
- super().__init__("Available NI Devices")
- self._target = target_edit
- self._scanner = None
-
- lay = QVBoxLayout(self)
- lay.setSpacing(4)
+ self._pending = 0
- top = QHBoxLayout()
- self._scan_btn = QPushButton("πŸ” Scan NI Devices")
- self._scan_btn.setObjectName("addTraceBtn")
- self._scan_btn.clicked.connect(self._scan)
- self._status = QLabel("Click Scan to detect NI devices")
- self._status.setObjectName("traceSource")
- top.addWidget(self._scan_btn)
- top.addWidget(self._status, 1)
- lay.addLayout(top)
+ self._pending += 1
+ self._port_scanner = PortScanThread()
+ self._port_scanner.ports_found.connect(self._on_ports)
+ self._port_scanner.start()
- self._list = QListWidget()
- self._list.setMaximumHeight(100)
- self._list.setObjectName("portList")
- self._list.itemClicked.connect(self._on_select)
- self._list.setToolTip("Click a device to select it")
- lay.addWidget(self._list)
+ self._pending += 1
+ self._ni_scanner = NIScanThread()
+ self._ni_scanner.devices_found.connect(self._on_ni)
+ self._ni_scanner.start()
- hint = QLabel("↑ Click a device above to fill the NI Device field")
- hint.setObjectName("traceSource")
- lay.addWidget(hint)
-
- def _scan(self):
- self._scan_btn.setEnabled(False)
- self._status.setText("Scanning…")
- self._list.clear()
- self._scanner = NIScanThread()
- self._scanner.devices_found.connect(self._on_found)
- self._scanner.start()
+ try:
+ from plugins.motion_capture.camera_panel import CameraScanThread
+ self._pending += 1
+ self._cam_scanner = CameraScanThread()
+ self._cam_scanner.cameras_found.connect(self._on_cameras)
+ self._cam_scanner.start()
+ except Exception:
+ pass
+
+ def _check_done(self):
+ self._pending -= 1
+ if self._pending <= 0:
+ self._scan_btn.setEnabled(True)
+ n = sum(
+ 1 for i in range(self._list.count())
+ if self._list.item(i).flags() & Qt.ItemFlag.ItemIsSelectable
+ )
+ self._status.setText(f"{n} device(s) found" if n else "No devices found")
+
+ def _on_ports(self, ports):
+ for device, desc in ports:
+ label = f"[Serial] {device}"
+ if desc and desc != device:
+ label += f" β€” {desc}"
+ item = QListWidgetItem(label)
+ item.setData(Qt.ItemDataRole.UserRole, ("serial", device))
+ self._list.addItem(item)
+ self._check_done()
+
+ def _on_ni(self, devices):
+ for name, product in devices:
+ label = f"[NI-DAQ] {name}"
+ if product:
+ label += f" β€” {product}"
+ item = QListWidgetItem(label)
+ item.setData(Qt.ItemDataRole.UserRole, ("ni", name))
+ self._list.addItem(item)
+ self._check_done()
- def _on_found(self, devices):
- self._scan_btn.setEnabled(True)
- self._list.clear()
- if not devices:
- self._status.setText("No NI devices found")
- item = QListWidgetItem(" No NI devices detected")
- item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsSelectable)
+ def _on_cameras(self, cameras):
+ for idx, label_str in cameras:
+ item = QListWidgetItem(f"[Camera] {label_str}")
+ item.setData(Qt.ItemDataRole.UserRole, ("camera", idx))
self._list.addItem(item)
- else:
- self._status.setText(f"{len(devices)} device(s) found")
- for name, product in devices:
- label = f" {name}"
- if product:
- label += f" β€” {product}"
- item = QListWidgetItem(label)
- item.setData(Qt.ItemDataRole.UserRole, name)
- self._list.addItem(item)
+ self._check_done()
def _on_select(self, item: QListWidgetItem):
- name = item.data(Qt.ItemDataRole.UserRole)
- if name:
- self._target.setText(name)
-
+ data = item.data(Qt.ItemDataRole.UserRole)
+ if data:
+ self.device_selected.emit(data[0], data[1])
-# ── Per-type config panels ────────────────────────────────────────────────────
-class ArduinoPanel(QWidget):
- """Config fields for ArduinoDevice (analog + digital I/O combined)."""
+# ── Per-type panels (remaining type-specific fields only) ─────────────────────
+class SerialPanel(QWidget):
def __init__(self):
super().__init__()
- lay = QFormLayout(self)
+ lay = QVBoxLayout(self)
lay.setContentsMargins(0, 4, 0, 4)
+ note = QLabel("Additional protocol settings available in the device config dialog.")
+ note.setObjectName("traceSource")
+ note.setWordWrap(True)
+ lay.addWidget(note)
+ lay.addStretch()
- self.port_edit = QLineEdit("")
- self.port_edit.setPlaceholderText("e.g. COM3 or /dev/ttyUSB0")
- lay.addRow("Port:", self.port_edit)
- self.baud_cb = QComboBox()
- self.baud_cb.addItems(["9600", "57600", "115200", "230400"])
- self.baud_cb.setCurrentText("115200")
- lay.addRow("Baud Rate:", self.baud_cb)
+class ArduinoPanel(QWidget):
+ def __init__(self):
+ super().__init__()
+ lay = QFormLayout(self)
+ lay.setContentsMargins(0, 4, 0, 4)
self.analog_pins_edit = QLineEdit("0, 1, 2, 3")
self.analog_pins_edit.setPlaceholderText("e.g. 0, 1, 2, 3 (indices into A0–A5)")
@@ -204,39 +178,13 @@ class ArduinoPanel(QWidget):
self.do_pins_edit.setPlaceholderText("e.g. 5, 6, 7, 9")
lay.addRow("Digital Output Pins:", self.do_pins_edit)
- self.sim_chk = QCheckBox("Simulation mode")
- self.sim_chk.setChecked(False)
- lay.addRow(self.sim_chk)
-
- lay.addRow(PortScanGroup(self.port_edit))
-
- def build_device(self, device_id: str) -> ArduinoDevice:
- from devices.arduino_device import _parse_pin_edit, _parse_analog_pin_edit
- analog = _parse_analog_pin_edit(self.analog_pins_edit.text()) or None
- di = _parse_pin_edit(self.di_pins_edit.text()) or None
- do = _parse_pin_edit(self.do_pins_edit.text()) or None
- return ArduinoDevice(
- device_id=device_id,
- analog_pins=analog,
- di_pins=di,
- do_pins=do,
- simulate=self.sim_chk.isChecked(),
- port=self.port_edit.text().strip() or "COM3",
- baud=int(self.baud_cb.currentText()),
- )
-
class NidaqmxPanel(QWidget):
- """Config fields for NidaqmxDevice (analog + digital I/O combined)."""
-
def __init__(self):
super().__init__()
lay = QFormLayout(self)
lay.setContentsMargins(0, 4, 0, 4)
- self.ni_device_edit = QLineEdit("Dev1")
- lay.addRow("NI Device:", self.ni_device_edit)
-
self.analog_spin = QSpinBox()
self.analog_spin.setRange(0, 16)
self.analog_spin.setValue(4)
@@ -264,90 +212,29 @@ class NidaqmxPanel(QWidget):
self.do_spin.setValue(4)
lay.addRow("Digital Outputs:", self.do_spin)
- self.sim_chk = QCheckBox("Simulation mode")
- self.sim_chk.setChecked(False)
- lay.addRow(self.sim_chk)
-
- lay.addRow(NIScanGroup(self.ni_device_edit))
-
- def build_device(self, device_id: str) -> NidaqmxDevice:
- return NidaqmxDevice(
- device_id=device_id,
- num_analog=self.analog_spin.value(),
- min_v=self.min_v_spin.value(),
- max_v=self.max_v_spin.value(),
- num_di=self.di_spin.value(),
- num_do=self.do_spin.value(),
- simulate=self.sim_chk.isChecked(),
- ni_device=self.ni_device_edit.text().strip() or "Dev1",
- )
-
-
-class SerialPanel(QWidget):
- """Config fields for SerialDevice β€” port, baud, protocol/format, simulate."""
-
- def __init__(self):
- super().__init__()
- lay = QVBoxLayout(self)
- lay.setContentsMargins(0, 4, 0, 4)
- lay.setSpacing(8)
-
- form = QFormLayout()
- self.port_edit = QLineEdit("")
- self.port_edit.setPlaceholderText("e.g. COM3 or /dev/ttyUSB0")
- form.addRow("Port:", self.port_edit)
+# ── Panel registry (order matters: first = default) ───────────────────────────
- self.baud_cb = QComboBox()
- self.baud_cb.addItems(["1200", "2400", "4800", "9600", "19200", "38400",
- "57600", "115200", "230400", "460800"])
- self.baud_cb.setCurrentText("115200")
- form.addRow("Baud Rate:", self.baud_cb)
-
- self.fmt_cb = QComboBox()
- self.fmt_cb.addItems(list(_FORMAT_LABELS.keys()))
- form.addRow("Protocol / Format:", self.fmt_cb)
-
- self.sim_chk = QCheckBox("Simulation mode")
- self.sim_chk.setChecked(False)
- form.addRow(self.sim_chk)
-
- note = QLabel("Protocol-specific settings available in device config dialog.")
- note.setObjectName("traceSource")
- note.setWordWrap(True)
-
- lay.addLayout(form)
- lay.addWidget(note)
- lay.addWidget(PortScanGroup(self.port_edit))
+_PANELS = {
+ "Serial / UART": (SerialPanel, "ser"),
+ "Arduino": (ArduinoPanel, "ard"),
+ "NI-DAQ": (NidaqmxPanel, "ni"),
+}
- def build_device(self, device_id: str) -> SerialDevice:
- fmt = _FORMAT_LABELS.get(self.fmt_cb.currentText(), "key:val")
- return SerialDevice(
- device_id=device_id,
- port=self.port_edit.text().strip() or "COM3",
- baud_rate=int(self.baud_cb.currentText()),
- parse_format=fmt,
- simulate=self.sim_chk.isChecked(),
- )
+_SERIAL_TYPES = {"Serial / UART", "Arduino"}
+_NI_TYPES = {"NI-DAQ"}
# ── Main dialog ───────────────────────────────────────────────────────────────
-_PANELS = {
- "Arduino": (ArduinoPanel, "ard"),
- "NI-DAQ": (NidaqmxPanel, "ni"),
- "Serial / UART":(SerialPanel, "ser"),
-}
-
-
class AddDeviceDialog(QDialog):
def __init__(self, registry: DeviceRegistry, parent=None):
super().__init__(parent)
self.registry = registry
self.created_device = None
self.setWindowTitle("Add Device")
- self.setMinimumSize(480, 480)
- self.resize(500, 620)
+ self.setMinimumSize(480, 580)
+ self.resize(500, 700)
self._build()
def _build(self):
@@ -358,26 +245,67 @@ class AddDeviceDialog(QDialog):
hdr.setObjectName("devWindowTitle")
root.addWidget(hdr)
- div = QFrame()
- div.setFrameShape(QFrame.Shape.HLine)
- div.setObjectName("devWindowDivider")
- root.addWidget(div)
+ root.addWidget(_divider())
+
+ # ── Available Devices ─────────────────────────────────────────────
+ avail_grp = QGroupBox("Available Devices")
+ avail_lay = QVBoxLayout(avail_grp)
+ avail_lay.setSpacing(6)
+
+ self._scanner = _DeviceListScanner()
+ self._scanner.device_selected.connect(self._on_device_selected)
+ avail_lay.addWidget(self._scanner)
+
+ conn_form = QFormLayout()
+ conn_form.setSpacing(6)
+
+ self._port_edit = QLineEdit()
+ self._port_edit.setPlaceholderText("e.g. COM3 or /dev/ttyUSB0")
+ self._port_lbl = QLabel("Port:")
+ conn_form.addRow(self._port_lbl, self._port_edit)
+
+ self._baud_cb = QComboBox()
+ self._baud_cb.addItems(["1200", "2400", "4800", "9600", "19200", "38400",
+ "57600", "115200", "230400", "460800"])
+ self._baud_cb.setCurrentText("115200")
+ self._baud_lbl = QLabel("Baud Rate:")
+ conn_form.addRow(self._baud_lbl, self._baud_cb)
+
+ self._ni_edit = QLineEdit("Dev1")
+ self._ni_lbl = QLabel("NI Device:")
+ conn_form.addRow(self._ni_lbl, self._ni_edit)
+
+ self._sim_chk = QCheckBox("Simulation mode")
+ conn_form.addRow(self._sim_chk)
+
+ avail_lay.addLayout(conn_form)
+ root.addWidget(avail_grp)
+
+ root.addWidget(_divider())
+
+ # ── Configuration ─────────────────────────────────────────────────
+ cfg_hdr = QLabel("Configuration")
+ cfg_hdr.setObjectName("devWindowTitle")
+ root.addWidget(cfg_hdr)
+
+ cfg_form = QFormLayout()
+ cfg_form.setSpacing(6)
- type_row = QFormLayout()
self._type_cb = QComboBox()
self._type_cb.addItems(list(_PANELS.keys()))
self._type_cb.currentIndexChanged.connect(self._on_type_changed)
- type_row.addRow("Device Type:", self._type_cb)
+ cfg_form.addRow("Device Type:", self._type_cb)
self._id_edit = QLineEdit()
self._id_edit.setPlaceholderText("Leave blank for auto")
- type_row.addRow("Device ID:", self._id_edit)
- root.addLayout(type_row)
+ cfg_form.addRow("Device ID:", self._id_edit)
+
+ self._fmt_cb = QComboBox()
+ self._fmt_cb.addItems(list(_FORMAT_LABELS.keys()))
+ self._fmt_lbl = QLabel("Protocol / Format:")
+ cfg_form.addRow(self._fmt_lbl, self._fmt_cb)
- div2 = QFrame()
- div2.setFrameShape(QFrame.Shape.HLine)
- div2.setObjectName("devWindowDivider")
- root.addWidget(div2)
+ root.addLayout(cfg_form)
self._stack = QStackedWidget()
self._panels = {}
@@ -387,28 +315,59 @@ class AddDeviceDialog(QDialog):
self._stack.addWidget(panel)
root.addWidget(self._stack, 1)
- div3 = QFrame()
- div3.setFrameShape(QFrame.Shape.HLine)
- div3.setObjectName("devWindowDivider")
- root.addWidget(div3)
+ root.addWidget(_divider())
btn_row = QHBoxLayout()
btn_row.addStretch()
cancel = QPushButton("Cancel")
cancel.clicked.connect(self.reject)
- add = QPushButton("Add Device")
- add.setObjectName("applyButton")
- add.setDefault(True)
- add.clicked.connect(self._on_add)
+ self._add_btn = QPushButton("Add Device")
+ self._add_btn.setObjectName("applyButton")
+ self._add_btn.setDefault(True)
+ self._add_btn.clicked.connect(self._on_add)
btn_row.addWidget(cancel)
- btn_row.addWidget(add)
+ btn_row.addWidget(self._add_btn)
root.addLayout(btn_row)
self._on_type_changed(0)
+ # ── Handlers ──────────────────────────────────────────────────────────
+
+ def _on_device_selected(self, kind: str, value):
+ if kind == "serial":
+ self._port_edit.setText(value)
+ self._set_type("Serial / UART")
+ elif kind == "ni":
+ self._ni_edit.setText(value)
+ self._set_type("NI-DAQ")
+ elif kind == "camera":
+ self._set_type("Camera")
+ panel = self._panels.get("Camera")
+ if panel and hasattr(panel, "set_camera_index"):
+ panel.set_camera_index(value)
+
+ def _set_type(self, name: str):
+ idx = self._type_cb.findText(name)
+ if idx >= 0:
+ self._type_cb.setCurrentIndex(idx)
+
def _on_type_changed(self, idx: int):
self._stack.setCurrentIndex(idx)
type_name = self._type_cb.currentText()
+
+ is_serial = type_name in _SERIAL_TYPES
+ is_ni = type_name in _NI_TYPES
+ is_serial_uart = type_name == "Serial / UART"
+
+ self._port_lbl.setVisible(is_serial)
+ self._port_edit.setVisible(is_serial)
+ self._baud_lbl.setVisible(is_serial)
+ self._baud_cb.setVisible(is_serial)
+ self._ni_lbl.setVisible(is_ni)
+ self._ni_edit.setVisible(is_ni)
+ self._fmt_lbl.setVisible(is_serial_uart)
+ self._fmt_cb.setVisible(is_serial_uart)
+
_, prefix = _PANELS.get(type_name, (None, "dev"))
existing = {d.info.device_id for d in self.registry.all_instances()}
for i in range(100):
@@ -420,7 +379,22 @@ class AddDeviceDialog(QDialog):
def _on_add(self):
type_name = self._type_cb.currentText()
panel = self._panels[type_name]
- dev_id = self._id_edit.text().strip()
+ sim = self._sim_chk.isChecked()
+
+ if type_name in _SERIAL_TYPES and not sim and not self._port_edit.text().strip():
+ QMessageBox.warning(self, "Port Required",
+ "Enter a port (e.g. COM3 or /dev/ttyUSB0)\n"
+ "or enable Simulation mode.")
+ self._port_edit.setFocus()
+ return
+ if type_name in _NI_TYPES and not sim and not self._ni_edit.text().strip():
+ QMessageBox.warning(self, "NI Device Required",
+ "Enter an NI device name (e.g. Dev1)\n"
+ "or enable Simulation mode.")
+ self._ni_edit.setFocus()
+ return
+
+ dev_id = self._id_edit.text().strip()
if not dev_id:
_, prefix = _PANELS.get(type_name, (None, "dev"))
@@ -437,9 +411,52 @@ class AddDeviceDialog(QDialog):
f"Choose a different ID.")
return
+ port = self._port_edit.text().strip() or "COM3"
+ baud = int(self._baud_cb.currentText())
+ ni = self._ni_edit.text().strip() or "Dev1"
+ fmt = _FORMAT_LABELS.get(self._fmt_cb.currentText(), "key:val")
+
try:
- dev = panel.build_device(dev_id)
+ if type_name == "Serial / UART":
+ dev = SerialDevice(
+ device_id=dev_id, port=port, baud_rate=baud,
+ parse_format=fmt, simulate=sim,
+ )
+ elif type_name == "Arduino":
+ from devices.arduino_device import _parse_pin_edit, _parse_analog_pin_edit
+ analog = _parse_analog_pin_edit(panel.analog_pins_edit.text()) or None
+ di = _parse_pin_edit(panel.di_pins_edit.text()) or None
+ do = _parse_pin_edit(panel.do_pins_edit.text()) or None
+ dev = ArduinoDevice(
+ device_id=dev_id, analog_pins=analog, di_pins=di, do_pins=do,
+ simulate=sim, port=port, baud=baud,
+ )
+ elif type_name == "NI-DAQ":
+ dev = NidaqmxDevice(
+ device_id=dev_id,
+ num_analog=panel.analog_spin.value(),
+ min_v=panel.min_v_spin.value(),
+ max_v=panel.max_v_spin.value(),
+ num_di=panel.di_spin.value(),
+ num_do=panel.do_spin.value(),
+ simulate=sim,
+ ni_device=ni,
+ )
+ else:
+ if hasattr(panel, "set_simulate"):
+ panel.set_simulate(sim)
+ dev = panel.build_device(dev_id)
+
self.created_device = dev
self.accept()
except Exception as e:
QMessageBox.critical(self, "Error creating device", str(e))
+
+
+# ── Helpers ───────────────────────────────────────────────────────────────────
+
+def _divider() -> QFrame:
+ div = QFrame()
+ div.setFrameShape(QFrame.Shape.HLine)
+ div.setObjectName("devWindowDivider")
+ return div