mirror of
https://github.com/agessaman/MeshCore.git
synced 2026-08-27 22:34:14 +00:00
feat(mqtt): add neighbors JSON payload builder (host-tested)
Add buildNeighborsMessage to the pure MQTTPayloadBuilder core and a thin delegating wrapper + NeighborsMessageEntry alias on MQTTMessageBuilder, so the neighbors topic is built by the same firmware-facing API as status/ packet/raw while the layout logic stays exercisable by native tests. The document is bounded to the publish buffer: entries arrive ordered most- to least-useful and the tail is dropped once the next entry would overflow, so a fixed PSRAM buffer can never be handed truncated JSON. Uses ArduinoJson v7 idioms (.to<JsonObject>()/.add<JsonObject>()) to stay warning-clean under -Werror, unlike the deprecated createNested* forms. Adds three test_mqtt_payload_builder cases: self+entry round-trip, empty table / null scopes, and bounded-growth tail-drop under a tight buffer.
This commit is contained in:
@@ -102,6 +102,22 @@ int MQTTMessageBuilder::buildRawMessage(
|
||||
origin, origin_id, timestamp, raw, buffer, buffer_size);
|
||||
}
|
||||
|
||||
int MQTTMessageBuilder::buildNeighborsMessage(
|
||||
JsonDocument& doc,
|
||||
const char* origin,
|
||||
const char* origin_id,
|
||||
const char* timestamp,
|
||||
const char* self_scopes,
|
||||
const NeighborsMessageEntry* neighbors,
|
||||
int neighbor_count,
|
||||
char* buffer,
|
||||
size_t buffer_size
|
||||
) {
|
||||
return MQTTPayloadBuilder::buildNeighborsMessage(
|
||||
doc, origin, origin_id, timestamp, self_scopes, neighbors, neighbor_count,
|
||||
buffer, buffer_size);
|
||||
}
|
||||
|
||||
int MQTTMessageBuilder::buildPacketJSON(
|
||||
JsonDocument& doc,
|
||||
mesh::Packet* packet,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "MeshCore.h"
|
||||
#include <ArduinoJson.h>
|
||||
#include "MQTTPayloadBuilder.h"
|
||||
#include <Mesh.h>
|
||||
#include <Timezone.h>
|
||||
|
||||
@@ -153,6 +154,22 @@ public:
|
||||
size_t buffer_size
|
||||
);
|
||||
|
||||
// Neighbors table entry + JSON builder. The layout logic lives in the pure,
|
||||
// host-tested MQTTPayloadBuilder; this is the firmware-facing alias/delegate,
|
||||
// matching the status/packet/raw builders.
|
||||
using NeighborsMessageEntry = MQTTPayloadBuilder::NeighborsMessageEntry;
|
||||
static int buildNeighborsMessage(
|
||||
JsonDocument& doc,
|
||||
const char* origin,
|
||||
const char* origin_id,
|
||||
const char* timestamp,
|
||||
const char* self_scopes,
|
||||
const NeighborsMessageEntry* neighbors,
|
||||
int neighbor_count,
|
||||
char* buffer,
|
||||
size_t buffer_size
|
||||
);
|
||||
|
||||
/**
|
||||
* Convert packet to JSON message
|
||||
*
|
||||
|
||||
@@ -184,3 +184,48 @@ int MQTTPayloadBuilder::buildRawMessage(
|
||||
|
||||
return serializeComplete(root, buffer, buffer_size);
|
||||
}
|
||||
|
||||
int MQTTPayloadBuilder::buildNeighborsMessage(
|
||||
JsonDocument& doc,
|
||||
const char* origin,
|
||||
const char* origin_id,
|
||||
const char* timestamp,
|
||||
const char* self_scopes,
|
||||
const NeighborsMessageEntry* neighbors,
|
||||
int neighbor_count,
|
||||
char* buffer,
|
||||
size_t buffer_size
|
||||
) {
|
||||
if (!buffer || buffer_size == 0) return 0;
|
||||
|
||||
doc.clear();
|
||||
JsonObject root = doc.to<JsonObject>();
|
||||
root["timestamp"] = timestamp;
|
||||
root["origin"] = origin;
|
||||
root["origin_id"] = origin_id;
|
||||
|
||||
JsonObject self = root["self"].to<JsonObject>();
|
||||
self["scopes"] = self_scopes ? self_scopes : "";
|
||||
|
||||
JsonArray arr = root["neighbors"].to<JsonArray>();
|
||||
if (measureJson(root) >= buffer_size) return 0;
|
||||
|
||||
for (int i = 0; i < neighbor_count; i++) {
|
||||
JsonObject nb = arr.add<JsonObject>();
|
||||
nb["pubkey"] = neighbors[i].pubkey_hex;
|
||||
nb["snr"] = neighbors[i].snr;
|
||||
nb["heard_secs_ago"] = neighbors[i].heard_secs_ago;
|
||||
nb["scopes"] = neighbors[i].scopes ? neighbors[i].scopes : "";
|
||||
nb["status"] = neighbors[i].status;
|
||||
|
||||
// Entries arrive ordered most- to least-useful. Stop as soon as the next
|
||||
// one would fill the fixed publish buffer, dropping the remaining tail so
|
||||
// document growth stays bounded.
|
||||
if (measureJson(root) >= buffer_size) {
|
||||
arr.remove(arr.size() - 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return serializeComplete(root, buffer, buffer_size);
|
||||
}
|
||||
|
||||
@@ -68,5 +68,28 @@ public:
|
||||
char* buffer,
|
||||
size_t buffer_size
|
||||
);
|
||||
|
||||
struct NeighborsMessageEntry {
|
||||
const char* pubkey_hex;
|
||||
float snr;
|
||||
uint32_t heard_secs_ago;
|
||||
const char* scopes;
|
||||
const char* status;
|
||||
};
|
||||
|
||||
// Build neighbors-table JSON for the meshcore/{iata}/{device}/neighbors topic.
|
||||
// Callers order entries most- to least-useful; document growth is bounded to
|
||||
// buffer_size and the remaining tail is dropped once the next entry won't fit.
|
||||
static int buildNeighborsMessage(
|
||||
JsonDocument& doc,
|
||||
const char* origin,
|
||||
const char* origin_id,
|
||||
const char* timestamp,
|
||||
const char* self_scopes,
|
||||
const NeighborsMessageEntry* neighbors,
|
||||
int neighbor_count,
|
||||
char* buffer,
|
||||
size_t buffer_size
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -227,6 +227,87 @@ TEST(MQTTPayloadBuilder, MaximumRepresentativePacketAndRawPayloadsRemainValid) {
|
||||
EXPECT_EQ(510U, strlen(parsed_raw["data"].as<const char*>()));
|
||||
}
|
||||
|
||||
TEST(MQTTPayloadBuilder, NeighborsMessageRoundTripsSelfAndEntries) {
|
||||
MQTTPayloadBuilder::NeighborsMessageEntry neighbors[] = {
|
||||
{"0011223344556677", 9.75f, 42, "DEN,APRS", "active"},
|
||||
{"8899AABBCCDDEEFF", -3.5f, 3600, "", "stale"},
|
||||
};
|
||||
|
||||
JsonDocument scratch;
|
||||
char buffer[1024];
|
||||
int len = MQTTPayloadBuilder::buildNeighborsMessage(
|
||||
scratch, "DEN Repeater", "0123456789ABCDEF", kTimestamp, "DEN,APRS",
|
||||
neighbors, 2, buffer, sizeof(buffer));
|
||||
|
||||
ASSERT_GT(len, 0);
|
||||
EXPECT_EQ(static_cast<size_t>(len), strlen(buffer));
|
||||
JsonDocument parsed;
|
||||
ASSERT_FALSE(deserializeJson(parsed, buffer));
|
||||
EXPECT_STREQ("2026-07-18T12:34:56.123456+00:00", parsed["timestamp"].as<const char*>());
|
||||
EXPECT_STREQ("DEN Repeater", parsed["origin"].as<const char*>());
|
||||
EXPECT_STREQ("0123456789ABCDEF", parsed["origin_id"].as<const char*>());
|
||||
EXPECT_STREQ("DEN,APRS", parsed["self"]["scopes"].as<const char*>());
|
||||
|
||||
JsonArray arr = parsed["neighbors"].as<JsonArray>();
|
||||
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(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_STREQ("", arr[1]["scopes"].as<const char*>());
|
||||
EXPECT_STREQ("stale", arr[1]["status"].as<const char*>());
|
||||
}
|
||||
|
||||
TEST(MQTTPayloadBuilder, NeighborsMessageHandlesEmptyTableAndNullScopes) {
|
||||
JsonDocument scratch;
|
||||
char buffer[256];
|
||||
int len = MQTTPayloadBuilder::buildNeighborsMessage(
|
||||
scratch, "node", "id", kTimestamp, nullptr, nullptr, 0,
|
||||
buffer, sizeof(buffer));
|
||||
|
||||
ASSERT_GT(len, 0);
|
||||
JsonDocument parsed;
|
||||
ASSERT_FALSE(deserializeJson(parsed, buffer));
|
||||
EXPECT_STREQ("", parsed["self"]["scopes"].as<const char*>());
|
||||
JsonArray arr = parsed["neighbors"].as<JsonArray>();
|
||||
ASSERT_TRUE(arr.isNull() == false);
|
||||
EXPECT_EQ(0U, arr.size());
|
||||
}
|
||||
|
||||
TEST(MQTTPayloadBuilder, NeighborsMessageDropsTailWhenBufferFills) {
|
||||
// Twenty entries far exceed a tight buffer; the builder must emit a prefix
|
||||
// that still parses as complete JSON rather than truncating mid-document.
|
||||
MQTTPayloadBuilder::NeighborsMessageEntry neighbors[20];
|
||||
static char keys[20][17];
|
||||
for (int i = 0; i < 20; i++) {
|
||||
snprintf(keys[i], sizeof(keys[i]), "%016X", i);
|
||||
neighbors[i].pubkey_hex = keys[i];
|
||||
neighbors[i].snr = static_cast<float>(i);
|
||||
neighbors[i].heard_secs_ago = static_cast<uint32_t>(i) * 10U;
|
||||
neighbors[i].scopes = "DEN";
|
||||
neighbors[i].status = "active";
|
||||
}
|
||||
|
||||
JsonDocument scratch;
|
||||
char buffer[512];
|
||||
int len = MQTTPayloadBuilder::buildNeighborsMessage(
|
||||
scratch, "node", "id", kTimestamp, "DEN", neighbors, 20,
|
||||
buffer, sizeof(buffer));
|
||||
|
||||
ASSERT_GT(len, 0);
|
||||
EXPECT_LT(static_cast<size_t>(len), sizeof(buffer));
|
||||
JsonDocument parsed;
|
||||
ASSERT_FALSE(deserializeJson(parsed, buffer));
|
||||
JsonArray arr = parsed["neighbors"].as<JsonArray>();
|
||||
ASSERT_FALSE(arr.isNull());
|
||||
EXPECT_GT(arr.size(), 0U);
|
||||
EXPECT_LT(arr.size(), 20U);
|
||||
// Kept entries are the head of the input, in order.
|
||||
EXPECT_STREQ(keys[0], arr[0]["pubkey"].as<const char*>());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
Reference in New Issue
Block a user