LoRa Scanner: MeshCore by default, and its public channel read
CI / build (pull_request) Successful in 2m19s
Site / build (pull_request) Successful in 9s

The MeshCore preset is the default for a new device, in the Settings and
in the radio before the Scanner is opened. The setting now accepts it:
it only took the 7 Meshtastic presets, so MeshCore picked in the App was
not saved.

Messages on MeshCore's public channel are decrypted with the channel's
published key (AES-128, checked with 2 bytes of HMAC-SHA256): the row
shows who says they sent it and the start of the text, the details the
sender, the time and the whole text. Other channels and private messages
stay encrypted.

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 22:19:14 +02:00
co-authored by Claude Opus 5.5
parent 3339d9ee59
commit 305818466f
18 changed files with 344 additions and 21 deletions
+28 -1
View File
@@ -2,7 +2,9 @@
#include <cmath>
#include <cstdio>
#include <ctime>
#include "meshcore_channel.h"
#include "meshcore_packet.h"
#include "meshtastic_header.h"
#include "meshtastic_presets.h"
@@ -22,14 +24,31 @@ std::string hex(const uint8_t* p, size_t n) {
return out;
}
// `room` characters at most: a node's name is cut to fit.
// Onto lines of `cols` characters at most, broken at spaces where there are some.
void wrapInto(std::vector<std::string>& lines, std::string text, size_t cols) {
while (text.size() > cols) {
size_t cut = text.rfind(' ', cols);
if (cut == std::string::npos || cut == 0) cut = cols;
lines.push_back(text.substr(0, cut));
text.erase(0, text[cut] == ' ' ? cut + 1 : cut);
}
if (!text.empty()) lines.push_back(text);
}
// `room` characters at most: a node's name or a message 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;
meshcore::Discover d;
meshcore::ChannelText t;
if (meshcore::parseDiscover(m, d)) {
out = d.response ? "here " + hex(d.id, 2) : "who's there?";
} else if (meshcore::readChannelText(m, t)) {
out = (t.sender.empty() ? "" : t.sender + ": ") + t.text;
out = out.substr(0, room > hops.size() ? room - hops.size() : 0);
while (!out.empty() && out.back() == ' ') out.pop_back();
return out + hops;
} 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));
@@ -90,6 +109,14 @@ std::vector<std::string> meshcoreLines(const uint8_t* data, size_t len) {
} 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 (meshcore::ChannelText t; meshcore::readChannelText(m, t)) {
lines.push_back(std::string("On the ") + t.channel + " channel" + (t.sender.empty() ? "" : ", from " + t.sender));
std::time_t sent = t.timestamp;
std::tm tm{};
gmtime_r(&sent, &tm);
std::strftime(s, sizeof s, "Sent %Y-%m-%d %H:%M:%S UTC", &tm);
lines.push_back(s);
wrapInto(lines, t.text, 38);
} 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));
+3 -2
View File
@@ -22,14 +22,15 @@ 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", "who's there?", "here 18a6") 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");
// a message on the public channel is read: "Alice: hello fro 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);
// 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.
// advert) who the node says it is, and a message on its public channel. 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
+2 -1
View File
@@ -8,7 +8,7 @@
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
// EU_868, LongFast first, then MeshCore's EU/UK (Narrow), the default. The order is kept: the
// chosen one is stored by its number.
struct ScanPreset {
const char* name;
@@ -19,6 +19,7 @@ struct ScanPreset {
Protocol protocol;
};
constexpr size_t kDefaultScanPreset = 7; // MeshCore: the Settings' default says the same
size_t scanPresetCount();
ScanPreset scanPreset(size_t i); // i < scanPresetCount()
// By name, whatever its case. False when there is none.