mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-08-28 23:00:28 +00:00
fix: align telemetry with pinned field semantics
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user