From c026c24e073d565de05b1b17700fb038beaeb9db Mon Sep 17 00:00:00 2001 From: agessaman Date: Fri, 7 Aug 2026 21:14:49 -0700 Subject: [PATCH] feat(mqtt): include RSSI in neighbor data for improved metrics --- .wt-station-g3-prod | 1 + MQTT_IMPLEMENTATION.md | 4 ++- examples/companion_radio/MyMesh.cpp | 8 ++--- examples/simple_repeater/MyMesh.cpp | 32 ++++++++++++------- examples/simple_repeater/MyMesh.h | 10 ++++-- examples/simple_room_server/MyMesh.cpp | 32 ++++++++++++------- examples/simple_room_server/MyMesh.h | 10 ++++-- src/Dispatcher.cpp | 4 ++- src/Packet.h | 2 ++ src/helpers/MQTTPayloadBuilder.cpp | 1 + src/helpers/MQTTPayloadBuilder.h | 1 + .../test_mqtt_payload_builder.cpp | 25 ++++++++------- 12 files changed, 85 insertions(+), 45 deletions(-) create mode 160000 .wt-station-g3-prod diff --git a/.wt-station-g3-prod b/.wt-station-g3-prod new file mode 160000 index 00000000..bfc43e94 --- /dev/null +++ b/.wt-station-g3-prod @@ -0,0 +1 @@ +Subproject commit bfc43e94f8495307346c16e9f8014617fd2e5cbe diff --git a/MQTT_IMPLEMENTATION.md b/MQTT_IMPLEMENTATION.md index dc04bb84..77678b32 100644 --- a/MQTT_IMPLEMENTATION.md +++ b/MQTT_IMPLEMENTATION.md @@ -710,7 +710,7 @@ Full packet data with RF characteristics and metadata. Minimal raw packet data for map integration. ### Neighbors Topic: `meshcore/{IATA}/{DEVICE_PUBLIC_KEY}/neighbors` -Cached zero-hop repeater neighbors with SNR, last-heard age, and flood-allowed scopes. Published on `discover.scopes` or periodically when `mqtt.neighbors` is enabled (observer builds with neighbors compiled in; non-PSRAM builds cap the table at 20 entries and set `truncated`). Goes to every configured slot's `neighbors` topic at QoS 0, retained only where the preset allows it. +Cached zero-hop repeater neighbors with SNR, RSSI, last-heard age, and flood-allowed scopes. Published on `discover.scopes` or periodically when `mqtt.neighbors` is enabled (observer builds with neighbors compiled in; non-PSRAM builds cap the table at 20 entries and set `truncated`). Goes to every configured slot's `neighbors` topic at QoS 0, retained only where the preset allows it. Periodic publishing first runs a 60-second zero-hop neighbor refresh equivalent to `discover.neighbors`, then queries the refreshed table for scopes and publishes when the scope-query phase completes. @@ -810,6 +810,7 @@ While `mqtt.neighbors` is on, `get mqtt.status` appends `nbr: /` — { "pubkey": "0011223344556677...", "snr": 9.75, + "rssi": -87, "heard_secs_ago": 42, "scopes": "DEN,APRS", "status": "responded" @@ -817,6 +818,7 @@ While `mqtt.neighbors` is on, `get mqtt.status` appends `nbr: /` — { "pubkey": "8899AABBCCDDEEFF...", "snr": 12.5, + "rssi": -95, "heard_secs_ago": null, "scopes": "DEN", "status": "responded" diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 46f70d78..6bb2e66d 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -786,8 +786,8 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) { } int i = 0; out_frame[i++] = PUSH_CODE_CONTROL_DATA; - out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4); - out_frame[i++] = (int8_t)(_radio->getLastRSSI()); + out_frame[i++] = (int8_t)(packet->getSNR() * 4); + out_frame[i++] = (int8_t)packet->getRSSI(); out_frame[i++] = packet->path_len; memcpy(&out_frame[i], packet->payload, packet->payload_len); i += packet->payload_len; @@ -806,8 +806,8 @@ void MyMesh::onRawDataRecv(mesh::Packet *packet) { } int i = 0; out_frame[i++] = PUSH_CODE_RAW_DATA; - out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4); - out_frame[i++] = (int8_t)(_radio->getLastRSSI()); + out_frame[i++] = (int8_t)(packet->getSNR() * 4); + out_frame[i++] = (int8_t)packet->getRSSI(); out_frame[i++] = 0xFF; // reserved (possibly path_len in future) memcpy(&out_frame[i], packet->payload, packet->payload_len); i += packet->payload_len; diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index efb6532b..e9ac7370 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -71,7 +71,7 @@ #define LAZY_CONTACTS_WRITE_DELAY 5000 -void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) { +void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr, int16_t rssi) { #if MAX_NEIGHBOURS // check if neighbours enabled // find existing neighbour, else use least recently updated uint32_t oldest_timestamp = 0xFFFFFFFF; @@ -95,6 +95,7 @@ void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float sn neighbour->advert_timestamp = timestamp; neighbour->heard_timestamp = getRTCClock()->getCurrentTime(); neighbour->snr = (int8_t)(snr * 4); + neighbour->rssi = rssi; #endif } @@ -756,7 +757,7 @@ void MyMesh::onAdvertRecv(mesh::Packet *packet, const mesh::Identity &id, uint32 if (packet->getPathHashCount() == 0 && !isShare(packet)) { AdvertDataParser parser(app_data, app_data_len); if (parser.isValid() && parser.getType() == ADV_TYPE_REPEATER) { // just keep neigbouring Repeaters - putNeighbour(id, timestamp, packet->getSNR()); + putNeighbour(id, timestamp, packet->getSNR(), packet->getRSSI()); } } } @@ -770,7 +771,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, if (neighbor_discover_active && i >= NEIGHBOR_DISCOVER_PEER_BASE) { int oi = i - NEIGHBOR_DISCOVER_PEER_BASE; if (type == PAYLOAD_TYPE_RESPONSE && oi >= 0 && oi < neighbor_discover_count) { - handleNeighborDiscoverResponse(oi, data, len); + handleNeighborDiscoverResponse(oi, data, len, packet->getSNR(), packet->getRSSI()); } return; } @@ -786,7 +787,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, if (neighbor_discover_active && type == PAYLOAD_TYPE_RESPONSE) { for (int oi = 0; oi < neighbor_discover_count; oi++) { if (client->id.matches(neighbor_discover[oi].id) - && handleNeighborDiscoverResponse(oi, data, len)) { + && handleNeighborDiscoverResponse(oi, data, len, packet->getSNR(), packet->getRSSI())) { return; } } @@ -960,7 +961,7 @@ void MyMesh::onControlDataRecv(mesh::Packet* packet) { if (id.matches(self_id)) { return; } - putNeighbour(id, rtc_clock.getCurrentTime(), packet->getSNR()); + putNeighbour(id, rtc_clock.getCurrentTime(), packet->getSNR(), packet->getRSSI()); } } @@ -1932,6 +1933,7 @@ bool MyMesh::completeNeighborDiscoverEntry() { MQTTMessageBuilder::NeighborsMessageEntry measured = { pubkey_hex, entry.snr / 4.0f, + entry.rssi, UINT32_MAX, entry.scopes, entry.status == ND_RESPONDED ? "responded" @@ -1955,7 +1957,8 @@ bool MyMesh::completeNeighborDiscoverEntry() { // Match a RESPONSE against the pending overlay entry by tag; copy its scope // string (payload after the 8-byte {tag}{clock} header) into the entry. -bool MyMesh::handleNeighborDiscoverResponse(int overlay_idx, const uint8_t* data, size_t len) { +bool MyMesh::handleNeighborDiscoverResponse(int overlay_idx, const uint8_t* data, size_t len, + float snr, int16_t rssi) { if (overlay_idx < 0 || overlay_idx >= neighbor_discover_count) return false; NeighborDiscoverEntry& entry = neighbor_discover[overlay_idx]; if (entry.status != ND_PENDING || len < 8) return false; @@ -1972,19 +1975,24 @@ bool MyMesh::handleNeighborDiscoverResponse(int overlay_idx, const uint8_t* data entry.scopes[scope_len] = 0; entry.status = ND_RESPONDED; // A zero-hop reply is proof we heard this neighbour now, so re-stamp both the - // snapshot and the live table; a stamp taken before time sync heals here. + // snapshot and the live table with this packet's RX metrics; a stamp taken + // before time sync heals here. entry.heard_timestamp = getRTCClock()->getCurrentTime(); - touchNeighbourHeard(entry.id, entry.heard_timestamp); + entry.snr = (int8_t)(snr * 4); + entry.rssi = rssi; + touchNeighbourHeard(entry.id, entry.heard_timestamp, snr, rssi); return true; } -// Refresh a live neighbour's heard time only: a scope reply carries no advert -// timestamp or SNR to update. -void MyMesh::touchNeighbourHeard(const mesh::Identity& id, uint32_t heard_timestamp) { +// Refresh a live neighbour's heard time and RX metrics from a scope reply. +void MyMesh::touchNeighbourHeard(const mesh::Identity& id, uint32_t heard_timestamp, + float snr, int16_t rssi) { #if MAX_NEIGHBOURS for (int i = 0; i < MAX_NEIGHBOURS; i++) { if (id.matches(neighbours[i].id)) { neighbours[i].heard_timestamp = heard_timestamp; + neighbours[i].snr = (int8_t)(snr * 4); + neighbours[i].rssi = rssi; return; } } @@ -2107,6 +2115,7 @@ void MyMesh::finishNeighborDiscover() { mesh::Utils::toHex(hex, entry.id.pub_key, PUB_KEY_SIZE); entries[i].pubkey_hex = hex; entries[i].snr = entry.snr / 4.0f; + entries[i].rssi = entry.rssi; bool heard_known = neighborHeardAgeUsable(entry.heard_timestamp, now_secs); entries[i].heard_unknown = !heard_known; entries[i].heard_secs_ago = heard_known ? (now_secs - entry.heard_timestamp) : 0; @@ -2250,6 +2259,7 @@ bool MyMesh::startNeighborDiscover(char* reply) { entry.id = neighbours[i].id; entry.heard_timestamp = neighbours[i].heard_timestamp; entry.snr = neighbours[i].snr; + entry.rssi = neighbours[i].rssi; entry.scopes[0] = 0; entry.tag = 0; entry.status = ND_UNSENT; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index ec84dbce..b954e3de 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -76,6 +76,7 @@ struct NeighbourInfo { uint32_t advert_timestamp; uint32_t heard_timestamp; int8_t snr; // multiplied by 4, user should divide to get float value + int16_t rssi; // dBm from last heard packet }; #ifndef FIRMWARE_BUILD_DATE @@ -164,6 +165,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks mesh::Identity id; // immutable snapshot: neighbour table can change mid-pass uint32_t heard_timestamp; int8_t snr; // multiplied by 4 + int16_t rssi; // dBm from last heard packet uint32_t tag; // anon-regions request tag we're waiting on char scopes[96]; // scope names from the response uint8_t status; // NeighborDiscoverStatus @@ -194,8 +196,10 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks bool startNeighborDiscover(char* reply); void loopNeighborDiscover(); void finishNeighborDiscover(); - bool handleNeighborDiscoverResponse(int overlay_idx, const uint8_t* data, size_t len); - void touchNeighbourHeard(const mesh::Identity& id, uint32_t heard_timestamp); + bool handleNeighborDiscoverResponse(int overlay_idx, const uint8_t* data, size_t len, + float snr, int16_t rssi); + void touchNeighbourHeard(const mesh::Identity& id, uint32_t heard_timestamp, + float snr, int16_t rssi); void getLocalScopes(char* buf, size_t len); // Overlay peer indices are offset by this base so onPeerDataRecv can tell a // discovery response apart from a normal ACL-client index. @@ -204,7 +208,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks static const int NEIGHBOR_DISCOVER_MIN_FREE_PACKETS = 5; #endif - void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); + void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr, int16_t rssi); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); uint8_t handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index f2e447c6..306ee1d1 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -518,7 +518,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, if (neighbor_discover_active && i >= NEIGHBOR_DISCOVER_PEER_BASE) { int oi = i - NEIGHBOR_DISCOVER_PEER_BASE; if (type == PAYLOAD_TYPE_RESPONSE && oi >= 0 && oi < neighbor_discover_count) { - handleNeighborDiscoverResponse(oi, data, len); + handleNeighborDiscoverResponse(oi, data, len, packet->getSNR(), packet->getRSSI()); } return; } @@ -534,7 +534,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, if (neighbor_discover_active && type == PAYLOAD_TYPE_RESPONSE) { for (int oi = 0; oi < neighbor_discover_count; oi++) { if (client->id.matches(neighbor_discover[oi].id) - && handleNeighborDiscoverResponse(oi, data, len)) { + && handleNeighborDiscoverResponse(oi, data, len, packet->getSNR(), packet->getRSSI())) { return; } } @@ -734,7 +734,7 @@ void MyMesh::onAckRecv(mesh::Packet *packet, uint32_t ack_crc) { #define CTL_TYPE_NODE_DISCOVER_REQ 0x80 #define CTL_TYPE_NODE_DISCOVER_RESP 0x90 -void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) { +void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr, int16_t rssi) { // find existing neighbour, else use least recently updated uint32_t oldest_timestamp = 0xFFFFFFFF; NeighbourInfo *neighbour = &neighbours[0]; @@ -757,6 +757,7 @@ void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float sn neighbour->advert_timestamp = timestamp; neighbour->heard_timestamp = getRTCClock()->getCurrentTime(); neighbour->snr = (int8_t)(snr * 4); + neighbour->rssi = rssi; } static bool isShare(const mesh::Packet *packet) { @@ -774,7 +775,7 @@ void MyMesh::onAdvertRecv(mesh::Packet *packet, const mesh::Identity &id, uint32 if (packet->getPathHashCount() == 0 && !isShare(packet)) { AdvertDataParser parser(app_data, app_data_len); if (parser.isValid() && parser.getType() == ADV_TYPE_REPEATER) { // just keep neigbouring Repeaters - putNeighbour(id, timestamp, packet->getSNR()); + putNeighbour(id, timestamp, packet->getSNR(), packet->getRSSI()); } } } @@ -808,7 +809,7 @@ void MyMesh::onControlDataRecv(mesh::Packet* packet) { if (id.matches(self_id)) { return; } - putNeighbour(id, getRTCClock()->getCurrentTime(), packet->getSNR()); + putNeighbour(id, getRTCClock()->getCurrentTime(), packet->getSNR(), packet->getRSSI()); } } @@ -1818,6 +1819,7 @@ bool MyMesh::completeNeighborDiscoverEntry() { MQTTMessageBuilder::NeighborsMessageEntry measured = { pubkey_hex, entry.snr / 4.0f, + entry.rssi, UINT32_MAX, entry.scopes, entry.status == ND_RESPONDED ? "responded" @@ -1841,7 +1843,8 @@ bool MyMesh::completeNeighborDiscoverEntry() { // Match a RESPONSE against the pending overlay entry by tag; copy its scope // string (payload after the 8-byte {tag}{clock} header) into the entry. -bool MyMesh::handleNeighborDiscoverResponse(int overlay_idx, const uint8_t* data, size_t len) { +bool MyMesh::handleNeighborDiscoverResponse(int overlay_idx, const uint8_t* data, size_t len, + float snr, int16_t rssi) { if (overlay_idx < 0 || overlay_idx >= neighbor_discover_count) return false; NeighborDiscoverEntry& entry = neighbor_discover[overlay_idx]; if (entry.status != ND_PENDING || len < 8) return false; @@ -1858,18 +1861,23 @@ bool MyMesh::handleNeighborDiscoverResponse(int overlay_idx, const uint8_t* data entry.scopes[scope_len] = 0; entry.status = ND_RESPONDED; // A zero-hop reply is proof we heard this neighbour now, so re-stamp both the - // snapshot and the live table; a stamp taken before time sync heals here. + // snapshot and the live table with this packet's RX metrics; a stamp taken + // before time sync heals here. entry.heard_timestamp = getRTCClock()->getCurrentTime(); - touchNeighbourHeard(entry.id, entry.heard_timestamp); + entry.snr = (int8_t)(snr * 4); + entry.rssi = rssi; + touchNeighbourHeard(entry.id, entry.heard_timestamp, snr, rssi); return true; } -// Refresh a live neighbour's heard time only: a scope reply carries no advert -// timestamp or SNR to update. -void MyMesh::touchNeighbourHeard(const mesh::Identity& id, uint32_t heard_timestamp) { +// Refresh a live neighbour's heard time and RX metrics from a scope reply. +void MyMesh::touchNeighbourHeard(const mesh::Identity& id, uint32_t heard_timestamp, + float snr, int16_t rssi) { for (int i = 0; i < MAX_NEIGHBOURS; i++) { if (id.matches(neighbours[i].id)) { neighbours[i].heard_timestamp = heard_timestamp; + neighbours[i].snr = (int8_t)(snr * 4); + neighbours[i].rssi = rssi; return; } } @@ -1991,6 +1999,7 @@ void MyMesh::finishNeighborDiscover() { mesh::Utils::toHex(hex, entry.id.pub_key, PUB_KEY_SIZE); entries[i].pubkey_hex = hex; entries[i].snr = entry.snr / 4.0f; + entries[i].rssi = entry.rssi; bool heard_known = neighborHeardAgeUsable(entry.heard_timestamp, now_secs); entries[i].heard_unknown = !heard_known; entries[i].heard_secs_ago = heard_known ? (now_secs - entry.heard_timestamp) : 0; @@ -2134,6 +2143,7 @@ bool MyMesh::startNeighborDiscover(char* reply) { entry.id = neighbours[i].id; entry.heard_timestamp = neighbours[i].heard_timestamp; entry.snr = neighbours[i].snr; + entry.rssi = neighbours[i].rssi; entry.scopes[0] = 0; entry.tag = 0; entry.status = ND_UNSENT; diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index f5c6f036..052494db 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -104,6 +104,7 @@ struct NeighbourInfo { uint32_t advert_timestamp; uint32_t heard_timestamp; int8_t snr; // multiplied by 4, user should divide to get float value + int16_t rssi; // dBm from last heard packet }; class MyMesh : public mesh::Mesh, public CommonCLICallbacks @@ -156,6 +157,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks mesh::Identity id; uint32_t heard_timestamp; int8_t snr; + int16_t rssi; uint32_t tag; char scopes[96]; uint8_t status; @@ -177,7 +179,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks char self_default_scope_buf[31]; char neighbor_discover_origin[32]; - void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); + void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr, int16_t rssi); void sendNodeDiscoverReq(); mesh::Packet* sendAnonRegionsReq(const mesh::Identity& target, uint32_t& tag); bool cancelNeighborDiscoverRequest(); @@ -188,8 +190,10 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks bool startNeighborDiscover(char* reply); void loopNeighborDiscover(); void finishNeighborDiscover(); - bool handleNeighborDiscoverResponse(int overlay_idx, const uint8_t* data, size_t len); - void touchNeighbourHeard(const mesh::Identity& id, uint32_t heard_timestamp); + bool handleNeighborDiscoverResponse(int overlay_idx, const uint8_t* data, size_t len, + float snr, int16_t rssi); + void touchNeighbourHeard(const mesh::Identity& id, uint32_t heard_timestamp, + float snr, int16_t rssi); void getLocalScopes(char* buf, size_t len); static const int NEIGHBOR_DISCOVER_PEER_BASE = 1000; static const unsigned long NEIGHBOR_DISCOVER_QUEUE_TIMEOUT_MS = 29000; diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 2a491a61..784f81f6 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -237,6 +237,7 @@ void Dispatcher::checkRecv() { } else { if (tryParsePacket(pkt, raw, len)) { pkt->_snr = _radio->getLastSNR() * 4.0f; + pkt->_rssi = (int16_t)_radio->getLastRSSI(); score = _radio->packetScore(_radio->getLastSNR(), len); air_time = _radio->getEstAirtimeFor(len); rx_air_time += air_time; @@ -254,7 +255,7 @@ void Dispatcher::checkRecv() { Serial.print(getLogDateTime()); Serial.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d time=%d", pkt->getRawLength(), pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len, - (int)pkt->getSNR(), (int)_radio->getLastRSSI(), (int)(score*1000), air_time); + (int)pkt->getSNR(), (int)pkt->getRSSI(), (int)(score*1000), air_time); static uint8_t packet_hash[MAX_HASH_SIZE]; pkt->calculatePacketHash(packet_hash); @@ -393,6 +394,7 @@ Packet* Dispatcher::obtainNewPacket() { } else { pkt->payload_len = pkt->path_len = 0; pkt->_snr = 0; + pkt->_rssi = 0; } return pkt; } diff --git a/src/Packet.h b/src/Packet.h index c19d9e9d..c2734120 100644 --- a/src/Packet.h +++ b/src/Packet.h @@ -49,6 +49,7 @@ public: uint8_t path[MAX_PATH_SIZE]; uint8_t payload[MAX_PACKET_PAYLOAD]; int8_t _snr; + int16_t _rssi; // dBm, frozen at RX with _snr /** * \brief calculate the hash of payload + type @@ -90,6 +91,7 @@ public: bool isMarkedDoNotRetransmit() const { return header == 0xFF; } float getSNR() const { return ((float)_snr) / 4.0f; } + int16_t getRSSI() const { return _rssi; } /** * \returns the encoded/wire format length of this packet diff --git a/src/helpers/MQTTPayloadBuilder.cpp b/src/helpers/MQTTPayloadBuilder.cpp index f1b49223..f6bd32d8 100644 --- a/src/helpers/MQTTPayloadBuilder.cpp +++ b/src/helpers/MQTTPayloadBuilder.cpp @@ -220,6 +220,7 @@ static void addNeighborsMessageEntry( JsonObject nb = arr.add(); nb["pubkey"] = neighbor.pubkey_hex; nb["snr"] = neighbor.snr; + nb["rssi"] = neighbor.rssi; if (neighbor.heard_unknown) { nb["heard_secs_ago"] = nullptr; // age unknown, not zero } else { diff --git a/src/helpers/MQTTPayloadBuilder.h b/src/helpers/MQTTPayloadBuilder.h index 24e56124..8a6b76aa 100644 --- a/src/helpers/MQTTPayloadBuilder.h +++ b/src/helpers/MQTTPayloadBuilder.h @@ -72,6 +72,7 @@ public: struct NeighborsMessageEntry { const char* pubkey_hex; float snr; + int rssi; // dBm from the last heard packet uint32_t heard_secs_ago; const char* scopes; const char* status; diff --git a/test/test_mqtt_payload_builder/test_mqtt_payload_builder.cpp b/test/test_mqtt_payload_builder/test_mqtt_payload_builder.cpp index a3b0866f..8f89123f 100644 --- a/test/test_mqtt_payload_builder/test_mqtt_payload_builder.cpp +++ b/test/test_mqtt_payload_builder/test_mqtt_payload_builder.cpp @@ -235,8 +235,8 @@ TEST(MQTTPayloadBuilder, MaximumRepresentativePacketAndRawPayloadsRemainValid) { TEST(MQTTPayloadBuilder, NeighborsMessageRoundTripsSelfAndEntries) { MQTTPayloadBuilder::NeighborsMessageEntry neighbors[] = { - {"0011223344556677", 9.75f, 42, "DEN,APRS", "active"}, - {"8899AABBCCDDEEFF", -3.5f, 3600, "", "stale"}, + {"0011223344556677", 9.75f, -87, 42, "DEN,APRS", "active"}, + {"8899AABBCCDDEEFF", -3.5f, -110, 3600, "", "stale"}, }; JsonDocument scratch; @@ -262,18 +262,20 @@ TEST(MQTTPayloadBuilder, NeighborsMessageRoundTripsSelfAndEntries) { ASSERT_EQ(2U, arr.size()); EXPECT_STREQ("0011223344556677", arr[0]["pubkey"].as()); EXPECT_FLOAT_EQ(9.75f, arr[0]["snr"].as()); + EXPECT_EQ(-87, arr[0]["rssi"].as()); EXPECT_EQ(42U, arr[0]["heard_secs_ago"].as()); EXPECT_STREQ("DEN,APRS", arr[0]["scopes"].as()); EXPECT_STREQ("active", arr[0]["status"].as()); EXPECT_STREQ("8899AABBCCDDEEFF", arr[1]["pubkey"].as()); + EXPECT_EQ(-110, arr[1]["rssi"].as()); EXPECT_STREQ("", arr[1]["scopes"].as()); EXPECT_STREQ("stale", arr[1]["status"].as()); } TEST(MQTTPayloadBuilder, NeighborsMessageMeasurementsMatchCompletePayload) { MQTTPayloadBuilder::NeighborsMessageEntry neighbors[] = { - {"0011223344556677", 9.75f, UINT32_MAX, "DEN,APRS", "responded"}, - {"8899AABBCCDDEEFF", -3.5f, UINT32_MAX, "", "timeout"}, + {"0011223344556677", 9.75f, -87, UINT32_MAX, "DEN,APRS", "responded"}, + {"8899AABBCCDDEEFF", -3.5f, -110, UINT32_MAX, "", "timeout"}, }; size_t measured = @@ -302,7 +304,7 @@ TEST(MQTTPayloadBuilder, NeighborsMessageMeasuredPrefixStopsBeforeOverflow) { for (int i = 0; i < 20; i++) { snprintf(keys[i], sizeof(keys[i]), "%016X", i); neighbors[i] = { - keys[i], static_cast(i), UINT32_MAX, + keys[i], static_cast(i), -90 - i, UINT32_MAX, long_scopes, "responded"}; } @@ -345,8 +347,8 @@ TEST(MQTTPayloadBuilder, NeighborsMessageMeasuredPrefixStopsBeforeOverflow) { TEST(MQTTPayloadBuilder, NeighborsMessageFallbackMarksTruncated) { MQTTPayloadBuilder::NeighborsMessageEntry neighbors[] = { - {"0011223344556677", 9.75f, 1, "DEN", "responded"}, - {"8899AABBCCDDEEFF", -3.5f, 2, "APRS", "responded"}, + {"0011223344556677", 9.75f, -87, 1, "DEN", "responded"}, + {"8899AABBCCDDEEFF", -3.5f, -110, 2, "APRS", "responded"}, }; size_t first_only_size = MQTTPayloadBuilder::measureNeighborsMessageBase( @@ -369,7 +371,7 @@ TEST(MQTTPayloadBuilder, NeighborsMessageFallbackMarksTruncated) { TEST(MQTTPayloadBuilder, NeighborsMessageFailsCleanlyOnAllocationFailure) { MQTTPayloadBuilder::NeighborsMessageEntry neighbor = { - "0011223344556677", 9.75f, 1, "DEN", "responded"}; + "0011223344556677", 9.75f, -87, 1, "DEN", "responded"}; RejectAllJsonAllocations allocator; JsonDocument scratch(&allocator); char buffer[512]; @@ -406,6 +408,7 @@ TEST(MQTTPayloadBuilder, NeighborsMessageDropsTailWhenBufferFills) { snprintf(keys[i], sizeof(keys[i]), "%016X", i); neighbors[i].pubkey_hex = keys[i]; neighbors[i].snr = static_cast(i); + neighbors[i].rssi = -90 - i; neighbors[i].heard_secs_ago = static_cast(i) * 10U; neighbors[i].heard_unknown = false; neighbors[i].scopes = "DEN"; @@ -435,8 +438,8 @@ TEST(MQTTPayloadBuilder, NeighborsMessageRendersUnknownHeardAgeAsNull) { // A neighbour stamped before the clock was set has no usable age. The key // stays present and null so a consumer cannot read it as "heard just now". MQTTPayloadBuilder::NeighborsMessageEntry neighbors[2]; - neighbors[0] = {"0011223344556677", 9.75f, 42, "DEN", "responded"}; - neighbors[1] = {"8899AABBCCDDEEFF", -3.5f, 0, "DEN", "responded"}; + neighbors[0] = {"0011223344556677", 9.75f, -87, 42, "DEN", "responded"}; + neighbors[1] = {"8899AABBCCDDEEFF", -3.5f, -110, 0, "DEN", "responded"}; neighbors[1].heard_unknown = true; JsonDocument scratch; @@ -463,7 +466,7 @@ TEST(MQTTPayloadBuilder, NeighborsMessageUnknownHeardAgeFitsMeasuredWidth) { // Paced discovery measures each entry with a known UINT32_MAX age; a null age // must never serialize wider than what that reserved. MQTTPayloadBuilder::NeighborsMessageEntry measured = { - "0011223344556677", 9.75f, UINT32_MAX, "DEN", "responded"}; + "0011223344556677", 9.75f, -87, UINT32_MAX, "DEN", "responded"}; MQTTPayloadBuilder::NeighborsMessageEntry unknown = measured; unknown.heard_unknown = true; unknown.heard_secs_ago = 0;