From 4db119fb9c04428e7aef6f1a278c1639aa867baf Mon Sep 17 00:00:00 2001 From: Christian Kolset Date: Tue, 21 Apr 2026 22:58:30 -0600 Subject: Major update to Output control Including: - Control panel - Change status of simulation after devices creation. - Arduino serial conflict with multiple channels. - Claude instructions. - Update to arduino on board code. --- api_layers/__pycache__/__init__.cpython-312.pyc | Bin 322 -> 309 bytes .../__pycache__/arduino_layer.cpython-312.pyc | Bin 30786 -> 31077 bytes .../__pycache__/nidaqmx_layer.cpython-312.pyc | Bin 8512 -> 8492 bytes .../__pycache__/port_registry.cpython-312.pyc | Bin 0 -> 5972 bytes api_layers/arduino_layer.py | 12 +- api_layers/port_registry.py | 124 +++++++++++++++++++++ 6 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 api_layers/__pycache__/port_registry.cpython-312.pyc create mode 100644 api_layers/port_registry.py (limited to 'api_layers') diff --git a/api_layers/__pycache__/__init__.cpython-312.pyc b/api_layers/__pycache__/__init__.cpython-312.pyc index 8aa354b..3c56ecd 100644 Binary files a/api_layers/__pycache__/__init__.cpython-312.pyc and b/api_layers/__pycache__/__init__.cpython-312.pyc differ diff --git a/api_layers/__pycache__/arduino_layer.cpython-312.pyc b/api_layers/__pycache__/arduino_layer.cpython-312.pyc index f6c3ff9..a0191ad 100644 Binary files a/api_layers/__pycache__/arduino_layer.cpython-312.pyc and b/api_layers/__pycache__/arduino_layer.cpython-312.pyc differ diff --git a/api_layers/__pycache__/nidaqmx_layer.cpython-312.pyc b/api_layers/__pycache__/nidaqmx_layer.cpython-312.pyc index de2c034..93a5ef2 100644 Binary files a/api_layers/__pycache__/nidaqmx_layer.cpython-312.pyc and b/api_layers/__pycache__/nidaqmx_layer.cpython-312.pyc differ diff --git a/api_layers/__pycache__/port_registry.cpython-312.pyc b/api_layers/__pycache__/port_registry.cpython-312.pyc new file mode 100644 index 0000000..44a52e8 Binary files /dev/null and b/api_layers/__pycache__/port_registry.cpython-312.pyc differ 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() -- cgit v1.2.3