python-can interface plugin for a configurable binary serial CAN protocol
Project description
python-can-flexible-serial
A python-can interface plugin that speaks a
configurable, framed binary protocol over a serial port. It follows the
python-can plugin interface,
so once installed it is usable anywhere python-can accepts an interface=
name — can.Bus, python -m can.viewer, python -m can.logger,
python -m can.bridge, etc. — alongside the other community interface
plugins listed on that page (python-can-canine, python-can-cvector,
python-can-remote, python-can-sontheim, python-can-cando,
python-can-candle, ...).
Unlike the fixed-layout slcan and serial interfaces bundled with
python-can, the frame layout here is configurable via constructor kwargs
(SOF/EOF delimiters, ID width, optional CRC, optional flags byte, optional
timestamp) so it can be adapted to match firmware on the other end of the
serial link without changing code.
Frame layout
| Section | Field | Size | Optional / Configurable |
|---|---|---|---|
| — | SOF | 1+ bytes | Configurable delimiter (default 0xAA) |
| Header | timestamp | 0 or 4 bytes | Optional — omitted when no_timestamp=True |
| Header | dlc | 1 byte | Always present |
| Header | flags | 0 or 1 byte | Optional — only present when flags_byte=True |
| Header | id | 2 or 4 bytes | Sizeable — 2 bytes when uint16_id=True, else 4 |
| Payload | data | 0-64 bytes | Sizeable — length equals dlc |
| — | crc | 0 or N bytes | Optional — size/presence set by crc_type |
| — | EOF | 1+ bytes | Configurable delimiter (default 0xBB) |
Details on each section:
- SOF / EOF — arbitrary-length byte sequences used purely as framing delimiters. They are never covered by the CRC.
- timestamp (optional, fixed 4 bytes when present) —
uint32milliseconds. Omit entirely withno_timestamp=Trueto save 4 bytes per frame if the far end doesn't supply/need one; python-can still assigns a local receive timestamp in that case. - dlc (always present, fixed 1 byte) — raw payload length in bytes;
also used to size the
datasection on receive. - flags (optional, fixed 1 byte when present) — see "Flags byte" below
for the bit layout. When absent, the same metadata is instead packed into
spare bits of the
idfield (see below). - id (always present, sizeable: 2 or 4 bytes) — arbitration ID.
uint16_id=Trueshrinks it to 2 bytes but rejects extended (29-bit) IDs; the default 4-byte field supports both standard and extended IDs. - data (sizeable, 0-64 bytes) — the CAN payload; length is whatever
dlcsays, up to 64 bytes for CAN FD-sized frames or 8 bytes otherwise. - crc (optional, sizeable: 0 or N bytes) — set by
crc_type("none"or"crc16-ccitt"); covers the header and payload, but not SOF/EOF.
Byte order for the header and CRC fields is little-endian by default, or
big-endian when big_endian=True.
Flags byte
When flags_byte=True, extended/error/remote/FD metadata is carried in a
dedicated byte instead of being packed into spare bits of the arbitration ID:
| Bit | Mask | Meaning |
|---|---|---|
| 0 | 0x01 |
Error frame |
| 1 | 0x02 |
Remote frame (RTR) |
| 2 | 0x04 |
Extended (29-bit) ID |
| 3 | 0x08 |
CAN FD frame |
| 4 | 0x10 |
CAN FD bit-rate switch |
| 5 | 0x20 |
Frame originated as RX (vs. TX) |
When flags_byte=False, extended/error/remote status is instead OR'd into
spare high bits of the 32-bit arbitration ID field (standard python-can
convention: CAN_EFF_FLAG, CAN_ERR_FLAG, CAN_RTR_FLAG). This only applies
when uint16_id=False, since a 16-bit ID field has no spare bits for flags.
Error frames
Protocol-level errors (frame desync, bad DLC, EOF mismatch, bad CRC) are
surfaced as Message objects with is_error_frame=True rather than raising
exceptions, matching how other python-can interfaces report bus errors. Use
FlexibleSerial.decode_serial_can_error(msg) to get a human-readable
description of an error message's contents.
Installation
How you install this depends on whether you're using it as a dependency of some other project, or working on this repo itself.
Using this plugin from another project
If that project is managed with uv, add this repo as an editable path dependency, pointing at wherever you've cloned it:
uv add --editable plugins/python-can-flexible-serial
Run this from that project's directory, not from within this repo — uv add adds a dependency to the project in your current directory, so running
it from inside python-can-flexible-serial itself is a self-dependency and
uv will refuse it.
With plain pip, from that project's environment:
python -m pip install -e /path/to/python-can-flexible-serial
Working on this repo itself
Use uv sync (a uv-managed project installs itself editable by default):
uv sync
Or with plain pip, from this repo's root:
python -m pip install -e .
Either way, this registers the plugin with python-can via the can.interface
entry point (flexible_serial = "flexible_serial.bus:FlexibleSerial"), so it
becomes available as interface="flexible_serial" anywhere python-can
resolves interfaces, without any further imports.
Usage
import can
bus = can.interface.Bus(
interface="flexible_serial",
channel="COM3",
baudrate=115200,
)
Constructor arguments
| Argument | Type | Default | Description |
|---|---|---|---|
channel |
str |
(required) | Serial device, e.g. "COM3" or "/dev/ttyUSB0" |
baudrate |
int |
115200 |
Serial baud rate |
timeout |
float |
0.1 |
Serial read timeout, in seconds |
rtscts |
bool |
False |
Enable RTS/CTS hardware handshake |
sof |
bytes | hex str |
0xAA |
Start-of-frame delimiter |
eof |
bytes | hex str |
0xBB |
End-of-frame delimiter |
big_endian |
bool |
False |
Byte order for header/CRC fields |
uint16_id |
bool |
False |
Use a 2-byte arbitration ID instead of 4-byte; rejects extended IDs |
flags_byte |
bool |
False |
Carry frame metadata in a dedicated byte (see "Flags byte" above) |
no_timestamp |
bool |
False |
Omit the 4-byte timestamp header field |
crc_type |
str |
"none" |
One of "none", "crc16-ccitt" |
rx_bytes_callback |
Callable[[bytes, bool], None] | None |
None |
Called with (raw_bytes, is_rx) whenever a frame is sent or a chunk of buffer is consumed on receive; useful for logging/debugging the raw wire traffic |
sof and eof accept either raw bytes (when constructing a Bus from
Python) or a hex string (when passed as a CLI --bus-kwargs value, since
python-can CLI tools pass kwargs as strings):
python -m can.logger -i flexible_serial -c COM3 --bus-kwargs sof=0xAA eof=0x0D0A
Prefer the 0x prefix for CLI hex values, especially if the delimiter
contains only digits, since python-can auto-converts bare numeric kwargs.
CLI examples
python -m can.viewer -i flexible_serial -c COM6 --bus-kwargs sof=0x4040 eof=0x0A uint16_id=True crc_type=crc16-ccitt flags_byte=False no_timestamp=True
Bridging to another interface:
python -m can.bridge --bus1-interface flexible_serial --bus1-channel COM7 --bus1-bus-kwargs sof=0x4040 eof=0x0A uint16_id=True crc_type=crc16-ccitt flags_byte=False no_timestamp=True --bus2-interface kvaser --bus2-channel 0 --bus2-bitrate 125000
Autodetection
Like the built-in serial interface, available serial ports are surfaced
through python-can's autodetection API:
import can
print(can.detect_available_configs(interfaces=["flexible_serial"]))
Layout
python-can-flexible-serial/
pyproject.toml
README.md
src/
flexible_serial/
__init__.py
bus.py
tests/
test_hex_bytes.py
test_bus.py
message_helper.py
Development
Install with the dev extra to pull in test dependencies (pytest):
uv sync --extra dev
or
python -m pip install -e .[dev]
Run the test suite with:
uv run -m pytest
or
python -m pytest
tests/test_bus.py scaffolds round-trip tests against two backends — a fully
in-memory mocked serial port and pyserial's loop:// — mirroring the
structure of python-can's own serial interface test suite. The test
bodies are currently stubs (skipTest); fill them in as coverage is added.
Future improvement ideas
can_filterssupport — already works today for free:BusABC.recv()always calls_matches_filters()in software unless_recv_internalreports the message as pre-filtered, and this interface never does. What's still missing is a hardware-style optimization: implement the optional_apply_filters()hook so frames can be dropped before the CRC check runs, instead of always paying the full decode+CRC cost and filtering afterward.- Support the CAN FD protocol for the
BusABCparent (can_protocolcurrently reportsCAN_20unconditionally). - Add an option for arbitrary (non-CAN) data length up to 255 bytes, since
dlcis only 1 byte — useful for passing arbitrary serial payloads through python-can tooling without being constrained to CAN/CAN FD payload sizes.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file python_can_flexible_serial-0.1.0.tar.gz.
File metadata
- Download URL: python_can_flexible_serial-0.1.0.tar.gz
- Upload date:
- Size: 18.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9b2701602190624a1eb187acb9f7742d3f46bb28e10714af23375efbe0d118cb
|
|
| MD5 |
5bf8ce9884d251391af8ace1009975ca
|
|
| BLAKE2b-256 |
73f44e2cca311397c93f9f857e7deda95a920efde0757cbc39418c445c80b4d8
|
Provenance
The following attestation bundles were made for python_can_flexible_serial-0.1.0.tar.gz:
Publisher:
publish.yml on mattbulow/python-can-flexible-serial
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
python_can_flexible_serial-0.1.0.tar.gz -
Subject digest:
9b2701602190624a1eb187acb9f7742d3f46bb28e10714af23375efbe0d118cb - Sigstore transparency entry: 2256767567
- Sigstore integration time:
-
Permalink:
mattbulow/python-can-flexible-serial@76b1533b2445d4e47fd4e734b2c6c46007a3032c -
Branch / Tag:
refs/heads/main - Owner: https://github.com/mattbulow
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@76b1533b2445d4e47fd4e734b2c6c46007a3032c -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file python_can_flexible_serial-0.1.0-py3-none-any.whl.
File metadata
- Download URL: python_can_flexible_serial-0.1.0-py3-none-any.whl
- Upload date:
- Size: 14.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
643f9b5ef5b020f23b711b9e08734ff28f580724c03757b3aa8a431c8e3f8d20
|
|
| MD5 |
0683cc61247e9acbfbb8fe9e885bd26e
|
|
| BLAKE2b-256 |
0f0a977d732af728276366ce648736b5e40b31e7814f5ca69a2c619e7c95c461
|
Provenance
The following attestation bundles were made for python_can_flexible_serial-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on mattbulow/python-can-flexible-serial
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
python_can_flexible_serial-0.1.0-py3-none-any.whl -
Subject digest:
643f9b5ef5b020f23b711b9e08734ff28f580724c03757b3aa8a431c8e3f8d20 - Sigstore transparency entry: 2256767572
- Sigstore integration time:
-
Permalink:
mattbulow/python-can-flexible-serial@76b1533b2445d4e47fd4e734b2c6c46007a3032c -
Branch / Tag:
refs/heads/main - Owner: https://github.com/mattbulow
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@76b1533b2445d4e47fd4e734b2c6c46007a3032c -
Trigger Event:
workflow_dispatch
-
Statement type: