Wiki

Technical reference for system integrators and engineers.

GATT Data Sync (drain)

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.

Client Device {"cmd":"count","type":"sensor"} write 0x5130 {"count":42} read 0x5131 {"cmd":"records","type":"sensor","max":4} device prepares a chunk — erases nothing protobuf SensorRecordChunk read completes → records released Connection lost before the read completes? Nothing is erased.
The read event on 0x5131 is the acknowledgement. Until it happens the device keeps every record.

Commands

{"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.

Acknowledged delete

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 encoding — Protobuf

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.

Control replies JSON count · version · time · errors human-readable, small, ≤ 512 B Record chunks Protobuf (proto3) sensor · gnss · match_points package locator.nrf Two carriers, two encodings device → anchor : TLV over the radio device → phone : Protobuf over GATT
The same telemetry leaves the device in two encodings, each chosen for its transport. Format follows transport; the data does not change.

Schemas

All messages live in the locator.nrf package, so one client generates decoders for the whole device line from a single set of definitions:

SchemaMessagesStore
sensor.protoSensorRecord, SensorRecordChunksensor
gnss.protoGnssRecord, GnssRecordChunkgnss
mach_point.protoMatchPoint, MatchPointChunkmatch_points

Match point records

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;
}

Sensor records

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:

MessageFields
ImuAggregatewindow_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
BaroAggregatewindow_ms, sample_count, altitude_cm (differential), alt_min_cm, alt_max_cm, vert_vel_cm_s, pressure_pa, floor_index (255 = unknown), flags
TempRecordsource, temp_dc (deci-degrees C)

Decoding notes

  • An absent sub-message means that sensor produced nothing for the window. proto3 omits zero-valued scalars, so absent and present-with-zeros are different facts — a flat record could not express the distinction.
  • Deduplicate on 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.
  • Alarm state and alarm type are separate fields here, while the TLV record packs both into the bits of a single byte. A client decodes two integers rather than hand-rolling bit masks.
  • beacon_seq = 4294967295 marks an unsynced sample, which must not be fused with a ranging measurement.

Why Protobuf on this path

The same telemetry leaves the device over two carriers, and each uses the encoding suited to its transport:

Radio — TLVGATT drain — Protobuf
Payload budgeta UWB frame caps the payload at 99 bytesno comparable cap
Decoderversion-matched C, compiled from the same headerclient on an independent release schedule
Compatibility contractrecord lengthfield numbers
Effect of a denser encodingprotobuf 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.