modlink_sdk

Public exports for driver authors using the ModLink SDK.

 1"""Public exports for driver authors using the ModLink SDK."""
 2
 3from .driver import (
 4    Driver,
 5    DriverContext,
 6    DriverFactory,
 7    LoopDriver,
 8)
 9from .models import (
10    FrameEnvelope,
11    PayloadType,
12    SearchResult,
13    StreamDescriptor,
14)
15
16__all__ = [
17    "Driver",
18    "DriverContext",
19    "DriverFactory",
20    "LoopDriver",
21    "FrameEnvelope",
22    "PayloadType",
23    "SearchResult",
24    "StreamDescriptor",
25]
class Driver:
 49class Driver:
 50    """Base class for all ModLink drivers.
 51
 52    A driver instance represents one host-managed device endpoint. The host
 53    loads the driver through a zero-argument factory, reads its static
 54    metadata, and then executes lifecycle methods on a dedicated worker
 55    thread.
 56
 57    General contract:
 58
 59    - ``device_id`` must be stable, non-empty, and use ``name.XX`` form.
 60    - ``descriptors()`` must describe every stream the driver may emit.
 61    - control methods are synchronous and run on the driver worker thread.
 62    - ``start_streaming()`` should return quickly after streaming begins.
 63    - one driver instance manages at most one active device connection.
 64    """
 65
 66    supported_providers: tuple[str, ...] = ()
 67    """Provider identifiers accepted by ``search()``."""
 68
 69    @property
 70    def device_id(self) -> str:
 71        raise NotImplementedError(f"{type(self).__name__} must implement device_id")
 72
 73    def __init__(self) -> None:
 74        self._context: DriverContext | None = None
 75
 76    @property
 77    def display_name(self) -> str:
 78        return self.device_id
 79
 80    def bind(self, context: DriverContext) -> None:
 81        """Attach the host runtime context to this driver instance."""
 82        self._context = context
 83
 84    def on_runtime_started(self) -> None:
 85        """Optional hook called after the driver worker thread starts."""
 86
 87    def descriptors(self) -> list[StreamDescriptor]:
 88        raise NotImplementedError(f"{type(self).__name__} must implement descriptors")
 89
 90    def on_shutdown(self) -> None:
 91        """Optional hook called before the driver worker thread exits.
 92
 93        Override to release resources, close connections, etc.
 94        """
 95
 96    def emit_frame(self, frame: FrameEnvelope) -> bool:
 97        return self._require_context().emit_frame(frame)
 98
 99    def emit_connection_lost(self, detail: object) -> None:
100        self._require_context().emit_connection_lost(detail)
101
102    def _require_context(self) -> DriverContext:
103        if self._context is None:
104            raise RuntimeError("driver is not bound to a runtime context")
105        return self._context
106
107    def search(self, provider: str) -> list[SearchResult]:
108        raise NotImplementedError(f"{type(self).__name__} must implement search")
109
110    def connect_device(self, config: SearchResult) -> None:
111        raise NotImplementedError(
112            f"{type(self).__name__} must implement connect_device if it supports connecting"
113        )
114
115    def disconnect_device(self) -> None:
116        raise NotImplementedError(
117            f"{type(self).__name__} must implement disconnect_device if it supports connecting"
118        )
119
120    def start_streaming(self) -> None:
121        raise NotImplementedError(f"{type(self).__name__} must implement start_streaming")
122
123    def stop_streaming(self) -> None:
124        raise NotImplementedError(f"{type(self).__name__} must implement stop_streaming")

Base class for all ModLink drivers.

A driver instance represents one host-managed device endpoint. The host loads the driver through a zero-argument factory, reads its static metadata, and then executes lifecycle methods on a dedicated worker thread.

General contract:

  • device_id must be stable, non-empty, and use name.XX form.
  • descriptors() must describe every stream the driver may emit.
  • control methods are synchronous and run on the driver worker thread.
  • start_streaming() should return quickly after streaming begins.
  • one driver instance manages at most one active device connection.
supported_providers: tuple[str, ...] = ()

Provider identifiers accepted by search().

device_id: str
69    @property
70    def device_id(self) -> str:
71        raise NotImplementedError(f"{type(self).__name__} must implement device_id")
display_name: str
76    @property
77    def display_name(self) -> str:
78        return self.device_id
def bind(self, context: DriverContext) -> None:
80    def bind(self, context: DriverContext) -> None:
81        """Attach the host runtime context to this driver instance."""
82        self._context = context

Attach the host runtime context to this driver instance.

def on_runtime_started(self) -> None:
84    def on_runtime_started(self) -> None:
85        """Optional hook called after the driver worker thread starts."""

Optional hook called after the driver worker thread starts.

def descriptors(self) -> list[StreamDescriptor]:
87    def descriptors(self) -> list[StreamDescriptor]:
88        raise NotImplementedError(f"{type(self).__name__} must implement descriptors")
def on_shutdown(self) -> None:
90    def on_shutdown(self) -> None:
91        """Optional hook called before the driver worker thread exits.
92
93        Override to release resources, close connections, etc.
94        """

Optional hook called before the driver worker thread exits.

Override to release resources, close connections, etc.

def emit_frame(self, frame: FrameEnvelope) -> bool:
96    def emit_frame(self, frame: FrameEnvelope) -> bool:
97        return self._require_context().emit_frame(frame)
def emit_connection_lost(self, detail: object) -> None:
 99    def emit_connection_lost(self, detail: object) -> None:
100        self._require_context().emit_connection_lost(detail)
def search(self, provider: str) -> list[SearchResult]:
107    def search(self, provider: str) -> list[SearchResult]:
108        raise NotImplementedError(f"{type(self).__name__} must implement search")
def connect_device(self, config: SearchResult) -> None:
110    def connect_device(self, config: SearchResult) -> None:
111        raise NotImplementedError(
112            f"{type(self).__name__} must implement connect_device if it supports connecting"
113        )
def disconnect_device(self) -> None:
115    def disconnect_device(self) -> None:
116        raise NotImplementedError(
117            f"{type(self).__name__} must implement disconnect_device if it supports connecting"
118        )
def start_streaming(self) -> None:
120    def start_streaming(self) -> None:
121        raise NotImplementedError(f"{type(self).__name__} must implement start_streaming")
def stop_streaming(self) -> None:
123    def stop_streaming(self) -> None:
124        raise NotImplementedError(f"{type(self).__name__} must implement stop_streaming")
class DriverContext:
12class DriverContext:
13    """Runtime callbacks exposed to one driver instance.
14
15    The host may close a context during shutdown. After closure, late frames
16    and late connection-lost notifications are ignored so helper threads
17    cannot mutate host state after teardown.
18    """
19
20    def __init__(
21        self,
22        *,
23        frame_sink: Callable[[FrameEnvelope], object | None],
24        connection_lost_sink: Callable[[object], None],
25    ) -> None:
26        self._frame_sink = frame_sink
27        self._connection_lost_sink = connection_lost_sink
28        self._lock = RLock()
29        self._closed = False
30
31    def emit_frame(self, frame: FrameEnvelope) -> bool:
32        with self._lock:
33            if self._closed:
34                return False
35            result = self._frame_sink(frame)
36            return result is not False
37
38    def emit_connection_lost(self, detail: object) -> None:
39        with self._lock:
40            if self._closed:
41                return
42            self._connection_lost_sink(detail)
43
44    def _close(self) -> None:
45        with self._lock:
46            self._closed = True

Runtime callbacks exposed to one driver instance.

The host may close a context during shutdown. After closure, late frames and late connection-lost notifications are ignored so helper threads cannot mutate host state after teardown.

DriverContext( *, frame_sink: Callable[[FrameEnvelope], object | None], connection_lost_sink: Callable[[object], None])
20    def __init__(
21        self,
22        *,
23        frame_sink: Callable[[FrameEnvelope], object | None],
24        connection_lost_sink: Callable[[object], None],
25    ) -> None:
26        self._frame_sink = frame_sink
27        self._connection_lost_sink = connection_lost_sink
28        self._lock = RLock()
29        self._closed = False
def emit_frame(self, frame: FrameEnvelope) -> bool:
31    def emit_frame(self, frame: FrameEnvelope) -> bool:
32        with self._lock:
33            if self._closed:
34                return False
35            result = self._frame_sink(frame)
36            return result is not False
def emit_connection_lost(self, detail: object) -> None:
38    def emit_connection_lost(self, detail: object) -> None:
39        with self._lock:
40            if self._closed:
41                return
42            self._connection_lost_sink(detail)
DriverFactory = collections.abc.Callable[[], Driver]
class LoopDriver(modlink_sdk.Driver):
130class LoopDriver(Driver):
131    """Convenience base class for bounded polling drivers with a helper thread.
132
133    This helper favors simple driver code over strict shutdown guarantees.
134    ``loop()`` should stay bounded and return quickly. Long blocking or
135    non-interruptible I/O belongs in a custom ``Driver`` implementation.
136    The host only guarantees that frames and connection-lost callbacks are
137    ignored after shutdown; it does not guarantee the helper thread has
138    already finished by the time teardown returns.
139    """
140
141    loop_interval_ms = 10
142
143    def __init__(self) -> None:
144        super().__init__()
145        self._loop_lock = RLock()
146        self._loop_thread: Thread | None = None
147        self._loop_stop: Event | None = None
148
149    @property
150    def is_looping(self) -> bool:
151        with self._loop_lock:
152            thread = self._loop_thread
153            return thread is not None and thread.is_alive()
154
155    def start_streaming(self) -> None:
156        with self._loop_lock:
157            thread = self._loop_thread
158            if thread is not None and thread.is_alive():
159                return
160        self.on_loop_started()
161        stop_event = Event()
162        thread = Thread(
163            target=self._run_loop_thread,
164            args=(stop_event,),
165            name=f"modlink.loop.{self.device_id.strip()}",
166            daemon=True,
167        )
168        with self._loop_lock:
169            self._loop_stop = stop_event
170            self._loop_thread = thread
171        thread.start()
172
173    def stop_streaming(self) -> None:
174        with self._loop_lock:
175            stop_event = self._loop_stop
176            thread = self._loop_thread
177        if stop_event is None or thread is None:
178            return
179        stop_event.set()
180        if thread is not current_thread():
181            thread.join(timeout=2.0)
182
183    def on_loop_started(self) -> None:
184        """Optional hook called once before loop execution starts."""
185
186    def on_loop_stopped(self) -> None:
187        """Optional hook called once after loop execution stops."""
188
189    def loop(self) -> None:
190        raise NotImplementedError(f"{type(self).__name__} must implement loop")
191
192    def on_shutdown(self) -> None:
193        """Best-effort shutdown hook for the helper loop thread."""
194        self.stop_streaming()
195
196    def _run_loop_thread(self, stop_event: Event) -> None:
197        interval_seconds = max(0.0, float(self.loop_interval_ms)) / 1000.0
198        try:
199            while not stop_event.wait(interval_seconds):
200                self.loop()
201        except Exception as exc:
202            self.emit_connection_lost(f"LOOP_FAILED: {type(exc).__name__}: {exc}")
203        finally:
204            with self._loop_lock:
205                if self._loop_stop is stop_event:
206                    self._loop_stop = None
207                if self._loop_thread is current_thread():
208                    self._loop_thread = None
209            self.on_loop_stopped()

Convenience base class for bounded polling drivers with a helper thread.

This helper favors simple driver code over strict shutdown guarantees. loop() should stay bounded and return quickly. Long blocking or non-interruptible I/O belongs in a custom Driver implementation. The host only guarantees that frames and connection-lost callbacks are ignored after shutdown; it does not guarantee the helper thread has already finished by the time teardown returns.

loop_interval_ms = 10
is_looping: bool
149    @property
150    def is_looping(self) -> bool:
151        with self._loop_lock:
152            thread = self._loop_thread
153            return thread is not None and thread.is_alive()
def start_streaming(self) -> None:
155    def start_streaming(self) -> None:
156        with self._loop_lock:
157            thread = self._loop_thread
158            if thread is not None and thread.is_alive():
159                return
160        self.on_loop_started()
161        stop_event = Event()
162        thread = Thread(
163            target=self._run_loop_thread,
164            args=(stop_event,),
165            name=f"modlink.loop.{self.device_id.strip()}",
166            daemon=True,
167        )
168        with self._loop_lock:
169            self._loop_stop = stop_event
170            self._loop_thread = thread
171        thread.start()
def stop_streaming(self) -> None:
173    def stop_streaming(self) -> None:
174        with self._loop_lock:
175            stop_event = self._loop_stop
176            thread = self._loop_thread
177        if stop_event is None or thread is None:
178            return
179        stop_event.set()
180        if thread is not current_thread():
181            thread.join(timeout=2.0)
def on_loop_started(self) -> None:
183    def on_loop_started(self) -> None:
184        """Optional hook called once before loop execution starts."""

Optional hook called once before loop execution starts.

def on_loop_stopped(self) -> None:
186    def on_loop_stopped(self) -> None:
187        """Optional hook called once after loop execution stops."""

Optional hook called once after loop execution stops.

def loop(self) -> None:
189    def loop(self) -> None:
190        raise NotImplementedError(f"{type(self).__name__} must implement loop")
def on_shutdown(self) -> None:
192    def on_shutdown(self) -> None:
193        """Best-effort shutdown hook for the helper loop thread."""
194        self.stop_streaming()

Best-effort shutdown hook for the helper loop thread.

@dataclass(slots=True)
class FrameEnvelope:
41@dataclass(slots=True)
42class FrameEnvelope:
43    """One emitted payload chunk from a driver.
44
45    Drivers emit ``FrameEnvelope`` objects through their bound runtime
46    context. The host forwards them to the stream bus, recording backends, and
47    UI consumers.
48    """
49
50    device_id: str
51    """Identifier of the device that produced this payload."""
52
53    stream_key: str
54    """Device-local stream key for this payload, such as ``eeg`` or ``video``."""
55
56    stream_id: str = field(init=False)
57    """Derived stable identifier of the stream that produced this payload."""
58
59    timestamp_ns: int
60    """Driver-supplied timestamp in nanoseconds."""
61
62    data: np.ndarray
63    """Payload array for this emitted chunk.
64
65    The expected array shape depends on the matching ``StreamDescriptor``.
66    """
67
68    seq: int | None = None
69    """Optional monotonically increasing sequence number."""
70
71    def __post_init__(self) -> None:
72        self.device_id = normalize_device_id(self.device_id)
73        self.stream_key = normalize_stream_key(self.stream_key)
74        self.stream_id = make_stream_id(self.device_id, self.stream_key)

One emitted payload chunk from a driver.

Drivers emit FrameEnvelope objects through their bound runtime context. The host forwards them to the stream bus, recording backends, and UI consumers.

FrameEnvelope( device_id: str, stream_key: str, timestamp_ns: int, data: numpy.ndarray, seq: int | None = None)
device_id: str

Identifier of the device that produced this payload.

stream_key: str

Device-local stream key for this payload, such as eeg or video.

stream_id: str

Derived stable identifier of the stream that produced this payload.

timestamp_ns: int

Driver-supplied timestamp in nanoseconds.

data: numpy.ndarray

Payload array for this emitted chunk.

The expected array shape depends on the matching StreamDescriptor.

seq: int | None

Optional monotonically increasing sequence number.

type PayloadType = Literal['signal', 'raster', 'field', 'video']
@dataclass(slots=True)
class SearchResult:
16@dataclass(slots=True)
17class SearchResult:
18    """One discovery candidate returned by ``Driver.search()``.
19
20    Hosts use ``title`` and ``subtitle`` for presentation. ``extra`` is owned
21    by the driver and is passed back to ``connect_device()`` unchanged.
22    """
23
24    title: str
25    """Primary label shown to the user."""
26
27    subtitle: str = ""
28    """Optional secondary label shown to the user."""
29
30    device_id: str | None = None
31    """Optional canonical device identifier suggested by the driver."""
32
33    extra: dict[str, Any] = field(default_factory=dict)
34    """Driver-owned JSON-friendly payload for later connection."""
35
36    def __post_init__(self) -> None:
37        if self.device_id is not None:
38            self.device_id = normalize_device_id(self.device_id)

One discovery candidate returned by Driver.search().

Hosts use title and subtitle for presentation. extra is owned by the driver and is passed back to connect_device() unchanged.

SearchResult( title: str, subtitle: str = '', device_id: str | None = None, extra: dict[str, typing.Any] = <factory>)
title: str

Primary label shown to the user.

subtitle: str

Optional secondary label shown to the user.

device_id: str | None

Optional canonical device identifier suggested by the driver.

extra: dict[str, typing.Any]

Driver-owned JSON-friendly payload for later connection.

@dataclass(slots=True)
class StreamDescriptor:
 77@dataclass(slots=True)
 78class StreamDescriptor:
 79    """Static metadata describing one stream exposed by a driver.
 80
 81    Hosts may read descriptors before a device is connected. The returned
 82    values should remain stable for the lifetime of the driver instance.
 83    """
 84
 85    device_id: str
 86    """Stable device identifier in ``name.XX`` form."""
 87
 88    stream_key: str
 89    """Device-local stream key, such as ``eeg``, ``accel``, or ``audio``."""
 90
 91    stream_id: str = field(init=False)
 92    """Derived stable identifier for this stream."""
 93
 94    payload_type: PayloadType
 95    """Payload type used to interpret ``FrameEnvelope.data``."""
 96
 97    nominal_sample_rate_hz: float
 98    """Nominal sample rate in Hz."""
 99
100    chunk_size: int
101    """Expected number of samples or frames per emitted chunk."""
102
103    channel_names: tuple[str, ...] = ()
104    """Optional channel labels, usually matching axis 0 for signal payloads."""
105
106    display_name: str | None = None
107    """Optional human-readable stream label."""
108
109    metadata: dict[str, Any] = field(default_factory=dict)
110    """Additional metadata, including payload-specific fields such as ``unit``."""
111
112    def __post_init__(self) -> None:
113        self.device_id = normalize_device_id(self.device_id)
114        self.stream_key = normalize_stream_key(self.stream_key)
115        self.stream_id = make_stream_id(self.device_id, self.stream_key)
116        self.metadata = dict(self.metadata)

Static metadata describing one stream exposed by a driver.

Hosts may read descriptors before a device is connected. The returned values should remain stable for the lifetime of the driver instance.

StreamDescriptor( device_id: str, stream_key: str, payload_type: PayloadType, nominal_sample_rate_hz: float, chunk_size: int, channel_names: tuple[str, ...] = (), display_name: str | None = None, metadata: dict[str, typing.Any] = <factory>)
device_id: str

Stable device identifier in name.XX form.

stream_key: str

Device-local stream key, such as eeg, accel, or audio.

stream_id: str

Derived stable identifier for this stream.

payload_type: PayloadType

Payload type used to interpret FrameEnvelope.data.

nominal_sample_rate_hz: float

Nominal sample rate in Hz.

chunk_size: int

Expected number of samples or frames per emitted chunk.

channel_names: tuple[str, ...]

Optional channel labels, usually matching axis 0 for signal payloads.

display_name: str | None

Optional human-readable stream label.

metadata: dict[str, typing.Any]

Additional metadata, including payload-specific fields such as unit.