fix(mqtt): omit SNR/RSSI instead of publishing placeholder values

The reconstruction-less fallback path in buildPacketJSON (queued packet
with no captured raw frame that cannot be re-serialized) published
fabricated SNR 12.5 / RSSI -65 that consumers could not distinguish
from real radio readings. It now passes NAN / MQTT_RSSI_UNKNOWN, and
buildPacketMessage omits the SNR/RSSI keys entirely when no measurement
exists, mirroring the existing score omission.

Ports the fix from ACETyr's PR #13 (0a037040) on mqtt-bridge-implementation.

Co-Authored-By: ACETyr <35745290+ACETyr@users.noreply.github.com>
This commit is contained in:
agessamanandACETyr committed 2026-09-26 10:46:11 -07:00
1 parent 91d9db9c7f
commit a91009e3dd
4 files changed
+93 -6

No files matched your search

+2 -2
View File
@@ -209,8 +209,8 @@ int MQTTMessageBuilder::buildPacketJSON(
packet_type, route_str,
packet->payload_len,
raw_hex,
12.5f, // SNR - using reasonable default
-65, // RSSI - using reasonable default
NAN, // SNR - unknown on this reconstruction-less fallback path
MQTT_RSSI_UNKNOWN, // RSSI - unknown on this reconstruction-less fallback path
NAN, // score - unknown on this reconstruction-less fallback path
hash_str,
has_path ? packet->path : nullptr,
+10 -4
View File
@@ -120,8 +120,6 @@ int MQTTPayloadBuilder::buildPacketMessage(
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);
snprintf(snr_str, sizeof(snr_str), "%.1f", snr);
snprintf(rssi_str, sizeof(rssi_str), "%d", rssi);
root["timestamp"] = timestamp;
root["hash"] = hash;
@@ -138,8 +136,16 @@ int MQTTPayloadBuilder::buildPacketMessage(
root["origin_id"] = origin_id;
if (direction && strcmp(direction, "rx") == 0) {
root["SNR"] = snr_str;
root["RSSI"] = rssi_str;
// Omit SNR/RSSI entirely when no measurement exists rather than publish
// a placeholder a consumer could mistake for a real reading.
if (!isnan(snr)) {
snprintf(snr_str, sizeof(snr_str), "%.1f", snr);
root["SNR"] = snr_str;
}
if (rssi != MQTT_RSSI_UNKNOWN) {
snprintf(rssi_str, sizeof(rssi_str), "%d", rssi);
root["RSSI"] = rssi_str;
}
if (!isnan(score)) {
snprintf(score_str, sizeof(score_str), "%d", static_cast<int>(score * 1000));
root["score"] = score_str;
+5
View File
@@ -7,6 +7,11 @@
// Mesh-independent JSON serialization core for MQTT publication payloads.
// MQTTMessageBuilder keeps the firmware-facing API and delegates these three
// deterministic contracts here so they can be exercised by native tests.
// RSSI sentinel for "no radio measurement": far outside the small negative ints
// a radio actually reports, so a real reading can never collide with it.
static const int MQTT_RSSI_UNKNOWN = INT16_MIN;
class MQTTPayloadBuilder {
public:
static int buildStatusMessage(
@@ -36,6 +36,16 @@ static int buildRepresentativePacket(JsonDocument& scratch, const char* directio
path_hash_size, 64, buffer, buffer_size);
}
static int buildPacketWithMetrics(JsonDocument& scratch, const char* direction,
float snr, int rssi, float score,
char* buffer, size_t buffer_size) {
return MQTTPayloadBuilder::buildPacketMessage(
scratch, "DEN Repeater", "0123456789ABCDEF", kTimestamp, direction,
"12:34:56", "18/07/2026", 42, 4, "D", 20, "A0B1C2D3",
snr, rssi, score, "89ABCDEF01234567", nullptr, 0, 0, 64,
buffer, buffer_size);
}
TEST(MQTTPayloadBuilder, MinimalStatusHasExactRequiredContract) {
JsonDocument scratch;
char buffer[768];
@@ -169,6 +179,72 @@ TEST(MQTTPayloadBuilder, RxPacketOmitsUnknownNanScore) {
EXPECT_FALSE(parsed["score"].is<JsonVariant>());
}
TEST(MQTTPayloadBuilder, RxPacketOmitsUnknownSnrAndRssi) {
JsonDocument scratch;
char buffer[2048];
int len = buildPacketWithMetrics(
scratch, "rx", NAN, MQTT_RSSI_UNKNOWN, NAN, buffer, sizeof(buffer));
ASSERT_GT(len, 0);
JsonDocument parsed;
ASSERT_FALSE(deserializeJson(parsed, buffer));
EXPECT_FALSE(parsed["SNR"].is<JsonVariant>());
EXPECT_FALSE(parsed["RSSI"].is<JsonVariant>());
EXPECT_FALSE(parsed["score"].is<JsonVariant>());
}
TEST(MQTTPayloadBuilder, RxPacketFormatsRealSnrAndRssi) {
JsonDocument scratch;
char buffer[2048];
int len = buildPacketWithMetrics(
scratch, "rx", 7.25f, -93, NAN, buffer, sizeof(buffer));
ASSERT_GT(len, 0);
JsonDocument parsed;
ASSERT_FALSE(deserializeJson(parsed, buffer));
EXPECT_STREQ("7.2", parsed["SNR"].as<const char*>());
EXPECT_STREQ("-93", parsed["RSSI"].as<const char*>());
EXPECT_FALSE(parsed["score"].is<JsonVariant>());
}
TEST(MQTTPayloadBuilder, RxPacketOmitsOnlyUnknownMetric) {
// SNR unknown, RSSI real: only the SNR key is omitted.
JsonDocument scratch;
char buffer[2048];
int len = buildPacketWithMetrics(
scratch, "rx", NAN, -93, NAN, buffer, sizeof(buffer));
ASSERT_GT(len, 0);
JsonDocument parsed;
ASSERT_FALSE(deserializeJson(parsed, buffer));
EXPECT_FALSE(parsed["SNR"].is<JsonVariant>());
EXPECT_STREQ("-93", parsed["RSSI"].as<const char*>());
// RSSI unknown, SNR real: only the RSSI key is omitted.
len = buildPacketWithMetrics(
scratch, "rx", 7.25f, MQTT_RSSI_UNKNOWN, NAN, buffer, sizeof(buffer));
ASSERT_GT(len, 0);
parsed.clear();
ASSERT_FALSE(deserializeJson(parsed, buffer));
EXPECT_STREQ("7.2", parsed["SNR"].as<const char*>());
EXPECT_FALSE(parsed["RSSI"].is<JsonVariant>());
}
TEST(MQTTPayloadBuilder, TxPacketOmitsSnrRssiAndScoreWithRealValues) {
JsonDocument scratch;
char buffer[2048];
int len = buildPacketWithMetrics(
scratch, "tx", 7.25f, -93, 0.5f, buffer, sizeof(buffer));
ASSERT_GT(len, 0);
JsonDocument parsed;
ASSERT_FALSE(deserializeJson(parsed, buffer));
EXPECT_FALSE(parsed["SNR"].is<JsonVariant>());
EXPECT_FALSE(parsed["RSSI"].is<JsonVariant>());
EXPECT_FALSE(parsed["score"].is<JsonVariant>());
}
TEST(MQTTPayloadBuilder, RawMessageHasExactContractAndEscapesData) {
char buffer[512];
JsonDocument doc;