zelos-can
Rust-first CAN codec for the Zelos ecosystem. DBC-driven signal decode + trace emission at wire speed, with optional python-can–compatible bus implementations for live SocketCAN and in-memory testing.
Install
pip install zelos-can
# python-can compat layer (optional — only needed if you want to use
# can.Bus(interface="zelos-socketcan", ...)):
pip install 'zelos-can[python-can]'
Prebuilt wheels ship for Linux, macOS, and Windows. Headless decode
(CanDecoder, VirtualBus, CanCodec) works on all three; the live
zelos-socketcan bus is Linux-only (see the feature matrix).
Three ways to use it
1. Drop-in python-can bus (live capture on Linux)
import can
bus = can.Bus(interface="zelos-socketcan", channel="can0")
# Kernel-level filters, SO_RCVBUF tuning, SO_TIMESTAMPNS,
# auto-reconnect on link-down — all handled in Rust.
Compatible with can.Notifier, bus.send_periodic, bus.state, and the
bus.socket escape hatch. Drop-in for interface="socketcan".
2. Bring your own frame source (this is the main pattern)
CanDecoder is a thread-safe handle that takes raw frames and emits decoded
signals into the Zelos trace pipeline. Where frames come from is your
problem — anything that can produce arbitration_id + data + timestamp works.
The DBC is optional. With no database_file / database_bytes the decoder
skips signal decode and just traces the raw frames (log_raw_frames defaults on
in that case) — handy for capturing a bus you don't have a DBC for yet:
from zelos_can import CanDecoder
raw = CanDecoder(source_name="can") # no DBC → raw frames only
raw.decode_frame(arbitration_id=0x123, data=b"\x01\x02")
from zelos_can import CanDecoder
decoder = CanDecoder(database_file="vehicle.dbc", source_name="can")
# From python-can (PEAK/Vector/Kvaser/socketcan/whatever):
for msg in my_bus:
decoder.decode_message(msg)
# From a network gateway (cannelloni/SLCAN/your protocol):
decoder.decode_frame(
arbitration_id=0x123,
data=b"\x01\x02",
timestamp_ns=time.time_ns(),
is_extended=False,
is_fd=False,
)
# From a .asc/.blf/.trc/.mf4 file via python-can readers:
for msg in can.BLFReader("capture.blf"):
decoder.decode_message(msg)
decode_frame and decode_message release the GIL for the duration of the
Rust decode + trace emit — the caller thread stays free.
Complete examples in examples/:
file_replay.py— .asc/.blf/.trc/.mf4/.log/.csv → .trzpython_can_forward.py— any python-can interface → live trace + .trzudp_source.py— skeleton for a network-CAN gatewayvirtual.py— in-memory VirtualBus + native Rust codecsocketcan.py— native Rust SocketCAN codec (Linux)
3. Native Rust capture pipeline (fastest)
For the tightest hot path (no Python callbacks per frame), use the native
CanCodec with either a VirtualBus or a SocketCAN channel:
from zelos_can import CanCodec, VirtualBus
bus = VirtualBus(channel="sim")
codec = CanCodec(database_file="vehicle.dbc", source_name="sim", bus=bus)
# codec reads frames in Rust; Python never sees individual frames.
Same applies to real hardware via channel="can0" (Linux SocketCAN):
codec = CanCodec(
database_file="vehicle.dbc",
source_name="can",
channel="can0",
log_raw_frames=True,
timestamp_mode="hardware",
)
Trace pipeline
Every decode path funnels through zelos-sdk's TraceNamespace. If
zelos_sdk.init() has been called the decoded signals stream live to the
Zelos agent over gRPC; wrap your code in a zelos_sdk.TraceWriter(...)
context to also record to a .trz file. Both work simultaneously.
Feature matrix
| feature | zelos-socketcan |
zelos-virtual |
CanDecoder |
|---|---|---|---|
python-can can.Bus compat |
✓ | ✓ | n/a |
kernel filter (CAN_RAW_FILTER) |
✓ | n/a | n/a |
bus.state / bus.socket |
✓ | n/a | n/a |
error frames (is_error_frame) |
✓ (on by default) | n/a | n/a |
CAN FD bitrate_switch / error_state_indicator |
✓ | n/a | n/a |
local_loopback / receive_own_messages |
✓ | n/a | n/a |
| native periodic transmit | kernel BCM (send_periodic) + stop_all_periodic_tasks |
Rust timer | n/a |
| DBC decode + trace in Rust | via Notifier + CanDecoder |
via CanCodec or CanDecoder |
always |
| Linux-only | yes | no | no |
Transmitted frames are traced like any other when the bus is opened with
receive_own_messages=True — TX frames echo back through the RX path (with
is_rx=False) and flow into the decode/trace pipeline, so a recording
captures exactly what hit the wire (including kernel BCM periodics).
Decoder semantics — divergences from cantools
zelos-can decodes per the DBC spec. For well-formed inputs we match
cantools exactly.
SIG_VALTYPE_ interpretation
Both decoders treat the : 1 / : 2 marker as advisory and pick the IEEE
754 binary format from the signal length:
| length | format |
|---|---|
| 16 | binary16 (half precision) |
| 32 | binary32 (single precision) |
| 64 | binary64 (double precision) |
| anything else | no canonical IEEE format → decode-time error |
For lengths that aren't a real IEEE binary width (e.g. SIG_VALTYPE_:1 + length=10):
cantoolsraises at decode time (expected float size of 16, 32, or 64 bits).zelos-canpreservesis_floatso the schema field stays typed as float, butdecode_signalreturnsNoneper-frame and thedecode_errorsmetric ticks. A parse-time warning surfaces the malformed DBC up front (cantools surfaces nothing at load time).
Storage typing for unsigned signals with negative offset
For an unsigned raw with a DBC offset that drives physical values negative
(e.g. (1, -1000) on a u16):
cantoolsreturns Pythonint(unbounded — sign is implicit).zelos-canpromotes the Arrow schema type to the signed sibling (Int{16,32,64}). The typed cache must store a fixed-width Arrow type; the unsigned variant would saturatef64 → uNcasts of negative physicals to zero, silently zeroing valid frames.
Everything else
Integer signals, factor/offset on integer raws, big/little endianness,
multiplexing, value tables, and well-formed IEEE float widths all decode
identically to cantools.
Metrics
m = decoder.metrics()
# messages_received / messages_decoded / unknown_messages / decode_errors
# kernel_drops (SocketCAN only, SO_RXQ_OVFL)
# broadcast_overflows (VirtualBus slow-consumer lag)
# reconnections (SocketCAN auto-recover)
Benchmarks
Numbers measured on a single apple-silicon core via criterion. Ballpark only —
reproduce with cargo bench.
| bench | throughput |
|---|---|
| decode_frame/dut_status_all_signals (9 sig) | ~217 Msig/s (41 ns) |
| virtual_bus_fanout/subscribers=1 | 21 Melem/s |
| virtual_bus_fanout/subscribers=4 | 38 Melem/s |
| virtual_bus_fanout/subscribers=16 | 50 Melem/s |
At 9 signals per message the decode path is ~24M full DBC-decoded messages
per second in pure Rust, which is well past any realistic CAN bus rate — the
GIL crossing on the Python boundary is what actually sets your ceiling, which
is why the CanDecoder + Notifier (no per-frame GIL re-entry) path
exists.
Broadcast fanout scales sublinearly (16-subscriber case delivers 50M total frames/s vs 21M for one subscriber), so adding listeners costs negligibly in the expected 1-10kHz CAN regime.
Release files for zelos-can 0.0.9
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| zelos_can-0.0.9-cp310-abi3-win_amd64.whl | CPython 3.10 | abi3 | Windows x86-64 | Details |
| zelos_can-0.0.9-cp310-abi3-manylinux_2_28_x86_64.whl | CPython 3.10 | abi3 | Linux glibc 2.28+ x86-64 | Details |
| zelos_can-0.0.9-cp310-abi3-manylinux_2_28_aarch64.whl | CPython 3.10 | abi3 | Linux glibc 2.28+ ARM64 | Details |
| zelos_can-0.0.9-cp310-abi3-macosx_11_0_arm64.whl | CPython 3.10 | abi3 | macOS 11.0+ ARM64 | Details |
Total release size: 8.3 MB
Release files / zelos_can-0.0.9-cp310-abi3-win_amd64.whl
| Download URL | zelos_can-0.0.9-cp310-abi3-win_amd64.whl |
|---|---|
| Size | 1.7 MB |
| Tags | CPython 3.10 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
99bf590f583f47dd154e3440f2f86f13e14c7ccda74aa4160aeffaae5ee5df83
|
|
BLAKE2b-256 checksum How to use checksums |
2f719c925a062ffe29f9db662a2ab5f33229dd6281b1dbb5be0514120f3fd1bb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|
Release files / zelos_can-0.0.9-cp310-abi3-manylinux_2_28_x86_64.whl
| Download URL | zelos_can-0.0.9-cp310-abi3-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 2.6 MB |
| Tags | CPython 3.10 Linux glibc 2.28+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
bfee29ef3900fab461e603af1ccdbd9f7c2a72ed1b851863d9d30db1ca992211
|
|
BLAKE2b-256 checksum How to use checksums |
07549d9b3dfa34cc4baa97496820f992ea5eaa2911dbd463581062ce373f826a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|
Release files / zelos_can-0.0.9-cp310-abi3-manylinux_2_28_aarch64.whl
| Download URL | zelos_can-0.0.9-cp310-abi3-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 2.3 MB |
| Tags | CPython 3.10 Linux glibc 2.28+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
0c45126891cd0443f5585076fbfe8b5b85e8052dd13d986f7467620dcfc07ac0
|
|
BLAKE2b-256 checksum How to use checksums |
690a9f60c2cd8c22ab40793db3b93e64bf61bd5d479a1527d09d8bf56f2300d5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|
Release files / zelos_can-0.0.9-cp310-abi3-macosx_11_0_arm64.whl
| Download URL | zelos_can-0.0.9-cp310-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 1.7 MB |
| Tags | CPython 3.10 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
6f5820f26e7f86e9429c6ce9af74eb62686ed8e315010a57c60b628a48c78a68
|
|
BLAKE2b-256 checksum How to use checksums |
3e17235c8436772d066154f737284f40040eb27ddfa9205d3e7f5b8c1617f10b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|