summaryrefslogtreecommitdiff
path: root/ui/add_device_dialog.py
diff options
context:
space:
mode:
authorChristian Kolset <christian.kolset@gmail.com>2026-04-27 16:53:14 -0600
committerChristian Kolset <christian.kolset@gmail.com>2026-04-27 16:53:14 -0600
commit2b9943ba0d28449a590acfa8a41555b174b6ba84 (patch)
treee8bef8c9bc61dd7e2f9c8a9b177058a4b09546db /ui/add_device_dialog.py
parent67cfa0a514c7de4605ed7360e15a81aa781e510e (diff)
Improved arduino interface. Now allowing the user to configure pins on the fly.
Diffstat (limited to 'ui/add_device_dialog.py')
-rw-r--r--ui/add_device_dialog.py263
1 files changed, 100 insertions, 163 deletions
diff --git a/ui/add_device_dialog.py b/ui/add_device_dialog.py
index 4df71a3..5258995 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 — type-aware, shows the right fields for each device.
-
-Each device type gets its own config panel so the user sees exactly
-the fields they need and nothing irrelevant.
-
-Serial port scanning populates a clickable list that sets the port field.
+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.
"""
import os
@@ -21,26 +18,22 @@ from PyQt6.QtCore import Qt, QThread, pyqtSignal
from PyQt6.QtGui import QFont
from devices.device_registry import DeviceRegistry
-from devices.analog_input import AnalogInputDevice
-from devices.digital_io import DigitalIODevice
+from devices.arduino_device import ArduinoDevice
+from devices.nidaqmx_device import NidaqmxDevice
from devices.serial_device import SerialDevice, _FORMAT_LABELS
-# ── Port scanner thread ───────────────────────────────────────────────────────
+# ── Background scan threads ───────────────────────────────────────────────────
class PortScanThread(QThread):
ports_found = pyqtSignal(list)
-
def run(self):
from api_layers.arduino_layer import ArduinoLayer
self.ports_found.emit(ArduinoLayer.list_ports())
-# ── Background scan threads ───────────────────────────────────────────────────
-
class NIScanThread(QThread):
devices_found = pyqtSignal(list)
-
def run(self):
try:
import nidaqmx # type: ignore
@@ -50,14 +43,14 @@ class NIScanThread(QThread):
self.devices_found.emit(devs)
-# ── Reusable port scanner widget ─────────────────────────────────────────────
+# ── Reusable scanner widgets ──────────────────────────────────────────────────
class PortScanGroup(QGroupBox):
"""Scan-and-click widget that fills a target QLineEdit with the chosen port."""
def __init__(self, target_edit: QLineEdit):
super().__init__("Available Serial Ports")
- self._target = target_edit
+ self._target = target_edit
self._scanner = None
lay = QVBoxLayout(self)
@@ -121,7 +114,7 @@ class NIScanGroup(QGroupBox):
def __init__(self, target_edit: QLineEdit):
super().__init__("Available NI Devices")
- self._target = target_edit
+ self._target = target_edit
self._scanner = None
lay = QVBoxLayout(self)
@@ -182,159 +175,111 @@ class NIScanGroup(QGroupBox):
# ── Per-type config panels ────────────────────────────────────────────────────
-class AnalogInputPanel(QWidget):
- """Config fields for AnalogInputDevice."""
+class ArduinoPanel(QWidget):
+ """Config fields for ArduinoDevice (analog + digital I/O combined)."""
def __init__(self):
super().__init__()
lay = QFormLayout(self)
lay.setContentsMargins(0, 4, 0, 4)
- # Backend
- self.backend_cb = QComboBox()
- self.backend_cb.addItems(["nidaqmx", "arduino"])
- lay.addRow("Backend:", self.backend_cb)
-
- # Channel count
- self.ch_spin = QSpinBox()
- self.ch_spin.setRange(1, 16)
- self.ch_spin.setValue(4)
- lay.addRow("Channels:", self.ch_spin)
-
- # Voltage range (NI)
- self.ni_device_edit = QLineEdit("Dev1")
- lay.addRow("NI Device:", self.ni_device_edit)
+ self.port_edit = QLineEdit("")
+ self.port_edit.setPlaceholderText("e.g. COM3 or /dev/ttyUSB0")
+ lay.addRow("Port:", self.port_edit)
- self.min_v_spin = QDoubleSpinBox()
- self.min_v_spin.setRange(-100.0, 0.0)
- self.min_v_spin.setValue(-10.0)
- self.min_v_spin.setSuffix(" V")
- lay.addRow("Min Voltage:", self.min_v_spin)
+ self.baud_cb = QComboBox()
+ self.baud_cb.addItems(["9600", "57600", "115200", "230400"])
+ self.baud_cb.setCurrentText("115200")
+ lay.addRow("Baud Rate:", self.baud_cb)
- self.max_v_spin = QDoubleSpinBox()
- self.max_v_spin.setRange(0.0, 100.0)
- self.max_v_spin.setValue(10.0)
- self.max_v_spin.setSuffix(" V")
- lay.addRow("Max Voltage:", self.max_v_spin)
+ self.analog_pins_edit = QLineEdit("0, 1, 2, 3")
+ self.analog_pins_edit.setPlaceholderText("e.g. 0, 1, 2, 3 (indices into A0–A5)")
+ lay.addRow("Analog Input Pins:", self.analog_pins_edit)
- self.ard_port_edit = QLineEdit("")
- self.ard_port_edit.setPlaceholderText("e.g. COM3 or /dev/ttyUSB0")
- lay.addRow("Arduino Port:", self.ard_port_edit)
+ self.di_pins_edit = QLineEdit("2, 3")
+ self.di_pins_edit.setPlaceholderText("e.g. 2, 3, 8")
+ lay.addRow("Digital Input Pins:", self.di_pins_edit)
- self.ard_baud_cb = QComboBox()
- self.ard_baud_cb.addItems(["9600", "57600", "115200", "230400"])
- self.ard_baud_cb.setCurrentText("115200")
- lay.addRow("Arduino Baud:", self.ard_baud_cb)
+ self.do_pins_edit = QLineEdit("5, 6, 7, 9")
+ self.do_pins_edit.setPlaceholderText("e.g. 5, 6, 7, 9")
+ lay.addRow("Digital Output Pins:", self.do_pins_edit)
- # Simulate
self.sim_chk = QCheckBox("Simulation mode")
self.sim_chk.setChecked(False)
lay.addRow(self.sim_chk)
- self._ni_scan_grp = NIScanGroup(self.ni_device_edit)
- lay.addRow(self._ni_scan_grp)
-
- self._ard_scan_grp = PortScanGroup(self.ard_port_edit)
- lay.addRow(self._ard_scan_grp)
-
- self._ni_widgets = [self.ni_device_edit, self.min_v_spin, self.max_v_spin, self._ni_scan_grp]
- self._ard_widgets = [self.ard_port_edit, self.ard_baud_cb, self._ard_scan_grp]
- self.backend_cb.currentTextChanged.connect(self._on_backend_changed)
- self._on_backend_changed(self.backend_cb.currentText())
-
- def _on_backend_changed(self, backend: str):
- ni = backend == "nidaqmx"
- lay = self.layout()
- for w in self._ni_widgets:
- w.setVisible(ni)
- lbl = lay.labelForField(w)
- if lbl:
- lbl.setVisible(ni)
- for w in self._ard_widgets:
- w.setVisible(not ni)
- lbl = lay.labelForField(w)
- if lbl:
- lbl.setVisible(not ni)
-
- def build_device(self, device_id: str) -> AnalogInputDevice:
- return AnalogInputDevice(
+ 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,
- num_channels=self.ch_spin.value(),
+ analog_pins=analog,
+ di_pins=di,
+ do_pins=do,
simulate=self.sim_chk.isChecked(),
- backend=self.backend_cb.currentText(),
- ni_device=self.ni_device_edit.text().strip() or "Dev1",
- ni_min_v=self.min_v_spin.value(),
- ni_max_v=self.max_v_spin.value(),
- ard_port=self.ard_port_edit.text().strip() or "COM3",
- ard_baud=int(self.ard_baud_cb.currentText()),
+ port=self.port_edit.text().strip() or "COM3",
+ baud=int(self.baud_cb.currentText()),
)
-class DigitalIOPanel(QWidget):
- """Config fields for DigitalIODevice."""
+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.backend_cb = QComboBox()
- self.backend_cb.addItems(["nidaqmx", "arduino"])
- lay.addRow("Backend:", self.backend_cb)
+ 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)
+ lay.addRow("Analog Inputs:", self.analog_spin)
- self.in_spin = QSpinBox()
- self.in_spin.setRange(0, 32)
- self.in_spin.setValue(4)
- lay.addRow("Digital Inputs:", self.in_spin)
+ self.min_v_spin = QDoubleSpinBox()
+ self.min_v_spin.setRange(-100.0, 0.0)
+ self.min_v_spin.setValue(-10.0)
+ self.min_v_spin.setSuffix(" V")
+ lay.addRow("Min Voltage:", self.min_v_spin)
- self.out_spin = QSpinBox()
- self.out_spin.setRange(0, 32)
- self.out_spin.setValue(4)
- lay.addRow("Digital Outputs:", self.out_spin)
+ self.max_v_spin = QDoubleSpinBox()
+ self.max_v_spin.setRange(0.0, 100.0)
+ self.max_v_spin.setValue(10.0)
+ self.max_v_spin.setSuffix(" V")
+ lay.addRow("Max Voltage:", self.max_v_spin)
- self.ni_device_edit = QLineEdit("Dev1")
- lay.addRow("NI Device:", self.ni_device_edit)
+ self.di_spin = QSpinBox()
+ self.di_spin.setRange(0, 32)
+ self.di_spin.setValue(2)
+ lay.addRow("Digital Inputs:", self.di_spin)
- self.ard_port_edit = QLineEdit("")
- self.ard_port_edit.setPlaceholderText("e.g. COM3 or /dev/ttyUSB0")
- lay.addRow("Arduino Port:", self.ard_port_edit)
+ self.do_spin = QSpinBox()
+ self.do_spin.setRange(0, 32)
+ 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)
- self._ni_scan_grp = NIScanGroup(self.ni_device_edit)
- lay.addRow(self._ni_scan_grp)
-
- self._ard_scan_grp = PortScanGroup(self.ard_port_edit)
- lay.addRow(self._ard_scan_grp)
-
- self.backend_cb.currentTextChanged.connect(self._on_backend_changed)
- self._on_backend_changed(self.backend_cb.currentText())
-
- def _on_backend_changed(self, backend: str):
- ni = backend == "nidaqmx"
- lay = self.layout()
- for w, show in [
- (self.ni_device_edit, ni),
- (self._ni_scan_grp, ni),
- (self.ard_port_edit, not ni),
- (self._ard_scan_grp, not ni),
- ]:
- w.setVisible(show)
- lbl = lay.labelForField(w)
- if lbl:
- lbl.setVisible(show)
-
- def build_device(self, device_id: str) -> DigitalIODevice:
- return DigitalIODevice(
+ lay.addRow(NIScanGroup(self.ni_device_edit))
+
+ def build_device(self, device_id: str) -> NidaqmxDevice:
+ return NidaqmxDevice(
device_id=device_id,
- num_inputs=self.in_spin.value(),
- num_outputs=self.out_spin.value(),
+ 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(),
- backend=self.backend_cb.currentText(),
ni_device=self.ni_device_edit.text().strip() or "Dev1",
- ard_port=self.ard_port_edit.text().strip() or "COM3",
)
@@ -367,7 +312,7 @@ class SerialPanel(QWidget):
self.sim_chk.setChecked(False)
form.addRow(self.sim_chk)
- note = QLabel("Protocol-specific settings (queries, registers, motors) available in device config dialog.")
+ note = QLabel("Protocol-specific settings available in device config dialog.")
note.setObjectName("traceSource")
note.setWordWrap(True)
@@ -389,9 +334,9 @@ class SerialPanel(QWidget):
# ── Main dialog ───────────────────────────────────────────────────────────────
_PANELS = {
- "Analog Input": AnalogInputPanel,
- "Digital I/O": DigitalIOPanel,
- "Serial / UART": SerialPanel,
+ "Arduino": (ArduinoPanel, "ard"),
+ "NI-DAQ": (NidaqmxPanel, "ni"),
+ "Serial / UART":(SerialPanel, "ser"),
}
@@ -401,23 +346,23 @@ class AddDeviceDialog(QDialog):
self.registry = registry
self.created_device = None
self.setWindowTitle("Add Device")
- self.setMinimumSize(480, 460)
- self.resize(500, 600)
+ self.setMinimumSize(480, 480)
+ self.resize(500, 620)
self._build()
def _build(self):
root = QVBoxLayout(self)
root.setSpacing(10)
- # Header
hdr = QLabel("Add New Device")
hdr.setObjectName("devWindowTitle")
root.addWidget(hdr)
- div = QFrame(); div.setFrameShape(QFrame.Shape.HLine)
- div.setObjectName("devWindowDivider"); root.addWidget(div)
+ div = QFrame()
+ div.setFrameShape(QFrame.Shape.HLine)
+ div.setObjectName("devWindowDivider")
+ root.addWidget(div)
- # Device type selector
type_row = QFormLayout()
self._type_cb = QComboBox()
self._type_cb.addItems(list(_PANELS.keys()))
@@ -425,26 +370,28 @@ class AddDeviceDialog(QDialog):
type_row.addRow("Device Type:", self._type_cb)
self._id_edit = QLineEdit()
- self._id_edit.setPlaceholderText("Leave blank for auto (e.g. ai_1, dio_0)")
+ self._id_edit.setPlaceholderText("Leave blank for auto")
type_row.addRow("Device ID:", self._id_edit)
root.addLayout(type_row)
- div2 = QFrame(); div2.setFrameShape(QFrame.Shape.HLine)
- div2.setObjectName("devWindowDivider"); root.addWidget(div2)
+ div2 = QFrame()
+ div2.setFrameShape(QFrame.Shape.HLine)
+ div2.setObjectName("devWindowDivider")
+ root.addWidget(div2)
- # Stacked type-specific panels
self._stack = QStackedWidget()
self._panels = {}
- for name, cls in _PANELS.items():
+ for name, (cls, _prefix) in _PANELS.items():
panel = cls()
self._panels[name] = panel
self._stack.addWidget(panel)
root.addWidget(self._stack, 1)
- div3 = QFrame(); div3.setFrameShape(QFrame.Shape.HLine)
- div3.setObjectName("devWindowDivider"); root.addWidget(div3)
+ div3 = QFrame()
+ div3.setFrameShape(QFrame.Shape.HLine)
+ div3.setObjectName("devWindowDivider")
+ root.addWidget(div3)
- # Buttons
btn_row = QHBoxLayout()
btn_row.addStretch()
cancel = QPushButton("Cancel")
@@ -457,16 +404,12 @@ class AddDeviceDialog(QDialog):
btn_row.addWidget(add)
root.addLayout(btn_row)
+ self._on_type_changed(0)
+
def _on_type_changed(self, idx: int):
self._stack.setCurrentIndex(idx)
- # Auto-suggest a device ID based on type
type_name = self._type_cb.currentText()
- prefixes = {
- "Analog Input": "ai",
- "Digital I/O": "dio",
- "Serial / UART": "ser",
- }
- prefix = prefixes.get(type_name, "dev")
+ _, prefix = _PANELS.get(type_name, (None, "dev"))
existing = {d.info.device_id for d in self.registry.all_instances()}
for i in range(100):
candidate = f"{prefix}_{i}"
@@ -479,14 +422,8 @@ class AddDeviceDialog(QDialog):
panel = self._panels[type_name]
dev_id = self._id_edit.text().strip()
- # Auto-generate ID if blank
if not dev_id:
- prefixes = {
- "Analog Input": "ai",
- "Digital I/O": "dio",
- "Serial / UART": "ser",
- }
- prefix = prefixes.get(type_name, "dev")
+ _, prefix = _PANELS.get(type_name, (None, "dev"))
existing = {d.info.device_id for d in self.registry.all_instances()}
for i in range(100):
candidate = f"{prefix}_{i}"