summaryrefslogtreecommitdiff
path: root/api_layers/port_registry.py
diff options
context:
space:
mode:
authorChristian Kolset <christian.kolset@gmail.com>2026-04-21 22:58:30 -0600
committerChristian Kolset <christian.kolset@gmail.com>2026-04-21 22:58:30 -0600
commit4db119fb9c04428e7aef6f1a278c1639aa867baf (patch)
treee0e0645559637e593f1bbfb9d713256028668527 /api_layers/port_registry.py
parent97e15f69b835af03b071c4eeb4547c65a02e6f80 (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.py124
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()