Public Access
LoRa Scanner: MeshCore searches for nodes and their answers, read
The first two MeshCore packets heard were these: a search for repeaters and a repeater answering. Both are in clear; they are the test's bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
This commit is contained in:
@@ -27,7 +27,10 @@ std::string meshcoreRow(const meshcore::Packet& m, size_t room) {
|
||||
std::string out = meshcore::payloadShort(m.type);
|
||||
std::string hops = m.hops ? " " + std::to_string(m.hops) + "h" : "";
|
||||
meshcore::Advert a;
|
||||
if (meshcore::parseAdvert(m, a)) {
|
||||
meshcore::Discover d;
|
||||
if (meshcore::parseDiscover(m, d)) {
|
||||
out = d.response ? "here " + hex(d.id, 2) : "who's there?";
|
||||
} else if (meshcore::parseAdvert(m, a)) {
|
||||
size_t used = out.size() + 1 + hops.size();
|
||||
out += " " + (a.name.empty() ? hex(a.key, 2) : a.name.substr(0, room > used ? room - used : 0));
|
||||
} else if (m.addressed() && m.payloadLen >= 2)
|
||||
@@ -65,7 +68,19 @@ std::vector<std::string> meshcoreLines(const uint8_t* data, size_t len) {
|
||||
lines.push_back(m.flood() ? "Heard first hand: no repeater yet" : "No route in it: to a neighbour");
|
||||
}
|
||||
meshcore::Advert a;
|
||||
if (meshcore::parseAdvert(m, a)) {
|
||||
meshcore::Discover d;
|
||||
if (meshcore::parseDiscover(m, d) && d.response) {
|
||||
lines.push_back(std::string(meshcore::kindName(d.kind)) + " answering a search");
|
||||
lines.push_back("Key " + hex(d.id, 8) + "...");
|
||||
std::snprintf(s, sizeof s, "It heard the search at %.1f dB SNR", d.snr);
|
||||
lines.push_back(s);
|
||||
} else if (meshcore::parseDiscover(m, d)) {
|
||||
std::string who;
|
||||
for (int kind = 1; kind <= 4; ++kind)
|
||||
if (d.kinds & (1 << kind)) who += std::string(who.empty() ? "" : ", ") + meshcore::kindName(static_cast<uint8_t>(kind));
|
||||
lines.push_back("A search for nodes nearby");
|
||||
lines.push_back("Wanted: " + (who.empty() ? std::string("any") : who));
|
||||
} else if (meshcore::parseAdvert(m, a)) {
|
||||
lines.push_back(std::string(meshcore::kindName(a.kind)) + (a.name.empty() ? "" : " " + a.name));
|
||||
lines.push_back("Key " + hex(a.key, 8) + "...");
|
||||
if (a.hasLocation) {
|
||||
|
||||
@@ -22,7 +22,7 @@ struct PacketSummary {
|
||||
|
||||
// One list row, at most about 36 characters: "21:45:07 -97 6.2 5678>all 1/3". Meshtastic nodes by their
|
||||
// default short name (the last 4 hex digits); headerLines() has the full numbers. MeshCore: what
|
||||
// it is, then who ("adv Brussels-R1", "txt a3>7f", "chan #11") and the hops in its path ("2h").
|
||||
// it is, then who ("adv Brussels-R1", "txt a3>7f", "chan #11", "who's there?", "here 18a6") and the hops in its path ("2h").
|
||||
std::string row(const PacketSummary& p, const std::string& time);
|
||||
|
||||
// Eight bytes a line, with the printable ones: "0000 ff ff ff ff 78 56 34 12 ....xV4.".
|
||||
|
||||
@@ -67,6 +67,27 @@ bool parseAdvert(const Packet& p, Advert& out) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseDiscover(const Packet& p, Discover& out) {
|
||||
if (p.type != Payload::Control || p.payloadLen < 6) return false;
|
||||
uint8_t sub = p.payload[0] >> 4;
|
||||
Discover d;
|
||||
d.tag = static_cast<uint32_t>(le32(p.payload + 2));
|
||||
if (sub == 0x8) {
|
||||
d.kinds = p.payload[1];
|
||||
} else if (sub == 0x9) {
|
||||
d.response = true;
|
||||
d.kind = p.payload[0] & 0x0F;
|
||||
d.snr = static_cast<int8_t>(p.payload[1]) / 4.0f;
|
||||
d.id = p.payload + 6;
|
||||
d.idLen = p.payloadLen - 6;
|
||||
if (d.idLen != 8 && d.idLen != 32) return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
out = d;
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* routeName(Route r) {
|
||||
switch (r) {
|
||||
case Route::TransportFlood: return "flood in a region";
|
||||
|
||||
@@ -58,6 +58,18 @@ struct Advert {
|
||||
};
|
||||
bool parseAdvert(const Packet& p, Advert& out);
|
||||
|
||||
// Control packets that look for nodes nearby, and the answers: sent in clear, to neighbours only.
|
||||
struct Discover {
|
||||
bool response = false;
|
||||
uint8_t kinds = 0; // a request: one bit per kind of node wanted (bit 2: repeaters)
|
||||
uint8_t kind = 0; // a response: what answers, as in an advert
|
||||
float snr = 0; // a response: how well it heard the request, dB
|
||||
uint32_t tag = 0; // random in the request, repeated in its responses
|
||||
const uint8_t* id = nullptr; // a response: the node's key, or its first 8 bytes
|
||||
size_t idLen = 0;
|
||||
};
|
||||
bool parseDiscover(const Packet& p, Discover& out);
|
||||
|
||||
const char* routeName(Route r); // "flood", "direct", ...
|
||||
const char* payloadName(Payload t); // "advert", "text", ...
|
||||
const char* payloadShort(Payload t); // for a list row: "adv", "txt", ...
|
||||
|
||||
@@ -20,7 +20,9 @@ For a **MeshCore** packet it shows what the packet is, who it concerns and how m
|
||||
| `adv Brussels-R1 2h` | A node saying who it is, by its name, heard through 2 repeaters |
|
||||
| `txt a3>7f` | A private message from the node whose key starts with `a3` to the one starting with `7f` (`req`, `rsp` and `path` look the same) |
|
||||
| `chan #11 1h` | A message on a channel; `#11` is the channel's byte, and `11` is the public channel's |
|
||||
| `ack`, `anon`, `trace`, `ctl` | An acknowledgement, a login, a trace, a discovery |
|
||||
| `who's there?` | Someone nearby searching for nodes (repeaters, usually) |
|
||||
| `here 18a6` | A node answering that search; `18a6` starts its key |
|
||||
| `ack`, `anon`, `trace`, `ctl` | An acknowledgement, a login, a trace, other control packets |
|
||||
|
||||
| Key | Does |
|
||||
|---|---|
|
||||
@@ -42,6 +44,7 @@ The **MeshCore** preset is the setting its networks use in Europe, "EU/UK (Narro
|
||||
- what it is and how it travels: **flood** (every repeater passes it on, and adds itself to the path) or **direct** (it carries the route to follow);
|
||||
- the path, one repeater a hop, each named by the first byte of its key;
|
||||
- for an **advert**, which is signed and never encrypted: the node's name, its kind (chat node, repeater, room server, sensor), the start of its key, and its position if it gives one;
|
||||
- for a **search** and its **answers**, also in clear: what kind of node is wanted; and in an answer the node's kind, the start of its key, and how well it heard the search;
|
||||
- for a message: the first byte of the sender's and the receiver's keys, or the channel's byte, and how many bytes are encrypted.
|
||||
|
||||
A byte is all a packet says about who it is from: two nodes can share it. The name comes only with a node's advert, which a node sends now and then, so a quiet network can take a while to show its names.
|
||||
|
||||
@@ -146,6 +146,47 @@ void test_detail_lines() {
|
||||
TEST_ASSERT_EQUAL_size_t(0, lora::headerLines(text, sizeof text, lora::Protocol::None).size());
|
||||
}
|
||||
|
||||
// As received near Brussels on 2026-10-09: a search for repeaters, and one answering.
|
||||
void test_discover_as_heard() {
|
||||
const uint8_t ask[] = {0x2e, 0x00, 0x80, 0x04, 0xbb, 0xdd, 0x5f, 0x5a, 0x00, 0x00, 0x00, 0x00};
|
||||
const uint8_t answer[] = {0x2e, 0x00, 0x92, 0x2d, 0xbb, 0xdd, 0x5f, 0x5a, 0x18, 0xa6, 0x3f, 0xae, 0xf4, 0x01, 0x9b, 0xee,
|
||||
0xd0, 0x4f, 0xfd, 0xd3, 0xd1, 0x72, 0x65, 0xba, 0x4b, 0x9e, 0xeb, 0xd0, 0xdf, 0xb0, 0xb2, 0xc4,
|
||||
0x94, 0xec, 0x50, 0x5b, 0x03, 0x20, 0x59, 0x9d};
|
||||
Packet p;
|
||||
Discover d;
|
||||
TEST_ASSERT_TRUE(parsePacket(ask, sizeof ask, p));
|
||||
TEST_ASSERT_TRUE(p.route == Route::Direct);
|
||||
TEST_ASSERT_TRUE(p.type == Payload::Control);
|
||||
TEST_ASSERT_TRUE(parseDiscover(p, d));
|
||||
TEST_ASSERT_FALSE(d.response);
|
||||
TEST_ASSERT_EQUAL_HEX8(0x04, d.kinds);
|
||||
TEST_ASSERT_EQUAL_HEX32(0x5a5fddbb, d.tag);
|
||||
TEST_ASSERT_TRUE(parsePacket(answer, sizeof answer, p));
|
||||
TEST_ASSERT_TRUE(parseDiscover(p, d));
|
||||
TEST_ASSERT_TRUE(d.response);
|
||||
TEST_ASSERT_EQUAL(2, d.kind);
|
||||
TEST_ASSERT_EQUAL_FLOAT(11.25f, d.snr);
|
||||
TEST_ASSERT_EQUAL_HEX32(0x5a5fddbb, d.tag);
|
||||
TEST_ASSERT_EQUAL_size_t(32, d.idLen);
|
||||
|
||||
lora::PacketSummary a{ask, sizeof ask, -107.0f, -7.0f, true, lora::Protocol::MeshCore};
|
||||
TEST_ASSERT_EQUAL_STRING("20:44:01 -107 -7.0 who's there?", lora::row(a, "20:44:01").c_str());
|
||||
lora::PacketSummary r{answer, sizeof answer, -96.0f, 3.0f, true, lora::Protocol::MeshCore};
|
||||
TEST_ASSERT_EQUAL_STRING("20:44:02 -96 3.0 here 18a6", lora::row(r, "20:44:02").c_str());
|
||||
|
||||
auto lines = lora::headerLines(ask, sizeof ask, lora::Protocol::MeshCore);
|
||||
TEST_ASSERT_EQUAL_size_t(4, lines.size());
|
||||
TEST_ASSERT_EQUAL_STRING("MeshCore control, direct", lines[0].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("No route in it: to a neighbour", lines[1].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("A search for nodes nearby", lines[2].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("Wanted: Repeater", lines[3].c_str());
|
||||
lines = lora::headerLines(answer, sizeof answer, lora::Protocol::MeshCore);
|
||||
TEST_ASSERT_EQUAL_size_t(5, lines.size());
|
||||
TEST_ASSERT_EQUAL_STRING("Repeater answering a search", lines[2].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("Key 18a63faef4019bee...", lines[3].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("It heard the search at 11.2 dB SNR", lines[4].c_str());
|
||||
}
|
||||
|
||||
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
|
||||
@@ -176,6 +217,7 @@ int main() {
|
||||
RUN_TEST(test_advert);
|
||||
RUN_TEST(test_rows);
|
||||
RUN_TEST(test_detail_lines);
|
||||
RUN_TEST(test_discover_as_heard);
|
||||
RUN_TEST(test_scan_presets);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user