python-can-flexible-serial 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_can_flexible_serial-0.1.0/LICENSE +21 -0
- python_can_flexible_serial-0.1.0/PKG-INFO +261 -0
- python_can_flexible_serial-0.1.0/README.md +246 -0
- python_can_flexible_serial-0.1.0/pyproject.toml +32 -0
- python_can_flexible_serial-0.1.0/setup.cfg +4 -0
- python_can_flexible_serial-0.1.0/src/flexible_serial/__init__.py +5 -0
- python_can_flexible_serial-0.1.0/src/flexible_serial/bus.py +780 -0
- python_can_flexible_serial-0.1.0/src/python_can_flexible_serial.egg-info/PKG-INFO +261 -0
- python_can_flexible_serial-0.1.0/src/python_can_flexible_serial.egg-info/SOURCES.txt +13 -0
- python_can_flexible_serial-0.1.0/src/python_can_flexible_serial.egg-info/dependency_links.txt +1 -0
- python_can_flexible_serial-0.1.0/src/python_can_flexible_serial.egg-info/entry_points.txt +2 -0
- python_can_flexible_serial-0.1.0/src/python_can_flexible_serial.egg-info/requires.txt +5 -0
- python_can_flexible_serial-0.1.0/src/python_can_flexible_serial.egg-info/top_level.txt +1 -0
- python_can_flexible_serial-0.1.0/tests/test_bus.py +145 -0
- python_can_flexible_serial-0.1.0/tests/test_hex_bytes.py +36 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matt Bulow
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-can-flexible-serial
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: python-can interface plugin for a configurable binary serial CAN protocol
|
|
5
|
+
Project-URL: python-can plugin interface, https://python-can.readthedocs.io/en/stable/plugin-interface.html
|
|
6
|
+
Keywords: can,python-can,serial,plugin
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: python-can<5,>=4.4
|
|
11
|
+
Requires-Dist: pyserial>=3.5
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# python-can-flexible-serial
|
|
17
|
+
|
|
18
|
+
A [python-can](https://python-can.readthedocs.io/) interface plugin that speaks a
|
|
19
|
+
configurable, framed binary protocol over a serial port. It follows the
|
|
20
|
+
[python-can plugin interface](https://python-can.readthedocs.io/en/stable/plugin-interface.html),
|
|
21
|
+
so once installed it is usable anywhere python-can accepts an `interface=`
|
|
22
|
+
name — `can.Bus`, `python -m can.viewer`, `python -m can.logger`,
|
|
23
|
+
`python -m can.bridge`, etc. — alongside the other community interface
|
|
24
|
+
plugins listed on that page (`python-can-canine`, `python-can-cvector`,
|
|
25
|
+
`python-can-remote`, `python-can-sontheim`, `python-can-cando`,
|
|
26
|
+
`python-can-candle`, ...).
|
|
27
|
+
|
|
28
|
+
Unlike the fixed-layout `slcan` and `serial` interfaces bundled with
|
|
29
|
+
python-can, the frame layout here is configurable via constructor kwargs
|
|
30
|
+
(SOF/EOF delimiters, ID width, optional CRC, optional flags byte, optional
|
|
31
|
+
timestamp) so it can be adapted to match firmware on the other end of the
|
|
32
|
+
serial link without changing code.
|
|
33
|
+
|
|
34
|
+
## Frame layout
|
|
35
|
+
|
|
36
|
+
| Section | Field | Size | Optional / Configurable |
|
|
37
|
+
|---|---|---|---|
|
|
38
|
+
| — | SOF | 1+ bytes | Configurable delimiter (default `0xAA`) |
|
|
39
|
+
| Header | timestamp | 0 or 4 bytes | Optional — omitted when `no_timestamp=True` |
|
|
40
|
+
| Header | dlc | 1 byte | Always present |
|
|
41
|
+
| Header | flags | 0 or 1 byte | Optional — only present when `flags_byte=True` |
|
|
42
|
+
| Header | id | 2 or 4 bytes | Sizeable — 2 bytes when `uint16_id=True`, else 4 |
|
|
43
|
+
| Payload | data | 0-64 bytes | Sizeable — length equals `dlc` |
|
|
44
|
+
| — | crc | 0 or N bytes | Optional — size/presence set by `crc_type` |
|
|
45
|
+
| — | EOF | 1+ bytes | Configurable delimiter (default `0xBB`) |
|
|
46
|
+
|
|
47
|
+
Details on each section:
|
|
48
|
+
|
|
49
|
+
- **SOF / EOF** — arbitrary-length byte sequences used purely as framing
|
|
50
|
+
delimiters. They are never covered by the CRC.
|
|
51
|
+
- **timestamp** *(optional, fixed 4 bytes when present)* — `uint32`
|
|
52
|
+
milliseconds. Omit entirely with `no_timestamp=True` to save 4 bytes per
|
|
53
|
+
frame if the far end doesn't supply/need one; python-can still assigns a
|
|
54
|
+
local receive timestamp in that case.
|
|
55
|
+
- **dlc** *(always present, fixed 1 byte)* — raw payload length in bytes;
|
|
56
|
+
also used to size the `data` section on receive.
|
|
57
|
+
- **flags** *(optional, fixed 1 byte when present)* — see "Flags byte" below
|
|
58
|
+
for the bit layout. When absent, the same metadata is instead packed into
|
|
59
|
+
spare bits of the `id` field (see below).
|
|
60
|
+
- **id** *(always present, sizeable: 2 or 4 bytes)* — arbitration ID.
|
|
61
|
+
`uint16_id=True` shrinks it to 2 bytes but rejects extended (29-bit) IDs;
|
|
62
|
+
the default 4-byte field supports both standard and extended IDs.
|
|
63
|
+
- **data** *(sizeable, 0-64 bytes)* — the CAN payload; length is whatever
|
|
64
|
+
`dlc` says, up to 64 bytes for CAN FD-sized frames or 8 bytes otherwise.
|
|
65
|
+
- **crc** *(optional, sizeable: 0 or N bytes)* — set by `crc_type`
|
|
66
|
+
(`"none"` or `"crc16-ccitt"`); covers the header and payload, but not
|
|
67
|
+
SOF/EOF.
|
|
68
|
+
|
|
69
|
+
Byte order for the header and CRC fields is little-endian by default, or
|
|
70
|
+
big-endian when `big_endian=True`.
|
|
71
|
+
|
|
72
|
+
### Flags byte
|
|
73
|
+
|
|
74
|
+
When `flags_byte=True`, extended/error/remote/FD metadata is carried in a
|
|
75
|
+
dedicated byte instead of being packed into spare bits of the arbitration ID:
|
|
76
|
+
|
|
77
|
+
| Bit | Mask | Meaning |
|
|
78
|
+
|------|------|---------|
|
|
79
|
+
| 0 | `0x01` | Error frame |
|
|
80
|
+
| 1 | `0x02` | Remote frame (RTR) |
|
|
81
|
+
| 2 | `0x04` | Extended (29-bit) ID |
|
|
82
|
+
| 3 | `0x08` | CAN FD frame |
|
|
83
|
+
| 4 | `0x10` | CAN FD bit-rate switch |
|
|
84
|
+
| 5 | `0x20` | Frame originated as RX (vs. TX) |
|
|
85
|
+
|
|
86
|
+
When `flags_byte=False`, extended/error/remote status is instead OR'd into
|
|
87
|
+
spare high bits of the 32-bit arbitration ID field (standard `python-can`
|
|
88
|
+
convention: `CAN_EFF_FLAG`, `CAN_ERR_FLAG`, `CAN_RTR_FLAG`). This only applies
|
|
89
|
+
when `uint16_id=False`, since a 16-bit ID field has no spare bits for flags.
|
|
90
|
+
|
|
91
|
+
### Error frames
|
|
92
|
+
|
|
93
|
+
Protocol-level errors (frame desync, bad DLC, EOF mismatch, bad CRC) are
|
|
94
|
+
surfaced as `Message` objects with `is_error_frame=True` rather than raising
|
|
95
|
+
exceptions, matching how other python-can interfaces report bus errors. Use
|
|
96
|
+
`FlexibleSerial.decode_serial_can_error(msg)` to get a human-readable
|
|
97
|
+
description of an error message's contents.
|
|
98
|
+
|
|
99
|
+
## Installation
|
|
100
|
+
|
|
101
|
+
How you install this depends on whether you're *using* it as a dependency
|
|
102
|
+
of some other project, or working on this repo itself.
|
|
103
|
+
|
|
104
|
+
### Using this plugin from another project
|
|
105
|
+
|
|
106
|
+
If that project is managed with [uv](https://docs.astral.sh/uv/), add this
|
|
107
|
+
repo as an editable path dependency, pointing at wherever you've cloned it:
|
|
108
|
+
|
|
109
|
+
```powershell
|
|
110
|
+
uv add --editable plugins/python-can-flexible-serial
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Run this from *that* project's directory, not from within this repo — `uv
|
|
114
|
+
add` adds a dependency to the project in your current directory, so running
|
|
115
|
+
it from inside `python-can-flexible-serial` itself is a self-dependency and
|
|
116
|
+
uv will refuse it.
|
|
117
|
+
|
|
118
|
+
With plain pip, from that project's environment:
|
|
119
|
+
|
|
120
|
+
```powershell
|
|
121
|
+
python -m pip install -e /path/to/python-can-flexible-serial
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Working on this repo itself
|
|
125
|
+
|
|
126
|
+
Use `uv sync` (a uv-managed project installs itself editable by default):
|
|
127
|
+
|
|
128
|
+
```powershell
|
|
129
|
+
uv sync
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Or with plain pip, from this repo's root:
|
|
133
|
+
|
|
134
|
+
```powershell
|
|
135
|
+
python -m pip install -e .
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Either way, this registers the plugin with python-can via the `can.interface`
|
|
139
|
+
entry point (`flexible_serial = "flexible_serial.bus:FlexibleSerial"`), so it
|
|
140
|
+
becomes available as `interface="flexible_serial"` anywhere python-can
|
|
141
|
+
resolves interfaces, without any further imports.
|
|
142
|
+
|
|
143
|
+
## Usage
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
import can
|
|
147
|
+
|
|
148
|
+
bus = can.interface.Bus(
|
|
149
|
+
interface="flexible_serial",
|
|
150
|
+
channel="COM3",
|
|
151
|
+
baudrate=115200,
|
|
152
|
+
)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Constructor arguments
|
|
156
|
+
|
|
157
|
+
| Argument | Type | Default | Description |
|
|
158
|
+
|---|---|---|---|
|
|
159
|
+
| `channel` | `str` | *(required)* | Serial device, e.g. `"COM3"` or `"/dev/ttyUSB0"` |
|
|
160
|
+
| `baudrate` | `int` | `115200` | Serial baud rate |
|
|
161
|
+
| `timeout` | `float` | `0.1` | Serial read timeout, in seconds |
|
|
162
|
+
| `rtscts` | `bool` | `False` | Enable RTS/CTS hardware handshake |
|
|
163
|
+
| `sof` | `bytes` \| hex `str` | `0xAA` | Start-of-frame delimiter |
|
|
164
|
+
| `eof` | `bytes` \| hex `str` | `0xBB` | End-of-frame delimiter |
|
|
165
|
+
| `big_endian` | `bool` | `False` | Byte order for header/CRC fields |
|
|
166
|
+
| `uint16_id` | `bool` | `False` | Use a 2-byte arbitration ID instead of 4-byte; rejects extended IDs |
|
|
167
|
+
| `flags_byte` | `bool` | `False` | Carry frame metadata in a dedicated byte (see "Flags byte" above) |
|
|
168
|
+
| `no_timestamp` | `bool` | `False` | Omit the 4-byte timestamp header field |
|
|
169
|
+
| `crc_type` | `str` | `"none"` | One of `"none"`, `"crc16-ccitt"` |
|
|
170
|
+
| `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 |
|
|
171
|
+
|
|
172
|
+
`sof` and `eof` accept either raw `bytes` (when constructing a `Bus` from
|
|
173
|
+
Python) or a hex string (when passed as a CLI `--bus-kwargs` value, since
|
|
174
|
+
python-can CLI tools pass kwargs as strings):
|
|
175
|
+
|
|
176
|
+
```powershell
|
|
177
|
+
python -m can.logger -i flexible_serial -c COM3 --bus-kwargs sof=0xAA eof=0x0D0A
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Prefer the `0x` prefix for CLI hex values, especially if the delimiter
|
|
181
|
+
contains only digits, since python-can auto-converts bare numeric kwargs.
|
|
182
|
+
|
|
183
|
+
### CLI examples
|
|
184
|
+
|
|
185
|
+
```powershell
|
|
186
|
+
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
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Bridging to another interface:
|
|
190
|
+
|
|
191
|
+
```powershell
|
|
192
|
+
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
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Autodetection
|
|
196
|
+
|
|
197
|
+
Like the built-in `serial` interface, available serial ports are surfaced
|
|
198
|
+
through python-can's autodetection API:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
import can
|
|
202
|
+
print(can.detect_available_configs(interfaces=["flexible_serial"]))
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Layout
|
|
206
|
+
|
|
207
|
+
```text
|
|
208
|
+
python-can-flexible-serial/
|
|
209
|
+
pyproject.toml
|
|
210
|
+
README.md
|
|
211
|
+
src/
|
|
212
|
+
flexible_serial/
|
|
213
|
+
__init__.py
|
|
214
|
+
bus.py
|
|
215
|
+
tests/
|
|
216
|
+
test_hex_bytes.py
|
|
217
|
+
test_bus.py
|
|
218
|
+
message_helper.py
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Development
|
|
222
|
+
|
|
223
|
+
Install with the `dev` extra to pull in test dependencies (`pytest`):
|
|
224
|
+
|
|
225
|
+
```powershell
|
|
226
|
+
uv sync --extra dev
|
|
227
|
+
```
|
|
228
|
+
or
|
|
229
|
+
```powershell
|
|
230
|
+
python -m pip install -e .[dev]
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Run the test suite with:
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
uv run -m pytest
|
|
237
|
+
```
|
|
238
|
+
or
|
|
239
|
+
```powershell
|
|
240
|
+
python -m pytest
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`tests/test_bus.py` scaffolds round-trip tests against two backends — a fully
|
|
244
|
+
in-memory mocked serial port and pyserial's `loop://` — mirroring the
|
|
245
|
+
structure of python-can's own `serial` interface test suite. The test
|
|
246
|
+
bodies are currently stubs (`skipTest`); fill them in as coverage is added.
|
|
247
|
+
|
|
248
|
+
## Future improvement ideas
|
|
249
|
+
|
|
250
|
+
- **`can_filters` support** — already works today for free: `BusABC.recv()`
|
|
251
|
+
always calls `_matches_filters()` in software unless `_recv_internal`
|
|
252
|
+
reports the message as pre-filtered, and this interface never does. What's
|
|
253
|
+
still missing is a hardware-style optimization: implement the optional
|
|
254
|
+
`_apply_filters()` hook so frames can be dropped *before* the CRC check
|
|
255
|
+
runs, instead of always paying the full decode+CRC cost and filtering
|
|
256
|
+
afterward.
|
|
257
|
+
- Support the CAN FD protocol for the `BusABC` parent (`can_protocol`
|
|
258
|
+
currently reports `CAN_20` unconditionally).
|
|
259
|
+
- Add an option for arbitrary (non-CAN) data length up to 255 bytes, since
|
|
260
|
+
`dlc` is only 1 byte — useful for passing arbitrary serial payloads through
|
|
261
|
+
python-can tooling without being constrained to CAN/CAN FD payload sizes.
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# python-can-flexible-serial
|
|
2
|
+
|
|
3
|
+
A [python-can](https://python-can.readthedocs.io/) interface plugin that speaks a
|
|
4
|
+
configurable, framed binary protocol over a serial port. It follows the
|
|
5
|
+
[python-can plugin interface](https://python-can.readthedocs.io/en/stable/plugin-interface.html),
|
|
6
|
+
so once installed it is usable anywhere python-can accepts an `interface=`
|
|
7
|
+
name — `can.Bus`, `python -m can.viewer`, `python -m can.logger`,
|
|
8
|
+
`python -m can.bridge`, etc. — alongside the other community interface
|
|
9
|
+
plugins listed on that page (`python-can-canine`, `python-can-cvector`,
|
|
10
|
+
`python-can-remote`, `python-can-sontheim`, `python-can-cando`,
|
|
11
|
+
`python-can-candle`, ...).
|
|
12
|
+
|
|
13
|
+
Unlike the fixed-layout `slcan` and `serial` interfaces bundled with
|
|
14
|
+
python-can, the frame layout here is configurable via constructor kwargs
|
|
15
|
+
(SOF/EOF delimiters, ID width, optional CRC, optional flags byte, optional
|
|
16
|
+
timestamp) so it can be adapted to match firmware on the other end of the
|
|
17
|
+
serial link without changing code.
|
|
18
|
+
|
|
19
|
+
## Frame layout
|
|
20
|
+
|
|
21
|
+
| Section | Field | Size | Optional / Configurable |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| — | SOF | 1+ bytes | Configurable delimiter (default `0xAA`) |
|
|
24
|
+
| Header | timestamp | 0 or 4 bytes | Optional — omitted when `no_timestamp=True` |
|
|
25
|
+
| Header | dlc | 1 byte | Always present |
|
|
26
|
+
| Header | flags | 0 or 1 byte | Optional — only present when `flags_byte=True` |
|
|
27
|
+
| Header | id | 2 or 4 bytes | Sizeable — 2 bytes when `uint16_id=True`, else 4 |
|
|
28
|
+
| Payload | data | 0-64 bytes | Sizeable — length equals `dlc` |
|
|
29
|
+
| — | crc | 0 or N bytes | Optional — size/presence set by `crc_type` |
|
|
30
|
+
| — | EOF | 1+ bytes | Configurable delimiter (default `0xBB`) |
|
|
31
|
+
|
|
32
|
+
Details on each section:
|
|
33
|
+
|
|
34
|
+
- **SOF / EOF** — arbitrary-length byte sequences used purely as framing
|
|
35
|
+
delimiters. They are never covered by the CRC.
|
|
36
|
+
- **timestamp** *(optional, fixed 4 bytes when present)* — `uint32`
|
|
37
|
+
milliseconds. Omit entirely with `no_timestamp=True` to save 4 bytes per
|
|
38
|
+
frame if the far end doesn't supply/need one; python-can still assigns a
|
|
39
|
+
local receive timestamp in that case.
|
|
40
|
+
- **dlc** *(always present, fixed 1 byte)* — raw payload length in bytes;
|
|
41
|
+
also used to size the `data` section on receive.
|
|
42
|
+
- **flags** *(optional, fixed 1 byte when present)* — see "Flags byte" below
|
|
43
|
+
for the bit layout. When absent, the same metadata is instead packed into
|
|
44
|
+
spare bits of the `id` field (see below).
|
|
45
|
+
- **id** *(always present, sizeable: 2 or 4 bytes)* — arbitration ID.
|
|
46
|
+
`uint16_id=True` shrinks it to 2 bytes but rejects extended (29-bit) IDs;
|
|
47
|
+
the default 4-byte field supports both standard and extended IDs.
|
|
48
|
+
- **data** *(sizeable, 0-64 bytes)* — the CAN payload; length is whatever
|
|
49
|
+
`dlc` says, up to 64 bytes for CAN FD-sized frames or 8 bytes otherwise.
|
|
50
|
+
- **crc** *(optional, sizeable: 0 or N bytes)* — set by `crc_type`
|
|
51
|
+
(`"none"` or `"crc16-ccitt"`); covers the header and payload, but not
|
|
52
|
+
SOF/EOF.
|
|
53
|
+
|
|
54
|
+
Byte order for the header and CRC fields is little-endian by default, or
|
|
55
|
+
big-endian when `big_endian=True`.
|
|
56
|
+
|
|
57
|
+
### Flags byte
|
|
58
|
+
|
|
59
|
+
When `flags_byte=True`, extended/error/remote/FD metadata is carried in a
|
|
60
|
+
dedicated byte instead of being packed into spare bits of the arbitration ID:
|
|
61
|
+
|
|
62
|
+
| Bit | Mask | Meaning |
|
|
63
|
+
|------|------|---------|
|
|
64
|
+
| 0 | `0x01` | Error frame |
|
|
65
|
+
| 1 | `0x02` | Remote frame (RTR) |
|
|
66
|
+
| 2 | `0x04` | Extended (29-bit) ID |
|
|
67
|
+
| 3 | `0x08` | CAN FD frame |
|
|
68
|
+
| 4 | `0x10` | CAN FD bit-rate switch |
|
|
69
|
+
| 5 | `0x20` | Frame originated as RX (vs. TX) |
|
|
70
|
+
|
|
71
|
+
When `flags_byte=False`, extended/error/remote status is instead OR'd into
|
|
72
|
+
spare high bits of the 32-bit arbitration ID field (standard `python-can`
|
|
73
|
+
convention: `CAN_EFF_FLAG`, `CAN_ERR_FLAG`, `CAN_RTR_FLAG`). This only applies
|
|
74
|
+
when `uint16_id=False`, since a 16-bit ID field has no spare bits for flags.
|
|
75
|
+
|
|
76
|
+
### Error frames
|
|
77
|
+
|
|
78
|
+
Protocol-level errors (frame desync, bad DLC, EOF mismatch, bad CRC) are
|
|
79
|
+
surfaced as `Message` objects with `is_error_frame=True` rather than raising
|
|
80
|
+
exceptions, matching how other python-can interfaces report bus errors. Use
|
|
81
|
+
`FlexibleSerial.decode_serial_can_error(msg)` to get a human-readable
|
|
82
|
+
description of an error message's contents.
|
|
83
|
+
|
|
84
|
+
## Installation
|
|
85
|
+
|
|
86
|
+
How you install this depends on whether you're *using* it as a dependency
|
|
87
|
+
of some other project, or working on this repo itself.
|
|
88
|
+
|
|
89
|
+
### Using this plugin from another project
|
|
90
|
+
|
|
91
|
+
If that project is managed with [uv](https://docs.astral.sh/uv/), add this
|
|
92
|
+
repo as an editable path dependency, pointing at wherever you've cloned it:
|
|
93
|
+
|
|
94
|
+
```powershell
|
|
95
|
+
uv add --editable plugins/python-can-flexible-serial
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Run this from *that* project's directory, not from within this repo — `uv
|
|
99
|
+
add` adds a dependency to the project in your current directory, so running
|
|
100
|
+
it from inside `python-can-flexible-serial` itself is a self-dependency and
|
|
101
|
+
uv will refuse it.
|
|
102
|
+
|
|
103
|
+
With plain pip, from that project's environment:
|
|
104
|
+
|
|
105
|
+
```powershell
|
|
106
|
+
python -m pip install -e /path/to/python-can-flexible-serial
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Working on this repo itself
|
|
110
|
+
|
|
111
|
+
Use `uv sync` (a uv-managed project installs itself editable by default):
|
|
112
|
+
|
|
113
|
+
```powershell
|
|
114
|
+
uv sync
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Or with plain pip, from this repo's root:
|
|
118
|
+
|
|
119
|
+
```powershell
|
|
120
|
+
python -m pip install -e .
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Either way, this registers the plugin with python-can via the `can.interface`
|
|
124
|
+
entry point (`flexible_serial = "flexible_serial.bus:FlexibleSerial"`), so it
|
|
125
|
+
becomes available as `interface="flexible_serial"` anywhere python-can
|
|
126
|
+
resolves interfaces, without any further imports.
|
|
127
|
+
|
|
128
|
+
## Usage
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
import can
|
|
132
|
+
|
|
133
|
+
bus = can.interface.Bus(
|
|
134
|
+
interface="flexible_serial",
|
|
135
|
+
channel="COM3",
|
|
136
|
+
baudrate=115200,
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Constructor arguments
|
|
141
|
+
|
|
142
|
+
| Argument | Type | Default | Description |
|
|
143
|
+
|---|---|---|---|
|
|
144
|
+
| `channel` | `str` | *(required)* | Serial device, e.g. `"COM3"` or `"/dev/ttyUSB0"` |
|
|
145
|
+
| `baudrate` | `int` | `115200` | Serial baud rate |
|
|
146
|
+
| `timeout` | `float` | `0.1` | Serial read timeout, in seconds |
|
|
147
|
+
| `rtscts` | `bool` | `False` | Enable RTS/CTS hardware handshake |
|
|
148
|
+
| `sof` | `bytes` \| hex `str` | `0xAA` | Start-of-frame delimiter |
|
|
149
|
+
| `eof` | `bytes` \| hex `str` | `0xBB` | End-of-frame delimiter |
|
|
150
|
+
| `big_endian` | `bool` | `False` | Byte order for header/CRC fields |
|
|
151
|
+
| `uint16_id` | `bool` | `False` | Use a 2-byte arbitration ID instead of 4-byte; rejects extended IDs |
|
|
152
|
+
| `flags_byte` | `bool` | `False` | Carry frame metadata in a dedicated byte (see "Flags byte" above) |
|
|
153
|
+
| `no_timestamp` | `bool` | `False` | Omit the 4-byte timestamp header field |
|
|
154
|
+
| `crc_type` | `str` | `"none"` | One of `"none"`, `"crc16-ccitt"` |
|
|
155
|
+
| `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 |
|
|
156
|
+
|
|
157
|
+
`sof` and `eof` accept either raw `bytes` (when constructing a `Bus` from
|
|
158
|
+
Python) or a hex string (when passed as a CLI `--bus-kwargs` value, since
|
|
159
|
+
python-can CLI tools pass kwargs as strings):
|
|
160
|
+
|
|
161
|
+
```powershell
|
|
162
|
+
python -m can.logger -i flexible_serial -c COM3 --bus-kwargs sof=0xAA eof=0x0D0A
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Prefer the `0x` prefix for CLI hex values, especially if the delimiter
|
|
166
|
+
contains only digits, since python-can auto-converts bare numeric kwargs.
|
|
167
|
+
|
|
168
|
+
### CLI examples
|
|
169
|
+
|
|
170
|
+
```powershell
|
|
171
|
+
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
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Bridging to another interface:
|
|
175
|
+
|
|
176
|
+
```powershell
|
|
177
|
+
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
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Autodetection
|
|
181
|
+
|
|
182
|
+
Like the built-in `serial` interface, available serial ports are surfaced
|
|
183
|
+
through python-can's autodetection API:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
import can
|
|
187
|
+
print(can.detect_available_configs(interfaces=["flexible_serial"]))
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Layout
|
|
191
|
+
|
|
192
|
+
```text
|
|
193
|
+
python-can-flexible-serial/
|
|
194
|
+
pyproject.toml
|
|
195
|
+
README.md
|
|
196
|
+
src/
|
|
197
|
+
flexible_serial/
|
|
198
|
+
__init__.py
|
|
199
|
+
bus.py
|
|
200
|
+
tests/
|
|
201
|
+
test_hex_bytes.py
|
|
202
|
+
test_bus.py
|
|
203
|
+
message_helper.py
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Development
|
|
207
|
+
|
|
208
|
+
Install with the `dev` extra to pull in test dependencies (`pytest`):
|
|
209
|
+
|
|
210
|
+
```powershell
|
|
211
|
+
uv sync --extra dev
|
|
212
|
+
```
|
|
213
|
+
or
|
|
214
|
+
```powershell
|
|
215
|
+
python -m pip install -e .[dev]
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Run the test suite with:
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
uv run -m pytest
|
|
222
|
+
```
|
|
223
|
+
or
|
|
224
|
+
```powershell
|
|
225
|
+
python -m pytest
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`tests/test_bus.py` scaffolds round-trip tests against two backends — a fully
|
|
229
|
+
in-memory mocked serial port and pyserial's `loop://` — mirroring the
|
|
230
|
+
structure of python-can's own `serial` interface test suite. The test
|
|
231
|
+
bodies are currently stubs (`skipTest`); fill them in as coverage is added.
|
|
232
|
+
|
|
233
|
+
## Future improvement ideas
|
|
234
|
+
|
|
235
|
+
- **`can_filters` support** — already works today for free: `BusABC.recv()`
|
|
236
|
+
always calls `_matches_filters()` in software unless `_recv_internal`
|
|
237
|
+
reports the message as pre-filtered, and this interface never does. What's
|
|
238
|
+
still missing is a hardware-style optimization: implement the optional
|
|
239
|
+
`_apply_filters()` hook so frames can be dropped *before* the CRC check
|
|
240
|
+
runs, instead of always paying the full decode+CRC cost and filtering
|
|
241
|
+
afterward.
|
|
242
|
+
- Support the CAN FD protocol for the `BusABC` parent (`can_protocol`
|
|
243
|
+
currently reports `CAN_20` unconditionally).
|
|
244
|
+
- Add an option for arbitrary (non-CAN) data length up to 255 bytes, since
|
|
245
|
+
`dlc` is only 1 byte — useful for passing arbitrary serial payloads through
|
|
246
|
+
python-can tooling without being constrained to CAN/CAN FD payload sizes.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "python-can-flexible-serial"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "python-can interface plugin for a configurable binary serial CAN protocol"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
keywords = ["can", "python-can", "serial", "plugin"]
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"python-can>=4.4,<5",
|
|
14
|
+
"pyserial>=3.5",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
dev = [
|
|
19
|
+
"pytest>=8",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
"python-can plugin interface" = "https://python-can.readthedocs.io/en/stable/plugin-interface.html"
|
|
24
|
+
|
|
25
|
+
[project.entry-points."can.interface"]
|
|
26
|
+
flexible_serial = "flexible_serial.bus:FlexibleSerial"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools]
|
|
29
|
+
package-dir = {"" = "src"}
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["src"]
|