Skip to content

Commit 5d5d698

Browse files
Add CAN report listener (#15)
* Add CAN report listener * r u f f * Bump to 0.3.1 --------- Co-authored-by: Adam Mitchell <adam.mitchell@brillpower.com>
1 parent ca9fadb commit 5d5d698

11 files changed

Lines changed: 1426 additions & 18 deletions

File tree

README.md

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@ pip install python-thingset
1717

1818
Each transport has its own constructor — there is no factory.
1919

20-
| Wire | Sync class | Async class |
21-
|---------------|------------------|----------------------------|
22-
| TCP/IP | `ThingSetTCP` | `AsyncThingSetTCP` |
23-
| CAN + ISO-TP | `ThingSetCAN` | _(not planned)_ |
24-
| Serial | `ThingSetSerial` | _(not planned)_ |
25-
| UDP (listen) | _(none)_ | `AsyncThingSetUDPReceiver` |
20+
| Wire | Sync class | Async class |
21+
|---------------|------------------|-----------------------------------|
22+
| TCP/IP | `ThingSetTCP` | `AsyncThingSetTCP` |
23+
| CAN + ISO-TP | `ThingSetCAN` | _(not planned)_ |
24+
| CAN (listen) | _(none)_ | `AsyncThingSetCANReportReceiver` |
25+
| Serial | `ThingSetSerial` | _(not planned)_ |
26+
| UDP (listen) | _(none)_ | `AsyncThingSetUDPReceiver` |
2627

2728
All classes are context managers; `with` / `async with` handles connection
2829
setup and tear-down.
@@ -140,6 +141,38 @@ reports from multiple publishers don't corrupt each other. The receive queue
140141
is bounded; on overflow the newest report is dropped rather than
141142
back-pressuring the event loop.
142143

144+
### CAN report receiver
145+
146+
Receives publish frames from ThingSet devices on a CAN bus. Both shapes are
147+
surfaced as `ThingSetReport`:
148+
149+
- **Single-frame report** (`type=0x2`): the 16-bit data ID is embedded in the
150+
CAN-ID; the payload is a bare CBOR-encoded value. Synthesised into a
151+
`ThingSetReport` with `subset_id=None` and a one-entry `values` map.
152+
- **Multi-frame report** (`type=0x1`): chunks reassemble per-sender via
153+
`msg#` and `seq#` in the CAN-ID. Carries `subset_id`, plus optional
154+
`eui` for `0x1E` enhanced reports.
155+
156+
```python
157+
import asyncio
158+
from python_thingset import AsyncThingSetCANReportReceiver
159+
160+
async def main():
161+
async with AsyncThingSetCANReportReceiver(bus="vcan0", fd=True) as receiver:
162+
async for (source_addr, bus_name), report in receiver:
163+
print(source_addr, report.subset_id, report.values)
164+
165+
asyncio.run(main())
166+
```
167+
168+
Reassembly buffers are keyed per source node address. On a sequence
169+
mismatch within an in-flight message the receiver skips the frame without
170+
advancing state — this matches the firmware-side reassembly behaviour and
171+
lets the receiver latch onto one stream even when a publisher interleaves
172+
two concurrent multi-frame reports with a shared `msg#`. `ThingSetReport`'s
173+
`subset_id` is therefore typed `int | None` (was `int` in 0.2.x) since
174+
single-frame reports don't carry one.
175+
143176
## Gateway forwarding
144177

145178
A TCP client can address a CAN-side module behind an IP↔CAN gateway (e.g. an
@@ -210,6 +243,17 @@ python examples/async_udp_sniffer.py --decorate \
210243
--record-fields examples/record_fields.example.json
211244
```
212245

246+
A matching CAN sniffer prints publish frames as they arrive on a CAN
247+
interface. `--decorate` fetches each source node's schema over ISO-TP in
248+
the background and annotates printed IDs with their schema path:
249+
250+
```sh
251+
python examples/async_can_sniffer.py -i vcan0 --source 10
252+
python examples/async_can_sniffer.py -i vcan0 --source 10 -v --decorate
253+
python examples/async_can_sniffer.py -i vcan0 --decorate \
254+
--record-fields examples/record_fields.example.json
255+
```
256+
213257
## Development
214258

215259
```sh

0 commit comments

Comments
 (0)