Transport¶
transport.
The protocols¶
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 Frame transport directly, they talk to a link object.
- class FrameSink(*args, **kwargs)[source]¶
Bases:
ProtocolCalled on the receive thread for every frame. Must never raise.
- class FrameTransport(*args, **kwargs)[source]¶
Bases:
ProtocolA bidirectional CAN frame link.
- send( ) None[source]¶
Put one frame on the wire, blocking up to
timeoutseconds.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
SendFailedrather 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.
- add_sink(sink: FrameSink) None[source]¶
Register a receiver. Sinks must be added before
start().Every registered sink is called on the receive thread for every frame, so a sink must never raise and must not block - see
FrameSink. An implementation is expected to contain and count an exception rather than let it kill reception for the other sinks.
python-can¶
python-can transport.
The bus is injected. CanTransport(bus) takes any can.BusABC you have already
constructed, so socketcan, slcan, gs_usb, PCAN, Kvaser and the virtual backend all work
without this module knowing about them. CanTransport.open() is sugar over the common
cases and is never the only path.
Nothing here shells out to change anything. There is exactly one subprocess call - a
read-only ip -details -json link show used for diagnostics, never on the data path,
never with sudo, and tolerant of ip being absent. TMotorCANControl instead runs
os.system('sudo /sbin/ip link set can0 up ...') from a singleton’s __new__, which
hard-codes the interface and the bitrate, requires root, only works on Linux, and leaves
no seam to inject a bus - which is why none of its thirty defects had a test.
- DEFAULT_BITRATE¶
The AK drivers use 1 Mbit/s. The manual says changing it is not recommended.
- class TransportStats(
- rx: int = 0,
- tx: int = 0,
- rx_ignored: int = 0,
- tx_failed: int = 0,
- errors: ~collections.deque[tuple[str,
- str]] = <factory>,
- tx_durations: ~collections.deque[float] = <factory>,
Bases:
objectCounters worth looking at when something is not working.
Mutable on purpose: this is diagnostics, not state that crosses a thread boundary as a snapshot.
- record_error(where: str, exc: BaseException) None[source]¶
Count and sample an exception, tagged with where it happened.
whereis one of"frame-build","sink","notifier"or"send". Called from the receive thread’sexceptblocks, so it must not raise; the sample list is bounded.
- tx_percentiles() dict[str, float][source]¶
p50/p95/max of send() wall time, in milliseconds.
Worth watching on slcan: a send is an ASCII line over USB CDC, typically 0.5-2 ms with scheduling spikes into the tens of milliseconds. That is what decides whether a given loop rate is realistic on a given adapter.
- class CanTransport(bus: BusABC, *, owns_bus: bool = False)[source]¶
Bases:
ListenerWraps an injected
can.BusABCand fans received frames out to sinks.- classmethod open( ) CanTransport[source]¶
Build a transport from
scheme:channel[@bitrate].socketcan:can0(Linux, production),slcan:/dev/tty.usbmodem1101@1M,gs_usb:0@1M,virtual:test.
- classmethod virtual(
- channel: str = 'cubemars',
An in-process bus. Used by the simulator and by CI on every platform.
- start() None[source]¶
Start the python-can notifier thread. Idempotent while open.
Sinks registered after this point are still called - the sink list is read per frame - but they will not see frames that already arrived.
- close() None[source]¶
Stop the notifier and shut the bus down. Idempotent, and never raises.
Only shuts down the underlying
can.BusABCif this transport created it: an injected bus belongs to the caller. Safe to call after a failedstart().
- property bus: BusABC¶
The wrapped
can.BusABC.Exposed so an application can reach backend-specific features this wrapper does not model. Send through
send()instead, so frames are counted and failures are wrapped.
- property channel_info: str¶
The backend’s own description of the channel, for diagnostics and messages.
Free-form and backend-specific - useful to a human, not something to parse.
- send( ) None[source]¶
Put one frame on the bus.
Raises
SendFailedrather than swallowing it.
- set_filters(filters: Any | None) None[source]¶
Install hardware/kernel receive filters.
On socketcan these are applied in the kernel, so the receive thread is not woken for traffic belonging to other nodes. Opt-in: a wrong filter drops frames silently, which is a worse failure than a few wasted wakeups.
- on_message_received(msg: Message) None[source]¶
python-can’s receive hook. Runs on the notifier thread and never raises.
Stamps arrival from
time.monotonic()here, not frommsg.timestamp: the bus timestamp is epoch-based and on some backends comes from the driver with an unrelated origin, which would make staleness meaningless. The bus value is passed along for logs.Error and remote frames are counted and dropped rather than decoded. Every sink is called inside its own
except BaseException- a sink raisingKeyboardInterruptmust not take the receive thread down and silence every motor on the bus.
- class LinkStatus(up: bool | None, bitrate: int | None, state: str | None)[source]¶
Bases:
objectEverything
doctorreports about one interface, from a singleipcall.
- read_link_status(interface: str) LinkStatus[source]¶
All three link facts from one
ipinvocation.The three single-fact readers below each run their own probe, which is right for standalone use and wasteful in a loop:
doctorwas forkingipup to three times per interface for three keys of one JSON object, multiplying the 2 s timeout by three.
- read_socketcan_bitrate(interface: str) int | None[source]¶
The kernel’s configured bitrate, or
Noneif it cannot be determined.python-can cannot set a socketcan bitrate -
ip linkdoes - so the only honest check is to read back what the interface is actually running at. A virtual interface has no bit timing at all, and returnsNone.
- socketcan_link_flags(interface: str) list[str][source]¶
Link flags for
interface, e.g.["NOARP", "UP", "LOWER_UP"].Empty if the interface does not exist or cannot be read. Never raises.
- socketcan_is_up(interface: str) bool | None[source]¶
Whether
interfaceis administratively up.Nonemeans “cannot tell”.Reads the
UPflag, notoperstate. A virtual CAN interface has no carrier, so it reportsstate UNKNOWNhowever healthy it is, while real CAN hardware reportsstate UP. Anything that keys off operstate will call a working vcan interface down.Falsealso covers “there is no such interface”. A definitive absence is an answer; reporting it as “cannot tell” hides a typo’d interface name behind a shrug.Noneis returned only when neither sysfs noripcould be consulted at all - no/sys, no iproute2, a slim container, a BusyBox rootfs, macOS. Reporting that as “down” is how a healthy interface gets blamed, which is the failure this replaced.sysfs is tried first so a host without iproute2 still gets a real answer.
- read_socketcan_state(interface: str) str | None[source]¶
The CAN controller’s error state, e.g.
ERROR-ACTIVEorBUS-OFF.Worth surfacing: a controller in ERROR-PASSIVE or BUS-OFF usually means nothing is acknowledging its frames - no other powered node, or missing termination - which looks identical to a software fault from the application’s side.