diff options
36 files changed, 394 insertions, 26 deletions
diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5347d9d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,81 @@ +# CLAUDE.md + +Guidance for Claude Code (claude.ai/code) working in this repo. + +## Running the app + +```bash +python main.py +``` + +No build step. Simulation mode default (no hardware needed). + +## Dependencies + +```bash +pip install PyQt6 pyqtgraph numpy pyserial +pip install nidaqmx # optional — only for real NI hardware +``` + +## Architecture + +### Data flow + +``` +Hardware / Simulation + ↓ + api_layers/ ArduinoLayer, NidaqmxLayer — low-level I/O, background threads + ↓ + devices/ BaseDevice subclasses — wrap api_layers, expose read_channels() / write_channel() + ↓ + core/acquisition.py AcquisitionEngine — polls devices at 100 ms, fills ChannelBuffers, emits new_data signal + ↓ + core/signal_processor.py SignalProcessor — filters + derived channels, re-emits processed_data + ↓ + ui/strip_chart.py StripChartWidget — consumes processed_data, renders via pyqtgraph +``` + +Control widgets (left panel) go other direction: UI → `ControlWidget._write()` → `DeviceRegistry.get_instance()` → `device.write_channel()` → api_layer. + +### Key design patterns + +**Shared serial port** — `api_layers/port_registry.py` holds module-level `port_registry` singleton. When multiple devices share one Arduino (e.g. `AnalogInputDevice` + `DigitalIODevice` on same port), both call `port_registry.get_layer(port, baud)`, get same `ArduinoLayer` instance. Never construct `ArduinoLayer` directly in device code. + +**Device auto-discovery** — `DeviceRegistry._discover()` scans `devices/` with `pkgutil`, imports every module, registers any class subclassing `BaseDevice`. New device type = drop file in `devices/`, no registration needed. + +**Backend switching at runtime** — `AnalogInputDevice` and `DigitalIODevice` both have `switch_backend(backend, simulate, ...)` — disconnects, reconfigures, reconnects without restart. Called from "Apply & Reconnect" in config dialog. + +**Profile persistence** — `core/profile.py` serialises full operator state (controls, plot layout, signal pipelines, derived channels) to `.labdaq` JSON files. `ProfileManager` handles save/load. + +### Directory map + +| Path | Purpose | +|------|---------| +| `api_layers/arduino_layer.py` | Serial protocol + simulation; `ARDUINO_FIRMWARE` string is the uploadable sketch | +| `api_layers/nidaqmx_layer.py` | NI-DAQmx wrapper with simulation fallback | +| `api_layers/port_registry.py` | Shared `ArduinoLayer` singleton per (port, baud) | +| `devices/base_device.py` | `BaseDevice`, `ChannelConfig`, `DeviceInfo`, `DeviceStatus` | +| `devices/analog_input.py` | Analog input — NI or Arduino backend | +| `devices/digital_io.py` | Digital I/O — NI or Arduino backend | +| `devices/serial_device.py` | Generic UART device | +| `core/acquisition.py` | `AcquisitionEngine` + `ChannelBuffer` | +| `core/signal_processor.py` | Filter chain + derived/virtual channels | +| `core/profile.py` | `.labdaq` profile save/load | +| `ui/main_window.py` | Top-level window, toolbar, demo device init | +| `ui/control_panel.py` | Left panel output widgets (`OnOffSwitch`, `MotorControl`, etc.) | +| `ui/strip_chart.py` | Live pyqtgraph chart, config-driven by `LayoutConfig` | +| `ui/windows/` | Floating tool windows (Devices, Signals, Plot, Settings) | +| `ui/style_dark.qss` / `style_light.qss` | Full app theme | + +### Arduino firmware + +Firmware embedded as `ARDUINO_FIRMWARE` in `api_layers/arduino_layer.py`. When editing: set `N_DIG_OUT` to match digital output pins in use (default 0 — leaves pins in INPUT mode, causes inverted write behaviour). Upload via Arduino IDE. + +Serial protocol: `A0:3.14,D2:1\n` stream from Arduino; `W:D7:1\n` / `P:D9:128\n` commands from PC. + +### Adding a new device type + +1. Subclass `BaseDevice` in new file under `devices/` +2. Implement: `connect()`, `disconnect()`, `read_channels() → Dict[str, float]`, `write_channel(channel_id, value) → bool`, `get_config_widget() → QWidget` +3. Include `switch_backend()` if device supports runtime reconfiguration +4. `DeviceRegistry` discovers it automatically on next run
\ No newline at end of file diff --git a/CLAUDE.original.md b/CLAUDE.original.md new file mode 100644 index 0000000..05c7092 --- /dev/null +++ b/CLAUDE.original.md @@ -0,0 +1,81 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Running the app + +```bash +python main.py +``` + +No build step. Runs in simulation mode by default (no hardware needed). + +## Dependencies + +```bash +pip install PyQt6 pyqtgraph numpy pyserial +pip install nidaqmx # optional — only for real NI hardware +``` + +## Architecture + +### Data flow + +``` +Hardware / Simulation + ↓ + api_layers/ ArduinoLayer, NidaqmxLayer — low-level I/O, background threads + ↓ + devices/ BaseDevice subclasses — wrap api_layers, expose read_channels() / write_channel() + ↓ + core/acquisition.py AcquisitionEngine — polls devices at 100 ms, fills ChannelBuffers, emits new_data signal + ↓ + core/signal_processor.py SignalProcessor — filters + derived channels, re-emits processed_data + ↓ + ui/strip_chart.py StripChartWidget — consumes processed_data, renders via pyqtgraph +``` + +Control widgets (left panel) go the other direction: UI → `ControlWidget._write()` → `DeviceRegistry.get_instance()` → `device.write_channel()` → api_layer. + +### Key design patterns + +**Shared serial port** — `api_layers/port_registry.py` holds a module-level `port_registry` singleton. When multiple devices share one Arduino (e.g. `AnalogInputDevice` + `DigitalIODevice` on the same port), both call `port_registry.get_layer(port, baud)` and get the same `ArduinoLayer` instance. Never construct `ArduinoLayer` directly in device code. + +**Device auto-discovery** — `DeviceRegistry._discover()` scans `devices/` with `pkgutil`, imports every module, and registers any class that subclasses `BaseDevice`. Adding a new device type = drop a file in `devices/`, no registration needed. + +**Backend switching at runtime** — `AnalogInputDevice` and `DigitalIODevice` both have `switch_backend(backend, simulate, ...)` that disconnects, reconfigures, and reconnects without restarting. Called from "Apply & Reconnect" in the config dialog. + +**Profile persistence** — `core/profile.py` serialises the full operator state (controls, plot layout, signal pipelines, derived channels) to `.labdaq` JSON files. `ProfileManager` handles save/load. + +### Directory map + +| Path | Purpose | +|------|---------| +| `api_layers/arduino_layer.py` | Serial protocol + simulation; `ARDUINO_FIRMWARE` string is the uploadable sketch | +| `api_layers/nidaqmx_layer.py` | NI-DAQmx wrapper with simulation fallback | +| `api_layers/port_registry.py` | Shared `ArduinoLayer` singleton per (port, baud) | +| `devices/base_device.py` | `BaseDevice`, `ChannelConfig`, `DeviceInfo`, `DeviceStatus` | +| `devices/analog_input.py` | Analog input — NI or Arduino backend | +| `devices/digital_io.py` | Digital I/O — NI or Arduino backend | +| `devices/serial_device.py` | Generic UART device | +| `core/acquisition.py` | `AcquisitionEngine` + `ChannelBuffer` | +| `core/signal_processor.py` | Filter chain + derived/virtual channels | +| `core/profile.py` | `.labdaq` profile save/load | +| `ui/main_window.py` | Top-level window, toolbar, demo device init | +| `ui/control_panel.py` | Left panel output widgets (`OnOffSwitch`, `MotorControl`, etc.) | +| `ui/strip_chart.py` | Live pyqtgraph chart, config-driven by `LayoutConfig` | +| `ui/windows/` | Floating tool windows (Devices, Signals, Plot, Settings) | +| `ui/style_dark.qss` / `style_light.qss` | Full app theme | + +### Arduino firmware + +The firmware is embedded as `ARDUINO_FIRMWARE` in `api_layers/arduino_layer.py`. When editing it, set `N_DIG_OUT` to match the number of digital output pins in use (default was 0 — leaving pins in INPUT mode causes inverted write behaviour). Upload via Arduino IDE. + +Serial protocol: `A0:3.14,D2:1\n` stream from Arduino; `W:D7:1\n` / `P:D9:128\n` commands from PC. + +### Adding a new device type + +1. Subclass `BaseDevice` in a new file under `devices/` +2. Implement: `connect()`, `disconnect()`, `read_channels() → Dict[str, float]`, `write_channel(channel_id, value) → bool`, `get_config_widget() → QWidget` +3. Include `switch_backend()` if the device supports runtime reconfiguration +4. `DeviceRegistry` discovers it automatically on next run diff --git a/api_layers/__pycache__/__init__.cpython-312.pyc b/api_layers/__pycache__/__init__.cpython-312.pyc Binary files differindex 8aa354b..3c56ecd 100644 --- a/api_layers/__pycache__/__init__.cpython-312.pyc +++ b/api_layers/__pycache__/__init__.cpython-312.pyc diff --git a/api_layers/__pycache__/arduino_layer.cpython-312.pyc b/api_layers/__pycache__/arduino_layer.cpython-312.pyc Binary files differindex f6c3ff9..a0191ad 100644 --- a/api_layers/__pycache__/arduino_layer.cpython-312.pyc +++ b/api_layers/__pycache__/arduino_layer.cpython-312.pyc diff --git a/api_layers/__pycache__/nidaqmx_layer.cpython-312.pyc b/api_layers/__pycache__/nidaqmx_layer.cpython-312.pyc Binary files differindex de2c034..93a5ef2 100644 --- a/api_layers/__pycache__/nidaqmx_layer.cpython-312.pyc +++ b/api_layers/__pycache__/nidaqmx_layer.cpython-312.pyc diff --git a/api_layers/__pycache__/port_registry.cpython-312.pyc b/api_layers/__pycache__/port_registry.cpython-312.pyc Binary files differnew file mode 100644 index 0000000..44a52e8 --- /dev/null +++ b/api_layers/__pycache__/port_registry.cpython-312.pyc diff --git a/api_layers/arduino_layer.py b/api_layers/arduino_layer.py index fb09a16..fed54f9 100644 --- a/api_layers/arduino_layer.py +++ b/api_layers/arduino_layer.py @@ -551,7 +551,7 @@ const int DIGITAL_IN[] = {2, 3, 4}; const int DIGITAL_OUT[] = {5, 6, 7, 9, 10, 11}; // 9,10,11 are PWM-capable const int N_ANALOG = 1; // ← set to how many analog pins you use const int N_DIG_IN = 0; // ← set to how many digital inputs you use -const int N_DIG_OUT = 0; // ← set to how many digital outputs you use +const int N_DIG_OUT = 6; // ← set to how many digital outputs you use const int SEND_INTERVAL = 50; // ms between data frames (50 = 20 Hz) const long BAUD_RATE = 115200; @@ -666,16 +666,22 @@ void sendData() { void sendFrame() { String out = ""; + bool first = true; // Analog inputs — converted to 0.0–5.0 V for (int i = 0; i < N_ANALOG; i++) { float v = analogRead(ANALOG_PINS[i]) * (5.0 / 1023.0); - if (i > 0) out += ","; + if (!first) out += ","; out += "A" + String(i) + ":" + String(v, 3); + first = false; } // Digital inputs (INPUT_PULLUP — invert so pressed=1) + // These share the same output line as analog — one line per frame. for (int i = 0; i < N_DIG_IN; i++) { - out += ",D" + String(DIGITAL_IN[i]) + ":" + String(!digitalRead(DIGITAL_IN[i])); + if (!first) out += ","; + out += "D" + String(DIGITAL_IN[i]) + ":" + String(!digitalRead(DIGITAL_IN[i])); + first = false; } Serial.println(out); + // Example combined output: A0:3.142,D2:0,D3:1 } """ diff --git a/api_layers/port_registry.py b/api_layers/port_registry.py new file mode 100644 index 0000000..e3c85fd --- /dev/null +++ b/api_layers/port_registry.py @@ -0,0 +1,124 @@ +""" +api_layers/port_registry.py + +PortRegistry — ensures one ArduinoLayer per physical serial port. + +Problem: if both an AnalogInputDevice and a DigitalIODevice are +configured for the same Arduino (/dev/ttyUSB0), they would each +create their own ArduinoLayer and both try to open the same serial +port → "device reports readiness to read but returned no data". + +Solution: all devices call PortRegistry.get_layer(port, baud) instead +of constructing ArduinoLayer directly. The registry returns an +existing layer if one is already open on that port, or creates a new +one. All reads/writes go through the single shared layer. + +The registry also merges analog_pins across devices so the layer +caches values for all channels on the Arduino. + +Usage (handled automatically by AnalogInputDevice and DigitalIODevice): + + from api_layers.port_registry import port_registry + layer = port_registry.get_layer("/dev/ttyUSB0", 115200, simulate=False) + layer.connect_if_needed() +""" + +from __future__ import annotations +import threading +from typing import Dict, List, Optional, Tuple + + +class PortRegistry: + """ + Global registry of shared ArduinoLayer instances. + Thread-safe singleton accessed via the module-level `port_registry`. + """ + + def __init__(self): + self._lock: threading.Lock = threading.Lock() + self._layers: Dict[Tuple[str, int], object] = {} # (port, baud) → ArduinoLayer + self._ref_counts: Dict[Tuple[str, int], int] = {} + + def get_layer(self, port: str, baud: int, + simulate: bool = False, + extra_pins: List[str] = None) -> "ArduinoLayer": # type: ignore + """ + Return a shared ArduinoLayer for (port, baud). + Creates one if it doesn't exist yet. + If simulate=True, a simulation layer is returned (always separate, + so simulated devices don't share state with each other). + """ + from api_layers.arduino_layer import ArduinoLayer + + if simulate: + # Simulated devices get their own independent layer + layer = ArduinoLayer(port=port, baud=baud, simulate=True) + if extra_pins: + layer.analog_pins = list(extra_pins) + return layer + + key = (port, baud) + with self._lock: + if key not in self._layers: + layer = ArduinoLayer(port=port, baud=baud, simulate=False) + if extra_pins: + layer.analog_pins = list(extra_pins) + self._layers[key] = layer + self._ref_counts[key] = 0 + else: + layer = self._layers[key] + # Merge any new pin names so the shared cache covers them + if extra_pins: + existing = set(layer.analog_pins) + for p in extra_pins: + if p not in existing: + layer.analog_pins.append(p) + self._ref_counts[key] += 1 + return layer + + def release(self, port: str, baud: int) -> None: + """ + Decrement ref count for a port. Disconnects and removes the + layer when no devices are using it any more. + """ + key = (port, baud) + with self._lock: + if key not in self._ref_counts: + return + self._ref_counts[key] -= 1 + if self._ref_counts[key] <= 0: + layer = self._layers.pop(key, None) + self._ref_counts.pop(key, None) + if layer: + try: + layer.disconnect() + except Exception: + pass + + def connect_all(self) -> None: + """Connect any layers that haven't been connected yet.""" + with self._lock: + layers = list(self._layers.values()) + for layer in layers: + if not layer.is_connected: + layer.connect() + + def all_layers(self): + with self._lock: + return list(self._layers.values()) + + def clear(self) -> None: + """Disconnect and remove all managed layers.""" + with self._lock: + layers = list(self._layers.values()) + self._layers.clear() + self._ref_counts.clear() + for layer in layers: + try: + layer.disconnect() + except Exception: + pass + + +# Module-level singleton +port_registry = PortRegistry() diff --git a/core/__pycache__/__init__.cpython-312.pyc b/core/__pycache__/__init__.cpython-312.pyc Binary files differindex 36a9dda..d1caea1 100644 --- a/core/__pycache__/__init__.cpython-312.pyc +++ b/core/__pycache__/__init__.cpython-312.pyc diff --git a/core/__pycache__/acquisition.cpython-312.pyc b/core/__pycache__/acquisition.cpython-312.pyc Binary files differindex 3d036ae..a929bdf 100644 --- a/core/__pycache__/acquisition.cpython-312.pyc +++ b/core/__pycache__/acquisition.cpython-312.pyc diff --git a/core/__pycache__/profile.cpython-312.pyc b/core/__pycache__/profile.cpython-312.pyc Binary files differindex 8637365..1c5afbf 100644 --- a/core/__pycache__/profile.cpython-312.pyc +++ b/core/__pycache__/profile.cpython-312.pyc diff --git a/core/__pycache__/signal_processor.cpython-312.pyc b/core/__pycache__/signal_processor.cpython-312.pyc Binary files differindex fe7bba0..fc31c25 100644 --- a/core/__pycache__/signal_processor.cpython-312.pyc +++ b/core/__pycache__/signal_processor.cpython-312.pyc diff --git a/devices/__init__.py b/devices/__init__.py index f79b333..7cee4ac 100644 --- a/devices/__init__.py +++ b/devices/__init__.py @@ -1,5 +1,4 @@ -# devices/__init__.py -from devices.base_device import BaseDevice, ChannelConfig, DeviceInfo, DeviceStatus +# devices/__init__.py from devices.base_device import BaseDevice, ChannelConfig, DeviceInfo, DeviceStatus from devices.device_registry import DeviceRegistry __all__ = ["BaseDevice", "ChannelConfig", "DeviceInfo", "DeviceStatus", "DeviceRegistry"] diff --git a/devices/__pycache__/__init__.cpython-312.pyc b/devices/__pycache__/__init__.cpython-312.pyc Binary files differindex bd9bb71..7c1bbbd 100644 --- a/devices/__pycache__/__init__.cpython-312.pyc +++ b/devices/__pycache__/__init__.cpython-312.pyc diff --git a/devices/__pycache__/analog_input.cpython-312.pyc b/devices/__pycache__/analog_input.cpython-312.pyc Binary files differindex 2ff8a31..fefede6 100644 --- a/devices/__pycache__/analog_input.cpython-312.pyc +++ b/devices/__pycache__/analog_input.cpython-312.pyc diff --git a/devices/__pycache__/base_device.cpython-312.pyc b/devices/__pycache__/base_device.cpython-312.pyc Binary files differindex f665ff1..bfab8b5 100644 --- a/devices/__pycache__/base_device.cpython-312.pyc +++ b/devices/__pycache__/base_device.cpython-312.pyc diff --git a/devices/__pycache__/device_registry.cpython-312.pyc b/devices/__pycache__/device_registry.cpython-312.pyc Binary files differindex 2694da3..d8b72e0 100644 --- a/devices/__pycache__/device_registry.cpython-312.pyc +++ b/devices/__pycache__/device_registry.cpython-312.pyc diff --git a/devices/__pycache__/digital_io.cpython-312.pyc b/devices/__pycache__/digital_io.cpython-312.pyc Binary files differindex 8c7375b..7fa10a5 100644 --- a/devices/__pycache__/digital_io.cpython-312.pyc +++ b/devices/__pycache__/digital_io.cpython-312.pyc diff --git a/devices/__pycache__/serial_device.cpython-312.pyc b/devices/__pycache__/serial_device.cpython-312.pyc Binary files differindex 7472878..5cacb6b 100644 --- a/devices/__pycache__/serial_device.cpython-312.pyc +++ b/devices/__pycache__/serial_device.cpython-312.pyc diff --git a/devices/analog_input.py b/devices/analog_input.py index 508804f..e1b8b09 100644 --- a/devices/analog_input.py +++ b/devices/analog_input.py @@ -90,11 +90,13 @@ class AnalogInputDevice(BaseDevice): ni_device, ni_min_v, ni_max_v, ard_port, ard_baud, channel_ids): if backend == "arduino": - return ArduinoLayer( + # Use shared layer so two devices on the same port don't conflict + from api_layers.port_registry import port_registry + return port_registry.get_layer( port=ard_port, baud=ard_baud, - analog_pins=channel_ids, simulate=simulate, + extra_pins=channel_ids, ) else: return NidaqmxLayer( @@ -110,13 +112,17 @@ class AnalogInputDevice(BaseDevice): def connect(self) -> bool: self._last_error = "" - # ArduinoLayer uses connect(); NidaqmxLayer uses start() + # ArduinoLayer: use connect_if_needed so shared layers aren't + # opened twice when multiple devices share the same port. + # NidaqmxLayer: uses start() if hasattr(self._layer, "connect"): - ok = self._layer.connect() + if self._layer.is_connected: + ok = True # already open — shared with another device + else: + ok = self._layer.connect() else: ok = self._layer.start() - # Surface the underlying error message if available if not ok and hasattr(self._layer, "last_error"): self._last_error = self._layer.last_error @@ -128,13 +134,19 @@ class AnalogInputDevice(BaseDevice): return ok def disconnect(self) -> None: - try: - if hasattr(self._layer, "disconnect"): - self._layer.disconnect() - elif hasattr(self._layer, "stop"): - self._layer.stop() - except Exception: - pass + # For shared Arduino layers, release our ref count via port_registry. + # The layer stays open until the last device using it disconnects. + if self.backend == "arduino" and not self.simulate: + from api_layers.port_registry import port_registry + port_registry.release(self._ard_port, self._ard_baud) + else: + try: + if hasattr(self._layer, "stop"): + self._layer.stop() + elif hasattr(self._layer, "disconnect"): + self._layer.disconnect() + except Exception: + pass self.status = DeviceStatus.DISCONNECTED def read_channels(self) -> Dict[str, float]: diff --git a/devices/digital_io.py b/devices/digital_io.py index 53014b5..3d24192 100644 --- a/devices/digital_io.py +++ b/devices/digital_io.py @@ -126,12 +126,18 @@ class DigitalIODevice(BaseDevice): return False def _ard_connect(self) -> bool: - from api_layers.arduino_layer import ArduinoLayer - self._ard_layer = ArduinoLayer( - port=self._ard_port, baud=self._ard_baud, - digital_pins=[c.channel_id for c in self.info.channels if c.channel_id.startswith("di")], + from api_layers.port_registry import port_registry + # Get shared layer — won't open a second connection if AnalogInputDevice + # is already connected on the same port + self._ard_layer = port_registry.get_layer( + port=self._ard_port, + baud=self._ard_baud, simulate=False, ) + if self._ard_layer.is_connected: + # Port already open — just register ourselves and return OK + self.status = DeviceStatus.CONNECTED + return True ok = self._ard_layer.connect() self.status = DeviceStatus.CONNECTED if ok else DeviceStatus.ERROR return ok @@ -143,8 +149,10 @@ class DigitalIODevice(BaseDevice): if self._ni_out_task: try: self._ni_out_task.stop(); self._ni_out_task.close() except Exception: pass - if self._ard_layer: - self._ard_layer.disconnect() + if self._ard_layer and self.backend == "arduino" and not self.simulate: + from api_layers.port_registry import port_registry + port_registry.release(self._ard_port, self._ard_baud) + self._ard_layer = None self.status = DeviceStatus.DISCONNECTED def read_channels(self) -> Dict[str, float]: @@ -226,6 +234,24 @@ class DigitalIODevice(BaseDevice): return self._ard_layer.set_parameter(name, value) return False + def switch_backend(self, backend: str, simulate: bool, + ni_device: str, ard_port: str, ard_baud: int) -> None: + was_running = self.status in (DeviceStatus.CONNECTED, DeviceStatus.SIMULATED) + if was_running: + self.disconnect() + + self.backend = backend + self.simulate = simulate + self._ni_device = ni_device + self._ard_port = ard_port + self._ard_baud = ard_baud + self._ard_layer = None + self._ni_in_task = None + self._ni_out_task = None + + if was_running: + self.connect() + def get_config_widget(self) -> QWidget: return DigitalIOConfigWidget(self) @@ -251,7 +277,27 @@ class DigitalIOConfigWidget(QWidget): self.sim_chk = QCheckBox("Simulate") self.sim_chk.setChecked(self.device.simulate) be_form.addRow(self.sim_chk) + + self.ni_dev_edit = QLineEdit(self.device._ni_device) + be_form.addRow("NI Device:", self.ni_dev_edit) + + self.ard_port_edit = QLineEdit(self.device._ard_port) + self.ard_port_edit.setPlaceholderText("e.g. /dev/ttyUSB0 or COM3") + be_form.addRow("Arduino Port:", self.ard_port_edit) + + self.ard_baud_cb = QComboBox() + self.ard_baud_cb.addItems(["9600", "19200", "57600", "115200", "230400"]) + self.ard_baud_cb.setCurrentText(str(self.device._ard_baud)) + be_form.addRow("Baud Rate:", self.ard_baud_cb) + + apply_btn = QPushButton("Apply & Reconnect") + apply_btn.setObjectName("applyButton") + apply_btn.clicked.connect(self._apply) + be_form.addRow(apply_btn) + + self.be_cb.currentTextChanged.connect(self._update_visibility) root.addWidget(be_grp) + self._update_visibility(self.device.backend) out_grp = QGroupBox("Digital Outputs") out_lay = QVBoxLayout(out_grp) @@ -277,3 +323,18 @@ class DigitalIOConfigWidget(QWidget): root.addWidget(out_grp) root.addStretch() + + def _update_visibility(self, backend: str = ""): + backend = backend or self.be_cb.currentText() + self.ni_dev_edit.setVisible(backend == "nidaqmx") + self.ard_port_edit.setVisible(backend == "arduino") + self.ard_baud_cb.setVisible(backend == "arduino") + + def _apply(self): + self.device.switch_backend( + backend=self.be_cb.currentText(), + simulate=self.sim_chk.isChecked(), + ni_device=self.ni_dev_edit.text().strip() or "Dev1", + ard_port=self.ard_port_edit.text().strip(), + ard_baud=int(self.ard_baud_cb.currentText()), + ) diff --git a/ui/__pycache__/__init__.cpython-312.pyc b/ui/__pycache__/__init__.cpython-312.pyc Binary files differindex 6ed2349..fb20d59 100644 --- a/ui/__pycache__/__init__.cpython-312.pyc +++ b/ui/__pycache__/__init__.cpython-312.pyc diff --git a/ui/__pycache__/add_device_dialog.cpython-312.pyc b/ui/__pycache__/add_device_dialog.cpython-312.pyc Binary files differnew file mode 100644 index 0000000..8af1f9c --- /dev/null +++ b/ui/__pycache__/add_device_dialog.cpython-312.pyc diff --git a/ui/__pycache__/config_dialog.cpython-312.pyc b/ui/__pycache__/config_dialog.cpython-312.pyc Binary files differnew file mode 100644 index 0000000..5a051dc --- /dev/null +++ b/ui/__pycache__/config_dialog.cpython-312.pyc diff --git a/ui/__pycache__/control_editor.cpython-312.pyc b/ui/__pycache__/control_editor.cpython-312.pyc Binary files differindex f1716c6..d12d50f 100644 --- a/ui/__pycache__/control_editor.cpython-312.pyc +++ b/ui/__pycache__/control_editor.cpython-312.pyc diff --git a/ui/__pycache__/control_panel.cpython-312.pyc b/ui/__pycache__/control_panel.cpython-312.pyc Binary files differindex c54cd25..834c948 100644 --- a/ui/__pycache__/control_panel.cpython-312.pyc +++ b/ui/__pycache__/control_panel.cpython-312.pyc diff --git a/ui/__pycache__/main_window.cpython-312.pyc b/ui/__pycache__/main_window.cpython-312.pyc Binary files differindex b99d7c2..c5383cb 100644 --- a/ui/__pycache__/main_window.cpython-312.pyc +++ b/ui/__pycache__/main_window.cpython-312.pyc diff --git a/ui/__pycache__/profile_manager_ui.cpython-312.pyc b/ui/__pycache__/profile_manager_ui.cpython-312.pyc Binary files differindex e66a754..c9fbb4a 100644 --- a/ui/__pycache__/profile_manager_ui.cpython-312.pyc +++ b/ui/__pycache__/profile_manager_ui.cpython-312.pyc diff --git a/ui/__pycache__/strip_chart.cpython-312.pyc b/ui/__pycache__/strip_chart.cpython-312.pyc Binary files differindex ba3ca5d..47dbea1 100644 --- a/ui/__pycache__/strip_chart.cpython-312.pyc +++ b/ui/__pycache__/strip_chart.cpython-312.pyc diff --git a/ui/control_editor.py b/ui/control_editor.py index 93b8110..4d18aef 100644 --- a/ui/control_editor.py +++ b/ui/control_editor.py @@ -233,7 +233,7 @@ class ControlEditorDialog(QDialog): btn_row = QHBoxLayout(); btn_row.addStretch() cancel = QPushButton("Cancel"); cancel.clicked.connect(self.reject) - ok = QPushButton("Save Control") + ok = QPushButton("Create Control") ok.setObjectName("applyButton"); ok.setDefault(True) ok.clicked.connect(self._on_ok) btn_row.addWidget(cancel); btn_row.addWidget(ok) diff --git a/ui/main_window.py b/ui/main_window.py index a82a389..a123677 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -59,10 +59,14 @@ class MainWindow(QMainWindow): # ── Demo ────────────────────────────────────────────────────────────── def _init_demo_devices(self): + # NOTE: if you have a real Arduino, configure ard_0 and dio_0 to use + # the same port — port_registry ensures only one serial connection + # is opened, and both devices share it automatically. 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), + AnalogInputDevice(device_id="ard_0", num_channels=1, simulate=True, backend="arduino"), + DigitalIODevice (device_id="dio_0", num_inputs=2, num_outputs=4, + simulate=True, backend="arduino"), SerialDevice (device_id="ser_0", num_channels=3, simulate=True), ]: dev.connect() diff --git a/ui/windows/__pycache__/__init__.cpython-312.pyc b/ui/windows/__pycache__/__init__.cpython-312.pyc Binary files differindex 244f840..37c4a45 100644 --- a/ui/windows/__pycache__/__init__.cpython-312.pyc +++ b/ui/windows/__pycache__/__init__.cpython-312.pyc diff --git a/ui/windows/__pycache__/devices_window.cpython-312.pyc b/ui/windows/__pycache__/devices_window.cpython-312.pyc Binary files differindex f2ce20a..ca01f31 100644 --- a/ui/windows/__pycache__/devices_window.cpython-312.pyc +++ b/ui/windows/__pycache__/devices_window.cpython-312.pyc diff --git a/ui/windows/__pycache__/plot_window.cpython-312.pyc b/ui/windows/__pycache__/plot_window.cpython-312.pyc Binary files differindex 5c957ed..c2e3745 100644 --- a/ui/windows/__pycache__/plot_window.cpython-312.pyc +++ b/ui/windows/__pycache__/plot_window.cpython-312.pyc diff --git a/ui/windows/__pycache__/settings_window.cpython-312.pyc b/ui/windows/__pycache__/settings_window.cpython-312.pyc Binary files differindex 7da4e1a..72f96e1 100644 --- a/ui/windows/__pycache__/settings_window.cpython-312.pyc +++ b/ui/windows/__pycache__/settings_window.cpython-312.pyc diff --git a/ui/windows/__pycache__/signals_window.cpython-312.pyc b/ui/windows/__pycache__/signals_window.cpython-312.pyc Binary files differindex 6b423d8..0526a85 100644 --- a/ui/windows/__pycache__/signals_window.cpython-312.pyc +++ b/ui/windows/__pycache__/signals_window.cpython-312.pyc |
