Add RX reject reasons to telemetry and UI

BACKWARD-INCOMPATIBLE: MeterBatch schema bumped to v2 with err_rx_reject.
- Track and log RX reject reasons (CRC/protocol/role/payload/length/id/batch)
- Include rx_reject in sender telemetry JSON and receiver web UI
- Add lora_receive reject reason logging under SERIAL_DEBUG_MODE
This commit is contained in:
2026-02-04 01:01:49 +01:00
parent 0e7214d606
commit 1024aa3dd0
10 changed files with 143 additions and 27 deletions

View File

@@ -105,6 +105,8 @@ static uint32_t g_sender_rx_window_ms = 0;
static uint32_t g_sender_sleep_ms = 0;
static uint32_t g_sender_power_log_ms = 0;
static uint8_t g_sender_timesync_mode = 0;
static RxRejectReason g_sender_rx_reject_reason = RxRejectReason::None;
static uint32_t g_sender_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;
@@ -253,6 +255,18 @@ static void receiver_note_timesync_drift(uint8_t sender_idx, uint32_t sender_ts_
}
}
static void sender_note_rx_reject(RxRejectReason reason, const char *context) {
if (reason == RxRejectReason::None) {
return;
}
g_sender_rx_reject_reason = reason;
uint32_t now_ms = millis();
if (SERIAL_DEBUG_MODE && now_ms - g_sender_rx_reject_log_ms >= 1000) {
g_sender_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;
@@ -548,6 +562,7 @@ static bool send_inflight_batch(uint32_t ts_for_display) {
input.err_d = g_sender_faults.decode_fail > 255 ? 255 : static_cast<uint8_t>(g_sender_faults.decode_fail);
input.err_tx = g_sender_faults.lora_tx_fail > 255 ? 255 : static_cast<uint8_t>(g_sender_faults.lora_tx_fail);
input.err_last = static_cast<uint8_t>(g_sender_last_error);
input.err_rx_reject = static_cast<uint8_t>(g_sender_rx_reject_reason);
for (uint8_t i = 0; i < g_inflight_count; ++i) {
input.energy_wh[i] = kwh_to_wh_from_float(g_inflight_samples[i].energy_total_kwh);
if (!float_to_i16_w(g_inflight_samples[i].phase_power_w[0], input.p1_w[i]) ||
@@ -824,6 +839,7 @@ static void sender_loop() {
}
data.battery_voltage_v = g_last_battery_voltage_v;
data.battery_percent = g_last_battery_percent;
data.rx_reject_reason = static_cast<uint8_t>(g_sender_rx_reject_reason);
uint32_t now_utc = time_get_utc();
data.ts_utc = now_utc > 0 ? now_utc : millis() / 1000;
@@ -852,7 +868,15 @@ static void sender_loop() {
if (SERIAL_DEBUG_MODE) {
g_sender_rx_window_ms += rx_elapsed;
}
if (got_ack && ack_pkt.payload_type == PayloadType::Ack && ack_pkt.payload_len >= 6 && ack_pkt.role == DeviceRole::Receiver) {
if (!got_ack) {
sender_note_rx_reject(lora_get_last_rx_reject_reason(), "ack");
} else if (ack_pkt.role != DeviceRole::Receiver) {
sender_note_rx_reject(RxRejectReason::WrongRole, "ack");
} else if (ack_pkt.payload_type != PayloadType::Ack) {
sender_note_rx_reject(RxRejectReason::WrongPayloadType, "ack");
} else if (ack_pkt.payload_len < 6) {
sender_note_rx_reject(RxRejectReason::LengthMismatch, "ack");
} else {
uint16_t ack_id = read_u16_le(ack_pkt.payload);
uint16_t ack_sender = read_u16_le(&ack_pkt.payload[2]);
uint16_t ack_receiver = read_u16_le(&ack_pkt.payload[4]);
@@ -861,9 +885,16 @@ static void sender_loop() {
g_last_acked_batch_id = ack_id;
serial_debug_printf("ack: rx ok batch_id=%u", ack_id);
finish_inflight_batch();
} else if (SERIAL_DEBUG_MODE) {
serial_debug_printf("ack: reject batch_id=%u sender=%u receiver=%u exp_batch=%u exp_sender=%u",
ack_id, ack_sender, ack_receiver, g_last_sent_batch_id, g_short_id);
} else {
if (ack_sender != g_short_id || ack_receiver != ack_pkt.device_id_short) {
sender_note_rx_reject(RxRejectReason::DeviceIdMismatch, "ack");
} else if (ack_id != g_last_sent_batch_id) {
sender_note_rx_reject(RxRejectReason::BatchIdMismatch, "ack");
}
if (SERIAL_DEBUG_MODE) {
serial_debug_printf("ack: reject batch_id=%u sender=%u receiver=%u exp_batch=%u exp_sender=%u",
ack_id, ack_sender, ack_receiver, g_last_sent_batch_id, g_short_id);
}
}
}
}
@@ -878,17 +909,27 @@ static void sender_loop() {
if (SERIAL_DEBUG_MODE) {
g_sender_rx_window_ms += rx_elapsed;
}
if (got && rx.payload_type == PayloadType::TimeSync) {
if (time_handle_timesync_payload(rx.payload, rx.payload_len)) {
g_sender_last_timesync_rx_ms = now_ms;
if (g_sender_timesync_error) {
g_sender_timesync_error = false;
display_set_last_error(FaultType::None, 0, 0);
}
serial_debug_printf("timesync: rx ok window_ms=%lu", static_cast<unsigned long>(window_ms));
if (!got) {
sender_note_rx_reject(lora_get_last_rx_reject_reason(), "timesync");
if (SERIAL_DEBUG_MODE) {
serial_debug_printf("timesync: rx miss window_ms=%lu", static_cast<unsigned long>(window_ms));
}
} else if (rx.role != DeviceRole::Receiver) {
sender_note_rx_reject(RxRejectReason::WrongRole, "timesync");
} else if (rx.payload_type != PayloadType::TimeSync) {
sender_note_rx_reject(RxRejectReason::WrongPayloadType, "timesync");
} else if (time_handle_timesync_payload(rx.payload, rx.payload_len)) {
g_sender_last_timesync_rx_ms = now_ms;
if (g_sender_timesync_error) {
g_sender_timesync_error = false;
display_set_last_error(FaultType::None, 0, 0);
}
serial_debug_printf("timesync: rx ok window_ms=%lu", static_cast<unsigned long>(window_ms));
} else {
sender_note_rx_reject(RxRejectReason::LengthMismatch, "timesync");
if (SERIAL_DEBUG_MODE) {
serial_debug_printf("timesync: rx miss window_ms=%lu", static_cast<unsigned long>(window_ms));
}
} else if (SERIAL_DEBUG_MODE) {
serial_debug_printf("timesync: rx miss window_ms=%lu", static_cast<unsigned long>(window_ms));
}
}
uint32_t timesync_age_ms = (g_sender_last_timesync_rx_ms > 0) ? (now_ms - g_sender_last_timesync_rx_ms)
@@ -1063,6 +1104,7 @@ static void receiver_loop() {
data.err_decode = batch.err_d;
data.err_lora_tx = batch.err_tx;
data.last_error = static_cast<FaultType>(batch.err_last);
data.rx_reject_reason = batch.err_rx_reject;
sd_logger_log_sample(data, (s + 1 == count) && data.last_error != FaultType::None);
}