summaryrefslogtreecommitdiff
path: root/api_layers/port_registry.py
blob: e3c85fd45210d7296c2434e4bf8ce728df3f0591 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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()