feat(mqtt): include RSSI in neighbor data for improved metrics

This commit is contained in:
agessaman
2026-08-07 21:14:49 -07:00
parent 3b11540e15
commit c026c24e07
12 changed files with 85 additions and 45 deletions
Submodule .wt-station-g3-prod added at bfc43e94f8
+3 -1
View File
@@ -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: <next>/<last>` —
{
"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: <next>/<last>` —
{
"pubkey": "8899AABBCCDDEEFF...",
"snr": 12.5,
"rssi": -95,
"heard_secs_ago": null,
"scopes": "DEN",
"status": "responded"
+4 -4
View File
@@ -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;
+21 -11
View File
@@ -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;
+7 -3
View File
@@ -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);
+21 -11
View File
@@ -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;
+7 -3
View File
@@ -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;
+3 -1
View File
@@ -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;
}
+2
View File
@@ -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
+1
View File
@@ -220,6 +220,7 @@ static void addNeighborsMessageEntry(
JsonObject nb = arr.add<JsonObject>();
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 {
+1
View File
@@ -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;
@@ -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<const char*>());
EXPECT_FLOAT_EQ(9.75f, arr[0]["snr"].as<float>());
EXPECT_EQ(-87, arr[0]["rssi"].as<int>());
EXPECT_EQ(42U, arr[0]["heard_secs_ago"].as<uint32_t>());
EXPECT_STREQ("DEN,APRS", arr[0]["scopes"].as<const char*>());
EXPECT_STREQ("active", arr[0]["status"].as<const char*>());
EXPECT_STREQ("8899AABBCCDDEEFF", arr[1]["pubkey"].as<const char*>());
EXPECT_EQ(-110, arr[1]["rssi"].as<int>());
EXPECT_STREQ("", arr[1]["scopes"].as<const char*>());
EXPECT_STREQ("stale", arr[1]["status"].as<const char*>());
}
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<float>(i), UINT32_MAX,
keys[i], static_cast<float>(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<float>(i);
neighbors[i].rssi = -90 - i;
neighbors[i].heard_secs_ago = static_cast<uint32_t>(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;