From c368600b52ef2cf53910f28403ca04b60a06d01a Mon Sep 17 00:00:00 2001 From: jvn314 Date: Sat, 25 Apr 2026 01:30:45 +0000 Subject: [PATCH] expose demod fields for plaintext packets, gate on header CRC --- FIXME-notes.md | 14 +++-- json_udp_data_format.readme.md | 15 ++++- .../demodmeshtastic/meshtasticdemod.cpp | 62 +++++++++++++++---- 3 files changed, 70 insertions(+), 21 deletions(-) diff --git a/FIXME-notes.md b/FIXME-notes.md index 29c884e45..e3f1bcc68 100644 --- a/FIXME-notes.md +++ b/FIXME-notes.md @@ -1,8 +1,3 @@ -## meshtasticdemodwebapiadapter.cpp:35 -Initializes SWGChirpChatDemodSettings instead of SWGMeshtasticDemodSettings. -Copy-paste artifact from ChirpChat plugin. Fix separately in its own commit. -Branch: feature-meshtastic-more-udp or open a dedicated bug-fix branch. - ## meshtasticdemodgui.cpp - checkbox signal pattern All checkboxes currently use stateChanged(int) pattern. Consider refactoring all to clicked(bool) for simplicity in a future cleanup commit. @@ -12,3 +7,12 @@ Affects: sendViaUDP, sendJsonViaUDP, and likely other checkboxes in the same fil m_lastMsgBytes[0] is read without an explicit non-empty size check before the APRS detection block. Pre-existing issue, not introduced by JSON UDP feature. Fix separately in its own commit. Safe fix: add `&& !m_lastMsgBytes.isEmpty()` to the APRS condition. + +## meshtasticdemod.cpp:658 - hash_matching_index fabricated +hash_matching_index is hardcoded to 0 for decrypted packets and "none" +for all others. The decoder sorts key candidates by hash match but does +not expose the actual index used in DecodeResult. The field is therefore +inaccurate for multi-key configurations. Fix requires extending +DecodeResult to expose the matched key index, then wiring it through +buildMeshtasticJsonPacket. For now the field remains but should not be +relied upon. diff --git a/json_udp_data_format.readme.md b/json_udp_data_format.readme.md index aa9d6c072..cf27676ba 100644 --- a/json_udp_data_format.readme.md +++ b/json_udp_data_format.readme.md @@ -25,7 +25,6 @@ rf.snr_db Signal minus noise in dB lora.packet_type meshtastic, lorawan, helium, unset, or custom lora.sync_word Hex string e.g. 0x2b - lora.frame_id Hex string e.g. 0x1 lora.header_fec ok, fix, err, or n/a lora.header_crc ok or err lora.payload_fec ok, fix, err, or n/a (n/a when early_eom is true) @@ -40,6 +39,7 @@ ## Fields Present Only When lora.packet_type Is meshtastic meshtastic.channel_hash Hex string e.g. 0x08 + meshtastic.packet_id Meshtastic 32-bit unique packet ID hex string e.g. 0x5a42f351 meshtastic.hash_matching_index Integer index of matched key, or none meshtastic.decryption decrypted, plaintext, or not_decrypted meshtastic.key_label Key name, no_key, or unknown_key @@ -78,6 +78,14 @@ n/a Not applicable, packet ended early before payload was received +## Note on packet_id + + The Meshtastic packet ID is a large random 32-bit value generated by the originating node. + Its primary purpose is to serve as a cryptographic nonce component for AES-CTR decryption, + combined with the from node ID. It also serves as a deduplication key in the mesh so that + relay nodes can discard copies of already-forwarded packets. It is not a sequential counter. + + ## Example: Fully Decoded Meshtastic Packet { @@ -89,12 +97,11 @@ "spreading_factor": 11, "signal_db": -46.0, "noise_db": -84.2, - "snr_db": 38.2 + "snr_db": 13.6 }, "lora": { "packet_type": "meshtastic", "sync_word": "0x2b", - "frame_id": "0x1", "header_fec": "ok", "header_crc": "ok", "payload_fec": "ok", @@ -107,6 +114,7 @@ }, "meshtastic": { "channel_hash": "0x08", + "packet_id": "0x5a42f351", "hash_matching_index": 0, "decryption": "decrypted", "key_label": "LongFast:default", @@ -144,6 +152,7 @@ }, "meshtastic": { "channel_hash": "0x08", + "packet_id": "unknown", "hash_matching_index": "none", "decryption": "not_decrypted", "key_label": "unknown_key", diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemod.cpp b/plugins/channelrx/demodmeshtastic/meshtasticdemod.cpp index eda99d587..3a9ba0f2f 100644 --- a/plugins/channelrx/demodmeshtastic/meshtasticdemod.cpp +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemod.cpp @@ -632,12 +632,59 @@ QString MeshtasticDemod::buildMeshtasticJsonPacket( if (syncWord == 0x2B) { QJsonObject mesh; + const bool headerCrcOk = msg.getHeaderCRCStatus(); const QString channelHash = getMeshField(meshResult, "header.channel_hash"); - mesh["channel_hash"] = channelHash.isEmpty() ? QStringLiteral("unknown") : channelHash; + if (headerCrcOk && !channelHash.isEmpty()) { + mesh["channel_hash"] = channelHash; + } else { + mesh["channel_hash"] = QStringLiteral("unknown"); + } const QString packetId = getMeshField(meshResult, "header.id"); - mesh["packet_id"] = packetId.isEmpty() ? QStringLiteral("unknown") : packetId; + if (headerCrcOk && !packetId.isEmpty()) { + mesh["packet_id"] = packetId; + } else { + mesh["packet_id"] = QStringLiteral("unknown"); + } + + const QString hopStart = getMeshField(meshResult, "header.hop_start"); + const QString hopLimit = getMeshField(meshResult, "header.hop_limit"); + const QString relayNode = getMeshField(meshResult, "header.relay_node"); + + if (headerCrcOk) + { + if (!hopStart.isEmpty()) { + mesh["hop_start"] = hopStart.toInt(); + } else { + mesh["hop_start"] = QStringLiteral("unknown"); + } + + if (!hopLimit.isEmpty()) { + mesh["hop_limit"] = hopLimit.toInt(); + } else { + mesh["hop_limit"] = QStringLiteral("unknown"); + } + + if (!hopStart.isEmpty() && !hopLimit.isEmpty()) { + mesh["hops_consumed"] = hopStart.toInt() - hopLimit.toInt(); + } else { + mesh["hops_consumed"] = QStringLiteral("unknown"); + } + + if (!relayNode.isEmpty()) { + mesh["relay_node"] = relayNode.toInt(); + } else { + mesh["relay_node"] = QStringLiteral("unknown"); + } + } + else + { + mesh["hop_start"] = QStringLiteral("unknown"); + mesh["hop_limit"] = QStringLiteral("unknown"); + mesh["hops_consumed"] = QStringLiteral("unknown"); + mesh["relay_node"] = QStringLiteral("unknown"); + } const QString decodePath = getMeshField(meshResult, "decode.path"); const QString keyLabel = getMeshField(meshResult, "decode.key_label"); @@ -672,17 +719,6 @@ QString MeshtasticDemod::buildMeshtasticJsonPacket( { mesh["channel_type"] = m_settings.m_meshtasticPresetName; - const QString hopStart = getMeshField(meshResult, "header.hop_start"); - const QString hopLimit = getMeshField(meshResult, "header.hop_limit"); - const QString relayNode = getMeshField(meshResult, "header.relay_node"); - - if (!hopStart.isEmpty()) { mesh["hop_start"] = hopStart.toInt(); } - if (!hopLimit.isEmpty()) { mesh["hop_limit"] = hopLimit.toInt(); } - if (!hopStart.isEmpty() && !hopLimit.isEmpty()) { - mesh["hops_consumed"] = hopStart.toInt() - hopLimit.toInt(); - } - if (!relayNode.isEmpty()) { mesh["relay_node"] = relayNode.toInt(); } - QJsonObject fields; for (const auto& field : meshResult.fields) {