fix: align telemetry with pinned field semantics

This commit is contained in:
torlando-agent[bot]
2026-08-07 01:23:08 +00:00
parent 71fd61050f
commit 299fd4af02
5 changed files with 114 additions and 11 deletions
+37 -5
View File
@@ -58,7 +58,9 @@ Portable code must avoid `Arduino.h`, `String`, LVGL, FreeRTOS, and filesystem h
- Plain value types, field IDs, validity/error enums, explicit units.
- `lib/tdeck_ui/Telemetry/LocationTelemetryCodec.h`
- `lib/tdeck_ui/Telemetry/LocationTelemetryCodec.cpp`
- Bounded MessagePack encoder/decoder over caller-owned buffers.
- Bounded MessagePack encoder/decoder over caller-owned buffers. Handles both
the inner Sideband Telemeter map and the outer raw MessagePack BIN span that
current pinned microLXMF stores in `LXMessage::FieldEntry`.
- `lib/tdeck_ui/Telemetry/LocationShareState.h`
- `lib/tdeck_ui/Telemetry/LocationShareState.cpp`
- Fixed-capacity peer locations and outbound sessions; deterministic eviction.
@@ -152,6 +154,8 @@ Telemetry::DecodeResult decodeLocationTelemetry(
- Skip unknown values with bounded depth and bounded aggregate item count.
- Reject overflow, truncation, invalid MessagePack types, duplicate location ambiguity, non-finite/out-of-range coordinates, impossible negative unsigned values, excessive nesting, and oversized payloads.
- Treat `last_update` as authoritative location timestamp; preserve outer sensor timestamp separately if useful.
- Name and store the speed fixed-point value as centi-km/h. Sideband's location
sensor uses km/h; TinyGPS `speed.kmph()` must feed the encoder, not `mps()`.
**TDD cycle per behavior:**
1. Add one failing vector assertion.
@@ -181,7 +185,7 @@ Telemetry::EncodeResult encodeLocationTelemetry(
**Behavior:**
- Caller-owned fixed buffer; explicit `BUFFER_TOO_SMALL` result.
- Big-endian fixed-point fields with documented rounding/truncation selected from the pinned Sideband reference.
- Clamp speed to non-negative and accuracy to unsigned 16-bit range.
- Clamp speed (km/h) to non-negative and accuracy to unsigned 16-bit range.
- Reject invalid latitude, longitude, bearing policy violations, non-finite values, and timestamps that cannot be represented by the selected wire contract.
- Compare exact bytes for canonical fixture inputs.
@@ -189,9 +193,31 @@ Telemetry::EncodeResult encodeLocationTelemetry(
**Commit:** `feat: encode canonical location telemetry`.
### Task 4: Implement `FIELD_CUSTOM_META` codec
### Task 4: Implement the pinned microLXMF raw-field envelope
**Objective:** Correctly encode/decode current Columba metadata at LXMF field `0xFD`.
**Objective:** Prove that the inner Telemeter map crosses current microLXMF as
the MessagePack BIN value Python LXMF/Sideband expects.
**Critical pinned-API contract:** microLXMF `d9bbc04` stores field keys and
values as raw MessagePack spans and emits both with `packRawBytes()`. Therefore
the raw key span for telemetry is positive-fixint `0x02`, while the raw value
span is `bin8`/`bin16`/`bin32` containing the packed Telemeter map. Passing the
inner map directly to `fields_set()` would emit a map value rather than bytes
and Sideband's `Telemeter.from_packed()` would reject it.
**Tests:**
- Wrap inner payload as bin8/bin16/bin32 at exact boundaries.
- Unwrap each valid BIN width without copying.
- Reject non-BIN, truncation, declared-length mismatch, and trailing bytes.
- Full pinned microLXMF pack → unpack → `fields_get(0x02)` → unwrap → Telemeter
decode, plus the reverse direction.
**Commit:** `feat: wrap telemetry for current microLXMF fields`.
### Task 4b: Implement optional Columba `FIELD_CUSTOM_META` codec
**Objective:** Correctly encode/decode the optional current Columba extension at
LXMF field `0xFD` without treating it as core Sideband telemetry.
**Files:** codec production and native test files.
@@ -202,7 +228,10 @@ Telemetry::EncodeResult encodeLocationTelemetry(
- Unknown keys skipped safely.
- Absent, malformed, false, and true cease values remain distinct.
- Empty metadata is omitted outbound.
- Cease frame requires both a valid zeroed Telemeter body and `{ "cease": true }` metadata for current Columba interoperability.
- Core stop-sharing semantics stop future sends locally. Sideband has no remote
history-deletion/cease contract. When Columba compatibility is enabled, its
cease frame requires both a valid zeroed Telemeter body and
`{ "cease": true }` metadata.
**Tests:** canonical byte vectors, key reordering, integer widths, malformed values, JSON legacy input explicitly rejected or separately compatibility-gated by a documented test.
@@ -324,6 +353,9 @@ Telemetry::EncodeResult encodeLocationTelemetry(
**Behavior:**
- Construct empty-content LXMF messages carrying field `0x02` and optional `0xFD`.
- Supply `fields_set()` with raw MessagePack spans: key `02` and a BIN-wrapped
Telemeter value. On receive, unwrap the raw BIN returned by `fields_get(02)`
before decoding the inner map.
- Prefer opportunistic delivery; allow router promotion when required, matching short UI messages.
- Do not fake a known destination identity.
- Advance scheduler only after `handle_outbound` accepts ownership/queueing under the available API.
@@ -100,7 +100,7 @@ bool readLocation(Cursor& cursor, LocationTelemetry& location) {
if (!cursor.readBinary(word, sizeof(word))) return false;
location.altitude_cm = decodeI32(word);
if (!cursor.readBinary(word, sizeof(word))) return false;
location.speed_cms = decodeU32(word);
location.speed_centi_kmh = decodeU32(word);
if (!cursor.readBinary(word, sizeof(word))) return false;
location.bearing_cdeg = decodeI32(word);
if (!cursor.readBinary(half, sizeof(half))) return false;
@@ -113,6 +113,46 @@ bool readLocation(Cursor& cursor, LocationTelemetry& location) {
} // namespace
FieldValueResult unwrapLxmfBinaryFieldValue(
const uint8_t* raw_value,
std::size_t raw_size,
BinaryView& output) {
if (raw_value == nullptr || raw_size == 0) {
return FieldValueResult::INVALID_ARGUMENT;
}
const uint8_t marker = raw_value[0];
std::size_t header_size = 0;
std::size_t payload_size = 0;
if (marker == 0xc4U) {
if (raw_size < 2) return FieldValueResult::MALFORMED;
header_size = 2;
payload_size = raw_value[1];
} else if (marker == 0xc5U) {
if (raw_size < 3) return FieldValueResult::MALFORMED;
header_size = 3;
payload_size = (static_cast<std::size_t>(raw_value[1]) << 8U) |
raw_value[2];
} else if (marker == 0xc6U) {
if (raw_size < 5) return FieldValueResult::MALFORMED;
header_size = 5;
payload_size = (static_cast<std::size_t>(raw_value[1]) << 24U) |
(static_cast<std::size_t>(raw_value[2]) << 16U) |
(static_cast<std::size_t>(raw_value[3]) << 8U) |
raw_value[4];
} else {
return FieldValueResult::NOT_BINARY;
}
if (payload_size != raw_size - header_size) {
return FieldValueResult::MALFORMED;
}
BinaryView candidate{raw_value + header_size, payload_size};
output = candidate;
return FieldValueResult::OK;
}
DecodeResult decodeLocationTelemetry(
const uint8_t* data,
std::size_t size,
@@ -16,7 +16,7 @@ struct LocationTelemetry {
int32_t latitude_e6 = 0;
int32_t longitude_e6 = 0;
int32_t altitude_cm = 0;
uint32_t speed_cms = 0;
uint32_t speed_centi_kmh = 0;
int32_t bearing_cdeg = 0;
uint16_t accuracy_cm = 0;
uint64_t timestamp_seconds = 0;
@@ -30,6 +30,23 @@ enum class DecodeResult : uint8_t {
MISSING_LOCATION,
};
enum class FieldValueResult : uint8_t {
OK,
INVALID_ARGUMENT,
NOT_BINARY,
MALFORMED,
};
struct BinaryView {
const uint8_t* data = nullptr;
std::size_t size = 0;
};
FieldValueResult unwrapLxmfBinaryFieldValue(
const uint8_t* raw_value,
std::size_t raw_size,
BinaryView& output);
DecodeResult decodeLocationTelemetry(
const uint8_t* data,
std::size_t size,
+17 -3
View File
@@ -23,7 +23,7 @@ void decodes_canonical_sideband_location() {
0xc4, 0x04, 0x02, 0x40, 0x66, 0x34, // 37.774900 degrees
0xc4, 0x04, 0xf8, 0xb4, 0x07, 0x38, // -122.419400 degrees
0xc4, 0x04, 0x00, 0x00, 0x06, 0x40, // 16.00 m
0xc4, 0x04, 0x00, 0x00, 0x00, 0x00, // 0.00 m/s
0xc4, 0x04, 0x00, 0x00, 0x04, 0xd2, // 12.34 km/h
0xc4, 0x04, 0x00, 0x00, 0x10, 0x68, // 42.00 degrees
0xc4, 0x02, 0x01, 0x5e, // 3.50 m
0xce, 0x65, 0x53, 0xf1, 0x00,
@@ -37,10 +37,24 @@ void decodes_canonical_sideband_location() {
CHECK(output.latitude_e6 == 37774900);
CHECK(output.longitude_e6 == -122419400);
CHECK(output.altitude_cm == 1600);
CHECK(output.speed_cms == 0);
CHECK(output.speed_centi_kmh == 1234);
CHECK(output.bearing_cdeg == 4200);
CHECK(output.accuracy_cm == 350);
CHECK(output.timestamp_seconds == 1700000000ULL);
uint8_t field_value[sizeof(packed) + 2]{};
field_value[0] = 0xc4; // MessagePack bin8
field_value[1] = static_cast<uint8_t>(sizeof(packed));
for (std::size_t index = 0; index < sizeof(packed); ++index) {
field_value[index + 2] = packed[index];
}
Telemetry::BinaryView inner{};
CHECK(Telemetry::unwrapLxmfBinaryFieldValue(
field_value, sizeof(field_value), inner) ==
Telemetry::FieldValueResult::OK);
CHECK(inner.data == field_value + 2);
CHECK(inner.size == sizeof(packed));
}
} // namespace
@@ -48,7 +62,7 @@ void decodes_canonical_sideband_location() {
int main() {
decodes_canonical_sideband_location();
if (failures == 0) {
std::cout << "location telemetry codec: 7 passed, 0 failed\n";
std::cout << "location telemetry codec: 10 passed, 0 failed\n";
}
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}
@@ -23,4 +23,4 @@ def test_location_telemetry_codec(tmp_path):
include_dirs=[PYXIS_ROOT / "lib" / "tdeck_ui"],
sanitize=True,
)
assert "location telemetry codec: 7 passed, 0 failed" in ran.stdout
assert "location telemetry codec: 10 passed, 0 failed" in ran.stdout