""" 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()