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.
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """Template python-can plugin for a flexible serial interface."""
2
+
3
+ from .bus import FlexibleSerial
4
+
5
+ __all__ = ["FlexibleSerial"]