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:
2026-10-09 20:30:29 +02:00
co-authored by Claude Opus 5.5
parent 861a60b73d
commit 8ba41f72b6
23 changed files with 622 additions and 62 deletions
+89 -2
View File
@@ -3,6 +3,7 @@
#include <cmath>
#include <cstdio>
#include "meshcore_packet.h"
#include "meshtastic_header.h"
#include "meshtastic_presets.h"
@@ -10,13 +11,97 @@ namespace roro::lora {
using namespace meshtastic;
namespace {
std::string hex(const uint8_t* p, size_t n) {
std::string out;
char s[4];
for (size_t i = 0; i < n; ++i) {
std::snprintf(s, sizeof s, "%02x", p[i]);
out += s;
}
return out;
}
// `room` characters at most: a node's name is cut to fit.
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)) {
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)
out += " " + hex(m.payload + 1, 1) + ">" + hex(m.payload, 1);
else if (m.group() && m.payloadLen >= 1)
out += " #" + hex(m.payload, 1);
else if (m.type == meshcore::Payload::AnonRequest && m.payloadLen >= 1)
out += " >" + hex(m.payload, 1);
return out + hops;
}
std::vector<std::string> meshcoreLines(const uint8_t* data, size_t len) {
meshcore::Packet m;
if (!meshcore::parsePacket(data, len, m)) return {};
char s[64];
std::vector<std::string> lines;
lines.push_back("MeshCore " + std::string(meshcore::payloadName(m.type)) + ", " + meshcore::routeName(m.route));
if (m.hasTransport) {
std::snprintf(s, sizeof s, "Region code %04x", m.transport[0]);
lines.push_back(s);
}
// A flood gathers who repeated it; a direct packet carries who is still to repeat it.
if (m.hops) {
std::string path = std::string(m.flood() ? "Through" : "Route");
for (uint8_t i = 0; i < m.hops; ++i) {
std::string hop = " " + hex(m.path + i * m.hashSize, m.hashSize);
if (path.size() + hop.size() > 36) { // onto another line
lines.push_back(path);
path = " ";
}
path += hop;
}
lines.push_back(path);
} else {
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)) {
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) {
std::snprintf(s, sizeof s, "At %.5f, %.5f", a.latE6 / 1e6, a.lonE6 / 1e6);
lines.push_back(s);
}
} else if (m.addressed() && m.payloadLen >= 4) {
std::snprintf(s, sizeof s, "From ..%02x to ..%02x, %u B encrypted", m.payload[1], m.payload[0], static_cast<unsigned>(m.payloadLen - 4));
lines.push_back(s);
} else if (m.group() && m.payloadLen >= 3) {
std::snprintf(s, sizeof s, "Channel 0x%02x%s, %u B encrypted", m.payload[0], m.payload[0] == meshcore::kPublicChannelHash ? " (public?)" : "",
static_cast<unsigned>(m.payloadLen - 3));
lines.push_back(s);
} else if (m.type == meshcore::Payload::AnonRequest && m.payloadLen >= 35) {
std::snprintf(s, sizeof s, "To ..%02x from key %s...", m.payload[0], hex(m.payload + 1, 4).c_str());
lines.push_back(s);
} else if (m.type == meshcore::Payload::Ack && m.payloadLen >= 4) {
lines.push_back("Acknowledges " + hex(m.payload, 4));
}
return lines;
}
} // namespace
Protocol protocolOf(uint8_t syncWord) {
return syncWord == meshtastic::kSyncWord ? Protocol::Meshtastic : syncWord == meshcore::kSyncWord ? Protocol::MeshCore : Protocol::None;
}
std::string row(const PacketSummary& p, const std::string& time) {
char s[64];
int n = std::snprintf(s, sizeof s, "%s %ld %.1f ", time.c_str(), std::lround(p.rssi), p.snr);
std::string out(s, n);
PacketHeader h;
if (!p.crcOk) return out + "bad CRC, " + std::to_string(p.len) + " B";
if (!p.meshtastic || !parseHeader(p.data, p.len, h)) return out + std::to_string(p.len) + " B";
meshcore::Packet m;
if (p.protocol == Protocol::MeshCore && meshcore::parsePacket(p.data, p.len, m)) return out + meshcoreRow(m, out.size() < 38 ? 38 - out.size() : 0);
if (p.protocol != Protocol::Meshtastic || !parseHeader(p.data, p.len, h)) return out + std::to_string(p.len) + " B";
auto shortId = [](uint32_t node) {
if (node == kBroadcast) return std::string("all");
char id[8];
@@ -48,7 +133,9 @@ std::vector<std::string> hexDump(const uint8_t* data, size_t len) {
return lines;
}
std::vector<std::string> headerLines(const uint8_t* data, size_t len) {
std::vector<std::string> headerLines(const uint8_t* data, size_t len, Protocol protocol) {
if (protocol == Protocol::MeshCore) return meshcoreLines(data, len);
if (protocol != Protocol::Meshtastic) return {};
PacketHeader h;
if (!parseHeader(data, len, h)) return {};
char s[64];
+11 -5
View File
@@ -7,23 +7,29 @@
namespace roro::lora {
// Whose packets the radio's settings were for, by their sync word: whose clear header to read.
enum class Protocol : uint8_t { None, Meshtastic, MeshCore };
Protocol protocolOf(uint8_t syncWord);
// What the LoRa Scanner shows of one packet (M3, Q96).
struct PacketSummary {
const uint8_t* data;
size_t len;
float rssi, snr;
bool crcOk;
bool meshtastic; // received on Meshtastic settings (sync word 0x2B): read its clear header
Protocol protocol; // what the settings it was received on were for
};
// One list row, at most about 36 characters: "21:45:07 -97 6.2 5678>all 1/3". Nodes by their default
// short name (the last 4 hex digits); headerLines() has the full numbers.
// 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").
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.".
std::vector<std::string> hexDump(const uint8_t* data, size_t len);
// The Meshtastic header, one field a line; empty when the packet is too short to have one.
std::vector<std::string> headerLines(const uint8_t* data, size_t len);
// What is in clear, one field a line: the Meshtastic header, or MeshCore's header, path and (for an
// advert) who the node says it is. Empty when the packet can't be read as that.
std::vector<std::string> headerLines(const uint8_t* data, size_t len, Protocol protocol = Protocol::Meshtastic);
} // namespace roro::lora
+34
View File
@@ -0,0 +1,34 @@
#include "scan_presets.h"
#include <cctype>
#include "meshcore_packet.h"
#include "meshtastic_presets.h"
namespace roro::lora {
size_t scanPresetCount() { return meshtastic::kEu868PresetCount + 1; }
ScanPreset scanPreset(size_t i) {
if (i < meshtastic::kEu868PresetCount) {
const meshtastic::Preset& p = meshtastic::kEu868Presets[i];
return {p.name, meshtastic::eu868FrequencyHz(p), p.bwKHz, p.sf, p.cr, meshtastic::kSyncWord, meshtastic::kPreambleLength, Protocol::Meshtastic};
}
return {meshcore::kPresetName, meshcore::kEuNarrowHz, meshcore::kEuNarrowBwKHz, meshcore::kEuNarrowSf, meshcore::kEuNarrowCr,
meshcore::kSyncWord, meshcore::kPreambleLength, Protocol::MeshCore};
}
bool findScanPreset(const char* name, ScanPreset& out) {
for (size_t i = 0; i < scanPresetCount(); ++i) {
ScanPreset p = scanPreset(i);
const char *a = p.name, *b = name;
while (*a && *b && std::tolower(static_cast<unsigned char>(*a)) == std::tolower(static_cast<unsigned char>(*b))) ++a, ++b;
if (!*a && !*b) {
out = p;
return true;
}
}
return false;
}
} // namespace roro::lora
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include "packet_view.h"
namespace roro::lora {
// What the LoRa Scanner can be told to listen to by name: the Meshtastic presets allowed in
// EU_868, LongFast (the default) first, then MeshCore's EU/UK (Narrow). The order is kept: the
// chosen one is stored by its number.
struct ScanPreset {
const char* name;
uint32_t frequencyHz;
float bwKHz;
uint8_t sf, cr, syncWord;
uint16_t preamble;
Protocol protocol;
};
size_t scanPresetCount();
ScanPreset scanPreset(size_t i); // i < scanPresetCount()
// By name, whatever its case. False when there is none.
bool findScanPreset(const char* name, ScanPreset& out);
} // namespace roro::lora