#include "packet_view.h" #include #include #include #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& 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 meshcoreLines(const uint8_t* data, size_t len) { meshcore::Packet m; if (!meshcore::parsePacket(data, len, m)) return {}; char s[64]; std::vector 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(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(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(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(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 hexDump(const uint8_t* data, size_t len) { std::vector lines; for (size_t at = 0; at < len; at += 8) { char s[48]; int n = std::snprintf(s, sizeof s, "%04x", static_cast(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(b) : '.'; } else { n += std::snprintf(s + n, sizeof s - n, " "); } } lines.push_back(std::string(s, n) + " " + text); } return lines; } std::vector 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 lines; lines.push_back("From " + nodeId(h.from) + " to " + nodeId(h.to)); std::snprintf(s, sizeof s, "Packet %08x%s%s", static_cast(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