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.
@@ -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.6.2
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.06.02"
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 = ["02.00"]
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.cansub_cert
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
- ssl_context = ssl.create_default_context(cafile=self.cansub_cert)
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
- from zeroconf import InterfaceChoice, ServiceBrowser, Zeroconf
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 so devices on multiple USB/Ethernet links
677
- # are all discovered (each CDC-NCM or Ethernet link is a separate interface).
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(2.0)
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
- if txt.get("api") not in CanSub._supported_api_versions:
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, verify=self.cansub_cert,
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
- verify=self.cansub_cert, json=0)
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.