diff --git a/MQTT_IMPLEMENTATION.md b/MQTT_IMPLEMENTATION.md index 12ba6519..548bef13 100644 --- a/MQTT_IMPLEMENTATION.md +++ b/MQTT_IMPLEMENTATION.md @@ -515,6 +515,8 @@ Minimal raw packet data for map integration. "stats": { "battery_mv": 4100, "uptime_secs": 3600, + "packets_sent": 42, + "packets_received": 128, "errors": 0, "queue_len": 0, "noise_floor": -110, @@ -529,6 +531,7 @@ Minimal raw packet data for map integration. **Notes:** - Timestamps are always emitted in UTC with an explicit `+00:00` offset. - The `stats` object is only included when at least one stat value is available; individual fields are omitted when their value is unavailable. +- `packets_sent` / `packets_received` are cumulative totals since boot (flood + direct), sourced from the dispatcher counters. ### Packet Message ```json @@ -547,15 +550,17 @@ Minimal raw packet data for map integration. "raw": "F5930103807E5F1E...", "SNR": "12.5", "RSSI": "-65", + "score": "234", "hash": "A1B2C3D4E5F67890", "path": ["aa", "bb", "cc"] } ``` **Notes:** -- All numeric fields (`len`, `packet_type`, `payload_len`, `SNR`, `RSSI`) are formatted as JSON strings. +- All numeric fields (`len`, `packet_type`, `payload_len`, `SNR`, `RSSI`, `score`) are formatted as JSON strings. - `time` and `date` are always UTC (`HH:MM:SS` and `DD/MM/YYYY`); `timestamp` is UTC with an explicit `+00:00` offset. -- `SNR` and `RSSI` are only present for RX packets (received from radio). TX packets omit these fields since the packet originates from this node. +- `SNR`, `RSSI`, and `score` are only present for RX packets (received from radio). TX packets omit these fields since the packet originates from this node. +- `score` is the firmware's rebroadcast score for the received packet (the same value used to compute flood-rebroadcast delay), scaled ×1000 to match the integer printed in the serial RX log — e.g. a score of `0.234` is emitted as `"234"` (range `0`–`1000`). It is recomputed at publish time from the packet's SNR and length via the radio's `packetScore()`, so it matches what the firmware used on receive. Omitted when unavailable (e.g. the non-PSRAM reconstruction-less fallback path). - `path` is only present for direct-route packets that carry path data. It is a JSON array of lowercase hex hop tokens, one element per hop — e.g. `["aa","bb","cc"]` for single-byte hashes, or `["aaaa","bbbb"]` for multi-byte hashes. This matches the `path` representation emitted by [meshcore-packet-capture](https://github.com/agessaman/meshcore-packet-capture). ### Raw Message diff --git a/src/helpers/MQTTMessageBuilder.cpp b/src/helpers/MQTTMessageBuilder.cpp index 5a387b41..e20ec631 100644 --- a/src/helpers/MQTTMessageBuilder.cpp +++ b/src/helpers/MQTTMessageBuilder.cpp @@ -1,6 +1,7 @@ #include "MQTTMessageBuilder.h" #include #include +#include #include #include #include "MeshCore.h" @@ -40,6 +41,8 @@ int MQTTMessageBuilder::buildStatusMessage( int rx_air_secs, int recv_errors, int internal_heap, + int packets_sent, + int packets_received, const char* repeat ) { // doc is provided by the caller (heap-allocated DynamicJsonDocument in MQTTBridge), @@ -62,7 +65,7 @@ int MQTTMessageBuilder::buildStatusMessage( // Add stats object if any stats are provided if (battery_mv >= 0 || uptime_secs >= 0 || errors >= 0 || queue_len >= 0 || noise_floor > -999 || tx_air_secs >= 0 || rx_air_secs >= 0 || recv_errors >= 0 || - internal_heap >= 0) { + internal_heap >= 0 || packets_sent >= 0 || packets_received >= 0) { JsonObject stats = root.createNestedObject("stats"); if (battery_mv >= 0) { @@ -71,6 +74,12 @@ int MQTTMessageBuilder::buildStatusMessage( if (uptime_secs >= 0) { stats["uptime_secs"] = uptime_secs; } + if (packets_sent >= 0) { + stats["packets_sent"] = packets_sent; + } + if (packets_received >= 0) { + stats["packets_received"] = packets_received; + } if (errors >= 0) { stats["errors"] = errors; } @@ -113,6 +122,7 @@ int MQTTMessageBuilder::buildPacketMessage( const char* raw, float snr, int rssi, + float score, const char* hash, const uint8_t* path_bytes, int path_hop_count, @@ -131,7 +141,8 @@ int MQTTMessageBuilder::buildPacketMessage( char payload_len_str[16]; char snr_str[16]; char rssi_str[16]; - + char score_str[16]; + snprintf(len_str, sizeof(len_str), "%d", len); snprintf(packet_type_str, sizeof(packet_type_str), "%d", packet_type); snprintf(payload_len_str, sizeof(payload_len_str), "%d", payload_len); @@ -155,6 +166,12 @@ int MQTTMessageBuilder::buildPacketMessage( if (strcmp(direction, "rx") == 0) { root["SNR"] = snr_str; root["RSSI"] = rssi_str; + // Firmware's rebroadcast "score" for this RX packet, scaled x1000 to match the + // integer form printed in the serial RX log (see Dispatcher::checkRecv()). + if (!isnan(score)) { + snprintf(score_str, sizeof(score_str), "%d", (int)(score * 1000)); + root["score"] = score_str; + } } // Routing path as an array of lowercase hex hop tokens, one element per hop @@ -262,6 +279,7 @@ int MQTTMessageBuilder::buildPacketJSON( raw_hex, 12.5f, // SNR - using reasonable default -65, // RSSI - using reasonable default + NAN, // score - unknown on this reconstruction-less fallback path hash_str, has_path ? packet->path : nullptr, has_path ? packet->getPathHashCount() : 0, @@ -280,6 +298,7 @@ int MQTTMessageBuilder::buildPacketJSONFromRaw( const char* origin_id, float snr, float rssi, + float score, Timezone* timezone, char* buffer, size_t buffer_size @@ -333,6 +352,7 @@ int MQTTMessageBuilder::buildPacketJSONFromRaw( raw_hex, snr, // Use actual SNR from radio rssi, // Use actual RSSI from radio + score, // Firmware rebroadcast score (NaN for tx / when unavailable) hash_str, has_path ? packet->path : nullptr, has_path ? packet->getPathHashCount() : 0, diff --git a/src/helpers/MQTTMessageBuilder.h b/src/helpers/MQTTMessageBuilder.h index 3ea424f9..f12d41ef 100644 --- a/src/helpers/MQTTMessageBuilder.h +++ b/src/helpers/MQTTMessageBuilder.h @@ -78,6 +78,8 @@ public: int rx_air_secs = -1, int recv_errors = -1, int internal_heap = -1, + int packets_sent = -1, + int packets_received = -1, const char* repeat = nullptr ); @@ -120,6 +122,7 @@ public: const char* raw, float snr, int rssi, + float score, const char* hash, const uint8_t* path_bytes, int path_hop_count, @@ -180,6 +183,7 @@ public: const char* origin_id, float snr, float rssi, + float score, Timezone* timezone, char* buffer, size_t buffer_size diff --git a/src/helpers/bridges/MQTTBridge.cpp b/src/helpers/bridges/MQTTBridge.cpp index e7817db1..d8c255cf 100644 --- a/src/helpers/bridges/MQTTBridge.cpp +++ b/src/helpers/bridges/MQTTBridge.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #ifdef WITH_SNMP #include "../SNMPAgent.h" @@ -1730,6 +1731,8 @@ void MQTTBridge::publishStatusToSlot(int index) { int tx_air_secs = -1; int rx_air_secs = -1; int recv_errors = -1; + int packets_sent = -1; + int packets_received = -1; if (_board) battery_mv = _board->getBattMilliVolts(); if (_ms) uptime_secs = _ms->getMillis() / 1000; @@ -1737,6 +1740,8 @@ void MQTTBridge::publishStatusToSlot(int index) { errors = _dispatcher->getErrFlags(); tx_air_secs = _dispatcher->getTotalAirTime() / 1000; rx_air_secs = _dispatcher->getReceiveAirTime() / 1000; + packets_sent = (int)(_dispatcher->getNumSentFlood() + _dispatcher->getNumSentDirect()); + packets_received = (int)(_dispatcher->getNumRecvFlood() + _dispatcher->getNumRecvDirect()); } if (_radio) { noise_floor = (int16_t)_radio->getNoiseFloor(); @@ -1752,6 +1757,7 @@ void MQTTBridge::publishStatusToSlot(int index) { client_version, "online", timestamp, json_buffer, STATUS_JSON_BUFFER_SIZE, battery_mv, uptime_secs, errors, _queue_count, noise_floor, tx_air_secs, rx_air_secs, recv_errors, internal_heap_free, + packets_sent, packets_received, _prefs->disable_fwd ? "off" : "on" ); @@ -2436,6 +2442,8 @@ bool MQTTBridge::publishStatus() { int tx_air_secs = -1; int rx_air_secs = -1; int recv_errors = -1; + int packets_sent = -1; + int packets_received = -1; if (_board) battery_mv = _board->getBattMilliVolts(); if (_ms) uptime_secs = _ms->getMillis() / 1000; @@ -2443,6 +2451,8 @@ bool MQTTBridge::publishStatus() { errors = _dispatcher->getErrFlags(); tx_air_secs = _dispatcher->getTotalAirTime() / 1000; rx_air_secs = _dispatcher->getReceiveAirTime() / 1000; + packets_sent = (int)(_dispatcher->getNumSentFlood() + _dispatcher->getNumSentDirect()); + packets_received = (int)(_dispatcher->getNumRecvFlood() + _dispatcher->getNumRecvDirect()); } if (_radio) { noise_floor = (int16_t)_radio->getNoiseFloor(); @@ -2458,6 +2468,7 @@ bool MQTTBridge::publishStatus() { client_version, "online", timestamp, json_buffer, STATUS_JSON_BUFFER_SIZE, battery_mv, uptime_secs, errors, _queue_count, noise_floor, tx_air_secs, rx_air_secs, recv_errors, internal_heap_free, + packets_sent, packets_received, _prefs->disable_fwd ? "off" : "on" ); @@ -2545,19 +2556,26 @@ bool MQTTBridge::publishPacket(mesh::Packet* packet, bool is_tx, strncpy(origin_id, _device_id, sizeof(origin_id) - 1); origin_id[sizeof(origin_id) - 1] = '\0'; + // Firmware rebroadcast "score" for this packet — only meaningful for RX packets + // (depends on receive SNR). Recomputed here exactly as Dispatcher::checkRecv() does, + // via the radio's packetScore(snr, len); NaN signals "omit" (tx, or no radio). + // buildPacketMessage scales it x1000 to match the integer in the serial RX log. + // Build packet message using raw radio data if provided int len; if (raw_data && raw_len > 0) { + float score = (_radio && !is_tx) ? _radio->packetScore(snr, raw_len) : NAN; len = MQTTMessageBuilder::buildPacketJSONFromRaw( _packet_json_doc, raw_data, raw_len, packet, is_tx, _origin, origin_id, - snr, rssi, _timezone, active_buffer, active_buffer_size + snr, rssi, score, _timezone, active_buffer, active_buffer_size ); } else if (!is_tx && _last_raw_data && _last_raw_len > 0 && (millis() - _last_raw_timestamp) < 1000) { + float score = _radio ? _radio->packetScore(_last_snr, _last_raw_len) : NAN; len = MQTTMessageBuilder::buildPacketJSONFromRaw( _packet_json_doc, _last_raw_data, _last_raw_len, packet, is_tx, _origin, origin_id, - _last_snr, _last_rssi, _timezone, active_buffer, active_buffer_size + _last_snr, _last_rssi, score, _timezone, active_buffer, active_buffer_size ); } else { // Reconstruct wire-format bytes from packet (same as MQTTMessageBuilder::packetToHex). @@ -2566,10 +2584,11 @@ bool MQTTBridge::publishPacket(mesh::Packet* packet, bool is_tx, uint8_t reconstructed[512]; uint8_t rlen = packet->writeTo(reconstructed); if (rlen > 0) { + float score = (_radio && !is_tx) ? _radio->packetScore(snr, rlen) : NAN; len = MQTTMessageBuilder::buildPacketJSONFromRaw( _packet_json_doc, reconstructed, rlen, packet, is_tx, _origin, origin_id, - snr, rssi, _timezone, active_buffer, active_buffer_size + snr, rssi, score, _timezone, active_buffer, active_buffer_size ); } else { len = MQTTMessageBuilder::buildPacketJSON(