diff options
| author | Christian Kolset <christian.kolset@gmail.com> | 2026-04-27 16:53:14 -0600 |
|---|---|---|
| committer | Christian Kolset <christian.kolset@gmail.com> | 2026-04-27 16:53:14 -0600 |
| commit | 2b9943ba0d28449a590acfa8a41555b174b6ba84 (patch) | |
| tree | e8bef8c9bc61dd7e2f9c8a9b177058a4b09546db /ui/add_device_dialog.py | |
| parent | 67cfa0a514c7de4605ed7360e15a81aa781e510e (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.py | 263 |
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}" |
