python-can-cansub 2026.5.22__py3-none-any.whl

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,346 @@
1
+ import binascii
2
+ import can
3
+ from enum import IntEnum
4
+ from typing import Callable
5
+ from can.util import len2dlc, dlc2len
6
+
7
+ HDLC_BOUNDARY_BYTE = 0x7E
8
+ HDLC_ESCAPE_BYTE = 0x7D
9
+
10
+ MSG_ENCODE_ERR_HEADER_SIZE = 7
11
+ MSG_ENCODE_STD_HEADER_SIZE = 9 # 7 + 2
12
+ MSG_ENCODE_EXT_HEADER_SIZE = 11 # 7 + 4
13
+ MSG_ENCODE_HEADER_MIN_SIZE = MSG_ENCODE_ERR_HEADER_SIZE
14
+ MSG_ENCODE_HEADER_MAX_SIZE = MSG_ENCODE_EXT_HEADER_SIZE
15
+
16
+ # The encoded timestamp is offset by date 2025/01/01 00:00:00 UTC.
17
+ MSG_ENCODE_TIME_MIN = 1735689600
18
+ MSG_ENCODE_TIME_MAX = MSG_ENCODE_TIME_MIN + (0xFF_FF_FF_FF_FF_FF / 1_000_000)
19
+
20
+ class CanSubErrorFrameType(IntEnum):
21
+ BIT = 0
22
+ ACK = 1
23
+ FORM = 2
24
+ STUFF = 3
25
+ CRC = 4
26
+ OTHER = 0xFF
27
+
28
+ def cansub_protocol_encode(msg: can.Message, data: bytearray) -> bytes:
29
+ """
30
+ Encodes msg into data. Can be called multiple times to encode multiple messages in the same data buffer. The
31
+ boundary bytes (start/stop), byte stuffing and, CRC32 will be updated to ensure that the frame in the data buffer
32
+ is always valid.
33
+ """
34
+
35
+ # Encode the new message into message buffer
36
+ msg_buf = message_encode(msg)
37
+
38
+ # Default crc32 value
39
+ crc32 = 0
40
+
41
+ # Check if output buffer already contains a start boundary flag */
42
+ if len(data) == 0:
43
+ # No, add flag
44
+ data.append(HDLC_BOUNDARY_BYTE)
45
+ else:
46
+ # Remove the current end boundary flag (such that more data can be added)
47
+ boundary_flag_end = data.pop()
48
+ if boundary_flag_end != HDLC_BOUNDARY_BYTE:
49
+ raise ValueError("Unexpected boundary end flag")
50
+
51
+ # Get current CRC (and remove from the end of buffer)
52
+ # WARNING: The CRC bytes can be byte-stuffed and take up more than 4 bytes!
53
+ for i in range(0, 4):
54
+
55
+ if len(data) < 1:
56
+ raise ValueError("Missing CRC32 bytes")
57
+
58
+ # Get byte and check if byte before is escape byte
59
+ byte = data.pop()
60
+ if len(data) > 0 and (data[-1] == HDLC_ESCAPE_BYTE):
61
+ # Yes, byte before is escape byte, de-stuff crc byte and remove escape byte
62
+ byte ^= 0x20
63
+ data.pop()
64
+
65
+ # NOTE: Be aware that the CRC32 bytes are read from the end of the buffer!
66
+ crc32 |= byte << (i * 8)
67
+
68
+ # Update crc32 with new data
69
+ crc32 = binascii.crc32(msg_buf, crc32)
70
+
71
+ # Append CRC32 bytes to message buffer
72
+ msg_buf.extend(crc32.to_bytes(4, "big"))
73
+
74
+ # Add the new message to the persistent buffer while applying HDLC byte stuffing */
75
+ for byte in msg_buf:
76
+
77
+ # Check if bytestuffing in required
78
+ if byte == HDLC_BOUNDARY_BYTE or byte == HDLC_ESCAPE_BYTE:
79
+ # Insert escape byte
80
+ data.append(HDLC_ESCAPE_BYTE)
81
+
82
+ # Modify data byte with XOR 0x20
83
+ byte ^= 0x20
84
+
85
+ # Insert data byte
86
+ data.append(byte)
87
+
88
+ # Insert the boundary end flag
89
+ data.append(HDLC_BOUNDARY_BYTE)
90
+
91
+ return data
92
+
93
+ def cansub_protocol_decode(data: bytearray, msg_cb: Callable[[can.Message], None]) -> None:
94
+ frames_decode(data, msg_cb)
95
+
96
+ def frames_decode(data: bytearray, msg_cb: Callable[[can.Message], None]) -> None:
97
+
98
+ # Decode all frames in buffer
99
+ while len(data) > 0:
100
+
101
+ # Pre-decode length of buffer
102
+ data_len_pre = len(data)
103
+
104
+ # Decode single frame
105
+ try:
106
+ frame_decode(data, msg_cb)
107
+
108
+ # Check if bytes consumed
109
+ if len(data) == data_len_pre:
110
+ # No, this is not an error. Just that we need more bytes to parse frame.
111
+ break
112
+ except Exception as e:
113
+ # Some error. Bytes consumed?
114
+ print(f"Error decoding frame {e}")
115
+ if data_len_pre == len(data):
116
+ # No, likely deadlock, reset buffer
117
+ data = bytearray()
118
+ break
119
+
120
+ def frame_decode(data: bytearray, msg_cb: Callable[[can.Message], None]) -> None:
121
+
122
+ # Find boundary start
123
+ frame_start = data.find(HDLC_BOUNDARY_BYTE)
124
+ if frame_start < 0:
125
+ # Boundary start byte should during normal operation always be present. However, this is not a hard fault.
126
+ return
127
+
128
+ # Find boundary end (starting from boundary_start + 1)
129
+ frame_end = data.find(HDLC_BOUNDARY_BYTE, frame_start + 1)
130
+ if frame_end < 0:
131
+ # No boundary end, wait for more data. Zero bytes consumed
132
+ return
133
+
134
+ # Create frame containing byte-stuffed bytes without boundary flags
135
+ frame = data[frame_start + 1:frame_end]
136
+
137
+ # Consume frame bytes (including boundary flags)
138
+ del data[:frame_end + 1]
139
+
140
+ # Perform byte de-stuffing
141
+ i_in = 0
142
+ i_out = 0
143
+ while i_in < len(frame):
144
+
145
+ # Read one byte
146
+ byte = frame[i_in]
147
+ i_in += 1
148
+
149
+ # Check if de-stuffing is required
150
+ if byte == HDLC_ESCAPE_BYTE:
151
+ # Yes, check that we have one more byte
152
+ if i_in < len(frame):
153
+ # Yes, de-stuff next byte
154
+ byte = frame[i_in] ^ 0x20
155
+ i_in += 1
156
+ else:
157
+ # No, this is an error
158
+ raise ValueError("Frame stuffed byte missing")
159
+
160
+ # Add de-stuffed byte to the output
161
+ frame[i_out] = byte
162
+ i_out += 1
163
+
164
+ del frame[i_out:]
165
+
166
+ # Check frame crc32
167
+ if len(frame) < 4:
168
+ raise ValueError("Frame CRC32 missing")
169
+
170
+ crc32_expected = int.from_bytes(frame[-4:], "big")
171
+ del frame[-4:]
172
+ crc32_actual = binascii.crc32(frame)
173
+
174
+ if crc32_expected != crc32_actual:
175
+ raise ValueError("Frame CRC32 mismatch")
176
+
177
+ # Decode message(s) in frame
178
+ messages_decode(frame, msg_cb)
179
+
180
+ def messages_decode(data: bytearray, msg_cb: Callable[[can.Message], None]) -> None:
181
+
182
+ # Decode all messages in buffer
183
+ while len(data) > 0:
184
+ # Pre-decode length of buffer
185
+ data_len_pre = len(data)
186
+
187
+ # Decode a single message
188
+ try:
189
+ message_decode(data, msg_cb)
190
+ result = True
191
+ except Exception as e:
192
+ print(f"Error decoding message {e}")
193
+ result = False
194
+ break
195
+
196
+ # If decode error or if zero bytes consumed, break
197
+ if not result or data_len_pre == len(data):
198
+ raise ValueError("Message decode error")
199
+
200
+ def message_decode(data: bytearray, msg_cb: Callable[[can.Message], None]) -> None:
201
+
202
+ # Backup of len before bytes are removed
203
+ data_len_orig = len(data)
204
+
205
+ # Check minimum header
206
+ if data_len_orig < MSG_ENCODE_HEADER_MIN_SIZE:
207
+ raise ValueError("Not enough data to parse header")
208
+
209
+ # Microseconds (48 bit)
210
+ timestamp_us = int.from_bytes(data[:6], byteorder='big')
211
+ timestamp = float(timestamp_us) / 1_000_000.0 + MSG_ENCODE_TIME_MIN
212
+ del data[:6]
213
+
214
+ # Flags
215
+ flags = data[0]
216
+ del data[0]
217
+
218
+ # Error frame ?
219
+ if (flags & 0xE0) != 0x20:
220
+
221
+ # No, is data frame
222
+ if data_len_orig < MSG_ENCODE_STD_HEADER_SIZE:
223
+ raise ValueError("Not enough data to parse header")
224
+
225
+ # Frame format
226
+ is_fdf = bool(flags & 0x80)
227
+
228
+ if not is_fdf:
229
+ is_rtr = bool(flags & 0x40)
230
+ is_brs = False
231
+ is_esi = False
232
+ else:
233
+ is_rtr = False
234
+ is_brs = bool(flags & 0x40)
235
+ is_esi = bool(flags & 0x20)
236
+
237
+ # Get TX (note, logic reversed)
238
+ is_rx = not bool(flags & 0x10)
239
+
240
+ # DLC
241
+ dlc = flags & 0x0F
242
+
243
+ # Is extended ID?
244
+ is_extended_id = bool(data[0] & 0x80)
245
+
246
+ # Arbitration ID
247
+ if not is_extended_id:
248
+ # Regular ID
249
+ if len(data) < 2:
250
+ raise ValueError("Not enough data to parse STD ID")
251
+ arbitration_id = int.from_bytes(bytes(data[:2]), byteorder='big') & 0x7FF
252
+ del data[:2]
253
+ else:
254
+ # Extended ID
255
+ if len(data) < 4:
256
+ raise ValueError("Not enough data to parse EXT ID")
257
+ arbitration_id = int.from_bytes(bytes(data[:4]), byteorder='big') & 0x1FFFFFFF
258
+ del data[:4]
259
+
260
+ # Payload
261
+ payload = b""
262
+ if not is_rtr:
263
+ payload_len = dlc2len(dlc)
264
+ if len(data) >= payload_len:
265
+ payload = data[:payload_len]
266
+ del data[:payload_len]
267
+ else:
268
+ raise ValueError("Not enough data bytes for payload")
269
+
270
+ msg = can.Message(timestamp=timestamp,
271
+ channel=None,
272
+ is_rx=is_rx,
273
+ arbitration_id=arbitration_id,
274
+ is_extended_id=is_extended_id,
275
+ is_remote_frame=is_rtr,
276
+ is_fd=is_fdf,
277
+ bitrate_switch=is_brs,
278
+ error_state_indicator=is_esi,
279
+ # Note: python-can stores actual byte length in Message.dlc
280
+ dlc=dlc2len(dlc) if is_rtr else len(payload),
281
+ data=payload,
282
+ check=True,
283
+ )
284
+ # Invoke callback
285
+ msg_cb(msg)
286
+
287
+ else:
288
+ # Yes, is error frame. Store the error code in the arbitration ID (like the python-can socketcan implementation)
289
+ error_type_val = flags & 0x1F
290
+
291
+ # Map error type to enum value, default to OTHER if unknown
292
+ try:
293
+ error_type = CanSubErrorFrameType(error_type_val)
294
+ except ValueError:
295
+ error_type = CanSubErrorFrameType.OTHER
296
+
297
+ msg = can.Message(timestamp=timestamp,
298
+ channel=None,
299
+ arbitration_id=int(error_type),
300
+ is_error_frame=True,
301
+ check=True,
302
+ )
303
+ # Invoke callback
304
+ msg_cb(msg)
305
+
306
+
307
+ def message_encode(message: can.Message) -> bytearray:
308
+
309
+ if message.is_error_frame:
310
+ raise ValueError("Encoding of error frames not supported")
311
+
312
+ buffer = bytearray()
313
+
314
+ # Microseconds since 00:00:00 01/01/2025 UTC (48 bit)
315
+ timestamp_us = 0
316
+ if (message.timestamp is not None ) and (MSG_ENCODE_TIME_MAX >= message.timestamp >= MSG_ENCODE_TIME_MIN):
317
+ timestamp_us = int((message.timestamp - MSG_ENCODE_TIME_MIN) * 1_000_000)
318
+ buffer.extend(timestamp_us.to_bytes(6, byteorder='big'))
319
+
320
+ # Note: python-can stores actual byte length in Message.dlc
321
+ dlc = len2dlc(len(message.data)) if not message.is_remote_frame else len2dlc(message.dlc)
322
+
323
+ # fd (1 bit), rtr/brs (1 bit), 0/esi (1 bit), tx (1 bit), dlc (4 bit)
324
+ flags = 0
325
+ flags |= 0x80 if message.is_fd else 0x00
326
+ if not message.is_fd:
327
+ flags |= 0x40 if message.is_remote_frame else 0x00
328
+ else:
329
+ flags |= 0x40 if message.bitrate_switch else 0x00
330
+ flags |= 0x20 if message.error_state_indicator else 0x00
331
+ flags |= 0x10 if not message.is_rx else 0x00
332
+ flags |= dlc & 0x0F
333
+ buffer.append(flags)
334
+
335
+ # ID (11 or 29 bit). Note: MSb set if extended ID.
336
+ if not message.is_extended_id:
337
+ arbitration_id = bytearray((message.arbitration_id & 0x7FF).to_bytes(2, byteorder='big'))
338
+ else:
339
+ arbitration_id = bytearray(((message.arbitration_id & 0x1FFFFFFF) | 0x80000000).to_bytes(4, byteorder='big'))
340
+ buffer.extend(arbitration_id)
341
+
342
+ # Payload (omit for RTR)
343
+ if not message.is_remote_frame:
344
+ buffer.extend(message.data)
345
+
346
+ return buffer
@@ -0,0 +1,11 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIBkDCCATegAwIBAgIUXNdJNtJWKeOF1jo//9d5b4ogt8MwCgYIKoZIzj0EAwIw
3
+ FjEUMBIGA1UEAwwLQ0FOc3ViIHJvb3QwHhcNMjYwNDIxMDQ1NDQwWhcNNDYwNDE2
4
+ MDQ1NDQwWjAWMRQwEgYDVQQDDAtDQU5zdWIgcm9vdDBZMBMGByqGSM49AgEGCCqG
5
+ SM49AwEHA0IABE/d9+b3ev4iv6R2K75IE+V+6Tiy7F0FZCOfh/XgcUDrHjGRFdG1
6
+ HM7g77ca+yBXebiZOLAhSaDgVghdF4nCLy6jYzBhMB0GA1UdDgQWBBQLrD8RSEnY
7
+ 1TuXPi9/acQRBcMD9zAfBgNVHSMEGDAWgBQLrD8RSEnY1TuXPi9/acQRBcMD9zAP
8
+ BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwICBDAKBggqhkjOPQQDAgNHADBE
9
+ AiAknEW1MJy1PUO5I8T94ruNSqoFJ0aqrBXM8qBSEmWYhQIgc1wRS3jTucldvwKZ
10
+ yXf3o3Cmm+ebthFrKw7Bk5LtbkY=
11
+ -----END CERTIFICATE-----
@@ -0,0 +1,339 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-can-cansub
3
+ Version: 2026.5.22
4
+ Summary: CANsub python-can interface
5
+ Project-URL: Homepage, https://csselectronics.com/
6
+ Project-URL: Source, https://github.com/CSS-Electronics/python-can-cansub
7
+ Author: CSS Electronics
8
+ Author-email: contact@csselectronics.com
9
+ License: MIT
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.12
14
+ Requires-Dist: python-can>=4.6.0
15
+ Requires-Dist: requests>=2.32.5
16
+ Requires-Dist: wsproto>=1.3.2
17
+ Requires-Dist: zeroconf>=0.131.0
18
+ Description-Content-Type: text/markdown
19
+
20
+ # python-can-cansub
21
+
22
+ A [python-can](https://python-can.readthedocs.io/) integration for the [CANsub](https://csselectronics.com/) CAN bus interface family by CSS Electronics. Source on [GitHub](https://github.com/CSS-Electronics/python-can-cansub).
23
+
24
+ This package registers the CANsub as a standard python-can interface, making it compatible with all python-can tools and workflows. It also adds a CSV logger compatible with the *webCAN* browser tool provided with the device.
25
+
26
+ > **Tip:** This README is optimized for LLMs. When using an AI coding assistant with this package, provide this file as context for accurate results.
27
+
28
+ ## python-can API
29
+
30
+ ### Installation
31
+
32
+ ```bash
33
+ pip install python-can-cansub
34
+ ```
35
+
36
+ ### Import
37
+
38
+ When `python-can-cansub` is installed, the `cansub` interface is automatically registered with python-can. Import with:
39
+
40
+ ```python
41
+ import can
42
+ ```
43
+
44
+ ### Configuration
45
+
46
+ Python-can defines a hardware *configuration* by an `interface` and a `channel` (a single interface can have multiple channels).
47
+
48
+ The CANsub `interface` is fixed `"cansub"`. The `channel` is constructed from the device hostname (unique) and channel index.
49
+
50
+ | Connection | Hostname | python-can `channel` string |
51
+ |------------|-------------------------|-----------------------------------|
52
+ | USB | `[DEVICE-ID]-usb.local` | `[DEVICE-ID]-usb.local@[channel]` |
53
+ | Ethernet | `[DEVICE-ID]-eth.local` | `[DEVICE-ID]-eth.local@[channel]` |
54
+
55
+ The device-ID is printed on the device label. Channel indexing is **1-based** - the first channel is `1`.
56
+
57
+ A configuration is passed to `can.Bus` to open a bus.
58
+
59
+ #### Fixed
60
+
61
+ Example of a fixed configuration:
62
+
63
+ ```python
64
+ configs = [{"interface": "cansub", "channel": "aabbccdd-usb.local@1"},
65
+ {"interface": "cansub", "channel": "aabbccdd-usb.local@2"}]
66
+ ```
67
+
68
+ #### Auto-detect
69
+
70
+ Example of using `detect_available_configs` to automatically discover (uses mDNS) all connected CANsub devices and channels:
71
+
72
+ ```python
73
+ configs = can.detect_available_configs(interfaces=["cansub"])
74
+ # e.g. [{"interface": "cansub", "channel": "aabbccdd-usb.local@1"},
75
+ # {"interface": "cansub", "channel": "aabbccdd-usb.local@2"}
76
+ # {"interface": "cansub", "channel": "11223344-eth.local@1"}
77
+ # {"interface": "cansub", "channel": "11223344-eth.local@2"}]
78
+ ```
79
+
80
+ In the above example two CANsub devices are detected, each with two channels. One device is connected via USB and the other via Ethernet.
81
+
82
+ ### Opening a Bus
83
+
84
+ #### Single bus - hardcoded
85
+
86
+ ```python
87
+ with can.Bus(interface="cansub", channel="aabbccdd-usb.local@1", bitrate=250_000, data_bitrate=1_000_000) as bus:
88
+ pass
89
+ ```
90
+
91
+ #### Single bus - from configs
92
+
93
+ ```python
94
+ with can.Bus(interface=configs[0]["interface"], channel=configs[0]["channel"], bitrate=250_000, data_bitrate=1_000_000) as bus:
95
+ pass
96
+ ```
97
+
98
+ #### Multiple buses - from configs
99
+
100
+ ```python
101
+ with (can.Bus(interface=configs[0]["interface"], channel=configs[0]["channel"], bitrate=250_000, data_bitrate=1_000_000) as bus1,
102
+ can.Bus(interface=configs[1]["interface"], channel=configs[1]["channel"], bitrate=250_000, data_bitrate=1_000_000) as bus2):
103
+ pass
104
+ ```
105
+
106
+ > **Tip:** `**config` unpacks a config dict directly into `can.Bus` keyword arguments:
107
+ >
108
+ > ```python
109
+ > with can.Bus(**configs[0], bitrate=250_000, data_bitrate=1_000_000) as bus:
110
+ > pass
111
+ > ```
112
+
113
+ ### Receive and Transmit
114
+
115
+ ```python
116
+ with can.Bus(**configs[0], bitrate=250_000, data_bitrate=1_000_000) as bus:
117
+
118
+ # Transmit
119
+ msg_tx = can.Message(is_extended_id=False, arbitration_id=0x123, data=[0x01, 0x02, 0x03, 0x04])
120
+ bus.send(msg_tx)
121
+
122
+ # Receive with timeout
123
+ msg_rx = bus.recv(timeout=1.0)
124
+ print(msg_rx)
125
+ ```
126
+
127
+ ### Filters
128
+
129
+ Apply hardware filters by passing `can_filters` to `can.Bus`. Each filter specifies a `can_id`, a `can_mask`, and whether to match standard (`extended=False`) or extended (`extended=True`) frames. A frame passes if `(frame_id & can_mask) == (can_id & can_mask)`.
130
+
131
+ ```python
132
+ filters = [
133
+ {"can_id": 0x123, "can_mask": 0x7FF, "extended": False}, # standard frames, exact ID match
134
+ {"can_id": 0x000, "can_mask": 0x000, "extended": True}, # all extended frames
135
+ ]
136
+
137
+ with can.Bus(**configs[0], bitrate=250_000, data_bitrate=1_000_000, can_filters=filters) as bus:
138
+ msg = bus.recv(timeout=1.0)
139
+ print(msg)
140
+ ```
141
+
142
+ > **Tip:** Applying hardware filters reduces the network load between the CANsub and the connected client.
143
+
144
+ ### Notifier and Listeners
145
+
146
+ `bus.recv()` blocks until a frame arrives. A `can.Notifier` runs a background thread that dispatches received frames to one or more *listeners*, allowing the main program to continue other work.
147
+
148
+ python-can provides built-in listeners including `can.Printer` (print to stdout) and `can.Logger` (log to file). The example below logs to a CSV file while the main program continues. Custom listeners can be implemented by subclassing `can.Listener`.
149
+
150
+ ```python
151
+ from time import sleep
152
+
153
+ print_listener = can.Printer()
154
+ csv_listener = can.Logger("log.csv")
155
+
156
+ with can.Bus(**configs[0], bitrate=250_000, data_bitrate=1_000_000) as bus:
157
+ with can.Notifier([bus], listeners=[print_listener, csv_listener]):
158
+
159
+ # Perform other tasks here while frames are received in the background
160
+ sleep(10)
161
+ ```
162
+
163
+ ### Broadcast Manager
164
+
165
+ Periodic transmission jobs can be started with `bus.send_periodic()`.
166
+
167
+ Most periodic transmission job types can be offloaded to the CANsub hardware, providing much better transmission time accuracy (compared to a host-scheduled transmission). A host-side background task is used only as a fallback when hardware transmission is not available.
168
+
169
+ ```python
170
+ from time import sleep
171
+
172
+ msgs = [
173
+ can.Message(is_extended_id=False, arbitration_id=0x123, data=[0x01, 0x02, 0x03, 0x04]),
174
+ can.Message(is_extended_id=False, arbitration_id=0x124, data=[0x05, 0x06, 0x07, 0x08]),
175
+ can.Message(is_extended_id=False, arbitration_id=0x125, data=[0x09, 0x0A, 0x0B, 0x0C]),
176
+ ]
177
+
178
+ with can.Bus(**configs[0], bitrate=250_000, data_bitrate=1_000_000) as bus:
179
+ # period: time between individual frames (sequence repeats every len(msgs) * period)
180
+ # duration: total transmission time in seconds (None = transmit indefinitely)
181
+ task = bus.send_periodic(msgs, period=0.1, duration=5.0)
182
+
183
+ # Perform other tasks here while frames are transmitted in the background
184
+ sleep(6)
185
+ ```
186
+
187
+ ### Replaying files
188
+
189
+ `can.MessageSync` can be used to replay messages from a log file.
190
+
191
+ ```python
192
+ with can.Bus(**configs[0], bitrate=250_000, data_bitrate=1_000_000) as bus:
193
+ with can.LogReader("log.csv") as reader:
194
+ for msg in can.MessageSync(messages=reader):
195
+ bus.send(msg)
196
+ ```
197
+
198
+ ## python-can tools
199
+
200
+ python-can includes several command line tools. All tools accept `--interface` and `--channel` to select the bus, following the same configuration as the API.
201
+
202
+ The common argument pattern for the CANsub:
203
+
204
+ ```
205
+ --interface cansub --channel aabbccdd-usb.local@1 --bitrate 250000 --data-bitrate 1000000
206
+ ```
207
+
208
+ ### can_logger
209
+
210
+ Log received frames to a file (CSV by default; format inferred from file extension):
211
+
212
+ ```bash
213
+ can_logger --interface cansub --channel aabbccdd-usb.local@1 --bitrate 250000 --data-bitrate 1000000 --output-file log.csv
214
+ ```
215
+
216
+ ### can_player
217
+
218
+ Play back a previously recorded log file:
219
+
220
+ ```bash
221
+ can_player --interface cansub --channel aabbccdd-usb.local@1 --bitrate 250000 --data-bitrate 1000000 log.csv
222
+ ```
223
+
224
+ ### can_viewer
225
+
226
+ Live terminal viewer showing received frames, updated counts, timestamps, and byte-level changes:
227
+
228
+ ```bash
229
+ can_viewer --interface cansub --channel aabbccdd-usb.local@1 --bitrate 250000 --data-bitrate 1000000
230
+ ```
231
+
232
+ ### can_bridge
233
+
234
+ Forward all frames received on one bus to another (e.g. bridge two CANsub channels):
235
+
236
+ ```bash
237
+ can_bridge --interface cansub --channel aabbccdd-usb.local@1 --bitrate 250000 --data-bitrate 1000000 \
238
+ --interface2 cansub --channel2 aabbccdd-usb.local@2 --bitrate2 250000 --data-bitrate2 1000000
239
+ ```
240
+
241
+ ### can_logconvert
242
+
243
+ Convert a log file between formats; the format is inferred from the file extension:
244
+
245
+ ```bash
246
+ can_logconvert log.csv log.asc
247
+ ```
248
+
249
+ ## Related Packages
250
+
251
+ The following packages complement `python-can-cansub` and are included here as inspiration for working with CAN data in Python.
252
+
253
+ ### cantools
254
+
255
+ [cantools](https://github.com/cantools/cantools) is a Python package for encoding and decoding CAN messages. Encoding/decoding rules can be created or loaded from DBC (and other) database files. It works directly with `can.Message` objects from python-can.
256
+
257
+ #### Installation
258
+
259
+ ```bash
260
+ pip install cantools
261
+ ```
262
+
263
+ #### Create database in code
264
+
265
+ A database can be constructed directly in Python without a database file:
266
+
267
+ ```python
268
+ import cantools
269
+
270
+ db = cantools.database.Database()
271
+
272
+ msg_def = cantools.database.can.Message(
273
+ frame_id=0x123,
274
+ name="Message1",
275
+ length=8,
276
+ signals=[
277
+ cantools.database.can.Signal(name="Signal1", start=0, length=16, scale=0.1, offset=0.0, minimum=0.0, maximum=100.0),
278
+ cantools.database.can.Signal(name="Signal2", start=16, length=16, scale=0.1, offset=0.0, minimum=0.0, maximum=100.0),
279
+ ]
280
+ )
281
+
282
+ db.add_message(msg_def)
283
+ ```
284
+
285
+ #### Load database from DBC file
286
+
287
+ ```python
288
+ import cantools
289
+
290
+ db = cantools.database.load_file("database.dbc")
291
+ msg_def = db.get_message_by_name("Message1")
292
+ ```
293
+
294
+ #### Encode
295
+
296
+ Encode signal values into the byte payload of a `can.Message`:
297
+
298
+ ```python
299
+ data = msg_def.encode({"Signal1": 1.0, "Signal2": 42.5})
300
+ msg_tx = can.Message(arbitration_id=msg_def.frame_id,
301
+ is_extended_id=msg_def.is_extended_frame,
302
+ data=data)
303
+
304
+ with can.Bus(**configs[0], bitrate=250_000, data_bitrate=1_000_000) as bus:
305
+ bus.send(msg_tx)
306
+ ```
307
+
308
+ #### Decode
309
+
310
+ Decode the byte payload of a received `can.Message` back into signal values:
311
+
312
+ ```python
313
+ with can.Bus(**configs[0], bitrate=250_000, data_bitrate=1_000_000) as bus:
314
+ msg_rx = bus.recv(timeout=1.0)
315
+ if msg_rx:
316
+ signals = db.decode_message(msg_rx.arbitration_id, msg_rx.data)
317
+ print(signals) # e.g. {'Signal1': 1.0, 'Signal2': 42.5}
318
+ ```
319
+
320
+ ### asammdf
321
+
322
+ [asammdf](https://github.com/danielhrisca/asammdf) is a Python package for reading and writing MDF (Measurement Data Format) files.
323
+
324
+ When `asammdf` is installed, python-can automatically gains support for reading MDF log files via `can.LogReader`, allowing MDF recordings to be played back directly using `can.MessageSync`:
325
+
326
+ #### Installation
327
+
328
+ ```bash
329
+ pip install asammdf
330
+ ```
331
+
332
+ #### Playback of MDF log file
333
+
334
+ ```python
335
+ with can.Bus(**configs[0], bitrate=250_000, data_bitrate=1_000_000) as bus:
336
+ with can.LogReader("recording.mf4") as reader:
337
+ for msg in can.MessageSync(messages=reader):
338
+ bus.send(msg)
339
+ ```
@@ -0,0 +1,8 @@
1
+ python_can_cansub/__init__.py,sha256=d6h_Gcl355zHd6n60XpGI9VVfMf63GMXkVmt7fOqiaU,225
2
+ python_can_cansub/cansub.py,sha256=rUn9fFIZXgLPMwHD-kn4WiH3nrcBkwiVEtMJm77a7yE,36702
3
+ python_can_cansub/cansub_protocol.py,sha256=tPYurcznSyR6oG6xN5sG7sgy9QHtt3WJ8UyGNP3LdT0,11315
4
+ python_can_cansub/cansub_root_cert.crt,sha256=g4ds4CtMhoX3QWX_fGnXbaBhxLBHCNN2I4ci4gMdlGw,603
5
+ python_can_cansub-2026.5.22.dist-info/METADATA,sha256=sb8jyA07UxsIKBPCVlcdZTND1rPIV8hDj9-WPOQ5nO8,11342
6
+ python_can_cansub-2026.5.22.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
7
+ python_can_cansub-2026.5.22.dist-info/entry_points.txt,sha256=2NrO_BE31bhjk-Gg0oSfOA5eUSS358RcN8hdxoEsd44,201
8
+ python_can_cansub-2026.5.22.dist-info/RECORD,,