Driver interface
A driver turns a source event of one device family into decoded records, and later encodes control actions. It knows nothing about the network the message came over. Drivers live in shared/device_drivers/<family>/ and are registered in shared/device_drivers/registry.py (decision D10). Use examples/device-drivers/example_device/ as the starting point.
Contract
class DeviceDriver(Protocol):
key: ClassVar[str] # "opencollar"
label: ClassVar[str]
capabilities: ClassVar[frozenset[str]] # {"gnss", "battery", ...}
timestamp_semantics: ClassVar[dict[str, TimestampSemantics]] # per record type
def decode(self, event: SourceEventData) -> DecodedRecords: ...
SourceEventData carries the payload, provider metadata, the network receive time, the ingest time and the device and device type settings. For a LoRaWAN device the decoder service has already extracted the application payload: event.frame holds the bytes and event.f_port the port, whatever adapter delivered the uplink (lorawan_frame in shared/device_drivers/base.py reads ChirpStack's base64 data or the frame_hex other adapters store). event.acquisition_channel says whether the delivery came over LoRaWAN, Web Bluetooth, a log file or satellite. DecodedRecords holds lists of DecodedPosition, DecodedMeasurement, DecodedState and DecodedEvent, each with its canonical time.
What a family needs
- The driver module under
shared/device_drivers/<family>/and its line inregistry.py; the registry test intests/shared/test_adapters_and_drivers.pylists the keys. - Metric keys that exist in the registry. The seeds are in
shared/metrics/seeds.pyand reach the database through a migration (seed_sql(), see migration 0003); a new key needs a new migration that runsseed_sql()again. Values with no metric of their own (a sensor's orientation, a dilution of precision) go into the position'sattributesor aDecodedState. - Control actions, when the device takes downlinks: a
control_actionsclass attribute withControlActionobjects fromshared/control/actions.py(parameters as a Pydantic model, an encoder returningEncodedCommandwith the payload and port, an optional interpreter that recognises the answer in later decoded records). The OpenCollar driver'scontrol.pyis the reference; device control explains the lifecycle. - Fixtures and golden tests under
tests/fixtures/payloads/<family>/with a README naming the source of every payload; the vendor manual's worked examples are acceptable until recorded uplinks exist. - A page under
docs/devices/(frame, what the driver produces, control, setup, testing), its line indocs/devices/index.mdandmkdocs.yml, and a changelog entry. - A device type in the running system: Server admin, Device types, with the driver key; the family's DevEUIs then resolve to devices of that type through external identities or Needs attention.
Rules
- Raise
ApplicationErrorwithPAYLOAD_DECODE_FAILEDorTIMESTAMP_INVALIDfor a payload the driver cannot read. Setuser_actionable=Truewhen an administrator can fix it andcomponent="driver.<family>"; the specifics go intocontext, a dict the trace shows. Do not return partial garbage. - Every time must be timezone-aware. Use the embedded device time when the protocol has one; declare
NETWORK_TIMEsemantics for record types that do not. - Set
fingerprinton a record when two different records can share device, time and type. - Metric keys are lowercase snake_case and exist in the metric registry with the canonical unit. Convert in the driver, never downstream.
- Keep provider specifics out: a driver never reads a ChirpStack or KPN field.
Firmware versions
SourceEventData.firmware_version is the device's firmware as last reported (the decoder writes it from a status state with firmware_version). A driver whose messages changed across firmware releases keeps a layout per range and picks it from that version; see the OpenCollar driver and ADR 0021.
Health
A driver may declare health, a tuple of HealthField (decision D104): the metric keys and state keys people should see as the device's health, each with a label, a kind (number, duration, text, bool, flags), a unit and thresholds (warn_below, critical_below, warn_above, critical_above; a flags field with flags_are_problems warns when any flag is set). The decoder keeps the newest value per metric and the time of the newest state on the device's current state; shared/domain/health.py turns both into the health card of the device page, the compact line in lists and the level on the map, and last seen moves on every record, not only positions. The OpenCollar driver declares battery, charging, temperature, uptime, errors, the last reset reason, firmware and hardware version, the last fix's satellites, accuracy and time to fix, the LoRa satellites and the flash use.
Testing
Golden tests over recorded payloads under tests/fixtures/payloads/<family>/, each with a README that says where the payload came from. The generic JSON driver and its tests in tests/shared/test_adapters_and_drivers.py show the shape.