Harden receiver ingest against unknown senders

This commit is contained in:
2026-02-17 00:30:06 +01:00
parent ee849433c8
commit 16aad906a0
4 changed files with 76 additions and 40 deletions

View File

@@ -15,7 +15,8 @@ enum class RxRejectReason : uint8_t {
InvalidMsgKind = 2,
LengthMismatch = 3,
DeviceIdMismatch = 4,
BatchIdMismatch = 5
BatchIdMismatch = 5,
UnknownSender = 6
};
struct FaultCounters {

View File

@@ -21,6 +21,8 @@ const char *rx_reject_reason_text(RxRejectReason reason) {
return "device_id_mismatch";
case RxRejectReason::BatchIdMismatch:
return "batch_id_mismatch";
case RxRejectReason::UnknownSender:
return "unknown_sender";
default:
return "none";
}

View File

@@ -96,6 +96,8 @@ static uint32_t g_sender_sleep_ms = 0;
static uint32_t g_sender_power_log_ms = 0;
static RxRejectReason g_sender_rx_reject_reason = RxRejectReason::None;
static uint32_t g_sender_rx_reject_log_ms = 0;
static RxRejectReason g_receiver_rx_reject_reason = RxRejectReason::None;
static uint32_t g_receiver_rx_reject_log_ms = 0;
static MeterData g_last_meter_data = {};
static bool g_last_meter_valid = false;
static uint32_t g_last_meter_rx_ms = 0;
@@ -421,6 +423,18 @@ static void sender_note_rx_reject(RxRejectReason reason, const char *context) {
}
}
static void receiver_note_rx_reject(RxRejectReason reason, const char *context) {
if (reason == RxRejectReason::None) {
return;
}
g_receiver_rx_reject_reason = reason;
uint32_t now_ms = millis();
if (SERIAL_DEBUG_MODE && now_ms - g_receiver_rx_reject_log_ms >= 1000) {
g_receiver_rx_reject_log_ms = now_ms;
serial_debug_printf("rx_reject: %s reason=%s", context, rx_reject_reason_text(reason));
}
}
static BatchBuffer *batch_queue_peek() {
if (g_batch_count == 0) {
return nullptr;
@@ -1419,8 +1433,31 @@ static void receiver_loop() {
}
}
bool duplicate = sender_idx >= 0 && g_last_batch_id_rx[sender_idx] == batch_id;
if (sender_idx >= 0) {
if (sender_idx < 0) {
receiver_note_rx_reject(RxRejectReason::UnknownSender, "batch");
note_fault(g_receiver_faults, g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms, FaultType::Decode);
display_set_last_error(g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms);
serial_debug_printf("batch: reject unknown_sender short_id=%04X sender_id=%u batch_id=%u",
pkt.device_id_short,
static_cast<unsigned>(batch.sender_id),
static_cast<unsigned>(batch_id));
goto receiver_loop_done;
}
uint16_t expected_sender_id = static_cast<uint16_t>(sender_idx + 1);
if (batch.sender_id != expected_sender_id) {
receiver_note_rx_reject(RxRejectReason::DeviceIdMismatch, "batch");
note_fault(g_receiver_faults, g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms, FaultType::Decode);
display_set_last_error(g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms);
serial_debug_printf("batch: reject device_id_mismatch short_id=%04X sender_id=%u expected=%u batch_id=%u",
pkt.device_id_short,
static_cast<unsigned>(batch.sender_id),
static_cast<unsigned>(expected_sender_id),
static_cast<unsigned>(batch_id));
goto receiver_loop_done;
}
bool duplicate = g_last_batch_id_rx[sender_idx] == batch_id;
SenderStatus &status = g_sender_statuses[sender_idx];
if (status.rx_batches_total < UINT32_MAX) {
status.rx_batches_total++;
@@ -1435,14 +1472,12 @@ static void receiver_loop() {
}
status.rx_last_duplicate_ts_utc = duplicate_ts;
}
}
send_batch_ack(batch_id, batch.n);
if (duplicate) {
goto receiver_loop_done;
}
if (sender_idx >= 0) {
g_last_batch_id_rx[sender_idx] = batch_id;
}
if (batch.n == 0) {
goto receiver_loop_done;
}
@@ -1520,7 +1555,6 @@ static void receiver_loop() {
goto receiver_loop_done;
}
if (sender_idx >= 0) {
web_server_set_last_batch(static_cast<uint8_t>(sender_idx), samples, count);
for (size_t s = 0; s < count; ++s) {
mqtt_publish_state(samples[s]);
@@ -1539,7 +1573,6 @@ static void receiver_loop() {
publish_faults_if_needed(samples[count - 1].device_id, g_sender_faults_remote[sender_idx], g_sender_faults_remote_published[sender_idx],
g_sender_last_error_remote[sender_idx], g_sender_last_error_remote_published[sender_idx],
g_sender_last_error_remote_utc[sender_idx], g_sender_last_error_remote_ms[sender_idx]);
}
} else if (decode_error) {
note_fault(g_receiver_faults, g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms, FaultType::Decode);
display_set_last_error(g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms);

View File

@@ -698,7 +698,7 @@ static void handle_manual() {
html += "<li>RSSI/SNR: LoRa link quality from last packet.</li>";
html += "<li>err_tx: sender-side LoRa TX error counter.</li>";
html += "<li>err_last: last error code (0=None, 1=MeterRead, 2=Decode, 3=LoraTx).</li>";
html += "<li>rx_reject: last RX reject reason (0=None, 1=crc_fail, 2=invalid_msg_kind, 3=length_mismatch, 4=device_id_mismatch, 5=batch_id_mismatch).</li>";
html += "<li>rx_reject: last RX reject reason (0=None, 1=crc_fail, 2=invalid_msg_kind, 3=length_mismatch, 4=device_id_mismatch, 5=batch_id_mismatch, 6=unknown_sender).</li>";
html += "<li>faults m/d/tx: receiver-side counters (meter read fails, decode fails, LoRa TX fails).</li>";
html += "<li>faults last: last receiver-side error code (same mapping as err_last).</li>";
html += "</ul>";