@@ -17,12 +17,13 @@ pip install python-thingset
1717
1818Each 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
2728All classes are context managers; ` with ` / ` async with ` handles connection
2829setup and tear-down.
@@ -140,6 +141,38 @@ reports from multiple publishers don't corrupt each other. The receive queue
140141is bounded; on overflow the newest report is dropped rather than
141142back-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
145178A 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