M3 step 4: the LoRa Scanner App, Sniffer and Captures

The Sniffer lists packets newest first (time, RSSI, SNR, and for
Meshtastic the sender, receiver and hops), with the clear header and a
hex dump on Enter (Q96); `p` picks an EU868 preset, kept in Settings
(Q95). `c` starts a Capture: pcap with LoRaTap in /captures/lora, its own
Clean-up category, recorded by a small Service so it carries on with the
App closed (Q97, Q100). The Status Bar shows L while listening, bright on
each packet, and CAP while capturing (Q101). StorageService gains raw
appends for binary files. Debug Builds get `lora inject` to test all of
this with no transmitter in range: a Capture made on the device reads
back in TShark field for field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-05 20:14:58 +02:00
co-authored by Claude Opus 5.5
parent 594748e99a
commit 2fce79aebd
23 changed files with 723 additions and 13 deletions
+5
View File
@@ -83,11 +83,16 @@ void test_rssi_clamped() {
TEST_ASSERT_EQUAL_UINT8(159, out[16 + 12]);
}
void test_capture_path() {
TEST_ASSERT_EQUAL_STRING("/captures/lora/20261005-200000.pcap", capturePath(1791230400).c_str());
}
int main() {
UNITY_BEGIN();
RUN_TEST(test_pcap_global_header);
RUN_TEST(test_record_with_loratap_v0);
RUN_TEST(test_negative_snr);
RUN_TEST(test_rssi_clamped);
RUN_TEST(test_capture_path);
return UNITY_END();
}
@@ -0,0 +1,75 @@
#include <unity.h>
#include <string>
#include <vector>
#include "packet_view.h"
using namespace roro::lora;
void setUp() {}
void tearDown() {}
static const uint8_t kMeshtastic[] = {
0xFF, 0xFF, 0xFF, 0xFF, 0x78, 0x56, 0x34, 0x12, 0xEF, 0xBE, 0xAD, 0xDE,
0x62, // hop start 3, hop limit 2: relayed once
0x08, 0x00, 0x42, 0x01, 0x02, 0x03,
};
void test_row_for_a_meshtastic_packet() {
PacketSummary p{kMeshtastic, sizeof kMeshtastic, -97.4f, 6.25f, true, true};
// Time, RSSI, SNR, then who sent it to whom (by default short name: the last 4 hex digits,
// as Meshtastic names a Node) and how far it came. Fits 40 columns with two node numbers.
TEST_ASSERT_EQUAL_STRING("21:45:07 -97 6.2 5678>all 1/3", row(p, "21:45:07").c_str());
const uint8_t direct[] = {0x0a, 0x0b, 0x0c, 0x0d, 0xc4, 0xd3, 0xb2, 0xa1, 1, 2, 3, 4, 0x6a, 8, 0, 0xaa};
PacketSummary d{direct, sizeof direct, -117.0f, -9.25f, true, true};
TEST_ASSERT_EQUAL_STRING("21:45:07 -117 -9.2 d3c4>0b0a 1/3", row(d, "21:45:07").c_str());
TEST_ASSERT_TRUE(row(d, "21:45:07").size() <= 38);
}
void test_row_for_something_else() {
const uint8_t data[] = {0x40, 0x01, 0x02};
PacketSummary p{data, sizeof data, -120.6f, -7.5f, true, false};
TEST_ASSERT_EQUAL_STRING("21:45:07 -121 -7.5 3 B", row(p, "21:45:07").c_str());
}
// Not on Meshtastic settings (LoRaWAN, say): the same bytes aren't read as a Meshtastic header.
void test_row_not_meshtastic() {
PacketSummary p{kMeshtastic, sizeof kMeshtastic, -97.0f, 6.0f, true, false};
TEST_ASSERT_EQUAL_STRING("21:45:07 -97 6.0 19 B", row(p, "21:45:07").c_str());
}
void test_row_with_a_bad_crc() {
PacketSummary p{kMeshtastic, sizeof kMeshtastic, -97.0f, 6.0f, false, true};
TEST_ASSERT_EQUAL_STRING("21:45:07 -97 6.0 bad CRC, 19 B", row(p, "21:45:07").c_str());
}
void test_hex_dump() {
auto lines = hexDump(kMeshtastic, sizeof kMeshtastic);
TEST_ASSERT_EQUAL_size_t(3, lines.size());
TEST_ASSERT_EQUAL_STRING("0000 ff ff ff ff 78 56 34 12 ....xV4.", lines[0].c_str());
TEST_ASSERT_EQUAL_STRING("0008 ef be ad de 62 08 00 42 ....b..B", lines[1].c_str());
TEST_ASSERT_EQUAL_STRING("0010 01 02 03 ...", lines[2].c_str());
}
void test_header_lines() {
auto lines = headerLines(kMeshtastic, sizeof kMeshtastic);
TEST_ASSERT_EQUAL_size_t(5, lines.size());
TEST_ASSERT_EQUAL_STRING("From !12345678 to all", lines[0].c_str());
TEST_ASSERT_EQUAL_STRING("Packet deadbeef", lines[1].c_str());
TEST_ASSERT_EQUAL_STRING("Hops 1 of 3, limit 2 left", lines[2].c_str());
TEST_ASSERT_EQUAL_STRING("Channel 0x08 (LongFast, default key)", lines[3].c_str());
TEST_ASSERT_EQUAL_STRING("Relayed by ..42", lines[4].c_str());
TEST_ASSERT_EQUAL_size_t(0, headerLines(kMeshtastic, 12).size()); // too short to be Meshtastic
}
int main() {
UNITY_BEGIN();
RUN_TEST(test_row_for_a_meshtastic_packet);
RUN_TEST(test_row_for_something_else);
RUN_TEST(test_row_not_meshtastic);
RUN_TEST(test_row_with_a_bad_crc);
RUN_TEST(test_hex_dump);
RUN_TEST(test_header_lines);
return UNITY_END();
}
+2
View File
@@ -28,6 +28,8 @@ void test_defaults_when_store_is_empty() {
TEST_ASSERT_TRUE(s.getBool(Setting::WifiEnabled));
TEST_ASSERT_TRUE(s.getBool(Setting::GnssEnabled)); // Q58: on by default
TEST_ASSERT_FALSE(s.getBool(Setting::CoordinatesDms)); // Q64: decimal degrees
TEST_ASSERT_EQUAL_INT32(0, s.getInt(Setting::LoraPreset)); // Q95: LongFast
TEST_ASSERT_FALSE(s.setInt(Setting::LoraPreset, 7)); // seven EU868 presets
TEST_ASSERT_EQUAL_STRING("EU868", s.getString(Setting::Region).c_str());
TEST_ASSERT_EQUAL_STRING("CET-1CEST,M3.5.0,M10.5.0/3", s.getString(Setting::Timezone).c_str());
}