This release is a pre-release and may not be stable for production use.
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 and runtime (optional; only needed for
# can.Bus(interface="zelos-socketcan", ...) or the python-can runtime below):
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, reading on
# through link drops and unplugs — 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",
)
python-can runtime
The bus layer the Zelos CAN, XCP and UDS extensions share: config to a running
bus on any python-can interface, DBC decode and transmit, ssh-bridged
SocketCAN, offline log conversion. Needs the python-can extra; importing a
module without it raises an ImportError naming the extra. zelos_can.bus
imports without it; open_python_can_bus needs it.
| module | what |
|---|---|
zelos_can.bus |
prepare_bus_config, open_python_can_bus, BUS_DEFAULTS, BusConfigError, discovery.list_interfaces |
zelos_can.codec |
CanCodec: one supervised bus with DBC decode (cantools), trace emit, TX and periodics, bus health and recovery; socketcan and ssh-socketcan run on the Rust CanCodec |
zelos_can.ssh_socketcan |
SshTransport: a remote SocketCAN bus over ssh + candump/cansend |
zelos_can.converter |
convert_can_trace: .asc/.blf/.trc/.log/.csv/.mf4 to .trz |
zelos_can.demo |
EV simulator and its DBC (interface: demo) |
zelos_can/bus/interfaces.schema.json |
per-interface config fields, for an extension's config.schema.json |
from pathlib import Path
from zelos_can.bus import open_python_can_bus, prepare_bus_config
entry = {"name": "pt", "interface": "pcan", "channel": "PCAN_USBBUS1", "bitrate": 500_000}
config = prepare_bus_config(entry, Path()) # 2nd arg: the DBC an `interface: demo` bus uses
bus = open_python_can_bus(config, "pt") # python-can BusABC, init retried 3 times
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.
A2L reader (zelos_can.a2l)
Reads an ASAM MCD-2 MC (ASAP2) file into one catalog dict for XCP measurement.
Not imported by import zelos_can.
from zelos_can import a2l
cat = a2l.load("ecu.a2l") # strict=False by default
for m in cat["measurements"]:
print(m["name"], m["address"], m["datatype"], m["conversion"]["kind"])
print(cat["transports"], cat["events"], cat["warnings"])
| Key | Content |
|---|---|
measurements |
name, address, address_extension, datatype, byte_order, bit_mask, dims, conversion, unit, lower, upper, events, groups |
events |
XCP event channels: name, channel, cycle, cycle_unit (code as stated), priority, max_daq_lists, consistency (DAQ, EVENT, ODT, NONE or None) |
transports |
XCP on UDP / TCP (host, port) and CAN (ids, bitrate, CAN FD), with max_cto / max_dto |
daq, epk |
DAQ type, limits and timestamp; EPK string and address |
warnings |
file:line: ... for parser warnings and every skipped entry |
The full shape is in the zelos_can.a2l docstring.
- No silent misreads. Parse faults, integers out of range for their field
(a2lfile narrows hex literals otherwise) and duplicate names after structure
flattening raise
a2l.A2lError(aValueError) withfileandline. - Skipped, with a reason in
warnings: calibration components, pointers,MSB_FIRST_MSW_LAST/MSB_LAST_MSW_FIRST,BIT_OPERATION, instanceOVERWRITE, missing conversions, transports other than UDP/TCP/CAN. - Structures:
INSTANCEs flatten toinst.member/inst[i].member. - Conversions: IDENTICAL, LINEAR, RAT_FUNC and TAB_VERB (incl. COMPU_VTAB_RANGE) in full; TAB_INTP, TAB_NOINTP and FORM carried, not evaluated.
- Missing
/include: dropped with a warning;strict=Trueraises instead. XCPlite files reference anXCP_104.amlthat is rarely shipped alongside.
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)
Tests against nodes (test/hil)
Tiers are pytest markers; a station file says which ones a host provides and the rest skip.
| tier | frames from | run with |
|---|---|---|
sim |
committed captures replayed on VirtualBus, any OS |
just test (station local) |
vcan |
the same captures replayed on vcan0 |
just test-vcan (station vcan) |
hw, hil |
the bench node on real adapters; hil flashes and resets it under a lock |
just hil [WHEEL] (station bench) |
Bench setup: copy test/hil/stations/bench.example.toml to bench.toml (gitignored) and
fill in the channels, lock file, firmware dir and flash/reset/console commands.
just hil-capture NODE SECONDS records a real capture that replaces the synthetic one.
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.
Metadata
Release files for zelos-can 0.0.11a1
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.11a1-cp310-abi3-win_amd64.whl | CPython 3.10 | abi3 | Windows x86-64 | Details |
| zelos_can-0.0.11a1-cp310-abi3-manylinux_2_28_x86_64.whl | CPython 3.10 | abi3 | Linux glibc 2.28+ x86-64 | Details |
| zelos_can-0.0.11a1-cp310-abi3-manylinux_2_28_aarch64.whl | CPython 3.10 | abi3 | Linux glibc 2.28+ ARM64 | Details |
| zelos_can-0.0.11a1-cp310-abi3-macosx_11_0_arm64.whl | CPython 3.10 | abi3 | macOS 11.0+ ARM64 | Details |
Total release size: 10.4 MB
Release files / zelos_can-0.0.11a1-cp310-abi3-win_amd64.whl
| Download URL | zelos_can-0.0.11a1-cp310-abi3-win_amd64.whl |
|---|---|
| Size | 2.3 MB |
| Tags | CPython 3.10 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
fc6d13ada28a7a2da04e01688b5334189105144c3f97fc4aac03dcde28685b0d
|
|
BLAKE2b-256 checksum How to use checksums |
5b7b3319a3d0c38f6ee9c75064ac795439c5e0bcc2e7b3693c7049e3d28974da
|
| 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.11a1-cp310-abi3-manylinux_2_28_x86_64.whl
| Download URL | zelos_can-0.0.11a1-cp310-abi3-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 3.1 MB |
| Tags | CPython 3.10 Linux glibc 2.28+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
429e217a245b343175c7dadbebc76103ec1f061844c3d76dd05561c61ee363b3
|
|
BLAKE2b-256 checksum How to use checksums |
b5309b0fec4fa035fb1ac932a8de9b1736def809086ca1fd42dc017f04d5445d
|
| 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.11a1-cp310-abi3-manylinux_2_28_aarch64.whl
| Download URL | zelos_can-0.0.11a1-cp310-abi3-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 2.8 MB |
| Tags | CPython 3.10 Linux glibc 2.28+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
2eeee7517ddfdfc09497253ce541feb5c928a4d16e91fe3725c773a15b6960d6
|
|
BLAKE2b-256 checksum How to use checksums |
533722fb47ca5121f709510f2d777fd75f319a2731e38d83357728fad1a4b6a2
|
| 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.11a1-cp310-abi3-macosx_11_0_arm64.whl
| Download URL | zelos_can-0.0.11a1-cp310-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 2.1 MB |
| Tags | CPython 3.10 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
f0c167d2abd6f750ee11828146222c4eae01e8bbe793c9e9f85f45929eea88b1
|
|
BLAKE2b-256 checksum How to use checksums |
22eb22acf9aaa1c4035733df74f668b7e6521bf38d17d1a83f5717b74d91cd71
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|
Security for zelos-can 0.0.11a1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.