Wiki

Technical reference for system integrators and engineers.

Sensor TLV Protocol

Sensor telemetry, alarms, battery status and GNSS data all travel in one compact record format, carried over any transport.

TLV stands for Type–Length–Value. Each record is a one-byte type, a one-byte length, then exactly that many bytes of payload:

┌──────┬─────┬───────────────────────┐
│ type │ len │ value — len bytes     │
└──────┴─────┴───────────────────────┘
  0x09    4     mv, percent, flags       ← a battery record

The length is what makes the format extensible. A decoder that does not recognise a type does not need to understand it — it advances by len bytes and carries on parsing, so a newer device is always safe against an older reader. Adding a record type never breaks a deployed backend.

The format is also transport-agnostic by design — the identical bytes ride UWB sensor-data frames (0x25) or BLE extended advertising, so a decoder does not care how a record arrived.

This page describes the envelope. What the individual sensors measure and how to interpret the numbers is on IMU, Barometer and Gas Sensors. GNSS also rides this envelope as record 0x07, but is documented under Device Protocols because it produces a position rather than context.

Frame layout

Frame header — 10 bytes version 1 B record_count 1 B seq 2 B beacon_seq 4 B offset_ms 2 B records follow → beacon_seq + offset_ms = measurement time Records — type, length, value type len value 0x04 baro agg · 18 B type len value 0x09 battery · 4 B type len value 0x08 alarm · 1 B an unknown type is skipped by its length — a newer device is always safe against an older reader A beacon_seq of 0xFFFFFFFF marks an unsynced sample: the server must not fuse it with a ranging measurement.
The timestamp is stamped when the aggregation window closes, on the same clock the ranging measurement uses — not at transmit, and not on arrival.

Record catalogue

TypePayloadBytes
0x01IMU raw — N × {accel[3], gyro[3]} int1612 / sample
0x02IMU aggregate24
0x03Baro raw6 / sample
0x04Baro aggregate18
0x05Temperature {source, reserved, temp_dc}4
0x06Origin EUI8
0x07GNSS data20
0x08Alarm1
0x09Battery4
A record's length is its compatibility contract. Decoders verify len == sizeof(record) and silently skip a mismatch. Existing records are never resized — a new capability gets a new type.

Alarm record

Alarm — one byte reserved bits 7–4 — zero type bits 3–1 pending bit 0 Types 0 — Button · 1 — Fall detection · 2 — Gas threshold 0x05 = gas threshold, pending 0x00 = cleared
A cleared alarm is a zero byte; the type bits are meaningful only while pending is set. The alarm is reported by level, not edge — it repeats in every frame while raised, so a lost broadcast can never lose the emergency.

Battery record

Battery — 4 bytes mv uint16 — terminal millivolts percent uint8 — 0..100 or 255 flags uint8 byte 0 2 3 Flags reserved bits 7–4 no cell bit 3 chg unknown bit 2 external bit 1 charging bit 0 4E 0F 48 00 — 3918 mV, 72%, on battery 00 00 FF 0A — no cell, external supply mv is little-endian.
The record is always emitted, even when no cell is fitted — an absent record already means something different.

Reading percent

percent = 255 is not a single condition. It has two causes, and bit 3 tells them apart:

bit 3percentMeaning
set255No cell fitted — running on external supply. mv is 0. This is normal, not a fault.
clear255Voltage outside a plausible cell range — a scaling or sense fault, not a flat cell.
clear0100Normal reading.
A consumer that treats every 255 as a fault will raise an alarm on every mains- or PoE-powered device on the site. Check bit 3 first.

An absent battery record is a fourth state again: firmware too old to report one. That is why a batteryless device still emits the record and sets bit 3 rather than staying silent — "this unit has no battery" and "this unit cannot tell me about its battery" call for opposite responses, and a missing record cannot express the difference.

Why both mv and percent

The percentage is derived from a discharge curve that is chemistry-specific and load-dependent. Shipping the raw terminal voltage alongside it means a consumer can apply its own curve, and a curve that turns out to be wrong can be re-derived from field data rather than mis-reporting invisibly. Where the distinction matters, prefer mv.

Flags

BitSet when
0the charger is actively charging
1an external supply is present
2charge sensing is not wired on this device — bits 0 and 1 carry no information
3no cell fitted; external supply only

Bit 3 is configured rather than detected. With a linear charger an empty battery terminal sits at the regulation voltage — the same reading as a fully charged cell on charge — so no measurement separates the two. The device is told.

A value of 255 therefore distinguishes a fault in the measurement path from a genuinely discharged cell.