Metadata-Version: 2.3
Name: zelos-can
Version: 0.0.4
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Rust
Classifier: License :: Other/Proprietary License
Classifier: Operating System :: POSIX :: Linux
Requires-Dist: zelos-sdk >=0.0.10a3
Requires-Dist: python-can >=4.0 ; extra == 'python-can'
Provides-Extra: python-can
Summary: Rust-first CAN codec for the Zelos ecosystem. SocketCAN tracing with DBC signal decoding.
Author-email: Zelos Cloud <info@zeloscloud.io>
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM

# 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

```bash
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]'
```

## Three ways to use it

### 1. Drop-in python-can bus (live capture on Linux)

```python
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.

```python
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/`](./examples):
- [`file_replay.py`](./examples/file_replay.py) — .asc/.blf/.trc/.mf4/.log/.csv → .trz
- [`python_can_forward.py`](./examples/python_can_forward.py) — any python-can interface → live trace + .trz
- [`udp_source.py`](./examples/udp_source.py) — skeleton for a network-CAN gateway
- [`virtual.py`](./examples/virtual.py) — in-memory VirtualBus + native Rust codec
- [`socketcan.py`](./examples/socketcan.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:

```python
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):

```python
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          |
| native periodic transmit        | python-can path   | Rust timer      | n/a          |
| DBC decode + trace in Rust      | via `Notifier` + `CanDecoder` | via `CanCodec` or `CanDecoder` | always |
| Linux-only                      | yes               | no              | no           |

## Metrics

```python
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`` in ``api/py/zelos-can``.

| 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.

