diff --git a/lib/tdeck_ui/Telemetry/LocationTelemetryCodec.cpp b/lib/tdeck_ui/Telemetry/LocationTelemetryCodec.cpp index 70784845..12cf7469 100644 --- a/lib/tdeck_ui/Telemetry/LocationTelemetryCodec.cpp +++ b/lib/tdeck_ui/Telemetry/LocationTelemetryCodec.cpp @@ -417,7 +417,7 @@ FieldValueResult wrapLxmfBinaryFieldValue( output[3] = static_cast(payload_size >> 8U); output[4] = static_cast(payload_size); } - if (payload_size != 0) std::memcpy(output + header_size, payload, payload_size); + if (payload_size != 0) std::memmove(output + header_size, payload, payload_size); written = header_size + payload_size; return FieldValueResult::OK; } @@ -473,13 +473,9 @@ EncodeResult encodeLocationTelemetry( Writer writer(temporary, sizeof(temporary)); uint8_t word[4]{}; uint8_t half[2]{}; - const uint64_t sensor_timestamp = input.sensor_timestamp_seconds == 0 - ? input.timestamp_seconds - : input.sensor_timestamp_seconds; - bool ok = writer.writeByte(0x82U) && writer.writeUnsigned(SID_TIME) && - writer.writeUnsigned(sensor_timestamp) && + writer.writeUnsigned(input.sensor_timestamp_seconds) && writer.writeUnsigned(SID_LOCATION) && writer.writeByte(0x97U); diff --git a/tests/fixtures/location_telemetry_vectors.json b/tests/fixtures/location_telemetry_vectors.json index 246d3418..8828a1ee 100644 --- a/tests/fixtures/location_telemetry_vectors.json +++ b/tests/fixtures/location_telemetry_vectors.json @@ -16,7 +16,8 @@ "accuracy": 3.5, "location_timestamp_seconds": 1700000000, "packed_hex": "8201ce6553f1000297c40402406634c404f8b40738c40400000640c404000004d2c40400001068c402015ece6553f100", - "microlxmf_raw_value_hex": "c4308201ce6553f1000297c40402406634c404f8b40738c40400000640c404000004d2c40400001068c402015ece6553f100" + "microlxmf_raw_value_hex": "c4308201ce6553f1000297c40402406634c404f8b40738c40400000640c404000004d2c40400001068c402015ece6553f100", + "expected_fixed": {"latitude_e6": 37774900, "longitude_e6": -122419400, "altitude_cm": 1600, "speed_centi_kmh": 1234, "bearing_cdeg": 4200, "accuracy_cm": 350} }, { "name": "bounds_signed", @@ -29,7 +30,8 @@ "accuracy": 655.35, "location_timestamp_seconds": 127, "packed_hex": "8201cc800297c404faa2b580c4040aba9500c404ffffcfc7c40400000000c404ffffee29c402ffff7f", - "microlxmf_raw_value_hex": "c4298201cc800297c404faa2b580c4040aba9500c404ffffcfc7c40400000000c404ffffee29c402ffff7f" + "microlxmf_raw_value_hex": "c4298201cc800297c404faa2b580c4040aba9500c404ffffcfc7c40400000000c404ffffee29c402ffff7f", + "expected_fixed": {"latitude_e6": -90000000, "longitude_e6": 180000000, "altitude_cm": -12345, "speed_centi_kmh": 0, "bearing_cdeg": -4567, "accuracy_cm": 65535} }, { "name": "southern", @@ -42,7 +44,36 @@ "accuracy": 0.01, "location_timestamp_seconds": 65535, "packed_hex": "8201ce000100000297c404fdfb3400c40409034554c404000016c1c404000022b8c40400008c9fc4020001cdffff", - "microlxmf_raw_value_hex": "c42e8201ce000100000297c404fdfb3400c40409034554c404000016c1c404000022b8c40400008c9fc4020001cdffff" + "microlxmf_raw_value_hex": "c42e8201ce000100000297c404fdfb3400c40409034554c404000016c1c404000022b8c40400008c9fc4020001cdffff", + "expected_fixed": {"latitude_e6": -33868800, "longitude_e6": 151209300, "altitude_cm": 5825, "speed_centi_kmh": 8888, "bearing_cdeg": 35999, "accuracy_cm": 1} + }, + { + "name": "zero_time_accuracy", + "sensor_timestamp_seconds": 0, + "latitude": 0.0, + "longitude": 0.0, + "altitude": 0.0, + "speed_kmh": 0.0, + "bearing": 0.0, + "accuracy": 0.0, + "location_timestamp_seconds": 1, + "packed_hex": "8201000297c40400000000c40400000000c40400000000c40400000000c40400000000c402000001", + "microlxmf_raw_value_hex": "c4288201000297c40400000000c40400000000c40400000000c40400000000c40400000000c402000001", + "expected_fixed": {"latitude_e6": 0, "longitude_e6": 0, "altitude_cm": 0, "speed_centi_kmh": 0, "bearing_cdeg": 0, "accuracy_cm": 0} + }, + { + "name": "uint64_timestamps", + "sensor_timestamp_seconds": 4294967296, + "latitude": 1.0, + "longitude": -1.0, + "altitude": 1.0, + "speed_kmh": 1.0, + "bearing": 1.0, + "accuracy": 1.0, + "location_timestamp_seconds": 4294967297, + "packed_hex": "8201cf00000001000000000297c404000f4240c404fff0bdc0c40400000064c40400000064c40400000064c4020064cf0000000100000001", + "microlxmf_raw_value_hex": "c4388201cf00000001000000000297c404000f4240c404fff0bdc0c40400000064c40400000064c40400000064c4020064cf0000000100000001", + "expected_fixed": {"latitude_e6": 1000000, "longitude_e6": -1000000, "altitude_cm": 100, "speed_centi_kmh": 100, "bearing_cdeg": 100, "accuracy_cm": 100} } ] } diff --git a/tests/microlxmf/CMakeLists.txt b/tests/microlxmf/CMakeLists.txt index 5dcde8f5..c15abfaf 100644 --- a/tests/microlxmf/CMakeLists.txt +++ b/tests/microlxmf/CMakeLists.txt @@ -11,11 +11,20 @@ endif() if(NOT DEFINED MICROLXMF_SRC) message(FATAL_ERROR "MICROLXMF_SRC is required") endif() +if(NOT DEFINED PYXIS_ROOT) + message(FATAL_ERROR "PYXIS_ROOT is required") +endif() add_subdirectory(${MICROLXMF_BRIDGE_DIR} microlxmf-bridge) -add_executable(test_pyxis_telemetry_field test_telemetry_field_roundtrip.cpp) +add_executable(test_pyxis_telemetry_field + test_telemetry_field_roundtrip.cpp + ${PYXIS_ROOT}/lib/tdeck_ui/Telemetry/LocationTelemetryCodec.cpp +) target_link_libraries(test_pyxis_telemetry_field PRIVATE MicroLXMFLib) +target_include_directories(test_pyxis_telemetry_field PRIVATE + ${PYXIS_ROOT}/lib/tdeck_ui +) target_compile_definitions(test_pyxis_telemetry_field PRIVATE MICROLXMF_NATIVE=1 NATIVE=1 diff --git a/tests/microlxmf/test_telemetry_field_roundtrip.cpp b/tests/microlxmf/test_telemetry_field_roundtrip.cpp index 87d42863..7b09def0 100644 --- a/tests/microlxmf/test_telemetry_field_roundtrip.cpp +++ b/tests/microlxmf/test_telemetry_field_roundtrip.cpp @@ -8,23 +8,38 @@ #include #include +#include "Telemetry/LocationTelemetryCodec.h" + int main() { - constexpr uint8_t key_bytes[] = {0x02}; - constexpr uint8_t inner_telemeter[] = { - 0x82, 0x01, 0xce, 0x65, 0x53, 0xf1, 0x00, 0x02, 0x97, - 0xc4, 0x04, 0x02, 0x40, 0x66, 0x34, - 0xc4, 0x04, 0xf8, 0xb4, 0x07, 0x38, - 0xc4, 0x04, 0x00, 0x00, 0x06, 0x40, - 0xc4, 0x04, 0x00, 0x00, 0x04, 0xd2, - 0xc4, 0x04, 0x00, 0x00, 0x10, 0x68, - 0xc4, 0x02, 0x01, 0x5e, - 0xce, 0x65, 0x53, 0xf1, 0x00, - }; - uint8_t raw_value[sizeof(inner_telemeter) + 2]{}; - raw_value[0] = 0xc4; - raw_value[1] = static_cast(sizeof(inner_telemeter)); - for (std::size_t index = 0; index < sizeof(inner_telemeter); ++index) { - raw_value[index + 2] = inner_telemeter[index]; + constexpr uint8_t key_bytes[] = {Telemetry::FIELD_TELEMETRY}; + constexpr std::size_t TELEMETRY_CAPACITY = 128; + + Telemetry::LocationTelemetry outbound{}; + outbound.latitude_e6 = 37774900; + outbound.longitude_e6 = -122419400; + outbound.altitude_cm = 1600; + outbound.speed_centi_kmh = 1234; + outbound.bearing_cdeg = 4200; + outbound.accuracy_cm = 350; + outbound.timestamp_seconds = 1700000000ULL; + outbound.sensor_timestamp_seconds = 1700000000ULL; + + uint8_t inner_telemeter[TELEMETRY_CAPACITY]{}; + std::size_t inner_size = 0; + if (Telemetry::encodeLocationTelemetry( + outbound, inner_telemeter, sizeof(inner_telemeter), inner_size) != + Telemetry::EncodeResult::OK) { + std::cerr << "codec failed to encode telemetry\n"; + return EXIT_FAILURE; + } + + uint8_t raw_value[TELEMETRY_CAPACITY + 5]{}; + std::size_t raw_size = 0; + if (Telemetry::wrapLxmfBinaryFieldValue( + inner_telemeter, inner_size, raw_value, sizeof(raw_value), raw_size) != + Telemetry::FieldValueResult::OK) { + std::cerr << "codec failed to wrap telemetry field\n"; + return EXIT_FAILURE; } RNS::Identity source_identity; @@ -40,21 +55,41 @@ int main() { destination, source, RNS::Bytes{}, RNS::Bytes{}, LXMF::Type::Message::OPPORTUNISTIC); const RNS::Bytes key(key_bytes, sizeof(key_bytes)); - const RNS::Bytes value(raw_value, sizeof(raw_value)); + const RNS::Bytes value(raw_value, raw_size); if (!message.fields_set(key, value)) { std::cerr << "fields_set rejected telemetry\n"; return EXIT_FAILURE; } const RNS::Bytes packed = message.pack(); - LXMF::LXMessage decoded = LXMF::LXMessage::unpack_from_bytes( + LXMF::LXMessage decoded_message = LXMF::LXMessage::unpack_from_bytes( packed, LXMF::Type::Message::OPPORTUNISTIC, true); - const RNS::Bytes* decoded_value = decoded.fields_get(key); + const RNS::Bytes* decoded_value = decoded_message.fields_get(key); if (decoded_value == nullptr || *decoded_value != value) { std::cerr << "raw telemetry BIN span changed across microLXMF pack/unpack\n"; return EXIT_FAILURE; } + Telemetry::BinaryView unwrapped{}; + if (Telemetry::unwrapLxmfBinaryFieldValue( + decoded_value->data(), decoded_value->size(), unwrapped) != + Telemetry::FieldValueResult::OK) { + std::cerr << "codec failed to unwrap microLXMF field\n"; + return EXIT_FAILURE; + } + Telemetry::LocationTelemetry inbound{}; + if (Telemetry::decodeLocationTelemetry( + unwrapped.data, unwrapped.size, inbound) != + Telemetry::DecodeResult::OK || + inbound.latitude_e6 != outbound.latitude_e6 || + inbound.longitude_e6 != outbound.longitude_e6 || + inbound.speed_centi_kmh != outbound.speed_centi_kmh || + inbound.timestamp_seconds != outbound.timestamp_seconds || + inbound.sensor_timestamp_seconds != outbound.sensor_timestamp_seconds) { + std::cerr << "telemetry changed across codec/microLXMF roundtrip\n"; + return EXIT_FAILURE; + } + std::cout << "microLXMF telemetry field roundtrip: passed\n"; return EXIT_SUCCESS; } diff --git a/tests/native/test_location_telemetry_codec.cpp b/tests/native/test_location_telemetry_codec.cpp index 580af72b..29af1df8 100644 --- a/tests/native/test_location_telemetry_codec.cpp +++ b/tests/native/test_location_telemetry_codec.cpp @@ -3,8 +3,10 @@ #include #include #include +#include #include "Telemetry/LocationTelemetryCodec.h" +#include "location_telemetry_vectors_generated.h" namespace { @@ -57,6 +59,35 @@ bool equalLocation(const Telemetry::LocationTelemetry& left, left.sensor_timestamp_seconds == right.sensor_timestamp_seconds; } +void validatesEveryCommittedSidebandVector() { + for (std::size_t index = 0; index < Fixture::VECTOR_COUNT; ++index) { + const auto& vector = Fixture::VECTORS[index]; + Telemetry::LocationTelemetry expected{}; + expected.latitude_e6 = vector.latitude_e6; + expected.longitude_e6 = vector.longitude_e6; + expected.altitude_cm = vector.altitude_cm; + expected.speed_centi_kmh = vector.speed_centi_kmh; + expected.bearing_cdeg = vector.bearing_cdeg; + expected.accuracy_cm = vector.accuracy_cm; + expected.timestamp_seconds = vector.location_timestamp_seconds; + expected.sensor_timestamp_seconds = vector.sensor_timestamp_seconds; + + Telemetry::LocationTelemetry decoded{}; + CHECK(Telemetry::decodeLocationTelemetry( + vector.packed, vector.packed_size, decoded) == + Telemetry::DecodeResult::OK); + CHECK(equalLocation(decoded, expected)); + + uint8_t encoded[128]{}; + std::size_t written = 0; + CHECK(Telemetry::encodeLocationTelemetry( + expected, encoded, sizeof(encoded), written) == + Telemetry::EncodeResult::OK); + CHECK(written == vector.packed_size); + CHECK(std::memcmp(encoded, vector.packed, written) == 0); + } +} + void decodesCanonicalSidebandLocation() { Telemetry::LocationTelemetry output{}; CHECK(Telemetry::decodeLocationTelemetry( @@ -153,15 +184,96 @@ void rejectsMalformedOuterFieldWithoutMutatingView() { CHECK(output.data == sentinel.data && output.size == sentinel.size); } +void coversEveryBinaryHeaderBoundaryAndOverlap() { + const std::size_t sizes[] = {255, 256, 65535, 65536}; + const uint8_t markers[] = {0xc4, 0xc5, 0xc5, 0xc6}; + const std::size_t header_sizes[] = {2, 3, 3, 5}; + for (std::size_t test = 0; test < 4; ++test) { + std::vector payload(sizes[test]); + for (std::size_t index = 0; index < payload.size(); ++index) { + payload[index] = static_cast(index); + } + std::vector wrapped(payload.size() + 5); + std::size_t written = 0; + CHECK(Telemetry::wrapLxmfBinaryFieldValue( + payload.data(), payload.size(), wrapped.data(), wrapped.size(), + written) == Telemetry::FieldValueResult::OK); + CHECK(wrapped[0] == markers[test]); + CHECK(written == payload.size() + header_sizes[test]); + + Telemetry::BinaryView view{}; + CHECK(Telemetry::unwrapLxmfBinaryFieldValue( + wrapped.data(), written, view) == + Telemetry::FieldValueResult::OK); + CHECK(view.size == payload.size()); + CHECK(std::memcmp(view.data, payload.data(), payload.size()) == 0); + } + + // An in-place prepend is a natural constrained-memory operation. The + // payload and destination overlap and must remain defined. + std::vector in_place(sizeof(CANONICAL) + 5); + std::memcpy(in_place.data() + 5, CANONICAL, sizeof(CANONICAL)); + std::size_t written = 0; + CHECK(Telemetry::wrapLxmfBinaryFieldValue( + in_place.data() + 5, sizeof(CANONICAL), in_place.data(), + in_place.size(), written) == Telemetry::FieldValueResult::OK); + CHECK(written == sizeof(CANONICAL) + 2); + CHECK(std::memcmp(in_place.data() + 2, CANONICAL, sizeof(CANONICAL)) == 0); +} + +void rejectsEveryBoundedParserLimitAndOversizedLength() { + constexpr uint8_t too_many_map_entries[] = {0xde, 0x00, 0x21}; + constexpr uint8_t too_many_location_elements[] = { + 0x81, 0x02, 0xdc, 0x00, 0x11, + }; + constexpr uint8_t excessive_depth[] = { + 0x82, 0x01, 0x00, 0x20, + 0x91, 0x91, 0x91, 0x91, 0x91, + 0x91, 0x91, 0x91, 0x91, 0x91, 0xc0, + }; + std::vector excessive_items = { + 0x82, 0x01, 0x00, 0x20, 0xdc, 0x00, 0x41, + }; + excessive_items.insert(excessive_items.end(), 65, 0xc0); + constexpr uint8_t oversized_binary[] = { + 0x81, 0x02, 0x97, 0xc6, 0xff, 0xff, 0xff, 0xff, + }; + std::vector trailing(CANONICAL, CANONICAL + sizeof(CANONICAL)); + trailing.push_back(0xc0); + + struct Case { + const uint8_t* data; + std::size_t size; + }; + const Case cases[] = { + {too_many_map_entries, sizeof(too_many_map_entries)}, + {too_many_location_elements, sizeof(too_many_location_elements)}, + {excessive_depth, sizeof(excessive_depth)}, + {excessive_items.data(), excessive_items.size()}, + {oversized_binary, sizeof(oversized_binary)}, + {trailing.data(), trailing.size()}, + }; + const auto sentinel = expectedLocation(); + for (const auto& item : cases) { + auto output = sentinel; + CHECK(Telemetry::decodeLocationTelemetry(item.data, item.size, output) != + Telemetry::DecodeResult::OK); + CHECK(equalLocation(output, sentinel)); + } +} + } // namespace int main() { + validatesEveryCommittedSidebandVector(); decodesCanonicalSidebandLocation(); emitsCanonicalSidebandLocation(); unwrapsAndWrapsCurrentMicroLxmfFieldValue(); preservesOutputOnEveryTruncation(); acceptsUnknownNestedSensorAndReorderedKeys(); rejectsMalformedOuterFieldWithoutMutatingView(); + coversEveryBinaryHeaderBoundaryAndOverlap(); + rejectsEveryBoundedParserLimitAndOversizedLength(); std::cout << "location telemetry codec: " << passed << " passed, " << failures << " failed\n"; return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; diff --git a/tests/native/test_location_telemetry_codec.py b/tests/native/test_location_telemetry_codec.py index 98925598..5310d837 100644 --- a/tests/native/test_location_telemetry_codec.py +++ b/tests/native/test_location_telemetry_codec.py @@ -1,14 +1,60 @@ """Compile and execute the portable location telemetry codec tests.""" +from __future__ import annotations + +import json from pathlib import Path from native_test import compile_and_run HERE = Path(__file__).resolve().parent PYXIS_ROOT = HERE.parent.parent +VECTORS = PYXIS_ROOT / "tests" / "fixtures" / "location_telemetry_vectors.json" + + +def _write_vector_header(path: Path) -> None: + fixture = json.loads(VECTORS.read_text()) + lines = [ + "#pragma once", + "#include ", + "#include ", + "namespace Fixture {", + "struct Vector {", + " const char* name; const uint8_t* packed; std::size_t packed_size;", + " int32_t latitude_e6; int32_t longitude_e6; int32_t altitude_cm;", + " uint32_t speed_centi_kmh; int32_t bearing_cdeg; uint16_t accuracy_cm;", + " uint64_t location_timestamp_seconds; uint64_t sensor_timestamp_seconds;", + "};", + ] + for index, vector in enumerate(fixture["vectors"]): + packed = bytes.fromhex(vector["packed_hex"]) + encoded = ", ".join(f"0x{byte:02x}" for byte in packed) + lines.append(f"static const uint8_t PACKED_{index}[] = {{{encoded}}};") + lines.append("static const Vector VECTORS[] = {") + for index, vector in enumerate(fixture["vectors"]): + fixed = vector["expected_fixed"] + lines.append( + " {" + f'"{vector["name"]}", PACKED_{index}, sizeof(PACKED_{index}), ' + f'{fixed["latitude_e6"]}, {fixed["longitude_e6"]}, ' + f'{fixed["altitude_cm"]}, {fixed["speed_centi_kmh"]}U, ' + f'{fixed["bearing_cdeg"]}, {fixed["accuracy_cm"]}U, ' + f'{vector["location_timestamp_seconds"]}ULL, ' + f'{vector["sensor_timestamp_seconds"]}ULL' + "}," + ) + lines.extend( + [ + "};", + "static const std::size_t VECTOR_COUNT = sizeof(VECTORS) / sizeof(VECTORS[0]);", + "} // namespace Fixture", + ] + ) + path.write_text("\n".join(lines) + "\n") def test_location_telemetry_codec(tmp_path): + _write_vector_header(tmp_path / "location_telemetry_vectors_generated.h") ran = compile_and_run( tmp_path, name="test_location_telemetry_codec", @@ -20,7 +66,7 @@ def test_location_telemetry_codec(tmp_path): / "Telemetry" / "LocationTelemetryCodec.cpp", ], - include_dirs=[PYXIS_ROOT / "lib" / "tdeck_ui"], + include_dirs=[tmp_path, PYXIS_ROOT / "lib" / "tdeck_ui"], sanitize=True, ) assert "location telemetry codec:" in ran.stdout diff --git a/tests/reference/test_microlxmf_telemetry_field.py b/tests/reference/test_microlxmf_telemetry_field.py index 5f8f0e30..09742878 100644 --- a/tests/reference/test_microlxmf_telemetry_field.py +++ b/tests/reference/test_microlxmf_telemetry_field.py @@ -41,6 +41,7 @@ def test_pinned_microlxmf_telemetry_field_roundtrip(tmp_path): str(build), f"-DMICROLXMF_BRIDGE_DIR={bridge}", f"-DMICROLXMF_SRC={source}", + f"-DPYXIS_ROOT={ROOT}", "-DCMAKE_BUILD_TYPE=Debug", ], capture_output=True, diff --git a/tests/reference/test_sideband_location_vectors.py b/tests/reference/test_sideband_location_vectors.py index e41b2db9..9e071fa3 100644 --- a/tests/reference/test_sideband_location_vectors.py +++ b/tests/reference/test_sideband_location_vectors.py @@ -13,6 +13,19 @@ import pytest ROOT = Path(__file__).resolve().parents[2] VECTORS = ROOT / "tests" / "fixtures" / "location_telemetry_vectors.json" DEFAULT_SIDEBAND = Path("/tmp/pyxis-sideband-reference-20260728") +REQUIRED_VECTOR_NAMES = { + "san_francisco", + "bounds_signed", + "southern", + "zero_time_accuracy", + "uint64_timestamps", +} +REQUIRED_VECTOR_KEYS = { + "name", "sensor_timestamp_seconds", "latitude", "longitude", + "altitude", "speed_kmh", "bearing", "accuracy", + "location_timestamp_seconds", "packed_hex", + "microlxmf_raw_value_hex", "expected_fixed", +} def _sideband_source() -> Path: @@ -22,6 +35,16 @@ def _sideband_source() -> Path: return source +def test_fixture_has_required_protocol_matrix_and_schema(): + fixture = json.loads(VECTORS.read_text()) + vectors = fixture["vectors"] + assert {vector["name"] for vector in vectors} == REQUIRED_VECTOR_NAMES + for vector in vectors: + assert set(vector) == REQUIRED_VECTOR_KEYS, vector["name"] + assert bytes.fromhex(vector["microlxmf_raw_value_hex"])[0] == 0xC4 + assert len(bytes.fromhex(vector["packed_hex"])) <= 0xFF + + def test_vectors_match_pinned_sideband_commit(): fixture = json.loads(VECTORS.read_text()) source = _sideband_source() @@ -39,32 +62,36 @@ def test_vectors_match_pinned_sideband_commit(): sense = importlib.import_module("sbapp.sideband.sense") from RNS.vendor import umsgpack - for vector in fixture["vectors"]: - now = vector["sensor_timestamp_seconds"] - sense.time.time = lambda now=now: now - telemeter = sense.Telemeter(from_packed=True) - telemeter.synthesize("location") - telemeter.sensors["location"].data = { - "latitude": vector["latitude"], - "longitude": vector["longitude"], - "altitude": vector["altitude"], - "speed": vector["speed_kmh"], - "bearing": vector["bearing"], - "accuracy": vector["accuracy"], - "last_update": vector["location_timestamp_seconds"], - } - packed = telemeter.packed() - assert packed.hex() == vector["packed_hex"], vector["name"] + original_time = sense.time.time + try: + for vector in fixture["vectors"]: + now = vector["sensor_timestamp_seconds"] + sense.time.time = lambda now=now: now + telemeter = sense.Telemeter(from_packed=True) + telemeter.synthesize("location") + telemeter.sensors["location"].data = { + "latitude": vector["latitude"], + "longitude": vector["longitude"], + "altitude": vector["altitude"], + "speed": vector["speed_kmh"], + "bearing": vector["bearing"], + "accuracy": vector["accuracy"], + "last_update": vector["location_timestamp_seconds"], + } + packed = telemeter.packed() + assert packed.hex() == vector["packed_hex"], vector["name"] - # Pinned microLXMF stores field values as raw MessagePack spans and - # splices them with packRawBytes(). FIELD_TELEMETRY must therefore be a - # raw BIN token whose decoded Python LXMF value is the packed bytes. - raw_value = bytes.fromhex(vector["microlxmf_raw_value_hex"]) - assert umsgpack.unpackb(raw_value) == packed, vector["name"] + # Pinned microLXMF stores field values as raw MessagePack spans and + # splices them with packRawBytes(). FIELD_TELEMETRY must therefore + # be a raw BIN token decoded by Python LXMF as packed bytes. + raw_value = bytes.fromhex(vector["microlxmf_raw_value_hex"]) + assert umsgpack.unpackb(raw_value) == packed, vector["name"] - decoded = sense.Telemeter.from_packed(packed) - assert decoded is not None, vector["name"] - readings = decoded.read_all() - assert readings["time"]["utc"] == vector["sensor_timestamp_seconds"] - assert readings["location"]["speed"] == vector["speed_kmh"] - assert readings["location"]["last_update"] == vector["location_timestamp_seconds"] + decoded = sense.Telemeter.from_packed(packed) + assert decoded is not None, vector["name"] + readings = decoded.read_all() + assert readings["time"]["utc"] == vector["sensor_timestamp_seconds"] + assert readings["location"]["speed"] == vector["speed_kmh"] + assert readings["location"]["last_update"] == vector["location_timestamp_seconds"] + finally: + sense.time.time = original_time