diff --git a/src/helpers/MQTTMessageBuilder.cpp b/src/helpers/MQTTMessageBuilder.cpp index 062f52edd..afb53ef0d 100644 --- a/src/helpers/MQTTMessageBuilder.cpp +++ b/src/helpers/MQTTMessageBuilder.cpp @@ -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, diff --git a/src/helpers/MQTTPayloadBuilder.cpp b/src/helpers/MQTTPayloadBuilder.cpp index 265969ae1..47a7b2e8b 100644 --- a/src/helpers/MQTTPayloadBuilder.cpp +++ b/src/helpers/MQTTPayloadBuilder.cpp @@ -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(score * 1000)); root["score"] = score_str; diff --git a/src/helpers/MQTTPayloadBuilder.h b/src/helpers/MQTTPayloadBuilder.h index 7bca170db..ee2a7cc99 100644 --- a/src/helpers/MQTTPayloadBuilder.h +++ b/src/helpers/MQTTPayloadBuilder.h @@ -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( 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 37486fd27..181f0a86c 100644 --- a/test/test_mqtt_payload_builder/test_mqtt_payload_builder.cpp +++ b/test/test_mqtt_payload_builder/test_mqtt_payload_builder.cpp @@ -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()); } +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()); + EXPECT_FALSE(parsed["RSSI"].is()); + EXPECT_FALSE(parsed["score"].is()); +} + +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()); + EXPECT_STREQ("-93", parsed["RSSI"].as()); + EXPECT_FALSE(parsed["score"].is()); +} + +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()); + EXPECT_STREQ("-93", parsed["RSSI"].as()); + + // 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()); + EXPECT_FALSE(parsed["RSSI"].is()); +} + +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()); + EXPECT_FALSE(parsed["RSSI"].is()); + EXPECT_FALSE(parsed["score"].is()); +} + TEST(MQTTPayloadBuilder, RawMessageHasExactContractAndEscapesData) { char buffer[512]; JsonDocument doc;