python-can-cansub 2026.6.2__tar.gz → 2026.7.2__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_cansub-2026.7.2/CHANGELOG.md +26 -0
- {python_can_cansub-2026.6.2 → python_can_cansub-2026.7.2}/PKG-INFO +71 -1
- {python_can_cansub-2026.6.2 → python_can_cansub-2026.7.2}/README.md +69 -0
- python_can_cansub-2026.7.2/private_repo/README_REPO.md +43 -0
- {python_can_cansub-2026.6.2 → python_can_cansub-2026.7.2}/pyproject.toml +2 -1
- {python_can_cansub-2026.6.2 → python_can_cansub-2026.7.2}/src/python_can_cansub/cansub.py +97 -19
- python_can_cansub-2026.6.2/CHANGELOG.md +0 -16
- python_can_cansub-2026.6.2/private_repo/README_REPO.md +0 -70
- {python_can_cansub-2026.6.2 → python_can_cansub-2026.7.2}/.gitignore +0 -0
- {python_can_cansub-2026.6.2 → python_can_cansub-2026.7.2}/private_repo/README.md +0 -0
- {python_can_cansub-2026.6.2 → python_can_cansub-2026.7.2}/private_repo/README_BUILD.md +0 -0
- {python_can_cansub-2026.6.2 → python_can_cansub-2026.7.2}/private_repo/README_DEV.md +0 -0
- {python_can_cansub-2026.6.2 → python_can_cansub-2026.7.2}/private_repo/README_PYPI.md +0 -0
- {python_can_cansub-2026.6.2 → python_can_cansub-2026.7.2}/src/python_can_cansub/__init__.py +0 -0
- {python_can_cansub-2026.6.2 → python_can_cansub-2026.7.2}/src/python_can_cansub/cansub_protocol.py +0 -0
- {python_can_cansub-2026.6.2 → python_can_cansub-2026.7.2}/src/python_can_cansub/cansub_root_cert.crt +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## 2026.07.02 (API 03.00)
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `server_cert`: path to the server certificate (`.crt` file).
|
|
9
|
+
- `client_cert`: tuple of paths to the client certificate and key (`.crt`, `.key`), used for TLS mutual authentication.
|
|
10
|
+
- Warning when devices with unsupported API versions are found.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Supported device API version changed from `02.00` to `03.00`.
|
|
14
|
+
|
|
15
|
+
## 2026.06.02 (API 02.00)
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- `CANSUB_ROOT_CERT`: path to the CANsub root certificate.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Transmit sequences (on the device) are deleted on bus open.
|
|
22
|
+
|
|
23
|
+
## 2026.06.01 (API 02.00)
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- `CyclicSendTask` with `duration=None` now working.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-can-cansub
|
|
3
|
-
Version: 2026.
|
|
3
|
+
Version: 2026.7.2
|
|
4
4
|
Summary: CANsub python-can interface
|
|
5
5
|
Project-URL: Homepage, https://csselectronics.com/
|
|
6
6
|
Project-URL: Source, https://github.com/CSS-Electronics/python-can-cansub
|
|
7
|
+
Project-URL: Changelog, https://github.com/CSS-Electronics/python-can-cansub/blob/master/CHANGELOG.md
|
|
7
8
|
Author: CSS Electronics
|
|
8
9
|
Author-email: contact@csselectronics.com
|
|
9
10
|
License: MIT
|
|
@@ -25,6 +26,17 @@ This package registers the CANsub as a standard python-can interface, making it
|
|
|
25
26
|
|
|
26
27
|
> **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
|
|
|
29
|
+
## Compatibility
|
|
30
|
+
|
|
31
|
+
The python-can-cansub package and the CANsub device communicate over a versioned API. They are compatible when the package supports the API version used by the device firmware.
|
|
32
|
+
|
|
33
|
+
- Each **python-can-cansub** release supports one API version. The supported API version for each release is listed in the [python-can-cansub changelog](https://github.com/CSS-Electronics/python-can-cansub/blob/master/CHANGELOG.md).
|
|
34
|
+
- Each **CANsub firmware** release uses one API version. The API version for each firmware release is listed in the CANsub changelog (provided with the device).
|
|
35
|
+
|
|
36
|
+
To check compatibility, look up the API version of the package release and of the device firmware release in their respective changelogs. If they match, the two are compatible.
|
|
37
|
+
|
|
38
|
+
If the package finds a connected device whose API version it does not support, it emits a warning. Update the package or the device firmware so their API versions align.
|
|
39
|
+
|
|
28
40
|
## python-can API
|
|
29
41
|
|
|
30
42
|
### Installation
|
|
@@ -81,6 +93,12 @@ In the above example two CANsub devices are detected, each with two channels. On
|
|
|
81
93
|
|
|
82
94
|
### Opening a Bus
|
|
83
95
|
|
|
96
|
+
> **Note:** `data_bitrate` is required even on a classic (non-FD) bus. On a non-FD bus, simply set it to e.g. `1_000_000` (1 Mbit/s).
|
|
97
|
+
|
|
98
|
+
> **Tip:** Pass `listen_only=True` to `can.Bus` to monitor a bus without transmitting or acknowledging frames.
|
|
99
|
+
|
|
100
|
+
> **Tip:** Pass `error_frames=True` to `can.Bus` to receive error frames.
|
|
101
|
+
|
|
84
102
|
#### Single bus - hardcoded
|
|
85
103
|
|
|
86
104
|
```python
|
|
@@ -110,6 +128,22 @@ with (can.Bus(interface=configs[0]["interface"], channel=configs[0]["channel"],
|
|
|
110
128
|
> pass
|
|
111
129
|
> ```
|
|
112
130
|
|
|
131
|
+
### TLS / Certificates
|
|
132
|
+
|
|
133
|
+
The data connection to the device is secured by TLS. When connecting via an IP address (where hostname verification will fail), `server_cert=None` can be used to disable certificate validation:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
with can.Bus(interface="cansub", channel="192.168.1.10@1", server_cert=None, bitrate=250_000, data_bitrate=1_000_000) as bus:
|
|
137
|
+
pass
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
If TLS mutual authentication is enabled, `client_cert` can be used to provide a tuple of paths to the client certificate (`.crt` file) and its unencrypted private key (`.key` file), i.e. `("cert", "key")`:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
with can.Bus(**configs[0], client_cert=("/path/to/client.crt", "/path/to/client.key"), bitrate=250_000, data_bitrate=1_000_000) as bus:
|
|
144
|
+
pass
|
|
145
|
+
```
|
|
146
|
+
|
|
113
147
|
### Receive and Transmit
|
|
114
148
|
|
|
115
149
|
```python
|
|
@@ -124,6 +158,20 @@ with can.Bus(**configs[0], bitrate=250_000, data_bitrate=1_000_000) as bus:
|
|
|
124
158
|
print(msg_rx)
|
|
125
159
|
```
|
|
126
160
|
|
|
161
|
+
### Error Frames
|
|
162
|
+
|
|
163
|
+
Error frame reporting is disabled by default; enable it by passing `error_frames=True` to `can.Bus`. Bus errors are then received as a `can.Message` with `is_error_frame` set. The error type is encoded in `arbitration_id`, which can be converted to a `CanSubErrorFrameType` enum:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from python_can_cansub import CanSubErrorFrameType
|
|
167
|
+
|
|
168
|
+
with can.Bus(**configs[0], bitrate=250_000, data_bitrate=1_000_000, error_frames=True) as bus:
|
|
169
|
+
msg = bus.recv(timeout=1.0)
|
|
170
|
+
if msg and msg.is_error_frame:
|
|
171
|
+
error_type = CanSubErrorFrameType(msg.arbitration_id)
|
|
172
|
+
print(f"Bus error: {error_type.name}") # e.g. "Bus error: ACK"
|
|
173
|
+
```
|
|
174
|
+
|
|
127
175
|
### Filters
|
|
128
176
|
|
|
129
177
|
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)`.
|
|
@@ -192,6 +240,28 @@ with can.Bus(**configs[0], bitrate=250_000, data_bitrate=1_000_000) as bus:
|
|
|
192
240
|
bus.send(msg)
|
|
193
241
|
```
|
|
194
242
|
|
|
243
|
+
### CSV Logger
|
|
244
|
+
|
|
245
|
+
On import, this package overrides the default python-can `.csv` reader and writer with a format compatible with the *webCAN* browser tool provided with the device. This applies automatically wherever `.csv` files are read or written, including `can.Logger`, `can.LogReader`, and the command-line tools.
|
|
246
|
+
|
|
247
|
+
The writer (`CanSubCSVWriter`) and reader (`CanSubCSVReader`) can also be used directly:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
from python_can_cansub import CanSubCSVWriter, CanSubCSVReader
|
|
251
|
+
|
|
252
|
+
# Write received messages to a webCAN-compatible CSV file
|
|
253
|
+
with can.Bus(**configs[0], bitrate=250_000, data_bitrate=1_000_000) as bus:
|
|
254
|
+
with CanSubCSVWriter("log.csv") as writer:
|
|
255
|
+
msg = bus.recv(timeout=1.0)
|
|
256
|
+
if msg:
|
|
257
|
+
writer.on_message_received(msg)
|
|
258
|
+
|
|
259
|
+
# Read messages back from the CSV file
|
|
260
|
+
with CanSubCSVReader("log.csv") as reader:
|
|
261
|
+
for msg in reader:
|
|
262
|
+
print(msg)
|
|
263
|
+
```
|
|
264
|
+
|
|
195
265
|
## python-can tools
|
|
196
266
|
|
|
197
267
|
python-can includes several command line tools. All tools accept `--interface` and `--channel` to select the bus, following the same configuration as the API.
|
|
@@ -6,6 +6,17 @@ This package registers the CANsub as a standard python-can interface, making it
|
|
|
6
6
|
|
|
7
7
|
> **Tip:** This README is optimized for LLMs. When using an AI coding assistant with this package, provide this file as context for accurate results.
|
|
8
8
|
|
|
9
|
+
## Compatibility
|
|
10
|
+
|
|
11
|
+
The python-can-cansub package and the CANsub device communicate over a versioned API. They are compatible when the package supports the API version used by the device firmware.
|
|
12
|
+
|
|
13
|
+
- Each **python-can-cansub** release supports one API version. The supported API version for each release is listed in the [python-can-cansub changelog](https://github.com/CSS-Electronics/python-can-cansub/blob/master/CHANGELOG.md).
|
|
14
|
+
- Each **CANsub firmware** release uses one API version. The API version for each firmware release is listed in the CANsub changelog (provided with the device).
|
|
15
|
+
|
|
16
|
+
To check compatibility, look up the API version of the package release and of the device firmware release in their respective changelogs. If they match, the two are compatible.
|
|
17
|
+
|
|
18
|
+
If the package finds a connected device whose API version it does not support, it emits a warning. Update the package or the device firmware so their API versions align.
|
|
19
|
+
|
|
9
20
|
## python-can API
|
|
10
21
|
|
|
11
22
|
### Installation
|
|
@@ -62,6 +73,12 @@ In the above example two CANsub devices are detected, each with two channels. On
|
|
|
62
73
|
|
|
63
74
|
### Opening a Bus
|
|
64
75
|
|
|
76
|
+
> **Note:** `data_bitrate` is required even on a classic (non-FD) bus. On a non-FD bus, simply set it to e.g. `1_000_000` (1 Mbit/s).
|
|
77
|
+
|
|
78
|
+
> **Tip:** Pass `listen_only=True` to `can.Bus` to monitor a bus without transmitting or acknowledging frames.
|
|
79
|
+
|
|
80
|
+
> **Tip:** Pass `error_frames=True` to `can.Bus` to receive error frames.
|
|
81
|
+
|
|
65
82
|
#### Single bus - hardcoded
|
|
66
83
|
|
|
67
84
|
```python
|
|
@@ -91,6 +108,22 @@ with (can.Bus(interface=configs[0]["interface"], channel=configs[0]["channel"],
|
|
|
91
108
|
> pass
|
|
92
109
|
> ```
|
|
93
110
|
|
|
111
|
+
### TLS / Certificates
|
|
112
|
+
|
|
113
|
+
The data connection to the device is secured by TLS. When connecting via an IP address (where hostname verification will fail), `server_cert=None` can be used to disable certificate validation:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
with can.Bus(interface="cansub", channel="192.168.1.10@1", server_cert=None, bitrate=250_000, data_bitrate=1_000_000) as bus:
|
|
117
|
+
pass
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
If TLS mutual authentication is enabled, `client_cert` can be used to provide a tuple of paths to the client certificate (`.crt` file) and its unencrypted private key (`.key` file), i.e. `("cert", "key")`:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
with can.Bus(**configs[0], client_cert=("/path/to/client.crt", "/path/to/client.key"), bitrate=250_000, data_bitrate=1_000_000) as bus:
|
|
124
|
+
pass
|
|
125
|
+
```
|
|
126
|
+
|
|
94
127
|
### Receive and Transmit
|
|
95
128
|
|
|
96
129
|
```python
|
|
@@ -105,6 +138,20 @@ with can.Bus(**configs[0], bitrate=250_000, data_bitrate=1_000_000) as bus:
|
|
|
105
138
|
print(msg_rx)
|
|
106
139
|
```
|
|
107
140
|
|
|
141
|
+
### Error Frames
|
|
142
|
+
|
|
143
|
+
Error frame reporting is disabled by default; enable it by passing `error_frames=True` to `can.Bus`. Bus errors are then received as a `can.Message` with `is_error_frame` set. The error type is encoded in `arbitration_id`, which can be converted to a `CanSubErrorFrameType` enum:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from python_can_cansub import CanSubErrorFrameType
|
|
147
|
+
|
|
148
|
+
with can.Bus(**configs[0], bitrate=250_000, data_bitrate=1_000_000, error_frames=True) as bus:
|
|
149
|
+
msg = bus.recv(timeout=1.0)
|
|
150
|
+
if msg and msg.is_error_frame:
|
|
151
|
+
error_type = CanSubErrorFrameType(msg.arbitration_id)
|
|
152
|
+
print(f"Bus error: {error_type.name}") # e.g. "Bus error: ACK"
|
|
153
|
+
```
|
|
154
|
+
|
|
108
155
|
### Filters
|
|
109
156
|
|
|
110
157
|
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)`.
|
|
@@ -173,6 +220,28 @@ with can.Bus(**configs[0], bitrate=250_000, data_bitrate=1_000_000) as bus:
|
|
|
173
220
|
bus.send(msg)
|
|
174
221
|
```
|
|
175
222
|
|
|
223
|
+
### CSV Logger
|
|
224
|
+
|
|
225
|
+
On import, this package overrides the default python-can `.csv` reader and writer with a format compatible with the *webCAN* browser tool provided with the device. This applies automatically wherever `.csv` files are read or written, including `can.Logger`, `can.LogReader`, and the command-line tools.
|
|
226
|
+
|
|
227
|
+
The writer (`CanSubCSVWriter`) and reader (`CanSubCSVReader`) can also be used directly:
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from python_can_cansub import CanSubCSVWriter, CanSubCSVReader
|
|
231
|
+
|
|
232
|
+
# Write received messages to a webCAN-compatible CSV file
|
|
233
|
+
with can.Bus(**configs[0], bitrate=250_000, data_bitrate=1_000_000) as bus:
|
|
234
|
+
with CanSubCSVWriter("log.csv") as writer:
|
|
235
|
+
msg = bus.recv(timeout=1.0)
|
|
236
|
+
if msg:
|
|
237
|
+
writer.on_message_received(msg)
|
|
238
|
+
|
|
239
|
+
# Read messages back from the CSV file
|
|
240
|
+
with CanSubCSVReader("log.csv") as reader:
|
|
241
|
+
for msg in reader:
|
|
242
|
+
print(msg)
|
|
243
|
+
```
|
|
244
|
+
|
|
176
245
|
## python-can tools
|
|
177
246
|
|
|
178
247
|
python-can includes several command line tools. All tools accept `--interface` and `--channel` to select the bus, following the same configuration as the API.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Repository Workflow Reminder
|
|
2
|
+
|
|
3
|
+
This project uses a **private repository for development** and a **public repository for cleaned releases**.
|
|
4
|
+
|
|
5
|
+
The private repository is hosted on Bitbucket while the public repository is hosted on GitHub (https://github.com/CSS-Electronics/python-can-cansub).
|
|
6
|
+
|
|
7
|
+
## Branches
|
|
8
|
+
|
|
9
|
+
- `master`: private development master. Full history. **Never pushed to the public repository.**
|
|
10
|
+
- `public-master`: temporary local branch used only to build and push a public release.
|
|
11
|
+
|
|
12
|
+
## Publish a Release
|
|
13
|
+
|
|
14
|
+
1. Fetch the public master into a local `public-master`. No GitHub remote is added, which avoids accidental pushes:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
git fetch git@github.com:CSS-Electronics/python-can-cansub.git master:public-master
|
|
18
|
+
git checkout public-master
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
2. Bring the release changes over from the private branch **manually**. In CLion, open the private branch's **"Show Diff with Working Tree"** and pick the individual changes to include. Leave out anything private (e.g. the `private_repo/` folder).
|
|
22
|
+
|
|
23
|
+
> Do not use `git merge` / `git cherry-pick` — hand-picking keeps the public history clean and prevents private content leaking.
|
|
24
|
+
|
|
25
|
+
3. Commit on `public-master`, then verify the diff against the private branch is exactly what should be public (versions in `CHANGELOG.md` and `pyproject.toml` match, no private files).
|
|
26
|
+
|
|
27
|
+
4. Push straight to the public GitHub master:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
git push git@github.com:CSS-Electronics/python-can-cansub.git public-master:master
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
5. Delete the local `public-master` when done:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git branch -D public-master
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Rule of Thumb
|
|
40
|
+
|
|
41
|
+
- `master` = private full history.
|
|
42
|
+
- `public-master` = throwaway branch, hand-curated per release.
|
|
43
|
+
- Never push private `master` to the public repository.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-can-cansub"
|
|
7
|
-
version = "2026.
|
|
7
|
+
version = "2026.07.02"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="CSS Electronics" },
|
|
10
10
|
{ email="contact@csselectronics.com" },
|
|
@@ -37,6 +37,7 @@ csv = "python_can_cansub.cansub:CanSubCSVReader"
|
|
|
37
37
|
[project.urls]
|
|
38
38
|
"Homepage" = "https://csselectronics.com/"
|
|
39
39
|
"Source" = "https://github.com/CSS-Electronics/python-can-cansub"
|
|
40
|
+
"Changelog" = "https://github.com/CSS-Electronics/python-can-cansub/blob/master/CHANGELOG.md"
|
|
40
41
|
|
|
41
42
|
[tool.pytest.ini_options]
|
|
42
43
|
pythonpath = [
|
|
@@ -8,10 +8,12 @@ import threading
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from datetime import datetime, timezone
|
|
10
10
|
from time import time, sleep
|
|
11
|
-
from typing import Optional, Sequence, TypedDict, Union, Callable, Generator, Any
|
|
11
|
+
from typing import Optional, Sequence, Tuple, TypedDict, Union, Callable, Generator, Any
|
|
12
12
|
from can import LimitedDurationCyclicSendTaskABC, BusABC, RestartableCyclicTaskABC
|
|
13
13
|
from can.io.generic import TextIOMessageWriter, TextIOMessageReader
|
|
14
14
|
from wsproto import ConnectionType, WSConnection, ConnectionState, events
|
|
15
|
+
from zeroconf import InterfacesType, ServiceListener, Zeroconf, ServiceBrowser, InterfaceChoice, IPVersion
|
|
16
|
+
|
|
15
17
|
from python_can_cansub.cansub_protocol import cansub_protocol_decode, cansub_protocol_encode
|
|
16
18
|
|
|
17
19
|
CANSUB_ROOT_CERT: Path = Path(__file__).with_name("cansub_root_cert.crt")
|
|
@@ -26,7 +28,7 @@ CanSubHwFilters = Sequence[CanSubHwFilter]
|
|
|
26
28
|
|
|
27
29
|
class CanSub(can.BusABC):
|
|
28
30
|
|
|
29
|
-
_supported_api_versions = ["
|
|
31
|
+
_supported_api_versions = ["03.00"]
|
|
30
32
|
_can_protocol = can.CanProtocol.CAN_FD_NON_ISO
|
|
31
33
|
_can_f_clock = 80_000_000
|
|
32
34
|
|
|
@@ -40,12 +42,24 @@ class CanSub(can.BusABC):
|
|
|
40
42
|
listen_only: Optional[bool] = False,
|
|
41
43
|
auto_reset: Optional[bool] = True,
|
|
42
44
|
error_frames: Optional[bool] = False,
|
|
45
|
+
server_cert: Optional[Union[str, Path]] = CANSUB_ROOT_CERT,
|
|
46
|
+
client_cert: Optional[Tuple[Union[str, Path], Union[str, Path]]] = None,
|
|
43
47
|
**kwargs: object
|
|
44
48
|
):
|
|
45
49
|
"""
|
|
46
50
|
Python-can compatible interface over a websocket connection. Functions bus.send and bus.recv are made
|
|
47
51
|
thread-safe by using queues.
|
|
48
52
|
|
|
53
|
+
:param channel:
|
|
54
|
+
The CAN channel to use. Can be an integer or a string. If a string contains '@',
|
|
55
|
+
it can specify both the device address and the channel (e.g., 'aabbccdd-usb.local@1').
|
|
56
|
+
|
|
57
|
+
:param can_filters:
|
|
58
|
+
Optional hardware filters to apply.
|
|
59
|
+
|
|
60
|
+
:param address:
|
|
61
|
+
The hostname or IP address of the device. If not provided, it must be part of the `channel` string.
|
|
62
|
+
|
|
49
63
|
:param timing:
|
|
50
64
|
CAN-bus bit-timing. Takes precedence over bitrate and data_bitrate.
|
|
51
65
|
|
|
@@ -64,6 +78,18 @@ class CanSub(can.BusABC):
|
|
|
64
78
|
:param error_frames:
|
|
65
79
|
CAN-bus error frame reporting.
|
|
66
80
|
|
|
81
|
+
:param server_cert:
|
|
82
|
+
Path to the server (device) public certificate (.crt file). If None, server verification is turned off.
|
|
83
|
+
Defaults to the built-in CANsub root certificate.
|
|
84
|
+
|
|
85
|
+
:param client_cert:
|
|
86
|
+
Tuple of paths to the client certificate (.crt file) and the unencrypted private key (.key file)
|
|
87
|
+
(i.e. ('cert', 'key')). Password protection is not supported.
|
|
88
|
+
Required when TLS mutual authentication is enabled.
|
|
89
|
+
|
|
90
|
+
:param kwargs:
|
|
91
|
+
Extra arguments passed to the parent `can.BusABC` class.
|
|
92
|
+
|
|
67
93
|
"""
|
|
68
94
|
|
|
69
95
|
# To be able to use python-can command line tool without custom arguments, support that channel can contain
|
|
@@ -85,6 +111,41 @@ class CanSub(can.BusABC):
|
|
|
85
111
|
host = address
|
|
86
112
|
port = 443
|
|
87
113
|
|
|
114
|
+
# Server (device) certificate
|
|
115
|
+
if not isinstance(server_cert, (type(None), str, Path)):
|
|
116
|
+
raise can.exceptions.CanInitializationError("Invalid server_cert")
|
|
117
|
+
|
|
118
|
+
self.server_cert = Path(server_cert) if server_cert else None
|
|
119
|
+
del server_cert
|
|
120
|
+
|
|
121
|
+
if self.server_cert is not None:
|
|
122
|
+
if not self.server_cert.is_file():
|
|
123
|
+
raise can.exceptions.CanInitializationError(f"Server certificate not found: {self.server_cert}")
|
|
124
|
+
if self.server_cert.suffix.lower() != ".crt":
|
|
125
|
+
raise can.exceptions.CanInitializationError(f"Server certificate must be a .crt file: {self.server_cert}")
|
|
126
|
+
|
|
127
|
+
# Client certificate (mTLS)
|
|
128
|
+
if client_cert is not None:
|
|
129
|
+
if not (isinstance(client_cert, (tuple, list)) and len(client_cert) == 2
|
|
130
|
+
and all(isinstance(p, (str, Path)) for p in client_cert)):
|
|
131
|
+
raise can.exceptions.CanInitializationError("Invalid client_cert")
|
|
132
|
+
|
|
133
|
+
self.client_cert = (Path(client_cert[0]), Path(client_cert[1]))
|
|
134
|
+
else:
|
|
135
|
+
self.client_cert = None
|
|
136
|
+
del client_cert
|
|
137
|
+
|
|
138
|
+
if self.client_cert is not None:
|
|
139
|
+
client_crt, client_key = self.client_cert
|
|
140
|
+
if not client_crt.is_file():
|
|
141
|
+
raise can.exceptions.CanInitializationError(f"Client certificate not found: {client_crt}")
|
|
142
|
+
if client_crt.suffix.lower() != ".crt":
|
|
143
|
+
raise can.exceptions.CanInitializationError(f"Client certificate must be a .crt file: {client_crt}")
|
|
144
|
+
if not client_key.is_file():
|
|
145
|
+
raise can.exceptions.CanInitializationError(f"Client key not found: {client_key}")
|
|
146
|
+
if client_key.suffix.lower() != ".key":
|
|
147
|
+
raise can.exceptions.CanInitializationError(f"Client key must be a .key file: {client_key}")
|
|
148
|
+
|
|
88
149
|
# Channel
|
|
89
150
|
self.channel = None
|
|
90
151
|
if isinstance(channel, can.typechecking.ChannelInt):
|
|
@@ -136,16 +197,14 @@ class CanSub(can.BusABC):
|
|
|
136
197
|
# Set channel info (required by python-can)
|
|
137
198
|
self.channel_info = f"{address}@{self.channel}"
|
|
138
199
|
|
|
139
|
-
# Path to device root certificate
|
|
140
|
-
self.cansub_cert = str(CANSUB_ROOT_CERT)
|
|
141
|
-
|
|
142
200
|
# Perform REST interaction with the device in a persistent session
|
|
143
201
|
self.api_url = f"https://{host}:{port}/api"
|
|
144
202
|
self._net_timeout = 3.0
|
|
145
203
|
|
|
146
204
|
# Create persistent session for REST API calls
|
|
147
205
|
self.session = requests.Session()
|
|
148
|
-
self.session.verify = self.
|
|
206
|
+
self.session.verify = str(self.server_cert) if self.server_cert else False
|
|
207
|
+
self.session.cert = (str(self.client_cert[0]), str(self.client_cert[1])) if self.client_cert else None
|
|
149
208
|
|
|
150
209
|
# Get api version (and test connection)
|
|
151
210
|
try:
|
|
@@ -217,7 +276,15 @@ class CanSub(can.BusABC):
|
|
|
217
276
|
raise can.exceptions.CanInitializationError(f"Failed to configure channel ({e})")
|
|
218
277
|
|
|
219
278
|
# Socket TLS configuration
|
|
220
|
-
|
|
279
|
+
if self.server_cert:
|
|
280
|
+
ssl_context = ssl.create_default_context(cafile=self.server_cert)
|
|
281
|
+
else:
|
|
282
|
+
ssl_context = ssl.create_default_context()
|
|
283
|
+
ssl_context.check_hostname = False
|
|
284
|
+
ssl_context.verify_mode = ssl.CERT_NONE
|
|
285
|
+
|
|
286
|
+
if self.client_cert:
|
|
287
|
+
ssl_context.load_cert_chain(certfile=self.client_cert[0], keyfile=self.client_cert[1])
|
|
221
288
|
|
|
222
289
|
# Socket
|
|
223
290
|
self.ws_sock = ssl_context.wrap_socket(sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM), server_hostname=host)
|
|
@@ -655,8 +722,16 @@ class CanSub(can.BusABC):
|
|
|
655
722
|
|
|
656
723
|
@staticmethod
|
|
657
724
|
def _detect_available_configs() -> Sequence[can.typechecking.AutoDetectedConfig]:
|
|
658
|
-
|
|
725
|
+
return CanSub.mdns_discover(interfaces=InterfaceChoice.All)
|
|
659
726
|
|
|
727
|
+
@staticmethod
|
|
728
|
+
def mdns_discover(interfaces: InterfacesType = InterfaceChoice.All, discovery_time: float = 2.0) -> Sequence[can.typechecking.AutoDetectedConfig]:
|
|
729
|
+
"""Discover available CANsub devices by performing a mDNS lookup.
|
|
730
|
+
|
|
731
|
+
@param interfaces: The interfaces to listen on. These must be either interface addresses or one of the zeroconf
|
|
732
|
+
InterfaceChoice enum values.
|
|
733
|
+
@return: A sequence of auto-detected CANbus configurations.
|
|
734
|
+
"""
|
|
660
735
|
# Collect DNS-SD service info objects keyed by service name.
|
|
661
736
|
# The device firmware advertises _cansub._tcp with TXT records:
|
|
662
737
|
# api=<version> - must match _supported_api_versions
|
|
@@ -665,7 +740,7 @@ class CanSub(can.BusABC):
|
|
|
665
740
|
# e.g. 1b5b9343-usb.local (USB) or 1b5b9343-eth.local (Ethernet).
|
|
666
741
|
discovered = {}
|
|
667
742
|
|
|
668
|
-
class _Listener:
|
|
743
|
+
class _Listener(ServiceListener):
|
|
669
744
|
def add_service(self, zc, type_, name):
|
|
670
745
|
info = zc.get_service_info(type_, name)
|
|
671
746
|
if info:
|
|
@@ -673,12 +748,11 @@ class CanSub(can.BusABC):
|
|
|
673
748
|
def remove_service(self, zc, type_, name): pass
|
|
674
749
|
def update_service(self, zc, type_, name): pass
|
|
675
750
|
|
|
676
|
-
# Listen on all interfaces
|
|
677
|
-
|
|
678
|
-
zc = Zeroconf(interfaces=InterfaceChoice.All)
|
|
751
|
+
# Listen on all specified interfaces.
|
|
752
|
+
zc = Zeroconf(interfaces=interfaces, ip_version=IPVersion.V4Only, use_asyncio=False)
|
|
679
753
|
try:
|
|
680
754
|
ServiceBrowser(zc, "_cansub._tcp.local.", _Listener())
|
|
681
|
-
sleep(
|
|
755
|
+
sleep(discovery_time)
|
|
682
756
|
finally:
|
|
683
757
|
zc.close()
|
|
684
758
|
|
|
@@ -692,7 +766,13 @@ class CanSub(can.BusABC):
|
|
|
692
766
|
}
|
|
693
767
|
|
|
694
768
|
# Skip devices running an unsupported API version.
|
|
695
|
-
|
|
769
|
+
api_version = txt.get("api")
|
|
770
|
+
if api_version not in CanSub._supported_api_versions:
|
|
771
|
+
warnings.warn(
|
|
772
|
+
f"Detected CANsub device {info.server.rstrip('.')} has an unsupported API version ({api_version}). "
|
|
773
|
+
f"Supported versions: [{', '.join(CanSub._supported_api_versions)}]",
|
|
774
|
+
UserWarning
|
|
775
|
+
)
|
|
696
776
|
continue
|
|
697
777
|
|
|
698
778
|
# info.server has a trailing dot (DNS convention); strip it.
|
|
@@ -727,7 +807,6 @@ class CyclicSendTask(LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC)
|
|
|
727
807
|
|
|
728
808
|
self.transmit_api_url = f"{cansub.api_url}/can/{channel}/transmit"
|
|
729
809
|
self._net_timeout = cansub._net_timeout
|
|
730
|
-
self.cansub_cert = cansub.cansub_cert
|
|
731
810
|
self.session = cansub.session
|
|
732
811
|
|
|
733
812
|
period_ms = int(len(self.messages) * self.period_ns / 1_000_000)
|
|
@@ -756,7 +835,7 @@ class CyclicSendTask(LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC)
|
|
|
756
835
|
}
|
|
757
836
|
|
|
758
837
|
# Set up new transmit sequence
|
|
759
|
-
response = self.session.post(url=self.transmit_api_url, timeout=self._net_timeout,
|
|
838
|
+
response = self.session.post(url=self.transmit_api_url, timeout=self._net_timeout,
|
|
760
839
|
json=transmit_sequence)
|
|
761
840
|
|
|
762
841
|
if response.status_code != 201:
|
|
@@ -775,14 +854,13 @@ class CyclicSendTask(LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC)
|
|
|
775
854
|
def start(self) -> None:
|
|
776
855
|
"""Restart a stopped periodic task."""
|
|
777
856
|
response = self.session.put(url=f"{self.transmit_api_url}/{self.transmit_id}/count", timeout=self._net_timeout,
|
|
778
|
-
|
|
857
|
+
json=0)
|
|
779
858
|
if response.status_code != 200:
|
|
780
859
|
raise can.exceptions.CanOperationError(f"Failed to (re)start transmit sequence ({response.status_code})")
|
|
781
860
|
|
|
782
861
|
def stop(self) -> None:
|
|
783
862
|
"""Stop periodic task."""
|
|
784
|
-
response = self.session.delete(url=f"{self.transmit_api_url}/{self.transmit_id}", timeout=self._net_timeout
|
|
785
|
-
verify=self.cansub_cert)
|
|
863
|
+
response = self.session.delete(url=f"{self.transmit_api_url}/{self.transmit_id}", timeout=self._net_timeout)
|
|
786
864
|
if response.status_code != 200:
|
|
787
865
|
raise can.exceptions.CanOperationError(f"Failed to stop transmit sequence ({response.status_code})")
|
|
788
866
|
return
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to this project will be documented in this file.
|
|
4
|
-
|
|
5
|
-
## 2026.06.02
|
|
6
|
-
|
|
7
|
-
### Added
|
|
8
|
-
- `CANSUB_ROOT_CERT`: path to the CANsub root certificate.
|
|
9
|
-
|
|
10
|
-
### Changed
|
|
11
|
-
- Transmit sequences (on the device) are deleted on bus open.
|
|
12
|
-
|
|
13
|
-
## 2026.06.01
|
|
14
|
-
|
|
15
|
-
### Fixed
|
|
16
|
-
- `CyclicSendTask` with `duration=None` now working.
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
# Repository Workflow Reminder
|
|
2
|
-
|
|
3
|
-
This project uses a **private repository for development** and a **public repository for cleaned releases**.
|
|
4
|
-
|
|
5
|
-
The private repository is hosted on Bitbucket while the public repository is hosted on GitHub (https://github.com/CSS-Electronics/python-can-cansub).
|
|
6
|
-
|
|
7
|
-
## Branches
|
|
8
|
-
|
|
9
|
-
### `master`
|
|
10
|
-
|
|
11
|
-
Private development master.
|
|
12
|
-
|
|
13
|
-
- Lives in the private repository.
|
|
14
|
-
- Contains the full commit history.
|
|
15
|
-
- Feature branches are reviewed and fast-forward merged here.
|
|
16
|
-
- This branch is **not pushed to the public repository**.
|
|
17
|
-
|
|
18
|
-
### `public-master`
|
|
19
|
-
|
|
20
|
-
Clean public-ready master.
|
|
21
|
-
|
|
22
|
-
- Lives in the private repository too.
|
|
23
|
-
- Contains squashed/clean commits only.
|
|
24
|
-
- This is the branch that gets pushed to the public repository's `master`.
|
|
25
|
-
|
|
26
|
-
## Normal Development
|
|
27
|
-
|
|
28
|
-
Work normally in the IDE using the private repository.
|
|
29
|
-
|
|
30
|
-
## Prepare Public Version
|
|
31
|
-
|
|
32
|
-
When a feature is ready for public release, squash it into `public-master`:
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
git checkout public-master
|
|
36
|
-
git merge --squash feature/my-feature
|
|
37
|
-
git commit -m "Add my feature"
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Validate that `public-master` content and history is correct.
|
|
41
|
-
|
|
42
|
-
Push the cleaned branch to the private repo:
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
git push origin public-master
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## Push to Public Repository
|
|
49
|
-
|
|
50
|
-
The public repository remote is not kept configured permanently.
|
|
51
|
-
|
|
52
|
-
Push only when intentionally publishing:
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
git push git@github.com:CSS-Electronics/python-can-cansub.git public-master:master
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
This pushes:
|
|
59
|
-
|
|
60
|
-
```text
|
|
61
|
-
local/private public-master -> public repo master
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
## Rule of Thumb
|
|
65
|
-
|
|
66
|
-
- `master` = private full history
|
|
67
|
-
- `public-master` = clean squashed history
|
|
68
|
-
- public repo `master` = copy of `public-master`
|
|
69
|
-
|
|
70
|
-
Never push private `master` to the public repository.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_can_cansub-2026.6.2 → python_can_cansub-2026.7.2}/src/python_can_cansub/cansub_protocol.py
RENAMED
|
File without changes
|
{python_can_cansub-2026.6.2 → python_can_cansub-2026.7.2}/src/python_can_cansub/cansub_root_cert.crt
RENAMED
|
File without changes
|