Public Access
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
213 lines
9.1 KiB
C++
213 lines
9.1 KiB
C++
#include "packet_view.h"
|
|
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <ctime>
|
|
|
|
#include "meshcore_channel.h"
|
|
#include "meshcore_packet.h"
|
|
#include "meshtastic_header.h"
|
|
#include "meshtastic_presets.h"
|
|
|
|
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;
|
|
}
|
|
|
|
// 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));
|
|
} 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;
|
|
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) {
|
|
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 (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));
|
|
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";
|
|
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];
|
|
std::snprintf(id, sizeof id, "%04x", static_cast<unsigned>(node & 0xFFFF));
|
|
return std::string(id);
|
|
};
|
|
out += shortId(h.from) + ">" + shortId(h.to);
|
|
if (h.hopsAway() >= 0) out += " " + std::to_string(h.hopsAway()) + "/" + std::to_string(h.hopStart());
|
|
return out;
|
|
}
|
|
|
|
std::vector<std::string> hexDump(const uint8_t* data, size_t len) {
|
|
std::vector<std::string> lines;
|
|
for (size_t at = 0; at < len; at += 8) {
|
|
char s[48];
|
|
int n = std::snprintf(s, sizeof s, "%04x", static_cast<unsigned>(at));
|
|
std::string text;
|
|
for (size_t i = 0; i < 8; ++i) {
|
|
if (at + i < len) {
|
|
n += std::snprintf(s + n, sizeof s - n, " %02x", data[at + i]);
|
|
uint8_t b = data[at + i];
|
|
text += b >= 0x20 && b < 0x7F ? static_cast<char>(b) : '.';
|
|
} else {
|
|
n += std::snprintf(s + n, sizeof s - n, " ");
|
|
}
|
|
}
|
|
lines.push_back(std::string(s, n) + " " + text);
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
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];
|
|
std::vector<std::string> lines;
|
|
lines.push_back("From " + nodeId(h.from) + " to " + nodeId(h.to));
|
|
std::snprintf(s, sizeof s, "Packet %08x%s%s", static_cast<unsigned>(h.id), h.wantAck() ? ", wants an ack" : "",
|
|
h.viaMqtt() ? ", via MQTT" : "");
|
|
lines.push_back(s);
|
|
if (h.hopsAway() >= 0)
|
|
std::snprintf(s, sizeof s, "Hops %d of %u, limit %u left", h.hopsAway(), h.hopStart(), h.hopLimit());
|
|
else
|
|
std::snprintf(s, sizeof s, "Hop limit %u left (old firmware)", h.hopLimit());
|
|
lines.push_back(s);
|
|
// Which preset's default Channel (named after it, with the public key) has this hash.
|
|
std::string channel;
|
|
for (size_t i = 0; i < kEu868PresetCount && channel.empty(); ++i)
|
|
if (channelHash(kEu868Presets[i].name, kDefaultKey, sizeof kDefaultKey) == h.channelHash)
|
|
channel = std::string(" (") + kEu868Presets[i].name + ", default key)";
|
|
std::snprintf(s, sizeof s, "Channel 0x%02x%s", h.channelHash, channel.c_str());
|
|
lines.push_back(s);
|
|
if (h.relayNode) {
|
|
std::snprintf(s, sizeof s, "Relayed by ..%02x", h.relayNode);
|
|
lines.push_back(s);
|
|
}
|
|
if (h.nextHop) {
|
|
std::snprintf(s, sizeof s, "Next hop ..%02x", h.nextHop);
|
|
lines.push_back(s);
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
} // namespace roro::lora
|