diff options
| author | Christian Kolset <christian.kolset@gmail.com> | 2026-04-21 22:58:30 -0600 |
|---|---|---|
| committer | Christian Kolset <christian.kolset@gmail.com> | 2026-04-21 22:58:30 -0600 |
| commit | 4db119fb9c04428e7aef6f1a278c1639aa867baf (patch) | |
| tree | e0e0645559637e593f1bbfb9d713256028668527 /api_layers/port_registry.py | |
| parent | 97e15f69b835af03b071c4eeb4547c65a02e6f80 (diff) | |
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.
Diffstat (limited to 'api_layers/port_registry.py')
| -rw-r--r-- | api_layers/port_registry.py | 124 |
1 files changed, 124 insertions, 0 deletions
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() |
