/////////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2026 // // SPDX-License-Identifier: GPL-3.0-or-later // /////////////////////////////////////////////////////////////////////////////////// #include "meshtasticpacket.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace Meshtastic { namespace { constexpr uint8_t kFlagHopLimitMask = 0x07; constexpr uint8_t kFlagWantAckMask = 0x08; constexpr uint8_t kFlagViaMqttMask = 0x10; constexpr uint8_t kFlagHopStartMask = 0xE0; constexpr int kHeaderLength = 16; constexpr uint32_t kBroadcastNode = 0xFFFFFFFFu; struct Header { uint32_t to = kBroadcastNode; uint32_t from = 0; uint32_t id = 0; uint8_t flags = 0; uint8_t channel = 0; uint8_t nextHop = 0; uint8_t relayNode = 0; }; struct DataFields { bool hasPortnum = false; uint32_t portnum = 0; QByteArray payload; bool wantResponse = false; bool hasDest = false; uint32_t dest = 0; bool hasSource = false; uint32_t source = 0; bool hasRequestId = false; uint32_t requestId = 0; bool hasReplyId = false; uint32_t replyId = 0; bool hasEmoji = false; uint32_t emoji = 0; bool hasBitfield = false; uint32_t bitfield = 0; }; struct KeyEntry { QString label; QString channelName; QByteArray key; bool hasExpectedHash = false; uint8_t expectedHash = 0; }; struct CommandConfig { Header header; DataFields data; bool encrypt = true; QByteArray key; QString keyLabel; QString channelName; QString presetName; bool hasRegion = false; QString regionName; bool hasChannelNum = false; uint32_t channelNum = 0; // 1-based bool hasOverrideFrequencyMHz = false; double overrideFrequencyMHz = 0.0; double frequencyOffsetMHz = 0.0; bool hasFrequencyOffsetMHz = false; }; struct RegionBand { const char* name; double freqStartMHz; double freqEndMHz; double spacingMHz; bool wideLora; }; static const uint8_t kDefaultChannelKey[16] = { 0xD4, 0xF1, 0xBB, 0x3A, 0x20, 0x29, 0x07, 0x59, 0xF0, 0xBC, 0xFF, 0xAB, 0xCF, 0x4E, 0x69, 0x01 }; // Port numbers from meshtastic/portnums.proto (high value subset is accepted as numeric fallback). static QMap makePortMap() { QMap m; m.insert("UNKNOWN_APP", 0); m.insert("TEXT", 1); m.insert("TEXT_MESSAGE_APP", 1); m.insert("REMOTE_HARDWARE_APP", 2); m.insert("POSITION_APP", 3); m.insert("NODEINFO_APP", 4); m.insert("ROUTING_APP", 5); m.insert("ADMIN_APP", 6); m.insert("TEXT_MESSAGE_COMPRESSED_APP", 7); m.insert("WAYPOINT_APP", 8); m.insert("AUDIO_APP", 9); m.insert("DETECTION_SENSOR_APP", 10); m.insert("ALERT_APP", 11); m.insert("KEY_VERIFICATION_APP", 12); m.insert("REPLY_APP", 32); m.insert("IP_TUNNEL_APP", 33); m.insert("PAXCOUNTER_APP", 34); m.insert("STORE_FORWARD_PLUSPLUS_APP", 35); m.insert("NODE_STATUS_APP", 36); m.insert("SERIAL_APP", 64); m.insert("STORE_FORWARD_APP", 65); m.insert("RANGE_TEST_APP", 66); m.insert("TELEMETRY_APP", 67); m.insert("ZPS_APP", 68); m.insert("SIMULATOR_APP", 69); m.insert("TRACEROUTE_APP", 70); m.insert("NEIGHBORINFO_APP", 71); m.insert("ATAK_PLUGIN", 72); return m; } static const QMap kPortMap = makePortMap(); static const RegionBand kRegionBands[] = { {"US", 902.0, 928.0, 0.0, false}, {"EU_433", 433.0, 434.0, 0.0, false}, {"EU_868", 869.4, 869.65, 0.0, false}, {"CN", 470.0, 510.0, 0.0, false}, {"JP", 920.5, 923.5, 0.0, false}, {"ANZ", 915.0, 928.0, 0.0, false}, {"ANZ_433", 433.05, 434.79, 0.0, false}, {"RU", 868.7, 869.2, 0.0, false}, {"KR", 920.0, 923.0, 0.0, false}, {"TW", 920.0, 925.0, 0.0, false}, {"IN", 865.0, 867.0, 0.0, false}, {"NZ_865", 864.0, 868.0, 0.0, false}, {"TH", 920.0, 925.0, 0.0, false}, {"UA_433", 433.0, 434.7, 0.0, false}, {"UA_868", 868.0, 868.6, 0.0, false}, {"MY_433", 433.0, 435.0, 0.0, false}, {"MY_919", 919.0, 924.0, 0.0, false}, {"SG_923", 917.0, 925.0, 0.0, false}, {"PH_433", 433.0, 434.7, 0.0, false}, {"PH_868", 868.0, 869.4, 0.0, false}, {"PH_915", 915.0, 918.0, 0.0, false}, {"KZ_433", 433.075, 434.775, 0.0, false}, {"KZ_863", 863.0, 868.0, 0.0, false}, {"NP_865", 865.0, 868.0, 0.0, false}, {"BR_902", 902.0, 907.5, 0.0, false}, {"LORA_24", 2400.0, 2483.5, 0.0, true} }; static const int kRegionBandsCount = static_cast(sizeof(kRegionBands) / sizeof(kRegionBands[0])); static QString trimQuotes(const QString& s) { QString out = s.trimmed(); if ((out.startsWith('"') && out.endsWith('"')) || (out.startsWith('\'') && out.endsWith('\''))) { out = out.mid(1, out.size() - 2); } return out; } static bool parseBool(const QString& s, bool& out) { const QString v = s.trimmed().toLower(); if (v == "1" || v == "true" || v == "yes" || v == "on") { out = true; return true; } if (v == "0" || v == "false" || v == "no" || v == "off") { out = false; return true; } return false; } static bool parseUInt(const QString& s, uint64_t& out) { QString v = s.trimmed(); v.remove('_'); bool ok = false; int base = 10; if (v.startsWith("0x") || v.startsWith("0X")) { base = 16; v = v.mid(2); } out = v.toULongLong(&ok, base); return ok; } static bool parseDouble(const QString& s, double& out) { QString v = s.trimmed(); v.remove('_'); bool ok = false; out = v.toDouble(&ok); return ok; } static QString normalizeToken(const QString& value) { QString v = value.trimmed().toUpper(); v.replace('-', '_'); v.replace(' ', '_'); return v; } static QByteArray normalizeHex(const QString& s) { QByteArray out; const QByteArray in = s.toLatin1(); for (char c : in) { const bool isHex = ((c >= '0') && (c <= '9')) || ((c >= 'a') && (c <= 'f')) || ((c >= 'A') && (c <= 'F')); if (isHex) { out.append(c); } } return out; } static bool parseHexBytes(const QString& s, QByteArray& out) { QByteArray hex = normalizeHex(s); if (hex.isEmpty() || (hex.size() % 2) != 0) { return false; } out = QByteArray::fromHex(hex); return !out.isEmpty(); } static QByteArray expandSimpleKey(unsigned int simple) { if (simple == 0) { return QByteArray(); } if (simple > 10) { return QByteArray(); } QByteArray key(reinterpret_cast(kDefaultChannelKey), sizeof(kDefaultChannelKey)); if (simple > 1) { const int offset = static_cast(simple - 1); key[15] = static_cast(static_cast(kDefaultChannelKey[15] + offset)); } return key; } static bool parseKeySpec(const QString& rawSpec, QByteArray& key, QString& label) { QString spec = rawSpec.trimmed(); const QString lower = spec.toLower(); if (lower.isEmpty() || lower == "default" || lower == "simple1") { key = expandSimpleKey(1); label = "default"; return true; } if (lower == "none" || lower == "unencrypted" || lower == "simple0" || lower == "0") { key = QByteArray(); label = "none"; return true; } if (lower.startsWith("simple")) { uint64_t n = 0; if (!parseUInt(lower.mid(6), n)) { return false; } if (n > 10) { return false; } key = expandSimpleKey(static_cast(n)); label = QString("simple%1").arg(n); return true; } if (lower.startsWith("hex:")) { if (!parseHexBytes(spec.mid(4), key)) { return false; } label = "hex"; return key.size() == 16 || key.size() == 32; } if (lower.startsWith("base64:") || lower.startsWith("b64:")) { const int p = spec.indexOf(':'); key = QByteArray::fromBase64(spec.mid(p + 1).trimmed().toLatin1()); if (key.isEmpty()) { return false; } label = "base64"; return key.size() == 16 || key.size() == 32; } if (lower == "1" || lower == "2" || lower == "3" || lower == "4" || lower == "5" || lower == "6" || lower == "7" || lower == "8" || lower == "9" || lower == "10") { uint64_t n = 0; parseUInt(lower, n); key = expandSimpleKey(static_cast(n)); label = QString("simple%1").arg(n); return true; } QByteArray parsed; if (parseHexBytes(spec, parsed) && (parsed.size() == 16 || parsed.size() == 32)) { key = parsed; label = "hex"; return true; } parsed = QByteArray::fromBase64(spec.toLatin1()); if (parsed.size() == 16 || parsed.size() == 32) { key = parsed; label = "base64"; return true; } return false; } static uint32_t readU32LE(const char* p) { return static_cast(static_cast(p[0])) | (static_cast(static_cast(p[1])) << 8) | (static_cast(static_cast(p[2])) << 16) | (static_cast(static_cast(p[3])) << 24); } static void writeU32LE(char* p, uint32_t v) { p[0] = static_cast(v & 0xFF); p[1] = static_cast((v >> 8) & 0xFF); p[2] = static_cast((v >> 16) & 0xFF); p[3] = static_cast((v >> 24) & 0xFF); } static bool parseHeader(const QByteArray& frame, Header& h) { if (frame.size() < kHeaderLength) { return false; } const char* p = frame.constData(); h.to = readU32LE(p + 0); h.from = readU32LE(p + 4); h.id = readU32LE(p + 8); h.flags = static_cast(p[12]); h.channel = static_cast(p[13]); h.nextHop = static_cast(p[14]); h.relayNode = static_cast(p[15]); return true; } static QByteArray encodeHeader(const Header& h) { QByteArray out(kHeaderLength, 0); char* p = out.data(); writeU32LE(p + 0, h.to); writeU32LE(p + 4, h.from); writeU32LE(p + 8, h.id); p[12] = static_cast(h.flags); p[13] = static_cast(h.channel); p[14] = static_cast(h.nextHop); p[15] = static_cast(h.relayNode); return out; } static bool readVarint(const QByteArray& bytes, int& pos, uint64_t& value) { value = 0; int shift = 0; while (pos < bytes.size() && shift <= 63) { const uint8_t b = static_cast(bytes[pos++]); value |= static_cast(b & 0x7F) << shift; if ((b & 0x80) == 0) { return true; } shift += 7; } return false; } static bool readFixed32(const QByteArray& bytes, int& pos, uint32_t& value) { if ((pos + 4) > bytes.size()) { return false; } value = readU32LE(bytes.constData() + pos); pos += 4; return true; } static bool skipField(const QByteArray& bytes, int& pos, uint32_t wireType) { switch (wireType) { case 0: { uint64_t v = 0; return readVarint(bytes, pos, v); } case 1: if ((pos + 8) > bytes.size()) { return false; } pos += 8; return true; case 2: { uint64_t len = 0; if (!readVarint(bytes, pos, len)) { return false; } if (len > static_cast(bytes.size() - pos)) { return false; } pos += static_cast(len); return true; } case 5: if ((pos + 4) > bytes.size()) { return false; } pos += 4; return true; default: return false; } } static bool parseData(const QByteArray& bytes, DataFields& d) { int pos = 0; while (pos < bytes.size()) { uint64_t rawTag = 0; if (!readVarint(bytes, pos, rawTag)) { return false; } const uint32_t field = static_cast(rawTag >> 3); const uint32_t wire = static_cast(rawTag & 0x7); switch (field) { case 1: { if (wire != 0) { return false; } uint64_t v = 0; if (!readVarint(bytes, pos, v)) { return false; } d.hasPortnum = true; d.portnum = static_cast(v); break; } case 2: { if (wire != 2) { return false; } uint64_t len = 0; if (!readVarint(bytes, pos, len)) { return false; } if (len > static_cast(bytes.size() - pos)) { return false; } d.payload = bytes.mid(pos, static_cast(len)); pos += static_cast(len); break; } case 3: { if (wire != 0) { return false; } uint64_t v = 0; if (!readVarint(bytes, pos, v)) { return false; } d.wantResponse = (v != 0); break; } case 4: { if (wire != 5) { return false; } uint32_t v = 0; if (!readFixed32(bytes, pos, v)) { return false; } d.hasDest = true; d.dest = v; break; } case 5: { if (wire != 5) { return false; } uint32_t v = 0; if (!readFixed32(bytes, pos, v)) { return false; } d.hasSource = true; d.source = v; break; } case 6: { if (wire != 5) { return false; } uint32_t v = 0; if (!readFixed32(bytes, pos, v)) { return false; } d.hasRequestId = true; d.requestId = v; break; } case 7: { if (wire != 5) { return false; } uint32_t v = 0; if (!readFixed32(bytes, pos, v)) { return false; } d.hasReplyId = true; d.replyId = v; break; } case 8: { if (wire != 5) { return false; } uint32_t v = 0; if (!readFixed32(bytes, pos, v)) { return false; } d.hasEmoji = true; d.emoji = v; break; } case 9: { if (wire != 0) { return false; } uint64_t v = 0; if (!readVarint(bytes, pos, v)) { return false; } d.hasBitfield = true; d.bitfield = static_cast(v); break; } default: if (!skipField(bytes, pos, wire)) { return false; } break; } } return d.hasPortnum; } static void writeVarint(QByteArray& out, uint64_t value) { while (true) { uint8_t b = static_cast(value & 0x7F); value >>= 7; if (value != 0) { b |= 0x80; out.append(static_cast(b)); } else { out.append(static_cast(b)); break; } } } static void writeTag(QByteArray& out, uint32_t field, uint32_t wire) { const uint64_t tag = (static_cast(field) << 3) | static_cast(wire); writeVarint(out, tag); } static void writeFixed32(QByteArray& out, uint32_t v) { char b[4]; writeU32LE(b, v); out.append(b, 4); } static QByteArray encodeData(const DataFields& d) { QByteArray out; writeTag(out, 1, 0); writeVarint(out, d.portnum); if (!d.payload.isEmpty()) { writeTag(out, 2, 2); writeVarint(out, static_cast(d.payload.size())); out.append(d.payload); } if (d.wantResponse) { writeTag(out, 3, 0); writeVarint(out, 1); } if (d.hasDest) { writeTag(out, 4, 5); writeFixed32(out, d.dest); } if (d.hasSource) { writeTag(out, 5, 5); writeFixed32(out, d.source); } if (d.hasRequestId) { writeTag(out, 6, 5); writeFixed32(out, d.requestId); } if (d.hasReplyId) { writeTag(out, 7, 5); writeFixed32(out, d.replyId); } if (d.hasEmoji) { writeTag(out, 8, 5); writeFixed32(out, d.emoji); } if (d.hasBitfield) { writeTag(out, 9, 0); writeVarint(out, d.bitfield); } return out; } static uint8_t xorHash(const QByteArray& bytes) { uint8_t h = 0; for (char c : bytes) { h ^= static_cast(c); } return h; } static uint8_t generateChannelHash(const QString& channelName, const QByteArray& key) { QByteArray name = channelName.toUtf8(); if (name.isEmpty()) { name = "X"; } return xorHash(name) ^ xorHash(key); } // Tiny AES implementation adapted from tiny-AES-c (public domain / unlicense). class AesCtx { public: bool init(const QByteArray& key) { if (key.size() != 16 && key.size() != 32) { return false; } m_nk = key.size() / 4; m_nr = m_nk + 6; const int words = 4 * (m_nr + 1); m_roundKey.resize(words * 4); keyExpansion(reinterpret_cast(key.constData())); return true; } void encryptBlock(const uint8_t in[16], uint8_t out[16]) const { uint8_t state[16]; memcpy(state, in, 16); addRoundKey(state, 0); for (int round = 1; round < m_nr; ++round) { subBytes(state); shiftRows(state); mixColumns(state); addRoundKey(state, round); } subBytes(state); shiftRows(state); addRoundKey(state, m_nr); memcpy(out, state, 16); } private: int m_nk = 0; int m_nr = 0; std::vector m_roundKey; static uint8_t xtime(uint8_t x) { return static_cast((x << 1) ^ (((x >> 7) & 1) * 0x1B)); } static uint8_t mul(uint8_t a, uint8_t b) { uint8_t res = 0; uint8_t x = a; uint8_t y = b; while (y) { if (y & 1) { res ^= x; } x = xtime(x); y >>= 1; } return res; } static uint8_t sub(uint8_t x) { static const uint8_t sbox[256] = { 0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76, 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0, 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15, 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75, 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84, 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf, 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8, 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2, 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73, 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb, 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79, 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08, 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a, 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e, 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf, 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16 }; return sbox[x]; } static void subBytes(uint8_t state[16]) { for (int i = 0; i < 16; ++i) { state[i] = sub(state[i]); } } static void shiftRows(uint8_t state[16]) { uint8_t t; t = state[1]; state[1] = state[5]; state[5] = state[9]; state[9] = state[13]; state[13] = t; t = state[2]; state[2] = state[10]; state[10] = t; t = state[6]; state[6] = state[14]; state[14] = t; t = state[3]; state[3] = state[15]; state[15] = state[11]; state[11] = state[7]; state[7] = t; } static void mixColumns(uint8_t state[16]) { for (int c = 0; c < 4; ++c) { uint8_t* col = &state[c * 4]; const uint8_t a0 = col[0]; const uint8_t a1 = col[1]; const uint8_t a2 = col[2]; const uint8_t a3 = col[3]; col[0] = static_cast(mul(a0, 2) ^ mul(a1, 3) ^ a2 ^ a3); col[1] = static_cast(a0 ^ mul(a1, 2) ^ mul(a2, 3) ^ a3); col[2] = static_cast(a0 ^ a1 ^ mul(a2, 2) ^ mul(a3, 3)); col[3] = static_cast(mul(a0, 3) ^ a1 ^ a2 ^ mul(a3, 2)); } } void addRoundKey(uint8_t state[16], int round) const { const uint8_t* rk = &m_roundKey[round * 16]; for (int i = 0; i < 16; ++i) { state[i] ^= rk[i]; } } static uint32_t subWord(uint32_t w) { return (static_cast(sub(static_cast((w >> 24) & 0xFF))) << 24) | (static_cast(sub(static_cast((w >> 16) & 0xFF))) << 16) | (static_cast(sub(static_cast((w >> 8) & 0xFF))) << 8) | static_cast(sub(static_cast(w & 0xFF))); } static uint32_t rotWord(uint32_t w) { return (w << 8) | (w >> 24); } void keyExpansion(const uint8_t* key) { static const uint8_t rcon[11] = {0x00,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1B,0x36}; const int words = 4 * (m_nr + 1); std::vector w(words, 0); for (int i = 0; i < m_nk; ++i) { w[i] = (static_cast(key[4 * i]) << 24) | (static_cast(key[4 * i + 1]) << 16) | (static_cast(key[4 * i + 2]) << 8) | static_cast(key[4 * i + 3]); } for (int i = m_nk; i < words; ++i) { uint32_t temp = w[i - 1]; if ((i % m_nk) == 0) { temp = subWord(rotWord(temp)) ^ (static_cast(rcon[i / m_nk]) << 24); } else if (m_nk > 6 && (i % m_nk) == 4) { temp = subWord(temp); } w[i] = w[i - m_nk] ^ temp; } for (int i = 0; i < words; ++i) { m_roundKey[4 * i] = static_cast((w[i] >> 24) & 0xFF); m_roundKey[4 * i + 1] = static_cast((w[i] >> 16) & 0xFF); m_roundKey[4 * i + 2] = static_cast((w[i] >> 8) & 0xFF); m_roundKey[4 * i + 3] = static_cast(w[i] & 0xFF); } } }; enum class CounterMode { BigEndian, LittleEndian }; static void incrementCounter4(uint8_t counter[16], CounterMode mode) { if (mode == CounterMode::BigEndian) { for (int i = 15; i >= 12; --i) { counter[i] = static_cast(counter[i] + 1); if (counter[i] != 0) { break; } } } else { for (int i = 12; i <= 15; ++i) { counter[i] = static_cast(counter[i] + 1); if (counter[i] != 0) { break; } } } } static void initNonce(uint8_t nonce[16], uint32_t fromNode, uint32_t packetId) { memset(nonce, 0, 16); const uint64_t packetId64 = packetId; memcpy(nonce, &packetId64, sizeof(packetId64)); memcpy(nonce + sizeof(packetId64), &fromNode, sizeof(fromNode)); } static QByteArray aesCtrCrypt(const QByteArray& in, const QByteArray& key, uint32_t fromNode, uint32_t packetId, CounterMode mode) { QByteArray out(in); if (key.isEmpty()) { return out; } AesCtx aes; if (!aes.init(key)) { return QByteArray(); } uint8_t counter[16]; initNonce(counter, fromNode, packetId); int pos = 0; while (pos < out.size()) { uint8_t keystream[16]; aes.encryptBlock(counter, keystream); const int remain = std::min(16, static_cast(out.size() - pos)); for (int i = 0; i < remain; ++i) { out[pos + i] = static_cast(static_cast(out[pos + i]) ^ keystream[i]); } pos += remain; incrementCounter4(counter, mode); } return out; } static QString formatNode(uint32_t node) { return QString("0x%1").arg(node, 8, 16, QChar('0')); } static QString payloadToText(const QByteArray& payload) { QString s = QString::fromUtf8(payload); if (s.isEmpty() && !payload.isEmpty()) { return QString(); } return s; } static QString portToName(uint32_t p) { for (auto it = kPortMap.constBegin(); it != kPortMap.constEnd(); ++it) { if (it.value() == p && it.key().endsWith("_APP")) { return it.key(); } } return QString("PORT_%1").arg(p); } static bool addKeyEntry( std::vector& keys, const QString& channelName, const QString& keySpec, QString* error = nullptr) { QByteArray key; QString label; if (!parseKeySpec(keySpec, key, label)) { if (error) { *error = QString("invalid key spec '%1'").arg(keySpec); } return false; } KeyEntry e; e.key = key; e.channelName = channelName; e.label = channelName.isEmpty() ? label : QString("%1:%2").arg(channelName, label); if (!channelName.isEmpty()) { e.hasExpectedHash = true; e.expectedHash = generateChannelHash(channelName, key); } keys.push_back(e); return true; } static bool parseKeyListEntry( const QString& rawEntry, QString& channelName, QString& keySpec, QString* error = nullptr) { const QString entry = rawEntry.trimmed(); if (entry.isEmpty() || entry.startsWith('#')) { return false; } QByteArray parsedKey; QString parsedLabel; if (parseKeySpec(entry, parsedKey, parsedLabel)) { channelName.clear(); keySpec = entry; return true; } const int eq = entry.indexOf('='); if (eq <= 0) { if (error) { *error = QString("invalid key entry '%1' (use 'channel=key' or key only)").arg(entry); } return false; } channelName = trimQuotes(entry.left(eq)); keySpec = trimQuotes(entry.mid(eq + 1)); if (channelName.isEmpty()) { if (error) { *error = QString("missing channel name in '%1'").arg(entry); } return false; } if (keySpec.isEmpty()) { if (error) { *error = QString("missing key spec in '%1'").arg(entry); } return false; } return true; } static bool parseKeySpecList( const QString& rawList, std::vector& keys, QString* error = nullptr, int* keyCount = nullptr, bool strict = true) { QString input = rawList; input.replace('\r', '\n'); input.replace(';', '\n'); input.replace(',', '\n'); const QStringList entries = input.split('\n', Qt::SkipEmptyParts); int parsedCount = 0; for (const QString& rawEntry : entries) { QString channelName; QString keySpec; QString entryError; const bool hasEntry = parseKeyListEntry(rawEntry, channelName, keySpec, &entryError); if (!hasEntry) { if (!entryError.isEmpty() && strict) { if (error) { *error = entryError; } return false; } continue; } if (!addKeyEntry(keys, channelName, keySpec, &entryError)) { if (strict) { if (error) { *error = QString("%1 in '%2'").arg(entryError, rawEntry.trimmed()); } return false; } continue; } parsedCount++; } if (keyCount) { *keyCount = parsedCount; } return true; } static std::vector defaultKeysFromEnv() { std::vector keys; QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); const QString envKeys = env.value("SDRANGEL_MESHTASTIC_KEYS").trimmed(); if (!envKeys.isEmpty()) { parseKeySpecList(envKeys, keys, nullptr, nullptr, false); } else { const QString channel = env.value("SDRANGEL_MESHTASTIC_CHANNEL_NAME", "LongFast").trimmed(); const QString keySpec = env.value("SDRANGEL_MESHTASTIC_KEY", "default").trimmed(); addKeyEntry(keys, channel, keySpec); addKeyEntry(keys, QString(), "none"); } if (keys.empty()) { addKeyEntry(keys, "LongFast", "default"); addKeyEntry(keys, QString(), "none"); } return keys; } static bool parsePortValue(const QString& raw, uint32_t& port) { uint64_t numeric = 0; if (parseUInt(raw, numeric)) { port = static_cast(numeric); return true; } const QString upper = raw.trimmed().toUpper(); if (kPortMap.contains(upper)) { port = kPortMap.value(upper); return true; } if (kPortMap.contains(upper + "_APP")) { port = kPortMap.value(upper + "_APP"); return true; } return false; } static bool parsePresetName(const QString& presetValue, QString& presetName) { QString p = normalizeToken(presetValue); p.remove('_'); if (p == "LONGFAST") { presetName = "LONG_FAST"; return true; } if (p == "LONGSLOW") { presetName = "LONG_SLOW"; return true; } if (p == "LONGTURBO") { presetName = "LONG_TURBO"; return true; } if (p == "LONGMODERATE") { presetName = "LONG_MODERATE"; return true; } if (p == "MEDIUMFAST") { presetName = "MEDIUM_FAST"; return true; } if (p == "MEDIUMSLOW") { presetName = "MEDIUM_SLOW"; return true; } if (p == "SHORTFAST") { presetName = "SHORT_FAST"; return true; } if (p == "SHORTSLOW") { presetName = "SHORT_SLOW"; return true; } if (p == "SHORTTURBO") { presetName = "SHORT_TURBO"; return true; } return false; } static bool presetToChannelName(const QString& presetName, QString& channelName) { if (presetName == "LONG_FAST") { channelName = "LongFast"; return true; } if (presetName == "LONG_SLOW") { channelName = "LongSlow"; return true; } if (presetName == "LONG_TURBO") { channelName = "LongTurbo"; return true; } if (presetName == "LONG_MODERATE") { channelName = "LongModerate"; return true; } if (presetName == "MEDIUM_FAST") { channelName = "MediumFast"; return true; } if (presetName == "MEDIUM_SLOW") { channelName = "MediumSlow"; return true; } if (presetName == "SHORT_FAST") { channelName = "ShortFast"; return true; } if (presetName == "SHORT_SLOW") { channelName = "ShortSlow"; return true; } if (presetName == "SHORT_TURBO") { channelName = "ShortTurbo"; return true; } return false; } static bool presetToParams(const QString& presetName, bool wideLora, int& bandwidthHz, int& spreadFactor, int& parityBits) { int bwKHz = 0; int sf = 0; int cr = 0; if (presetName == "SHORT_TURBO") { bwKHz = wideLora ? 1625 : 500; cr = 5; sf = 7; } else if (presetName == "SHORT_FAST") { bwKHz = wideLora ? 812 : 250; cr = 5; sf = 7; } else if (presetName == "SHORT_SLOW") { bwKHz = wideLora ? 812 : 250; cr = 5; sf = 8; } else if (presetName == "MEDIUM_FAST") { bwKHz = wideLora ? 812 : 250; cr = 5; sf = 9; } else if (presetName == "MEDIUM_SLOW") { bwKHz = wideLora ? 812 : 250; cr = 5; sf = 10; } else if (presetName == "LONG_TURBO") { bwKHz = wideLora ? 1625 : 500; cr = 8; sf = 11; } else if (presetName == "LONG_MODERATE") { bwKHz = wideLora ? 406 : 125; cr = 8; sf = 11; } else if (presetName == "LONG_SLOW") { bwKHz = wideLora ? 406 : 125; cr = 8; sf = 12; } else if (presetName == "LONG_FAST") { bwKHz = wideLora ? 812 : 250; cr = 5; sf = 11; } else { return false; } bandwidthHz = bwKHz * 1000; spreadFactor = sf; parityBits = std::max(1, std::min(4, cr - 4)); return true; } static QString presetToDisplayName(const QString& presetName) { QString channelName; if (presetToChannelName(presetName, channelName)) { return channelName; } return QString("LongFast"); } static uint32_t meshHashDjb2(const QString& s) { const QByteArray bytes = s.toUtf8(); uint32_t h = 5381u; for (char c : bytes) { h = ((h << 5) + h) + static_cast(c); } return h; } static const RegionBand* findRegionBand(const QString& regionValue) { QString r = normalizeToken(regionValue); r.remove('_'); if (r == "EU868") { r = "EU_868"; } else if (r == "EU433") { r = "EU_433"; } else if (r == "NZ865") { r = "NZ_865"; } else if (r == "UA868") { r = "UA_868"; } else if (r == "UA433") { r = "UA_433"; } else if (r == "MY433") { r = "MY_433"; } else if (r == "MY919") { r = "MY_919"; } else if (r == "SG923") { r = "SG_923"; } else if (r == "PH433") { r = "PH_433"; } else if (r == "PH868") { r = "PH_868"; } else if (r == "PH915") { r = "PH_915"; } else if (r == "KZ433") { r = "KZ_433"; } else if (r == "KZ863") { r = "KZ_863"; } else if (r == "NP865") { r = "NP_865"; } else if (r == "BR902") { r = "BR_902"; } else if (r == "ANZ433") { r = "ANZ_433"; } else if (r == "LORA24") { r = "LORA_24"; } else { r = normalizeToken(regionValue); } for (int i = 0; i < kRegionBandsCount; ++i) { if (r == kRegionBands[i].name) { return &kRegionBands[i]; } } return nullptr; } static bool parseCommand(const QString& command, CommandConfig& cfg, QString& error) { if (!Packet::isCommand(command)) { error = "command must start with MESH:"; return false; } cfg.header.to = kBroadcastNode; cfg.header.from = 0; cfg.header.id = QRandomGenerator::global()->generate(); cfg.header.channel = 0; cfg.header.nextHop = 0; cfg.header.relayNode = 0; cfg.data.hasPortnum = true; cfg.data.portnum = 1; // TEXT_MESSAGE_APP cfg.data.payload.clear(); cfg.encrypt = true; cfg.key = expandSimpleKey(1); cfg.keyLabel = "default"; cfg.channelName = "LongFast"; cfg.presetName = "LONG_FAST"; bool encryptAuto = true; uint8_t hopLimit = 3; uint8_t hopStart = 3; bool wantAck = false; bool viaMqtt = false; QString body = command.mid(5).trimmed(); QStringList parts = body.split(';', Qt::SkipEmptyParts); QStringList freeText; for (QString part : parts) { part = part.trimmed(); if (part.isEmpty()) { continue; } const int sep = part.indexOf('='); if (sep <= 0) { freeText.push_back(trimQuotes(part)); continue; } const QString key = part.left(sep).trimmed().toLower(); const QString value = trimQuotes(part.mid(sep + 1)); uint64_t u = 0; if (key == "to") { if (!parseUInt(value, u)) { error = "invalid to"; return false; } cfg.header.to = static_cast(u); } else if (key == "from") { if (!parseUInt(value, u)) { error = "invalid from"; return false; } cfg.header.from = static_cast(u); } else if (key == "id") { if (!parseUInt(value, u)) { error = "invalid id"; return false; } cfg.header.id = static_cast(u); } else if (key == "port" || key == "portnum") { uint32_t p = 0; if (!parsePortValue(value, p)) { error = "invalid port/portnum"; return false; } cfg.data.portnum = p; cfg.data.hasPortnum = true; } else if (key == "text") { cfg.data.payload = value.toUtf8(); } else if (key == "payload_hex") { QByteArray p; if (!parseHexBytes(value, p)) { error = "invalid payload_hex"; return false; } cfg.data.payload = p; } else if (key == "payload_b64" || key == "payload_base64") { QByteArray p = QByteArray::fromBase64(value.toLatin1()); if (p.isEmpty() && !value.isEmpty()) { error = "invalid payload_base64"; return false; } cfg.data.payload = p; } else if (key == "want_response") { bool b = false; if (!parseBool(value, b)) { error = "invalid want_response"; return false; } cfg.data.wantResponse = b; } else if (key == "dest") { if (!parseUInt(value, u)) { error = "invalid dest"; return false; } cfg.data.hasDest = true; cfg.data.dest = static_cast(u); } else if (key == "source") { if (!parseUInt(value, u)) { error = "invalid source"; return false; } cfg.data.hasSource = true; cfg.data.source = static_cast(u); } else if (key == "request_id") { if (!parseUInt(value, u)) { error = "invalid request_id"; return false; } cfg.data.hasRequestId = true; cfg.data.requestId = static_cast(u); } else if (key == "reply_id") { if (!parseUInt(value, u)) { error = "invalid reply_id"; return false; } cfg.data.hasReplyId = true; cfg.data.replyId = static_cast(u); } else if (key == "emoji") { if (!parseUInt(value, u)) { error = "invalid emoji"; return false; } cfg.data.hasEmoji = true; cfg.data.emoji = static_cast(u); } else if (key == "bitfield") { if (!parseUInt(value, u)) { error = "invalid bitfield"; return false; } cfg.data.hasBitfield = true; cfg.data.bitfield = static_cast(u); } else if (key == "hop_limit") { if (!parseUInt(value, u) || u > 7) { error = "invalid hop_limit"; return false; } hopLimit = static_cast(u); } else if (key == "hop_start") { if (!parseUInt(value, u) || u > 7) { error = "invalid hop_start"; return false; } hopStart = static_cast(u); } else if (key == "want_ack") { if (!parseBool(value, wantAck)) { error = "invalid want_ack"; return false; } } else if (key == "via_mqtt") { if (!parseBool(value, viaMqtt)) { error = "invalid via_mqtt"; return false; } } else if (key == "next_hop") { if (!parseUInt(value, u) || u > 255) { error = "invalid next_hop"; return false; } cfg.header.nextHop = static_cast(u); } else if (key == "relay_node") { if (!parseUInt(value, u) || u > 255) { error = "invalid relay_node"; return false; } cfg.header.relayNode = static_cast(u); } else if (key == "channel_hash" || key == "channel") { if (!parseUInt(value, u) || u > 255) { error = "invalid channel_hash"; return false; } cfg.header.channel = static_cast(u); cfg.channelName.clear(); } else if (key == "channel_name") { cfg.channelName = value; } else if (key == "preset" || key == "modem_preset") { QString presetName; if (!parsePresetName(value, presetName)) { error = "invalid preset/modem_preset"; return false; } cfg.presetName = presetName; presetToChannelName(cfg.presetName, cfg.channelName); } else if (key == "region" || key == "region_code") { const RegionBand* band = findRegionBand(value); if (!band) { error = "invalid region"; return false; } cfg.hasRegion = true; cfg.regionName = band->name; } else if (key == "channel_num" || key == "slot") { if (!parseUInt(value, u) || u < 1 || u > 10000) { error = "invalid channel_num"; return false; } cfg.hasChannelNum = true; cfg.channelNum = static_cast(u); } else if (key == "frequency" || key == "freq" || key == "override_frequency" || key == "frequency_mhz" || key == "freq_mhz") { double f = 0.0; if (!parseDouble(value, f) || f <= 0.0) { error = "invalid frequency"; return false; } if (f > 1000000.0) { cfg.overrideFrequencyMHz = f / 1000000.0; } else { cfg.overrideFrequencyMHz = f; } cfg.hasOverrideFrequencyMHz = true; } else if (key == "freq_hz" || key == "frequency_hz") { if (!parseUInt(value, u) || u < 1000000ull) { error = "invalid frequency_hz"; return false; } cfg.overrideFrequencyMHz = static_cast(u) / 1000000.0; cfg.hasOverrideFrequencyMHz = true; } else if (key == "frequency_offset" || key == "freq_offset") { double off = 0.0; if (!parseDouble(value, off)) { error = "invalid frequency_offset"; return false; } // Meshtastic uses MHz for frequency offset. We also accept Hz-like values. if (std::fabs(off) > 100000.0) { off /= 1000000.0; } cfg.frequencyOffsetMHz = off; cfg.hasFrequencyOffsetMHz = true; } else if (key == "frequency_offset_hz" || key == "freq_offset_hz") { double offHz = 0.0; if (!parseDouble(value, offHz)) { error = "invalid frequency_offset_hz"; return false; } cfg.frequencyOffsetMHz = offHz / 1000000.0; cfg.hasFrequencyOffsetMHz = true; } else if (key == "key" || key == "psk") { QByteArray parsedKey; QString keyLabel; if (!parseKeySpec(value, parsedKey, keyLabel)) { error = "invalid key/psk"; return false; } cfg.key = parsedKey; cfg.keyLabel = keyLabel; if (encryptAuto) { cfg.encrypt = !cfg.key.isEmpty(); } } else if (key == "encrypt") { const QString lower = value.toLower(); if (lower == "auto") { encryptAuto = true; cfg.encrypt = !cfg.key.isEmpty(); } else { bool b = false; if (!parseBool(value, b)) { error = "invalid encrypt"; return false; } encryptAuto = false; cfg.encrypt = b; } } else { error = QString("unknown key '%1'").arg(key); return false; } } if (cfg.data.payload.isEmpty() && !freeText.isEmpty()) { cfg.data.payload = freeText.join(';').toUtf8(); } if (cfg.header.channel == 0 && !cfg.channelName.isEmpty()) { cfg.header.channel = generateChannelHash(cfg.channelName, cfg.key); } if (encryptAuto) { cfg.encrypt = !cfg.key.isEmpty(); } if (cfg.encrypt && cfg.key.isEmpty()) { error = "encrypt=true but key resolves to none"; return false; } cfg.header.flags = static_cast(hopLimit & 0x07); if (wantAck) { cfg.header.flags |= kFlagWantAckMask; } if (viaMqtt) { cfg.header.flags |= kFlagViaMqttMask; } cfg.header.flags |= static_cast((hopStart & 0x07) << 5); return true; } static QString summarizeHeader(const Header& h) { const int hopLimit = h.flags & kFlagHopLimitMask; const int hopStart = (h.flags & kFlagHopStartMask) >> 5; const bool wantAck = (h.flags & kFlagWantAckMask) != 0; const bool viaMqtt = (h.flags & kFlagViaMqttMask) != 0; return QString("to=%1 from=%2 id=0x%3 ch=0x%4 hop=%5/%6 ack=%7 mqtt=%8 next=%9 relay=%10") .arg(formatNode(h.to)) .arg(formatNode(h.from)) .arg(h.id, 8, 16, QChar('0')) .arg(h.channel, 2, 16, QChar('0')) .arg(hopLimit) .arg(hopStart) .arg(wantAck ? 1 : 0) .arg(viaMqtt ? 1 : 0) .arg(h.nextHop) .arg(h.relayNode); } static QString summarizeData(const DataFields& d) { const QString portName = portToName(d.portnum); QString s = QString("port=%1(%2)").arg(portName).arg(d.portnum); if (d.wantResponse) { s += " want_response=1"; } if (d.hasDest) { s += QString(" dest=%1").arg(formatNode(d.dest)); } if (d.hasSource) { s += QString(" source=%1").arg(formatNode(d.source)); } if (d.hasRequestId) { s += QString(" request_id=0x%1").arg(d.requestId, 8, 16, QChar('0')); } if (d.hasReplyId) { s += QString(" reply_id=0x%1").arg(d.replyId, 8, 16, QChar('0')); } if (d.hasEmoji) { s += QString(" emoji=%1").arg(d.emoji); } if (d.hasBitfield) { s += QString(" bitfield=0x%1").arg(d.bitfield, 0, 16); } const QString text = payloadToText(d.payload); if (!text.isEmpty()) { s += QString(" text=\"%1\"").arg(text); } else if (!d.payload.isEmpty()) { const int n = std::min(32, static_cast(d.payload.size())); s += QString(" payload_hex=%1").arg(QString(d.payload.left(n).toHex())); if (d.payload.size() > n) { s += "..."; } } else { s += " payload="; } return s; } static void addDecodeField(DecodeResult& result, const QString& path, const QString& value) { DecodeResult::Field f; f.path = path; f.value = value; result.fields.append(f); } static void addDecodeField(DecodeResult& result, const QString& path, uint32_t value) { addDecodeField(result, path, QString::number(value)); } static void addDecodeField(DecodeResult& result, const QString& path, bool value) { addDecodeField(result, path, QString(value ? "true" : "false")); } static void appendHeaderDecodeFields(const Header& h, DecodeResult& result) { const int hopLimit = h.flags & kFlagHopLimitMask; const int hopStart = (h.flags & kFlagHopStartMask) >> 5; const bool wantAck = (h.flags & kFlagWantAckMask) != 0; const bool viaMqtt = (h.flags & kFlagViaMqttMask) != 0; addDecodeField(result, "header.to", formatNode(h.to)); addDecodeField(result, "header.from", formatNode(h.from)); addDecodeField(result, "header.id", QString("0x%1").arg(h.id, 8, 16, QChar('0'))); addDecodeField(result, "header.channel_hash", QString("0x%1").arg(h.channel, 2, 16, QChar('0'))); addDecodeField(result, "header.hop_limit", QString::number(hopLimit)); addDecodeField(result, "header.hop_start", QString::number(hopStart)); addDecodeField(result, "header.want_ack", wantAck); addDecodeField(result, "header.via_mqtt", viaMqtt); addDecodeField(result, "header.next_hop", QString::number(h.nextHop)); addDecodeField(result, "header.relay_node", QString::number(h.relayNode)); } static void appendDataDecodeFields(const DataFields& d, DecodeResult& result) { addDecodeField(result, "data.port_name", portToName(d.portnum)); addDecodeField(result, "data.portnum", d.portnum); addDecodeField(result, "data.want_response", d.wantResponse); if (d.hasDest) { addDecodeField(result, "data.dest", formatNode(d.dest)); } if (d.hasSource) { addDecodeField(result, "data.source", formatNode(d.source)); } if (d.hasRequestId) { addDecodeField(result, "data.request_id", QString("0x%1").arg(d.requestId, 8, 16, QChar('0'))); } if (d.hasReplyId) { addDecodeField(result, "data.reply_id", QString("0x%1").arg(d.replyId, 8, 16, QChar('0'))); } if (d.hasEmoji) { addDecodeField(result, "data.emoji", QString::number(d.emoji)); } if (d.hasBitfield) { addDecodeField(result, "data.bitfield", QString("0x%1").arg(d.bitfield, 0, 16)); } addDecodeField(result, "data.payload_len", QString::number(d.payload.size())); const QString text = payloadToText(d.payload); if (!text.isEmpty()) { addDecodeField(result, "data.text", text); } else if (!d.payload.isEmpty()) { addDecodeField(result, "data.payload_hex", QString(d.payload.toHex())); } } static bool deriveTxRadioSettingsFromConfig(const CommandConfig& cfg, TxRadioSettings& settings, QString& error) { settings = TxRadioSettings(); settings.hasCommand = true; settings.syncWord = 0x00; QString presetName = cfg.presetName; if (presetName.isEmpty()) { presetName = "LONG_FAST"; } bool wideLora = false; const RegionBand* region = nullptr; if (cfg.hasRegion) { region = findRegionBand(cfg.regionName); if (!region) { error = "invalid region"; return false; } wideLora = region->wideLora; } int bandwidthHz = 0; int spreadFactor = 0; int parityBits = 0; if (!presetToParams(presetName, wideLora, bandwidthHz, spreadFactor, parityBits)) { error = "invalid preset"; return false; } if (wideLora) { error = "LORA_24 wide LoRa presets are not supported by ChirpChat"; return false; } settings.hasLoRaParams = true; settings.bandwidthHz = bandwidthHz; settings.spreadFactor = spreadFactor; settings.parityBits = parityBits; settings.preambleChirps = wideLora ? 12 : 17; const double symbolTimeSec = static_cast(1u << spreadFactor) / static_cast(bandwidthHz); settings.deBits = (symbolTimeSec > 0.016) ? 2 : 0; // match classic LoRa low data rate optimization rule if (cfg.hasOverrideFrequencyMHz) { const double freqMHz = cfg.overrideFrequencyMHz + (cfg.hasFrequencyOffsetMHz ? cfg.frequencyOffsetMHz : 0.0); settings.hasCenterFrequency = true; settings.centerFrequencyHz = static_cast(std::llround(freqMHz * 1000000.0)); } else if (region) { const double bwMHz = static_cast(bandwidthHz) / 1000000.0; const double slotWidthMHz = region->spacingMHz + bwMHz; const double spanMHz = region->freqEndMHz - region->freqStartMHz; const uint32_t numChannels = static_cast(std::floor(spanMHz / slotWidthMHz)); if (numChannels == 0) { error = "region span too narrow for selected preset bandwidth"; return false; } uint32_t channelIndex = 0; if (cfg.hasChannelNum) { if (cfg.channelNum < 1 || cfg.channelNum > numChannels) { error = QString("channel_num out of range 1..%1").arg(numChannels); return false; } channelIndex = cfg.channelNum - 1; } else { const QString displayName = presetToDisplayName(presetName); channelIndex = meshHashDjb2(displayName) % numChannels; } const double centerMHz = region->freqStartMHz + (bwMHz / 2.0) + (channelIndex * slotWidthMHz) + (cfg.hasFrequencyOffsetMHz ? cfg.frequencyOffsetMHz : 0.0); settings.hasCenterFrequency = true; settings.centerFrequencyHz = static_cast(std::llround(centerMHz * 1000000.0)); } settings.summary = QString("preset=%1 sf=%2 cr=4/%3 bw=%4kHz de=%5") .arg(presetName) .arg(settings.spreadFactor) .arg(settings.parityBits + 4) .arg(settings.bandwidthHz / 1000) .arg(settings.deBits); settings.summary += QString(" preamble=%1").arg(settings.preambleChirps); if (region) { settings.summary += QString(" region=%1").arg(region->name); } if (cfg.hasChannelNum) { settings.summary += QString(" channel_num=%1").arg(cfg.channelNum); } if (settings.hasCenterFrequency) { settings.summary += QString(" freq=%1MHz").arg(settings.centerFrequencyHz / 1000000.0, 0, 'f', 6); } return true; } } // namespace bool Packet::isCommand(const QString& text) { return text.trimmed().startsWith("MESH:", Qt::CaseInsensitive); } bool Packet::buildFrameFromCommand(const QString& command, QByteArray& frame, QString& summary, QString& error) { CommandConfig cfg; if (!parseCommand(command, cfg, error)) { return false; } QByteArray payload = encodeData(cfg.data); if (cfg.encrypt) { payload = aesCtrCrypt(payload, cfg.key, cfg.header.from, cfg.header.id, CounterMode::BigEndian); if (payload.isEmpty()) { error = "failed to encrypt payload"; return false; } } frame = encodeHeader(cfg.header); frame.append(payload); summary = QString("MESH TX|%1 key=%2 encrypt=%3 %4") .arg(summarizeHeader(cfg.header)) .arg(cfg.keyLabel) .arg(cfg.encrypt ? 1 : 0) .arg(summarizeData(cfg.data)); return true; } bool Packet::decodeFrame(const QByteArray& frame, DecodeResult& result) { return decodeFrame(frame, result, QString()); } bool Packet::decodeFrame(const QByteArray& frame, DecodeResult& result, const QString& keySpecList) { result = DecodeResult(); Header h; if (!parseHeader(frame, h)) { return false; } result.isFrame = true; appendHeaderDecodeFields(h, result); const QByteArray encryptedPayload = frame.mid(kHeaderLength); addDecodeField(result, "decode.payload_encrypted_len", QString::number(encryptedPayload.size())); // 1) Plain decode attempt first (unencrypted / key-none packets) DataFields data; if (parseData(encryptedPayload, data)) { result.dataDecoded = true; result.decrypted = false; result.keyLabel = "none"; result.summary = QString("MESH RX|%1 key=none %2") .arg(summarizeHeader(h)) .arg(summarizeData(data)); addDecodeField(result, "decode.path", "plain"); addDecodeField(result, "decode.key_label", result.keyLabel); addDecodeField(result, "decode.decrypted", result.decrypted); appendDataDecodeFields(data, result); return true; } // 2) Try configured keys (hash-matched keys first) std::vector keys; if (!keySpecList.trimmed().isEmpty()) { QString error; int keyCount = 0; if (!parseKeySpecList(keySpecList, keys, &error, &keyCount, true)) { qWarning() << "Meshtastic::Packet::decodeFrame: invalid keySpecList:" << error; keys = defaultKeysFromEnv(); } else if (keyCount == 0) { keys = defaultKeysFromEnv(); } } else { keys = defaultKeysFromEnv(); } std::stable_sort(keys.begin(), keys.end(), [h](const KeyEntry& a, const KeyEntry& b) { const int as = (a.hasExpectedHash && a.expectedHash == h.channel) ? 1 : 0; const int bs = (b.hasExpectedHash && b.expectedHash == h.channel) ? 1 : 0; return as > bs; }); std::set tested; for (const KeyEntry& k : keys) { if (k.key.isEmpty()) { continue; } const QString fingerprint = QString("%1:%2").arg(QString(k.key.toHex()), k.label); if (tested.find(fingerprint) != tested.end()) { continue; } tested.insert(fingerprint); const QByteArray plainBe = aesCtrCrypt(encryptedPayload, k.key, h.from, h.id, CounterMode::BigEndian); if (!plainBe.isEmpty() && parseData(plainBe, data)) { result.dataDecoded = true; result.decrypted = true; result.keyLabel = k.label; result.summary = QString("MESH RX|%1 key=%2 ctr=be %3") .arg(summarizeHeader(h)) .arg(k.label) .arg(summarizeData(data)); addDecodeField(result, "decode.path", "aes_ctr_be"); addDecodeField(result, "decode.key_label", result.keyLabel); addDecodeField(result, "decode.decrypted", result.decrypted); appendDataDecodeFields(data, result); return true; } // Keep CTR mode strict to match Meshtastic reference decode path. } result.summary = QString("MESH RX|%1 undecoded payload_len=%2") .arg(summarizeHeader(h)) .arg(encryptedPayload.size()); addDecodeField(result, "decode.path", "undecoded"); addDecodeField(result, "decode.key_label", "none"); addDecodeField(result, "decode.decrypted", false); if (!encryptedPayload.isEmpty()) { addDecodeField(result, "decode.payload_encrypted_hex", QString(encryptedPayload.toHex())); } return true; } bool Packet::validateKeySpecList(const QString& keySpecList, QString& error, int* keyCount) { std::vector keys; if (!parseKeySpecList(keySpecList, keys, &error, keyCount, true)) { return false; } if (keyCount && *keyCount == 0) { error = "no keys found"; return false; } return true; } bool Packet::deriveTxRadioSettings(const QString& command, TxRadioSettings& settings, QString& error) { CommandConfig cfg; if (!parseCommand(command, cfg, error)) { return false; } return deriveTxRadioSettingsFromConfig(cfg, settings, error); } } // namespace Meshtastic