Source code for cubemarspycan.transport.base
"""Transport protocols.
Deliberately CAN-shaped. A serial link is a byte stream with its own framing and CRC, and
forcing both under one abstraction now would produce something that fits neither. The seam
that keeps servo-over-serial possible for v1.1 is one level up: the motor classes never
touch a :class:`~cubemarspycan.frame.Frame` transport directly, they talk to a link object.
"""
from __future__ import annotations
from typing import Protocol, runtime_checkable
from ..frame import Frame
[docs]
class FrameSink(Protocol):
"""Called on the receive thread for every frame. Must never raise."""
# Positional-only: a sink is a callback, so its parameter *names* are not part of
# the contract and a plain function must satisfy it.
def __call__(self, frame: Frame, rx_monotonic: float, bus_timestamp: float, /) -> None: ...
[docs]
@runtime_checkable
class FrameTransport(Protocol):
"""A bidirectional CAN frame link."""
[docs]
def send(self, frame: Frame, timeout: float | None = ...) -> None:
"""Put one frame on the wire, blocking up to ``timeout`` seconds.
Called on the **caller's** thread, from inside the control loop, so the blocking
window is the loop's jitter budget: a socketcan send is microseconds, an slcan
send over USB serial is 0.5-2 ms.
Raises :class:`~cubemarspycan.errors.SendFailed` rather than returning a status or
swallowing the error. A command that did not reach the motor is not a detail the
caller can be left to infer.
"""
...
[docs]
def add_sink(self, sink: FrameSink) -> None:
"""Register a receiver. Sinks must be added before :meth:`start`.
Every registered sink is called on the **receive** thread for every frame, so a
sink must never raise and must not block - see :class:`FrameSink`. An
implementation is expected to contain and count an exception rather than let it
kill reception for the other sinks.
"""
...
[docs]
def start(self) -> None:
"""Begin receiving. Idempotent: calling it twice must not start a second reader.
Until this is called, frames may be dropped by the underlying driver. Sinks
registered afterwards are not guaranteed to see earlier frames.
"""
...
[docs]
def close(self) -> None:
"""Stop receiving and release the link. Idempotent, and safe after a failed
:meth:`start`.
Must not raise: it runs from ``__exit__`` and from error-recovery paths, where
another exception is usually already unwinding.
"""
...