Skip to main content

A Python library for ThingSet functionality

Project description

python-thingset

Python client library for ThingSet, a protocol for IoT devices over serial, CAN and IP transports. Supports both synchronous and asynchronous I/O, structured schema discovery, broadcast report reception and gateway forwarding.

Requires Python ≥ 3.10.

Install

pip install python-thingset

Transports

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

Wire Sync class Async class
TCP/IP ThingSetTCP AsyncThingSetTCP
CAN + ISO-TP ThingSetCAN (not planned)
CAN (listen) (none) AsyncThingSetCANReportReceiver
Serial ThingSetSerial (not planned)
UDP (listen) (none) AsyncThingSetUDPReceiver

All classes are context managers; with / async with handles connection setup and tear-down.

Sync

Read a single value

from python_thingset import ThingSetTCP

with ThingSetTCP("192.0.2.1") as client:
    r = client.get(0xF03)
    print(r.status_string, r.data)    # CONTENT native_sim

Read a whole group

with ThingSetTCP("192.0.2.1") as client:
    r = client.get(0x0F)
    print(r.data)
    # {0xF01: '05a736ef', 0xF02: 'AdamMitchell',
    #  0xF03: 'native_sim', 0xF05: [0, 48, 0, 1]}

Enumerate a group's children

with ThingSetTCP("192.0.2.1") as client:
    r = client.fetch(0x00, [])     # root
    print(r.data)                  # [0x0E, 0x0F, 0x67, ...]

Update and execute

with ThingSetTCP("192.0.2.1") as client:
    client.update(0x509, True, parent_id=0x05)   # HMCU/sDFUOverride = True
    client.exec(0x52, [])                         # HMCU/xSaveNVM()

CAN

from python_thingset import ThingSetCAN

with ThingSetCAN("vcan0") as client:
    r = client.get(0xF03, node_id=0x10)   # target CAN node 0x10
    print(r.data)

Schema discovery

Walks the object tree via the device's metadata overlay and returns a structured SchemaTree with both by_id and by_path lookups.

with ThingSetTCP("192.0.2.1") as client:
    tree = client.discover_schema()
    print(f"{len(tree)} nodes, {len(tree.root)} top-level")
    print(tree.by_path["Metadata/rBoard"])
    # 0x0F03  Metadata/rBoard  (string)
    print(tree.by_id[0xF03].type)    # 'string'

Async

The async API is the primary target for asyncio applications that can't afford to block the event loop on ThingSet I/O.

RPCs

import asyncio
from python_thingset import AsyncThingSetTCP

async def main():
    async with AsyncThingSetTCP("192.0.2.1") as client:
        r = await client.get(0xF03)
        tree = await client.discover_schema()
        await client.update(0x509, True, parent_id=0x05)

asyncio.run(main())

Concurrent callers on the same client are internally serialised via an asyncio.Lock (ThingSet has no wire-level request correlation), so other coroutines in the loop keep running during an in-flight RPC.

UDP report receiver

Receives broadcast publish/subscribe messages from ThingSet devices on the subnet. Handles the 2-byte fragmentation framing and both report types:

  • 0x1F standard reports (source IP identifies the publisher)
  • 0x1E enhanced reports (carry the originating module's EUI-64, used when a gateway republishes CAN-side telemetry)
import asyncio
from python_thingset import AsyncThingSetUDPReceiver

async def main():
    async with AsyncThingSetUDPReceiver(port=9002) as receiver:
        async for addr, report in receiver:
            # report is ThingSetReport(subset_id, values, eui)
            print(addr, report.subset_id, report.values)

asyncio.run(main())

Reassembly buffers are keyed per (ip, port), so interleaved multi-frame reports from multiple publishers don't corrupt each other. The receive queue is bounded; on overflow the newest report is dropped rather than back-pressuring the event loop.

CAN report receiver

Receives publish frames from ThingSet devices on a CAN bus. Both shapes are surfaced as ThingSetReport:

  • Single-frame report (type=0x2): the 16-bit data ID is embedded in the CAN-ID; the payload is a bare CBOR-encoded value. Synthesised into a ThingSetReport with subset_id=None and a one-entry values map.
  • Multi-frame report (type=0x1): chunks reassemble per-sender via msg# and seq# in the CAN-ID. Carries subset_id, plus optional eui for 0x1E enhanced reports.
import asyncio
from python_thingset import AsyncThingSetCANReportReceiver

async def main():
    async with AsyncThingSetCANReportReceiver(bus="vcan0", fd=True) as receiver:
        async for (source_addr, bus_name), report in receiver:
            print(source_addr, report.subset_id, report.values)

asyncio.run(main())

Reassembly buffers are keyed per source node address. On a sequence mismatch within an in-flight message the receiver skips the frame without advancing state — this matches the firmware-side reassembly behaviour and lets the receiver latch onto one stream even when a publisher interleaves two concurrent multi-frame reports with a shared msg#. ThingSetReport's subset_id is therefore typed int | None (was int in 0.2.x) since single-frame reports don't carry one.

Gateway forwarding

A TCP client can address a CAN-side module behind an IP↔CAN gateway (e.g. an HMCU) by specifying the module's EUI-64. Every outgoing request is wrapped in a 0x1C-prefixed forward envelope; responses come back unwrapped, so the caller API is unchanged.

async with AsyncThingSetTCP(
    "192.0.2.1",
    target_eui=0xbadb1b0000000001,
) as client:
    r = await client.get(0xF03)            # module's own rBoard
    tree = await client.discover_schema()  # module's schema through the gateway

The same kwarg exists on the sync ThingSetTCP.

CLI

Installed as thingset on your PATH.

# TCP
thingset get    f03        -i 192.0.2.1
thingset get    0F         -i 192.0.2.1              # whole group
thingset fetch  0          -i 192.0.2.1              # root children
thingset update 5 509 true -i 192.0.2.1
thingset exec   52         -i 192.0.2.1
thingset schema            -i 192.0.2.1

# CAN (target node 0x10)
thingset get    f03        -c vcan0 -t 10
thingset schema            -c vcan0 -t 10

# Through a gateway to the CAN-side module
thingset get    f03        -i 192.0.2.1 -e badb1b0000000001
thingset schema            -i 192.0.2.1 -e badb1b0000000001

# Serial
thingset get    Metadata/rBoard -p /dev/ttyACM0
thingset schema                 -p /dev/ttyACM0

Output is decorated with names and types when the firmware exposes them via the metadata overlay:

$ thingset get f03 -i 192.0.2.1
0x85 (CONTENT)
  0x0F03  rBoard  (string): 'native_sim'

$ thingset fetch 0 -i 192.0.2.1
0x85 (CONTENT)
  0x0000  (group): [0xE DSM, 0xF Metadata, 0x67 xRebootDFU, ...]

Examples

The examples/ directory ships a UDP report sniffer with schema-aware decoration, EUI filtering, and per-device (IP, EUI) schema caching that transparently fetches through a gateway when a report carries a module EUI:

python examples/async_udp_sniffer.py --decorate -v
python examples/async_udp_sniffer.py --decorate --filter-eui badb1b0000000001
python examples/async_udp_sniffer.py --decorate \
    --record-fields examples/record_fields.example.json

A matching CAN sniffer prints publish frames as they arrive on a CAN interface. --decorate fetches each source node's schema over ISO-TP in the background and annotates printed IDs with their schema path:

python examples/async_can_sniffer.py -i vcan0 --source 10
python examples/async_can_sniffer.py -i vcan0 --source 10 -v --decorate
python examples/async_can_sniffer.py -i vcan0 --decorate \
    --record-fields examples/record_fields.example.json

Development

# Install with dev dependencies
pip install -e '.[dev]'

# Run tests
pytest

# Lint
ruff check python_thingset/ tests/ examples/

# Build and publish a release
rm -rf dist/
python -m build
python -m twine upload --repository pypi dist/*

License

Apache-2.0. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

python_thingset-0.3.1.tar.gz (58.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

python_thingset-0.3.1-py3-none-any.whl (42.5 kB view details)

Uploaded Python 3

File details

Details for the file python_thingset-0.3.1.tar.gz.

File metadata

  • Download URL: python_thingset-0.3.1.tar.gz
  • Upload date:
  • Size: 58.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.10.12

File hashes

Hashes for python_thingset-0.3.1.tar.gz
Algorithm Hash digest
SHA256 9800f4486f81280b54db6c4c3ec85fdf07f81115173de259f8a040ac440c9605
MD5 dcb8153ad0f751e74dcbd252e1fb348c
BLAKE2b-256 d39e0eae693c55cee2dfe43ac152ada41e074c3dd2e86b3bad423682066a51a9

See more details on using hashes here.

File details

Details for the file python_thingset-0.3.1-py3-none-any.whl.

File metadata

File hashes

Hashes for python_thingset-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 7bda352ec6730e35af0769cba77f3011587ea27d4ebfc62231ce874ce4d61899
MD5 0d7786496cbf059b62aaa298f0b27316
BLAKE2b-256 41032a1b0b1aca8fe4545c3c703881c0ccc66322006c73648adc432509142377

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page