Devices buffer records while they are out of contact, then hand them to a phone or gateway when one comes into range. Commands are JSON; the buffered records themselves are transferred as Protobuf (proto3).
The exchange happens over two characteristics:
0x5130 — Sync Request: a JSON command, ≤ 64 B, written by the client.0x5131 — Sync Response: the reply, ≤ 512 B — JSON for control replies, Protobuf for every record chunk.So a client speaks JSON to ask what is available and to control the transfer, and decodes Protobuf messages (package locator.nrf) to read the records themselves. See Record encoding — Protobuf below for the schemas.
The exchange completes within a single contact window and tolerates an interrupted connection.
0x5131 is the acknowledgement. Until it happens the device keeps every record.{"cmd":"count"} -> {"count":N}
{"cmd":"count","type":"sensor"} -> {"count":N}
{"cmd":"version"} -> {"fw":"1.0.0"}
{"cmd":"time"} -> {"utc":1755262798,"valid":true}
{"cmd":"records","type":"sensor","max":4} -> protobuf chunk
Control replies are JSON; record chunks are protobuf (a JSON encoding of the same records would be roughly 10× the bytes and would not fit the contact window the design is sized around).
The type selects the store: sensor, gnss, or match_points. When type is absent it resolves to the device's primary store — gnss on anchors and tags, match_points on reverse-RTLS devices.
A records request prepares a chunk and erases nothing. Records are dropped only once the response is read to its end.
The read event on the authorized 0x5131 characteristic both confirms delivery and triggers the deferred erase. If a connection is lost mid-transfer, the same records are offered again on the next connection.
Record chunks are encoded with Protocol Buffers (proto3). Only the control replies above are JSON; every record that leaves the device on 0x5131 is a serialised protobuf message.
All messages live in the locator.nrf package, so one client generates decoders for the whole device line from a single set of definitions:
| Schema | Messages | Store |
|---|---|---|
sensor.proto | SensorRecord, SensorRecordChunk | sensor |
gnss.proto | GnssRecord, GnssRecordChunk | gnss |
mach_point.proto | MatchPoint, MatchPointChunk | match_points |
syntax = "proto3";
package locator.nrf;
message MatchPoint {
uint32 beacon_a = 1;
uint32 beacon_b = 2;
uint32 timestamp_s = 3;
}
message MatchPointChunk {
repeated MatchPoint match_points = 1;
}
One SensorRecord is one aggregation window. IMU, barometric and temperature data are captured together and travel together, rather than as three separately timestamped streams:
message SensorRecord {
uint32 seq = 1;
uint32 beacon_seq = 2; // 4294967295 = unsynced
uint32 offset_ms = 3; // beacon → window close
ImuAggregate imu = 4;
BaroAggregate baro = 5;
TempRecord temp = 6;
uint32 alarm = 7; // 0 none, 1 pending
uint32 alarm_type = 8; // 0 button, 1 fall, 2 gas threshold
}
message SensorRecordChunk {
repeated SensorRecord records = 1;
uint32 first_seq = 2;
}
The sub-messages mirror the TLV aggregate records field for field, including units:
| Message | Fields |
|---|---|
ImuAggregate | window_ms, sample_count, gravity_x/y/z, accel_rms, accel_peak, gyro_rms, dom_freq_mhz, dom_amp, accel_fs, gyro_fs, motion_state, event_flags |
BaroAggregate | window_ms, sample_count, altitude_cm (differential), alt_min_cm, alt_max_cm, vert_vel_cm_s, pressure_pa, floor_index (255 = unknown), flags |
TempRecord | source, temp_dc (deci-degrees C) |
seq. The device holds its drain position in RAM, so after a reset it re-offers records a client has already taken. SensorRecordChunk.first_seq carries the sequence of the first record in the chunk.beacon_seq = 4294967295 marks an unsynced sample, which must not be fused with a ranging measurement.The same telemetry leaves the device over two carriers, and each uses the encoding suited to its transport:
| Radio — TLV | GATT drain — Protobuf | |
|---|---|---|
| Payload budget | a UWB frame caps the payload at 99 bytes | no comparable cap |
| Decoder | version-matched C, compiled from the same header | client on an independent release schedule |
| Compatibility contract | record length | field numbers |
| Effect of a denser encoding | protobuf would cut raw IMU from 7 samples per frame to 4 | — |
A client hardcoding byte offsets against a packed binary format reads garbage rather than erroring when a field is added; a protobuf decoder ignores unknown fields and keeps working. That is the property this path needs, because the device firmware and the client application are released independently.