Skip to main content

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.

It's just as useful with no CAN anywhere. Frame a plain serial link's messages this way and python-can's tooling works on them without writing a parser: can.logger traces (.blf, .asc, .mf4, ...), can.viewer, can.bridge, message filters, periodic sends, and DBC decoding of the payloads with cantools.

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) — uint32 milliseconds. It works the same way as in python-can's built-in serial interface: on send, msg.timestamp (seconds) is multiplied by 1000 and truncated; on receive, the field is divided by 1000 into msg.timestamp. It's whatever clock the sender uses (device uptime, say), not wall-clock time, and it wraps after about 49.7 days. If you use it, set msg.timestamp yourself: a message stamped with wall-clock time, like time.time(), doesn't fit. Out-of-range values are clamped to 0–0xFFFFFFFF rather than raising as serial does, so one frame with an odd clock doesn't stop a live trace; the first one on each bus logs a warning on the can.flexible_serial logger. Omit the field entirely with no_timestamp=True to save 4 bytes per frame; python-can then stamps each frame when it's read.

  • dlc (always present, fixed 1 byte) — the payload length in bytes (not a CAN DLC code), which also sizes the data section 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 id field (see below).

  • id (always present, sizeable: 2 or 4 bytes) — arbitration ID. uint16_id=True shrinks 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, dlc bytes long. How many bytes are allowed depends on how the layout says a frame is CAN FD:

    Layout CAN FD frames Max payload
    flags_byte=True the FD bit (and BRS bit) in the flags byte 64 for FD frames, 8 otherwise
    uint16_id=True, no flags byte any frame longer than 8 bytes is taken as FD 64
    4-byte ID, no flags byte not representable 8

    A longer payload than the layout allows is received as a BAD_DLC error frame (see "Error frames").

  • crc (optional, 0 or 2 bytes) — set by crc_type: "none", or "crc16-ccitt" (CRC-16/CCITT-FALSE: polynomial 0x1021, initial value 0xFFFF, no reflection, no final XOR — Python's binascii.crc_hqx(data, 0xFFFF)). It covers the flags byte (if present), the ID and the data — not the SOF, timestamp, length byte or EOF. That's by design: the length byte decides where the receiver reads the CRC and EOF from, so a corrupted length still fails the CRC or EOF check.

Byte order for the timestamp, ID 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 From / to can.Message
0 0x01 Error frame is_error_frame
1 0x02 Remote frame (RTR) is_remote_frame
2 0x04 Extended (29-bit) ID is_extended_id
3 0x08 CAN FD frame is_fd
4 0x10 CAN FD bit-rate switch bitrate_switch
5 0x20 Frame originated as RX (vs. TX) is_rx

Every bit is passed through as-is in both directions; the interface never rewrites them. That includes bit 5: can.Message defaults to is_rx=True, so a sent frame carries the RX bit unless you send it with is_rx=False. Forwarding a recorded trace keeps each frame's direction.

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: a 16-bit ID field has no spare bits, so with uint16_id=True and no flags byte, error and remote frames can't be represented and every received frame is a data frame with is_rx=True.

Error frames

Protocol-level errors are surfaced as Message objects with is_error_frame=True rather than raising exceptions, matching how other python-can interfaces report bus errors. Byte 0 of their data is the error code (FlexibleSerial.SerialError), and byte 1, when present, a detail:

Code Name Byte 1 What the receiver does next
1 DESYNC number of bytes dropped Skips the bytes before the next SOF
2 BAD_DLC the length received Drops one byte and resynchronizes; the rest of the bad frame then usually shows up as a DESYNC
3 BAD_EOF — Drops one byte and resynchronizes; the rest of the bad frame then usually shows up as a DESYNC
4 BAD_CRC — Drops the whole frame

Use FlexibleSerial.decode_serial_can_error(msg) to get a human-readable description, e.g. "Start-of-Frame desync -> 3 bytes dropped". BAD_DLC, BAD_EOF and BAD_CRC error frames carry the ID, timestamp and flags read from the bad frame's header; DESYNC ones have ID 0 and the time the junk was found.

Installation

From PyPI:

pip install python-can-flexible-serial

or, in a project managed with uv, uv add python-can-flexible-serial.

Using a local clone 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,
)

bus.send(can.Message(arbitration_id=0x123, data=[0x01, 0x02, 0x03], is_extended_id=False))
msg = bus.recv(timeout=1.0)   # None if nothing arrived in time

bus.shutdown()

Only opening the bus is specific to this plugin: the constructor arguments below pick the port and the frame layout. Everything after that is the standard python-can bus API, the same as with any other interface: send() and recv(), iterating over the bus, can.Notifier with listeners and loggers, send_periodic(), set_filters(), and with can.Bus(...) as bus: or shutdown(). The few behaviors specific to this interface are under "Sending and receiving" below.

Constructor arguments

Argument Type Default Description
channel str (required) Serial device, e.g. "COM3" or "/dev/ttyUSB0", or any pyserial URL: loop:// for testing, socket://host:port, rfc2217://host:port, ...
baudrate int 115200 Serial baud rate
timeout float 0.1 Serial read timeout, in seconds
rtscts bool False Enable RTS/CTS hardware handshake
low_latency bool True Linux: have the port hand over received bytes as they arrive (see "Low latency" below)
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 the timestamp, ID and 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"
receive_own_messages bool False python-can's standard option: every message you send is also returned by recv() (see "Sending and receiving" below)
data_processed_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, including bytes dropped while resynchronizing; useful for logging/debugging the raw wire traffic. It runs on the thread calling send() or recv(), so keep it quick.

The bool arguments must be real booleans in Python code. Any other keyword arguments are passed on to python-can's BusABC, such as can_filters.

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). A hex string can be written as 0x4040, 4040, 40 40 or \x40\x40, and may use _, -, : or , between bytes:

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.

Low latency

By default a serial port may hold received bytes back and deliver them in batches, which delays received frames and their timestamps (with no_timestamp=True, a frame is stamped when it is read). The bus turns that off when it opens the port, for the hardware that needs telling. It is Linux only and best effort: anything that can't be set may be logged as a warning and the port opens anyway. Pass low_latency=False to leave the port's own settings alone.

Hardware Port's own setting With low_latency=True (the default)
FTDI USB serial (ftdi_sio) 16 ms latency timer 1 ms, via pyserial's set_low_latency_mode. The adapter keeps it until it's replugged, so later opens by other programs get it too.
Advantech PCIE-16xx cards (adv17v35x driver) RX FIFO interrupt after 32 bytes, or after ~4 idle characters Interrupt on every byte. The driver puts it back to 32 the next time the port is opened with nothing else holding it open.
Other Linux ttys — pyserial's set_low_latency_mode; what that does depends on the driver

No root is needed for either. On Windows, set the FTDI latency timer in the port's Device Manager properties instead.

Sending and receiving

  • Threads: send() is safe to call from several threads at once, including python-can's periodic send tasks (bus.send_periodic()); frames are never interleaved on the wire.
  • Send timeout: send(msg, timeout=...) sets the serial write timeout for that write; a write that doesn't finish in time raises can.CanTimeoutError.
  • Bursts: each read takes every complete frame the port has buffered and stamps them in the same pass, so with no_timestamp=True a burst gets timestamps from when its bytes were read, not from when your code got round to each recv().
  • Own messages: with receive_own_messages=True, each message you send is returned by recv() exactly as you passed it, after it's written: nothing is re-encoded or rewritten, including is_rx, so set is_rx=False when sending if the direction matters downstream. A message with no timestamp (0.0, the can.Message default) is stamped with the time it was written. Echoes are queued in order with received frames.
  • fileno() returns the port's file descriptor where the platform has one, so the bus works with select-based tools and python-can's asyncio Notifier.

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"]))

It lists every port pyserial finds, except, on Linux, serial-core placeholder ttys with no UART behind them (such as the spare ttyS4–ttyS31 the 8250 driver always creates). Newer kernels (6.5+) stopped pyserial from hiding those itself.

Development

Test dependencies (pytest) are in the dev dependency group, which uv sync installs by default:

uv sync

or, with pip 25.1 or newer:

python -m pip install -e . --group dev

Run the test suite with:

uv run pytest

or

python -m pytest

To run it against a specific Python version, the way CI does: uv run --python 3.9 pytest.

tests/test_bus.py runs 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. A few tests exercise fcntl and are skipped on Windows; they run in CI.

CI and the release process, including publishing to PyPI, are described in RELEASING.md in the repository.

Future improvement ideas

  • can_filters support — already works today for free: BusABC.recv() always calls _matches_filters() in software unless _recv_internal reports 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 BusABC parent (can_protocol currently reports CAN_20 unconditionally).
  • Add an option for arbitrary (non-CAN) data length up to 255 bytes, since dlc is only 1 byte — useful for passing arbitrary serial payloads through python-can tooling without being constrained to CAN/CAN FD payload sizes.

Metadata

Release files for python-can-flexible-serial 0.2.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for python-can-flexible-serial 0.2.2
File Size Uploaded Provenance
python_can_flexible_serial-0.2.2.tar.gz 35.9 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for python-can-flexible-serial 0.2.2
File Interpreter ABI Platform
python_can_flexible_serial-0.2.2-py3-none-any.whl Python 3 none any Details

Total release size: 57.0 kB

Release files / python_can_flexible_serial-0.2.2.tar.gz

Download URL python_can_flexible_serial-0.2.2.tar.gz
Size 35.9 kB
Tags Source
SHA-256 checksum
How to use checksums
e4d03cdad5d1ad93f70b3b615f66f7ee624c0a9df9ebaf2bef5871444eb0afd2
BLAKE2b-256 checksum
How to use checksums
a27571e46ec2a17f88a2e1f970e15a07eb5d3cfb2db420ade78b2c3783cbef1b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.15 {"installer":{"name":"uv","version":"0.11.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / python_can_flexible_serial-0.2.2-py3-none-any.whl

Download URL python_can_flexible_serial-0.2.2-py3-none-any.whl
Size 21.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f94af65969f2758b41c487bd5dc932376c14bea809329fd8735415069a69c579
BLAKE2b-256 checksum
How to use checksums
cfd09dcea8221073266629d19ba128a20cb52d09d7b3ab5afde6f5288d179f00
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.15 {"installer":{"name":"uv","version":"0.11.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for python-can-flexible-serial 0.2.2

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.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page