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:
@@ -8,3 +8,24 @@ void init_device_ids(uint16_t &short_id, char *device_id, size_t device_id_len)
|
||||
short_id = (static_cast<uint16_t>(mac[4]) << 8) | mac[5];
|
||||
snprintf(device_id, device_id_len, "dd3-%04X", short_id);
|
||||
}
|
||||
|
||||
const char *rx_reject_reason_text(RxRejectReason reason) {
|
||||
switch (reason) {
|
||||
case RxRejectReason::CrcFail:
|
||||
return "crc_fail";
|
||||
case RxRejectReason::BadProtocol:
|
||||
return "bad_protocol_version";
|
||||
case RxRejectReason::WrongRole:
|
||||
return "wrong_role";
|
||||
case RxRejectReason::WrongPayloadType:
|
||||
return "wrong_payload_type";
|
||||
case RxRejectReason::LengthMismatch:
|
||||
return "length_mismatch";
|
||||
case RxRejectReason::DeviceIdMismatch:
|
||||
return "device_id_mismatch";
|
||||
case RxRejectReason::BatchIdMismatch:
|
||||
return "batch_id_mismatch";
|
||||
default:
|
||||
return "none";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,9 +118,9 @@ bool meterDataToJson(const MeterData &data, String &out_json) {
|
||||
if (data.err_lora_tx > 0) {
|
||||
doc["err_tx"] = data.err_lora_tx;
|
||||
}
|
||||
if (data.last_error != FaultType::None) {
|
||||
doc["err_last"] = static_cast<uint8_t>(data.last_error);
|
||||
}
|
||||
doc["err_last"] = static_cast<uint8_t>(data.last_error);
|
||||
doc["rx_reject"] = data.rx_reject_reason;
|
||||
doc["rx_reject_text"] = rx_reject_reason_text(static_cast<RxRejectReason>(data.rx_reject_reason));
|
||||
|
||||
out_json = "";
|
||||
size_t len = serializeJson(doc, out_json);
|
||||
@@ -162,6 +162,7 @@ bool jsonToMeterData(const String &json, MeterData &data) {
|
||||
data.err_decode = doc["err_d"] | 0;
|
||||
data.err_lora_tx = doc["err_tx"] | 0;
|
||||
data.last_error = static_cast<FaultType>(doc["err_last"] | 0);
|
||||
data.rx_reject_reason = static_cast<uint8_t>(doc["rx_reject"] | 0);
|
||||
|
||||
if (strlen(data.device_id) >= 8) {
|
||||
const char *suffix = data.device_id + strlen(data.device_id) - 4;
|
||||
@@ -196,9 +197,7 @@ bool meterBatchToJson(const MeterData *samples, size_t count, uint16_t batch_id,
|
||||
doc["err_tx"] = faults->lora_tx_fail;
|
||||
}
|
||||
}
|
||||
if (last_error != FaultType::None) {
|
||||
doc["err_last"] = static_cast<uint8_t>(last_error);
|
||||
}
|
||||
doc["err_last"] = static_cast<uint8_t>(last_error);
|
||||
if (!isnan(samples[count - 1].battery_voltage_v)) {
|
||||
char bat_buf[16];
|
||||
format_float_2(bat_buf, sizeof(bat_buf), samples[count - 1].battery_voltage_v);
|
||||
|
||||
@@ -3,6 +3,26 @@
|
||||
#include <SPI.h>
|
||||
#include <math.h>
|
||||
|
||||
static RxRejectReason g_last_rx_reject_reason = RxRejectReason::None;
|
||||
static uint32_t g_last_rx_reject_log_ms = 0;
|
||||
|
||||
static void note_reject(RxRejectReason reason) {
|
||||
g_last_rx_reject_reason = reason;
|
||||
if (SERIAL_DEBUG_MODE) {
|
||||
uint32_t now_ms = millis();
|
||||
if (now_ms - g_last_rx_reject_log_ms >= 1000) {
|
||||
g_last_rx_reject_log_ms = now_ms;
|
||||
Serial.printf("lora_rx: reject reason=%s\n", rx_reject_reason_text(reason));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RxRejectReason lora_get_last_rx_reject_reason() {
|
||||
RxRejectReason reason = g_last_rx_reject_reason;
|
||||
g_last_rx_reject_reason = RxRejectReason::None;
|
||||
return reason;
|
||||
}
|
||||
|
||||
static uint16_t crc16_ccitt(const uint8_t *data, size_t len) {
|
||||
uint16_t crc = 0xFFFF;
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
@@ -95,6 +115,7 @@ bool lora_receive(LoraPacket &pkt, uint32_t timeout_ms) {
|
||||
while (LoRa.available()) {
|
||||
LoRa.read();
|
||||
}
|
||||
note_reject(RxRejectReason::LengthMismatch);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -105,15 +126,18 @@ bool lora_receive(LoraPacket &pkt, uint32_t timeout_ms) {
|
||||
}
|
||||
|
||||
if (len < 7) {
|
||||
note_reject(RxRejectReason::LengthMismatch);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t crc_calc = crc16_ccitt(buffer, len - 2);
|
||||
uint16_t crc_rx = static_cast<uint16_t>(buffer[len - 2] << 8) | buffer[len - 1];
|
||||
if (crc_calc != crc_rx) {
|
||||
note_reject(RxRejectReason::CrcFail);
|
||||
return false;
|
||||
}
|
||||
if (buffer[0] != PROTOCOL_VERSION) {
|
||||
note_reject(RxRejectReason::BadProtocol);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -123,6 +147,7 @@ bool lora_receive(LoraPacket &pkt, uint32_t timeout_ms) {
|
||||
pkt.payload_type = static_cast<PayloadType>(buffer[4]);
|
||||
pkt.payload_len = len - 7;
|
||||
if (pkt.payload_len > LORA_MAX_PAYLOAD) {
|
||||
note_reject(RxRejectReason::LengthMismatch);
|
||||
return false;
|
||||
}
|
||||
memcpy(pkt.payload, &buffer[5], pkt.payload_len);
|
||||
|
||||
70
src/main.cpp
70
src/main.cpp
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include <limits.h>
|
||||
|
||||
static constexpr uint16_t kMagic = 0xDDB3;
|
||||
static constexpr uint8_t kSchema = 1;
|
||||
static constexpr uint8_t kSchema = 2;
|
||||
static constexpr uint8_t kFlags = 0x01;
|
||||
static constexpr size_t kMaxSamples = 30;
|
||||
|
||||
@@ -108,7 +108,7 @@ bool encode_batch(const BatchInput &in, uint8_t *out, size_t out_cap, size_t *ou
|
||||
return false;
|
||||
}
|
||||
size_t pos = 0;
|
||||
if (!ensure_capacity(20, out_cap, pos)) {
|
||||
if (!ensure_capacity(21, out_cap, pos)) {
|
||||
return false;
|
||||
}
|
||||
write_u16_le(&out[pos], kMagic);
|
||||
@@ -129,6 +129,7 @@ bool encode_batch(const BatchInput &in, uint8_t *out, size_t out_cap, size_t *ou
|
||||
out[pos++] = in.err_d;
|
||||
out[pos++] = in.err_tx;
|
||||
out[pos++] = in.err_last;
|
||||
out[pos++] = in.err_rx_reject;
|
||||
|
||||
if (!ensure_capacity(4, out_cap, pos)) {
|
||||
return false;
|
||||
@@ -183,7 +184,7 @@ bool decode_batch(const uint8_t *buf, size_t len, BatchInput *out) {
|
||||
return false;
|
||||
}
|
||||
size_t pos = 0;
|
||||
if (len < 20) {
|
||||
if (len < 21) {
|
||||
return false;
|
||||
}
|
||||
uint16_t magic = read_u16_le(&buf[pos]);
|
||||
@@ -207,6 +208,7 @@ bool decode_batch(const uint8_t *buf, size_t len, BatchInput *out) {
|
||||
out->err_d = buf[pos++];
|
||||
out->err_tx = buf[pos++];
|
||||
out->err_last = buf[pos++];
|
||||
out->err_rx_reject = buf[pos++];
|
||||
|
||||
if (out->n == 0 || out->n > kMaxSamples || out->dt_s == 0) {
|
||||
return false;
|
||||
@@ -283,6 +285,7 @@ bool payload_codec_self_test() {
|
||||
in.err_d = 1;
|
||||
in.err_tx = 3;
|
||||
in.err_last = 2;
|
||||
in.err_rx_reject = 1;
|
||||
in.energy_wh[0] = 100000;
|
||||
in.energy_wh[1] = 100001;
|
||||
in.energy_wh[2] = 100050;
|
||||
@@ -319,7 +322,8 @@ bool payload_codec_self_test() {
|
||||
|
||||
if (out.sender_id != in.sender_id || out.batch_id != in.batch_id || out.t_last != in.t_last ||
|
||||
out.dt_s != in.dt_s || out.n != in.n || out.battery_mV != in.battery_mV ||
|
||||
out.err_m != in.err_m || out.err_d != in.err_d || out.err_tx != in.err_tx || out.err_last != in.err_last) {
|
||||
out.err_m != in.err_m || out.err_d != in.err_d || out.err_tx != in.err_tx || out.err_last != in.err_last ||
|
||||
out.err_rx_reject != in.err_rx_reject) {
|
||||
Serial.println("payload_codec_self_test: header mismatch");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ struct BatchInput {
|
||||
uint8_t err_d;
|
||||
uint8_t err_tx;
|
||||
uint8_t err_last;
|
||||
uint8_t err_rx_reject;
|
||||
uint32_t energy_wh[30];
|
||||
int16_t p1_w[30];
|
||||
int16_t p2_w[30];
|
||||
|
||||
@@ -352,6 +352,8 @@ static String render_sender_block(const SenderStatus &status) {
|
||||
s += " err_tx:" + String(status.last_data.err_lora_tx);
|
||||
s += " err_last:" + String(static_cast<uint8_t>(status.last_data.last_error));
|
||||
s += " (" + String(fault_text(status.last_data.last_error)) + ")";
|
||||
s += " rx_reject:" + String(status.last_data.rx_reject_reason);
|
||||
s += " (" + String(rx_reject_reason_text(static_cast<RxRejectReason>(status.last_data.rx_reject_reason))) + ")";
|
||||
}
|
||||
s += format_faults(idx);
|
||||
s += "<br>";
|
||||
@@ -589,7 +591,7 @@ static void handle_sender() {
|
||||
html += "<h3>Last batch (" + String(g_last_batch_count[i]) + " samples)</h3>";
|
||||
html += "<table border='1' cellspacing='0' cellpadding='3'>";
|
||||
html += "<tr><th>#</th><th>ts</th><th>e_kwh</th><th>p_w</th><th>p1_w</th><th>p2_w</th><th>p3_w</th>";
|
||||
html += "<th>bat_v</th><th>bat_pct</th><th>rssi</th><th>snr</th><th>err_tx</th><th>err_last</th></tr>";
|
||||
html += "<th>bat_v</th><th>bat_pct</th><th>rssi</th><th>snr</th><th>err_tx</th><th>err_last</th><th>rx_reject</th></tr>";
|
||||
for (uint8_t r = 0; r < g_last_batch_count[i]; ++r) {
|
||||
const MeterData &d = g_last_batch[i][r];
|
||||
html += "<tr>";
|
||||
@@ -606,6 +608,8 @@ static void handle_sender() {
|
||||
html += "<td>" + String(d.link_snr_db, 1) + "</td>";
|
||||
html += "<td>" + String(d.err_lora_tx) + "</td>";
|
||||
html += "<td>" + String(static_cast<uint8_t>(d.last_error)) + " (" + String(fault_text(d.last_error)) + ")</td>";
|
||||
html += "<td>" + String(d.rx_reject_reason) + " (" +
|
||||
String(rx_reject_reason_text(static_cast<RxRejectReason>(d.rx_reject_reason))) + ")</td>";
|
||||
html += "</tr>";
|
||||
}
|
||||
html += "</table>";
|
||||
@@ -631,6 +635,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, 4=TimeSync).</li>";
|
||||
html += "<li>rx_reject: last RX reject reason (0=None, 1=crc_fail, 2=bad_protocol_version, 3=wrong_role, 4=wrong_payload_type, 5=length_mismatch, 6=device_id_mismatch, 7=batch_id_mismatch).</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>";
|
||||
|
||||
Reference in New Issue
Block a user