Skip to content

Decoder: add support for new ts2 (Smart Label v2) payload tags #196

Description

@KangooGuy

Decoder: add support for new ts2 (Smart Label v2) payload tags

Summary

Our payload decoder currently supports only the tags defined in the tagxl-v2 firmware (apps/trackit/settings.h). The ts2 / Smart Label v2 firmware (ts2/app/tracker/lorawan_proto.h) introduces a number of new TLV tags and uplink message types that the decoder does not yet handle. This issue asks the backend team to extend the decoder to cover them.

Tier 1 (do first): WiFi AP constraint (Min_AP), motion detection plan v2, battery temperature, and buffer level. Tier 2 (later): everything else — scan timers/quick scans, BLE config, batch count, battery level thresholds, reset count/cause, scan count, and the new localization uplinks. All are specified below.

Source of truth: the firmware code only. Everything in this issue is derived from ts2/app/tracker/lorawan_proto.h, lorawan_proto.c, and the handlers in logic.c. The repo also contains a ts2/Payload format.md draft — do not rely on it. It contradicts the compiled code in several places (e.g. it describes a 0x10/0x14/0x18/0x1C uplink "tag" scheme that does not exist in code, a multi-byte WiFi-AP filter that is actually a single byte, and BLE-compact ports 190/191/200/201 that the code does not implement). Decode against the code; confirm anything ambiguous with the firmware team.


Priority & sequencing

Tier 1 — implement first

Setting Set tag Get tag Size Format One-liner
WiFi AP constraint (Min_AP) 0x2A 0x4D 1 uint8 Minimum WiFi-AP count; if a scan finds ≥ this many APs the device skips the GNSS/GPS scan.
Motion detection plan v2 0x30 0x60 7 struct Slice-based movement-detection plan (replaces the simple accel threshold + delay).
Battery temperature 0x31 0x61 1 int8 °C Battery temperature reading, single signed byte, no scaling.
Buffer level 0x63 2 uint16 BE Number of stored unconfirmed messages (0–65535).

Full byte-level layouts for all four are in the TLV tag specifications section below.

Tier 2 — later (everything else)

Setting Set tag Get tag Size Format One-liner
Scan timers (quick scans) 0x32 0x62 3–5 struct Per-timer scan config; adds QuickMoving / QuickSteady "quick scan" types.
BLE config 0x2C 0x51 16 struct BLE scan/advert/filter configuration.
Batch count 0x2B 0x50 1 uint8 Messages per batch.
Battery level thresholds 0x29 0x4C 2 struct Critical + low battery percentages.
Reset count 0x49 2 uint16 BE Number of resets (read-only).
Reset cause 0x4A 4 uint32 BE Reset-cause code (read-only).
Scan count 0x4B 4 struct GNSS + WiFi scan counts (read-only).
New localization uplinks var by fPort BLE (160–163), WiFi-ts (200/201), GNSS-NA (192–195), GPS-SA (10).

Full byte-level layouts are in the TLV tag specifications and uplink sections below.


TLV tag specifications

All of the following are defined in lorawan_proto_tlv_tag_t (ts2/app/tracker/lorawan_proto.h) and are not in the tagxl-v2 decoder set.

Settings envelope (applies to every tag below). These TLVs travel inside the settings message (uplink and downlink both on port 151kLoraWanProtoUlSettings / kLoraWanProtoDlSettings). The payload is a 3-byte header lorawan_proto_settings_header_t = { tag=0x4C, len, count } followed by count TLVs, each [tag][len][value…]. The header tag 0x4C is verified in proto_decode_settings (kLoraWanTagSettings). All multi-byte integers are big-endian on the wire — firmware byte-swaps every uint16/uint32 with utils_rev16/utils_rev32 on both encode and decode. Sizes below are the value length only (excludes the 2-byte tag+len).

Each entry verified against the proto_decode/proto_encode handlers in ts2/app/tracker/logic.c.

Motion detection plan v2 — Set 0x30, Get 0x60 (Tier 1)

Size 7 bytes, lorawan_proto_settings_moving_v2_t. Slice-based movement detection; replaces tagxl's simple accel_sensitivity + accel_delay. Set requires exactly 7 bytes.

Offset Bytes Field Type Notes
0 2 acc_thresh_mg uint16 BE acceleration threshold, mG
2 1 slice_time_seconds uint8
3 1 window_slice_count uint8
4 1 window_slice_required uint8
5 1 start_movement_window_count uint8
6 1 stop_movement_window_count uint8

Battery temperature — Set 0x31, Get 0x61 (Tier 1)

Size 1 byte, int8 degrees Celsius. In logic.c the value is cast from the driver's int32 down to a single signed byte (int8_t tmpr = (int8_t) logic.bat.read.temperature), no scaling. Range −128…127 °C; one byte so no endianness concern.

Buffer level — Get 0x63 only (Tier 1)

Size 2 bytes, uint16 BE = number of stored unconfirmed messages (storage.record_count()). Only emitted when the device has the storage capability. No Set handler exists (0x33 is reserved in the enum but not implemented).

WiFi AP constraint / Min_AP — Set 0x2A, Get 0x4D (Tier 1)

Size 1 byte, uint8 (wifi_ap_constr). Set requires exactly 1 byte. Semantics (logic.c:2482–2497): it is the minimum number of WiFi APs that makes a WiFi fix "good enough" — when a scan finds wifi_scan_count >= wifi_ap_constr (and the value is non-zero), the device skips the GNSS/GPS scan for that cycle. A value of 0 disables the check. Note: the firmware implements this as a single count byte, not the multi-byte "Min RSSI + head/tail + MAC filter" structure described in the draft doc — decode as one byte.

Scan timers (quick scans) — Set 0x32, Get 0x62 (Tier 2 — later)

Deferred per product decision. Full spec retained below for when it is picked up.

Value is one lorawan_proto_settings_scan_timer_t = 5 bytes (so the full TLV is [0x32][len][value]), verified against the kLoraWanTlvTagScanTimersSet handler + update_localization_timers in logic.c.

typedef enum {
    kLoraWanScanInstanceTypeUnkown = 0,   // 0 invalid
    kLoraWanScanInstanceTypeMoving,        // 1
    kLoraWanScanInstanceTypeSteady,        // 2
    kLoraWanScanInstanceTypeQuickMoving,   // 3
    kLoraWanScanInstanceTypeQuickSteady,   // 4
} lorawan_proto_settings_timer_type;

typedef union __attribute__((packed)) {   // lorawan_proto_loc_method_t (1 byte)
    struct { uint8_t wifi:1, ble:1, gnss_na:1, gnss_sa:1; };  // bit0..bit3, bits4-7 unused
    uint8_t any;
} lorawan_proto_loc_method_t;
Offset Bytes Field Type Notes
0 1 type uint8 timer instance type (enum above)
1 2 period uint16 BE scan interval, seconds
3 1 method uint8 bitfield which scanners run: bit0 WiFi, bit1 BLE, bit2 GNSS-NA, bit3 GNSS-SA
4 1 cont_op uint8 bitfield "continue on" scanners, same bit layout
  • One timer instance per TLV — several 0x32 TLVs (one per type) are sent to configure multiple timers.
  • Partial TLVs accepted: the Set handler only requires type + period (min len = 3) and copies MIN(5, len) bytes; method/cont_op may be omitted.
  • Range checks: Moving/Steady period 120–65535 s; Quick variants not range-checked.
  • Get (0x62) is a no-op in this revision (packs no response), so the decoder currently only sees 0x32 in the Set direction.

BLE config — Set 0x2C, Get 0x51

Size 16 bytes, lorawan_proto_ble_config_t. Set requires exactly 16 bytes.

Offset Bytes Field Type Notes
0 1 scan_seconds uint8
1 1 advert_minutes uint8
2 1 max_scan_result uint8
3 1 min_rssi int8 signed
4 1 filter_type uint8
5 11 filter_data uint8[11] raw filter bytes

Batch count — Set 0x2B, Get 0x50

Size 1 byte, uint8 (batch_size). Set requires exactly 1 byte.

Battery level thresholds — Set 0x29, Get 0x4C

Size 2 bytes, lorawan_proto_battery_levels_t. Set requires exactly 2 bytes.

Offset Bytes Field Type Notes
0 1 level_critical uint8 percent
1 1 level_low uint8 percent

Reset count — Get 0x49 only

Size 2 bytes, uint16 BE = number of resets. Read-only counter.

Reset cause — Get 0x4A only

Size 4 bytes, uint32 BE = reset-cause code (lorawan_proto_reset_cause_t, values 0–10: 0 Unknown, 1 PowerOn, 2 ResetPin, 3 DeepSleep, 4 WDT, 5 HardFault, 6 Software, 7 HighVoltage, 8 LowVoltage, 9 Debug, 10 DCDC). Read-only.

Scan count — Get 0x4B only

Size 4 bytes, lorawan_proto_settings_scan_count_t. Read-only counter.

Offset Bytes Field Type Notes
0 2 gnss_scan_count uint16 BE
2 2 wifi_scan_count uint16 BE

⚠️ Set-tag collisions in the enum — but Get tags are unambiguous. In lorawan_proto.h three Set values are reused (0x29 = ResetCount/BatteryLevel, 0x2A = ResetCause/WifiApConstr, 0x2B = ScanCount/BatchCount). The firmware resolves this by implementing the Set direction only for BatteryLevel / WifiApConstr / BatchCount; ResetCount, ResetCause and ScanCount are Get-only read-only counters. Every Get tag is distinct, so decoding uplink (Get) responses is unambiguous — the collision only affects downlink Set of 0x29/0x2A/0x2B. Confirm the final mapping with the firmware team.


New uplink message types / ports

ts2 introduces localization uplinks that the current decoder does not parse. The message type is selected by the LoRaWAN fPort, not by a payload tag byte. Ports are from the lorawan_proto_ul_port_t enum in lorawan_proto.h, and the port selection is confirmed in logic.c (moving vs. steady is logic.loc.moving ? …Moving… : …Steady…; the timestamped variant is used when a timestamp is available).

fPort Message Timestamp Packer (lorawan_proto.c)
160 BLE, steady no proto_pack_ble
161 BLE, moving no proto_pack_ble
162 BLE, steady yes proto_pack_ble_ts
163 BLE, moving yes proto_pack_ble_ts
192 GNSS-NA, steady no proto_pack_gnss
193 GNSS-NA, moving no proto_pack_gnss
194 GNSS-NA, steady yes proto_pack_gnss_ts
195 GNSS-NA, moving yes proto_pack_gnss_ts
197 WiFi, steady no proto_pack_wifi
198 WiFi, moving no proto_pack_wifi
200 WiFi, steady yes proto_pack_wifi_ts
201 WiFi, moving yes proto_pack_wifi_ts
10 GPS-SA (stand-alone fix) yes proto_pack_gps_ts
151 Settings (TLV envelope, see above)

Note: the code assigns WiFi-timestamped to ports 200/201 — these are not BLE-compact messages (the draft doc is wrong on this). There is no compact WiFi/BLE format, no ports 190/191/181, and no sequence-numbered 0x800x8C tags anywhere in the code.

WiFi payload (proto_pack_wifi / proto_pack_wifi_ts)

// per-AP entry, 7 bytes:
struct { int8_t rssi; uint8_t addr[6]; }            // lorawan_proto_ul_wifi_mac_t

// port 197/198 (no ts):   [ tag(1) ] [ (rssi+mac) * n ]
// port 200/201 (with ts): [ ts(4) ] [ tag(1) ] [ (rssi+mac) * n ]
  • tag is a constant 0x01 (set in proto_pack_wifi/proto_pack_wifi_ts) — it is not a moving/steady/ts indicator; use the fPort for that.
  • In the timestamped variant the 4-byte ts comes before the tag byte (lorawan_proto_ul_wifi_ts_t), and ts is big-endian (utils_rev32).
  • MAC bytes are copied verbatim (6 bytes); rssi is signed.

BLE payload (proto_pack_ble / proto_pack_ble_ts)

Identical layout to WiFi: 7-byte { int8 rssi; uint8 addr[6] } entries, tag = 0x01, and the timestamped variant (ports 162/163) prefixes a big-endian uint32 ts before the tag byte.

GNSS-NA payload (proto_pack_gnss / proto_pack_gnss_ts)

// GHDR, 1 byte (bitfield):  bits[0:4] grp_token, bits[5:6] RFU, bit[7] eog
// port 192/193 (no ts):   [ GHDR(1) ] [ NAV bytes… ]
// port 194/195 (with ts): [ ts(4, big-endian) ] [ GHDR(1) ] [ NAV bytes… ]

NAV bytes is the raw LR11xx GNSS-NG scan result (opaque, copied verbatim). eog (end-of-group) marks the last message of a multi-message group.

GPS-SA payload (proto_pack_gps_ts, fPort 10)

lorawan_gps_ul_ts_t, all multi-byte fields big-endian (utils_rev16/utils_rev32):

Offset Bytes Field Type Notes (from proto_pack_gps_ts)
0 1 status uint8 hardcoded 0 in current code
1 4 lat uint32 BE
5 4 lon uint32 BE
9 2 alt uint16 BE
11 4 ts uint32 BE epoch
15 2 voltage_temp uint16 BE hardcoded 3700 in current code — combined voltage/temp field, not yet real
17 1 ttf uint8 passed as 0 at the call site
18 1 pdop uint8 fix.pdop / 100
19 1 sats uint8 satellite count

Several GPS-SA fields (status, voltage_temp, ttf) are placeholder/hardcoded in this firmware revision — confirm with firmware before relying on them.


Reference

  • tagxl-v2 (current decoder baseline): apps/trackit/settings.h, apps/trackit/main_trackit.c
  • ts2 (target, authoritative): ts2/app/tracker/lorawan_proto.h, ts2/app/tracker/lorawan_proto.c, ts2/app/tracker/logic.c
  • ts2/Payload format.mdoutdated draft, do not use (contradicts the code; see the source-of-truth note at the top)

Acceptance criteria

Tier 1 (do first):

  • Decoder parses WiFi AP constraint / Min_AP (0x2A/0x4D), motion detection plan v2 (0x30/0x60), battery temperature (0x31/0x61), and buffer level (0x63) on settings port 151.

Tier 2 (later — everything else):

  • Decoder parses scan timers / quick scans (0x32/0x62), BLE config (0x2C/0x51), batch count (0x2B/0x50), battery level thresholds (0x29/0x4C), reset count (0x49), reset cause (0x4A), and scan count (0x4B) on settings port 151.
  • Decoder parses the new localization uplinks by fPort: BLE (160–163), WiFi ts (200/201), GNSS-NA (192–195), GPS-SA (10).

Cross-cutting (both tiers):

  • Decoder keys message type off fPort (not off the constant 0x01 payload tag), and reads all multi-byte fields big-endian.
  • Set-tag collisions (0x29/0x2A/0x2B) and placeholder GPS-SA fields confirmed with firmware and documented.
  • Unit tests / decoder fixtures added for each new tag with a sample payload.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions