#include #include #include #include #include #include "file_views.h" #include "loratap.h" #include "track.h" using namespace roro; using namespace roro::files; void setUp() {} void tearDown() {} void test_hex_row() { const uint8_t d[] = {'H', 'e', 'l', 'l', 'o', ',', ' ', 'w', 0x00, 0xFF, 'x'}; TEST_ASSERT_EQUAL_STRING("00010 4865 6c6c 6f2c 2077 Hello, w", hexRow(0x10, d, 8).c_str()); TEST_ASSERT_EQUAL_STRING("00018 00ff 78 ..x", hexRow(0x18, d + 8, 3).c_str()); TEST_ASSERT_TRUE(hexRow(0xFFFFF, d, 8).size() <= 38); } // A Track as the GNSS Service writes it, fed in pieces of every size: the same summary each time. void test_gpx_summary() { setenv("TZ", "UTC", 1); tzset(); std::string file = gnss::gpx::header("test"); int64_t t0 = 1791230400; // 2026-10-05 20:00:00 UTC for (int i = 0; i < 50; i++) file += gnss::gpx::point(48.0 + i * 0.0001, 7.0, i % 2 == 0, 200, t0 + i * 5) + "\n"; file += gnss::gpx::footer(); for (size_t piece : {size_t(1), size_t(7), size_t(64), size_t(1000), file.size()}) { GpxSummary s; for (size_t at = 0; at < file.size(); at += piece) s.feed(file.data() + at, std::min(piece, file.size() - at)); TEST_ASSERT_EQUAL_UINT32(50, s.points()); TEST_ASSERT_EQUAL_INT64(t0, s.start()); TEST_ASSERT_EQUAL_INT64(t0 + 245, s.end()); TEST_ASSERT_FLOAT_WITHIN(1.0f, 49 * 11.12f, static_cast(s.meters())); // 0.0001 degree of latitude: 11.12 m } GpxSummary s; s.feed(file.data(), file.size()); auto lines = s.lines(); TEST_ASSERT_EQUAL_size_t(4, lines.size()); TEST_ASSERT_EQUAL_STRING("50 points", lines[0].c_str()); TEST_ASSERT_EQUAL_STRING("Started 2026-10-05 20:00", lines[1].c_str()); TEST_ASSERT_EQUAL_STRING("Lasted 4 min 05 s", lines[2].c_str()); TEST_ASSERT_EQUAL_STRING("Distance 545 m", lines[3].c_str()); } void test_gpx_from_elsewhere_and_not_gpx() { GpxSummary s; std::string other = "\n1\n"; s.feed(other.data(), other.size()); TEST_ASSERT_EQUAL_UINT32(2, s.points()); TEST_ASSERT_EQUAL_INT64(0, s.start()); TEST_ASSERT_FLOAT_WITHIN(5.0f, 1112.0f, static_cast(s.meters())); TEST_ASSERT_EQUAL_size_t(2, s.lines().size()); // no times: points and distance only GpxSummary none; std::string junk(10000, 'x'); none.feed(junk.data(), junk.size()); TEST_ASSERT_EQUAL_UINT32(0, none.points()); } void test_durations() { TEST_ASSERT_EQUAL_STRING("12 s", formatDuration(12).c_str()); TEST_ASSERT_EQUAL_STRING("4 min 10 s", formatDuration(250).c_str()); TEST_ASSERT_EQUAL_STRING("1 h 02 min", formatDuration(3725).c_str()); } // What the LoRa Capture Service writes reads back the same. void test_pcap_round_trip() { std::vector file; lora::appendPcapHeader(file); lora::RxInfo rx; rx.frequencyHz = 869525000; rx.bandwidthKHz = 250; rx.spreadingFactor = 11; rx.rssi = -97; rx.snr = -6.25f; rx.noiseFloor = -106; rx.syncWord = 0x2B; const uint8_t payload[] = {1, 2, 3, 4, 5}; lora::appendRecord(file, 1791230400, 250000, rx, payload, sizeof payload); lora::appendRecord(file, 1791230401, 0, rx, payload, 0); PcapHeader h = parsePcapHeader(file.data(), file.size()); TEST_ASSERT_TRUE(h.ok); TEST_ASSERT_EQUAL_UINT32(kLinkLoraTap, h.linkType); size_t at = kPcapHeaderSize; PcapRecord r; TEST_ASSERT_TRUE(parsePcapRecord(file.data() + at, file.size() - at, r)); TEST_ASSERT_EQUAL_UINT32(1791230400, r.seconds); TEST_ASSERT_EQUAL_UINT32(250000, r.micros); TEST_ASSERT_EQUAL_UINT32(lora::kLoraTapSize + 5, r.length); lora::RxInfo back; TEST_ASSERT_TRUE(parseLoraTap(file.data() + at + kPcapRecordSize, r.length, back)); TEST_ASSERT_EQUAL_UINT32(869525000, back.frequencyHz); TEST_ASSERT_EQUAL_FLOAT(250, back.bandwidthKHz); TEST_ASSERT_EQUAL_UINT8(11, back.spreadingFactor); TEST_ASSERT_EQUAL_FLOAT(-97, back.rssi); TEST_ASSERT_EQUAL_FLOAT(-6.25f, back.snr); TEST_ASSERT_EQUAL_FLOAT(-106, back.noiseFloor); TEST_ASSERT_EQUAL_HEX8(0x2B, back.syncWord); at += kPcapRecordSize + r.length; TEST_ASSERT_TRUE(parsePcapRecord(file.data() + at, file.size() - at, r)); TEST_ASSERT_EQUAL_UINT32(lora::kLoraTapSize, r.length); at += kPcapRecordSize + r.length; TEST_ASSERT_EQUAL_size_t(file.size(), at); TEST_ASSERT_FALSE(parsePcapRecord(file.data() + at, file.size() - at, r)); // the end } void test_not_a_pcap() { const uint8_t junk[24] = {'n', 'o', 'p', 'e'}; TEST_ASSERT_FALSE(parsePcapHeader(junk, sizeof junk).ok); TEST_ASSERT_FALSE(parsePcapHeader(junk, 10).ok); const uint8_t huge[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 0xFF, 0x7F}; PcapRecord r; TEST_ASSERT_FALSE(parsePcapRecord(huge, sizeof huge, r)); lora::RxInfo rx; TEST_ASSERT_FALSE(parseLoraTap(junk, 10, rx)); } int main() { UNITY_BEGIN(); RUN_TEST(test_hex_row); RUN_TEST(test_gpx_summary); RUN_TEST(test_gpx_from_elsewhere_and_not_gpx); RUN_TEST(test_durations); RUN_TEST(test_pcap_round_trip); RUN_TEST(test_not_a_pcap); return UNITY_END(); }