Public Access
LoRa Scanner: a MeshCore preset, and its packets read
- MeshCore joins the presets (EU/UK Narrow: 869.618 MHz, 62.5 kHz, SF8, CR 4/8, sync 0x12), after the Meshtastic ones so stored choices keep their number; `lora preset MeshCore` on the console - lib/meshcore reads what MeshCore sends in clear: header, path, and a node's advert (name, kind, key, position) - the list row, the details, a Capture's viewer and the console echo read a packet by the sync word it was received on - the bandwidth is printed as 62.5, not 62 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "meshcore_packet.h"
|
||||
#include "packet_view.h"
|
||||
#include "scan_presets.h"
|
||||
|
||||
using namespace roro;
|
||||
using namespace roro::meshcore;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
// A flood advert relayed by two repeaters (1-byte hashes), from a repeater that says where it is.
|
||||
static std::vector<uint8_t> advert(const char* name, bool location = true, uint8_t pathByte = 2) {
|
||||
std::vector<uint8_t> v = {static_cast<uint8_t>(0x04 << 2 | 0x01), pathByte};
|
||||
for (int i = 0; i < (pathByte & 0x3F) * ((pathByte >> 6) + 1); ++i) v.push_back(static_cast<uint8_t>(0xa3 + i));
|
||||
for (int i = 0; i < 32; ++i) v.push_back(static_cast<uint8_t>(0xc0 + i)); // key
|
||||
v.insert(v.end(), {0x00, 0x5e, 0xd0, 0x68}); // timestamp
|
||||
v.insert(v.end(), 64, 0x55); // signature
|
||||
v.push_back(static_cast<uint8_t>(0x80 | (location ? 0x10 : 0) | 0x02)); // named, repeater
|
||||
if (location) {
|
||||
int32_t lat = 50860320, lon = 4300480;
|
||||
for (int i = 0; i < 4; ++i) v.push_back(static_cast<uint8_t>(lat >> (8 * i)));
|
||||
for (int i = 0; i < 4; ++i) v.push_back(static_cast<uint8_t>(lon >> (8 * i)));
|
||||
}
|
||||
v.insert(v.end(), name, name + std::strlen(name));
|
||||
return v;
|
||||
}
|
||||
|
||||
void test_header_route_type_and_path() {
|
||||
auto v = advert("Brussels-R1");
|
||||
Packet p;
|
||||
TEST_ASSERT_TRUE(parsePacket(v.data(), v.size(), p));
|
||||
TEST_ASSERT_TRUE(p.route == Route::Flood);
|
||||
TEST_ASSERT_TRUE(p.type == Payload::Advert);
|
||||
TEST_ASSERT_FALSE(p.hasTransport);
|
||||
TEST_ASSERT_EQUAL(2, p.hops);
|
||||
TEST_ASSERT_EQUAL(1, p.hashSize);
|
||||
TEST_ASSERT_EQUAL_HEX8(0xa3, p.path[0]);
|
||||
TEST_ASSERT_EQUAL_HEX8(0xc0, p.payload[0]);
|
||||
}
|
||||
|
||||
void test_transport_codes_and_wider_hashes() {
|
||||
// Direct in a region, a text message, two hops of 2 bytes each.
|
||||
const uint8_t v[] = {0x02 << 2 | 0x03, 0x2b, 0x1a, 0x00, 0x00, 0x40 | 2, 0x11, 0x22, 0x33, 0x44, 0x7f, 0xa3, 0xde, 0xad, 1, 2, 3};
|
||||
Packet p;
|
||||
TEST_ASSERT_TRUE(parsePacket(v, sizeof v, p));
|
||||
TEST_ASSERT_TRUE(p.route == Route::TransportDirect);
|
||||
TEST_ASSERT_TRUE(p.hasTransport);
|
||||
TEST_ASSERT_EQUAL_HEX16(0x1a2b, p.transport[0]);
|
||||
TEST_ASSERT_EQUAL(2, p.hops);
|
||||
TEST_ASSERT_EQUAL(2, p.hashSize);
|
||||
TEST_ASSERT_EQUAL_size_t(7, p.payloadLen);
|
||||
TEST_ASSERT_EQUAL_HEX8(0x7f, p.payload[0]);
|
||||
TEST_ASSERT_TRUE(p.addressed());
|
||||
}
|
||||
|
||||
void test_what_is_not_a_meshcore_packet() {
|
||||
Packet p;
|
||||
const uint8_t version[] = {0x40 | 0x05, 0x00, 1, 2}; // a version that doesn't exist yet
|
||||
const uint8_t reserved[] = {0x0c << 2 | 0x01, 0x00, 1, 2}; // a reserved type
|
||||
const uint8_t longPath[] = {0x05, 0x09, 1, 2, 3}; // nine hops, three bytes left
|
||||
const uint8_t badHash[] = {0x05, 0xc1, 1, 2, 3, 4}; // 4-byte hashes aren't defined
|
||||
TEST_ASSERT_FALSE(parsePacket(version, sizeof version, p));
|
||||
TEST_ASSERT_FALSE(parsePacket(reserved, sizeof reserved, p));
|
||||
TEST_ASSERT_FALSE(parsePacket(longPath, sizeof longPath, p));
|
||||
TEST_ASSERT_FALSE(parsePacket(badHash, sizeof badHash, p));
|
||||
TEST_ASSERT_FALSE(parsePacket(version, 1, p));
|
||||
}
|
||||
|
||||
void test_advert() {
|
||||
auto v = advert("Brussels-R1");
|
||||
Packet p;
|
||||
Advert a;
|
||||
TEST_ASSERT_TRUE(parsePacket(v.data(), v.size(), p));
|
||||
TEST_ASSERT_TRUE(parseAdvert(p, a));
|
||||
TEST_ASSERT_EQUAL_STRING("Brussels-R1", a.name.c_str());
|
||||
TEST_ASSERT_EQUAL(2, a.kind);
|
||||
TEST_ASSERT_TRUE(a.hasLocation);
|
||||
TEST_ASSERT_EQUAL_INT32(50860320, a.latE6);
|
||||
TEST_ASSERT_EQUAL_INT32(4300480, a.lonE6);
|
||||
TEST_ASSERT_EQUAL_HEX32(0x68d05e00, a.timestamp);
|
||||
TEST_ASSERT_EQUAL_HEX8(0xc0, a.key[0]);
|
||||
|
||||
auto plain = advert("n", false, 0);
|
||||
TEST_ASSERT_TRUE(parsePacket(plain.data(), plain.size(), p));
|
||||
TEST_ASSERT_TRUE(parseAdvert(p, a));
|
||||
TEST_ASSERT_FALSE(a.hasLocation);
|
||||
TEST_ASSERT_EQUAL_STRING("n", a.name.c_str());
|
||||
|
||||
v.resize(60); // cut short: still a packet, not an advert that can be read
|
||||
TEST_ASSERT_TRUE(parsePacket(v.data(), v.size(), p));
|
||||
TEST_ASSERT_FALSE(parseAdvert(p, a));
|
||||
}
|
||||
|
||||
void test_rows() {
|
||||
auto v = advert("Brussels-R1");
|
||||
lora::PacketSummary s{v.data(), v.size(), -97.4f, 6.25f, true, lora::Protocol::MeshCore};
|
||||
TEST_ASSERT_EQUAL_STRING("21:45:07 -97 6.2 adv Brussels-R1 2h", lora::row(s, "21:45:07").c_str());
|
||||
auto longName = advert("A repeater with a very long name");
|
||||
lora::PacketSummary l{longName.data(), longName.size(), -117.0f, -12.25f, true, lora::Protocol::MeshCore};
|
||||
TEST_ASSERT_TRUE(lora::row(l, "21:45:07").size() <= 38);
|
||||
|
||||
const uint8_t text[] = {0x02 << 2 | 0x02, 0x00, 0x7f, 0xa3, 0xde, 0xad, 1, 2, 3};
|
||||
lora::PacketSummary t{text, sizeof text, -80.0f, 9.0f, true, lora::Protocol::MeshCore};
|
||||
TEST_ASSERT_EQUAL_STRING("21:45:07 -80 9.0 txt a3>7f", lora::row(t, "21:45:07").c_str());
|
||||
|
||||
const uint8_t group[] = {0x05 << 2 | 0x01, 0x01, 0x42, 0x11, 0xbe, 0xef, 1, 2, 3, 4};
|
||||
lora::PacketSummary g{group, sizeof group, -80.0f, 9.0f, true, lora::Protocol::MeshCore};
|
||||
TEST_ASSERT_EQUAL_STRING("21:45:07 -80 9.0 chan #11 1h", lora::row(g, "21:45:07").c_str());
|
||||
|
||||
// Not MeshCore after all (sync word 0x12 is anyone's): the bytes are counted, no more.
|
||||
const uint8_t other[] = {0xff, 0x00, 1, 2};
|
||||
lora::PacketSummary o{other, sizeof other, -80.0f, 9.0f, true, lora::Protocol::MeshCore};
|
||||
TEST_ASSERT_EQUAL_STRING("21:45:07 -80 9.0 4 B", lora::row(o, "21:45:07").c_str());
|
||||
}
|
||||
|
||||
void test_detail_lines() {
|
||||
auto v = advert("Brussels-R1");
|
||||
auto lines = lora::headerLines(v.data(), v.size(), lora::Protocol::MeshCore);
|
||||
TEST_ASSERT_EQUAL_size_t(5, lines.size());
|
||||
TEST_ASSERT_EQUAL_STRING("MeshCore advert, flood", lines[0].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("Through a3 a4", lines[1].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("Repeater Brussels-R1", lines[2].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("Key c0c1c2c3c4c5c6c7...", lines[3].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("At 50.86032, 4.30048", lines[4].c_str());
|
||||
|
||||
const uint8_t group[] = {0x05 << 2 | 0x01, 0x00, 0x11, 0xbe, 0xef, 1, 2, 3, 4};
|
||||
lines = lora::headerLines(group, sizeof group, lora::Protocol::MeshCore);
|
||||
TEST_ASSERT_EQUAL_size_t(3, lines.size());
|
||||
TEST_ASSERT_EQUAL_STRING("MeshCore channel message, flood", lines[0].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("Heard first hand: no repeater yet", lines[1].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("Channel 0x11 (public?), 4 B encrypted", lines[2].c_str());
|
||||
|
||||
const uint8_t text[] = {0x02 << 2 | 0x02, 0x01, 0x42, 0x7f, 0xa3, 0xde, 0xad, 1, 2, 3};
|
||||
lines = lora::headerLines(text, sizeof text, lora::Protocol::MeshCore);
|
||||
TEST_ASSERT_EQUAL_STRING("MeshCore text message, direct", lines[0].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("Route 42", lines[1].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("From ..a3 to ..7f, 3 B encrypted", lines[2].c_str());
|
||||
|
||||
for (auto& l : lines) TEST_ASSERT_TRUE(l.size() <= 38);
|
||||
TEST_ASSERT_EQUAL_size_t(0, lora::headerLines(text, sizeof text, lora::Protocol::None).size());
|
||||
}
|
||||
|
||||
void test_scan_presets() {
|
||||
TEST_ASSERT_EQUAL_size_t(8, lora::scanPresetCount());
|
||||
lora::ScanPreset p = lora::scanPreset(0); // the default stays first: it is stored by number
|
||||
TEST_ASSERT_EQUAL_STRING("LongFast", p.name);
|
||||
TEST_ASSERT_EQUAL_UINT32(869525000u, p.frequencyHz);
|
||||
TEST_ASSERT_EQUAL_HEX8(0x2B, p.syncWord);
|
||||
TEST_ASSERT_TRUE(lora::findScanPreset("meshcore", p));
|
||||
TEST_ASSERT_EQUAL_STRING("MeshCore", p.name);
|
||||
TEST_ASSERT_EQUAL_UINT32(869618000u, p.frequencyHz);
|
||||
TEST_ASSERT_EQUAL_FLOAT(62.5f, p.bwKHz);
|
||||
TEST_ASSERT_EQUAL(8, p.sf);
|
||||
TEST_ASSERT_EQUAL(8, p.cr);
|
||||
TEST_ASSERT_EQUAL_HEX8(0x12, p.syncWord);
|
||||
TEST_ASSERT_EQUAL(16, p.preamble);
|
||||
TEST_ASSERT_TRUE(p.protocol == lora::Protocol::MeshCore);
|
||||
TEST_ASSERT_TRUE(lora::protocolOf(0x12) == lora::Protocol::MeshCore);
|
||||
TEST_ASSERT_TRUE(lora::protocolOf(0x2B) == lora::Protocol::Meshtastic);
|
||||
TEST_ASSERT_TRUE(lora::protocolOf(0x34) == lora::Protocol::None);
|
||||
TEST_ASSERT_FALSE(lora::findScanPreset("ShortTurbo", p));
|
||||
TEST_ASSERT_FALSE(lora::findScanPreset("MeshCor", p));
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_header_route_type_and_path);
|
||||
RUN_TEST(test_transport_codes_and_wider_hashes);
|
||||
RUN_TEST(test_what_is_not_a_meshcore_packet);
|
||||
RUN_TEST(test_advert);
|
||||
RUN_TEST(test_rows);
|
||||
RUN_TEST(test_detail_lines);
|
||||
RUN_TEST(test_scan_presets);
|
||||
return UNITY_END();
|
||||
}
|
||||
@@ -17,30 +17,30 @@ static const uint8_t kMeshtastic[] = {
|
||||
};
|
||||
|
||||
void test_row_for_a_meshtastic_packet() {
|
||||
PacketSummary p{kMeshtastic, sizeof kMeshtastic, -97.4f, 6.25f, true, true};
|
||||
PacketSummary p{kMeshtastic, sizeof kMeshtastic, -97.4f, 6.25f, true, Protocol::Meshtastic};
|
||||
// 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};
|
||||
PacketSummary d{direct, sizeof direct, -117.0f, -9.25f, true, Protocol::Meshtastic};
|
||||
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};
|
||||
PacketSummary p{data, sizeof data, -120.6f, -7.5f, true, Protocol::None};
|
||||
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};
|
||||
PacketSummary p{kMeshtastic, sizeof kMeshtastic, -97.0f, 6.0f, true, Protocol::None};
|
||||
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};
|
||||
PacketSummary p{kMeshtastic, sizeof kMeshtastic, -97.0f, 6.0f, false, Protocol::Meshtastic};
|
||||
TEST_ASSERT_EQUAL_STRING("21:45:07 -97 6.0 bad CRC, 19 B", row(p, "21:45:07").c_str());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user