Decouple sender meter reads and harden meter RX robustness

This commit is contained in:
2026-02-13 10:44:26 +01:00
parent 9d5f5ed513
commit 7540af7d71
2 changed files with 147 additions and 30 deletions

View File

@@ -17,6 +17,9 @@
#ifdef ARDUINO_ARCH_ESP32 #ifdef ARDUINO_ARCH_ESP32
#include <esp_task_wdt.h> #include <esp_task_wdt.h>
#include <esp_system.h> #include <esp_system.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#endif #endif
static DeviceRole g_role = DeviceRole::Sender; static DeviceRole g_role = DeviceRole::Sender;
@@ -98,14 +101,26 @@ static uint32_t g_sender_rx_reject_log_ms = 0;
static MeterData g_last_meter_data = {}; static MeterData g_last_meter_data = {};
static bool g_last_meter_valid = false; static bool g_last_meter_valid = false;
static uint32_t g_last_meter_rx_ms = 0; static uint32_t g_last_meter_rx_ms = 0;
static uint32_t g_last_meter_seq = 0;
static uint32_t g_last_meter_seq_used = 0;
static uint32_t g_meter_stale_seconds = 0; static uint32_t g_meter_stale_seconds = 0;
static bool g_time_acquired = false; static bool g_time_acquired = false;
static uint32_t g_last_sync_request_ms = 0; static uint32_t g_last_sync_request_ms = 0;
static uint32_t g_build_attempts = 0; static uint32_t g_build_attempts = 0;
static uint32_t g_build_valid = 0; static uint32_t g_build_valid = 0;
static uint32_t g_build_invalid = 0; static uint32_t g_build_invalid = 0;
static constexpr uint32_t METER_SAMPLE_MAX_AGE_MS = 15000;
struct MeterSampleEvent {
MeterData data;
uint32_t rx_ms;
};
#ifdef ARDUINO_ARCH_ESP32
static QueueHandle_t g_meter_sample_queue = nullptr;
static TaskHandle_t g_meter_reader_task = nullptr;
static bool g_meter_reader_task_running = false;
static constexpr UBaseType_t METER_SAMPLE_QUEUE_LEN = 8;
static constexpr uint32_t METER_READER_TASK_STACK_WORDS = 4096;
static constexpr UBaseType_t METER_READER_TASK_PRIORITY = 2;
static constexpr BaseType_t METER_READER_TASK_CORE = 0;
#endif
static constexpr uint32_t DEBUG_FORCED_REBOOT_INTERVAL_MS = 3UL * 60UL * 60UL * 1000UL; static constexpr uint32_t DEBUG_FORCED_REBOOT_INTERVAL_MS = 3UL * 60UL * 60UL * 1000UL;
enum class TxBuildError : uint8_t { enum class TxBuildError : uint8_t {
@@ -130,6 +145,117 @@ static void serial_debug_printf(const char *fmt, ...) {
Serial.println(buf); Serial.println(buf);
} }
static void set_last_meter_sample(const MeterData &parsed, uint32_t rx_ms) {
g_last_meter_data = parsed;
g_last_meter_valid = true;
g_last_meter_rx_ms = rx_ms;
g_meter_stale_seconds = 0;
}
static bool parse_meter_frame_sample(const char *frame, size_t frame_len, MeterData &parsed) {
parsed = {};
parsed.energy_total_kwh = NAN;
parsed.total_power_w = NAN;
parsed.phase_power_w[0] = NAN;
parsed.phase_power_w[1] = NAN;
parsed.phase_power_w[2] = NAN;
parsed.valid = false;
return meter_parse_frame(frame, frame_len, parsed);
}
#ifdef ARDUINO_ARCH_ESP32
static void meter_queue_push_latest(const MeterSampleEvent &event) {
if (!g_meter_sample_queue) {
return;
}
if (xQueueSend(g_meter_sample_queue, &event, 0) == pdTRUE) {
return;
}
MeterSampleEvent dropped = {};
xQueueReceive(g_meter_sample_queue, &dropped, 0);
if (xQueueSend(g_meter_sample_queue, &event, 0) != pdTRUE && SERIAL_DEBUG_MODE) {
serial_debug_printf("meter: queue push failed");
}
}
static void meter_reader_task_entry(void *arg) {
(void)arg;
for (;;) {
const char *frame = nullptr;
size_t frame_len = 0;
if (!meter_poll_frame(frame, frame_len)) {
vTaskDelay(pdMS_TO_TICKS(5));
continue;
}
MeterData parsed = {};
if (parse_meter_frame_sample(frame, frame_len, parsed)) {
MeterSampleEvent event = {};
event.data = parsed;
event.rx_ms = millis();
meter_queue_push_latest(event);
}
}
}
static bool meter_reader_start() {
if (g_meter_reader_task_running) {
return true;
}
if (!g_meter_sample_queue) {
g_meter_sample_queue = xQueueCreate(METER_SAMPLE_QUEUE_LEN, sizeof(MeterSampleEvent));
if (!g_meter_sample_queue) {
if (SERIAL_DEBUG_MODE) {
serial_debug_printf("meter: queue alloc failed");
}
return false;
}
}
BaseType_t rc = xTaskCreatePinnedToCore(
meter_reader_task_entry,
"meter_reader",
METER_READER_TASK_STACK_WORDS,
nullptr,
METER_READER_TASK_PRIORITY,
&g_meter_reader_task,
METER_READER_TASK_CORE);
if (rc != pdPASS) {
if (SERIAL_DEBUG_MODE) {
serial_debug_printf("meter: task start failed rc=%ld", static_cast<long>(rc));
}
return false;
}
g_meter_reader_task_running = true;
serial_debug_printf("meter: reader task core=%ld queue=%u",
static_cast<long>(METER_READER_TASK_CORE),
static_cast<unsigned>(METER_SAMPLE_QUEUE_LEN));
return true;
}
#endif
static void meter_reader_pump(uint32_t now_ms) {
#ifdef ARDUINO_ARCH_ESP32
if (g_meter_reader_task_running && g_meter_sample_queue) {
MeterSampleEvent event = {};
while (xQueueReceive(g_meter_sample_queue, &event, 0) == pdTRUE) {
set_last_meter_sample(event.data, event.rx_ms);
}
return;
}
#endif
const char *frame = nullptr;
size_t frame_len = 0;
if (!meter_poll_frame(frame, frame_len)) {
return;
}
MeterData parsed = {};
if (parse_meter_frame_sample(frame, frame_len, parsed)) {
set_last_meter_sample(parsed, now_ms);
}
}
static uint16_t g_last_batch_id_rx[NUM_SENDERS] = {}; static uint16_t g_last_batch_id_rx[NUM_SENDERS] = {};
struct BatchRxState { struct BatchRxState {
@@ -831,6 +957,11 @@ void setup() {
power_sender_init(); power_sender_init();
power_configure_unused_pins_sender(); power_configure_unused_pins_sender();
meter_init(); meter_init();
#ifdef ARDUINO_ARCH_ESP32
if (!meter_reader_start()) {
serial_debug_printf("meter: using inline polling fallback");
}
#endif
g_last_sample_ms = millis() - METER_SAMPLE_INTERVAL_MS; g_last_sample_ms = millis() - METER_SAMPLE_INTERVAL_MS;
g_last_send_ms = millis(); g_last_send_ms = millis();
g_last_sync_request_ms = millis() - SYNC_REQUEST_INTERVAL_MS; g_last_sync_request_ms = millis() - SYNC_REQUEST_INTERVAL_MS;
@@ -888,28 +1019,9 @@ static void sender_loop() {
g_batch_retry_count); g_batch_retry_count);
} }
if (g_time_acquired) { meter_reader_pump(now_ms);
const char *frame = nullptr;
size_t frame_len = 0;
if (meter_poll_frame(frame, frame_len)) {
MeterData parsed = {};
parsed.energy_total_kwh = NAN;
parsed.total_power_w = NAN;
parsed.phase_power_w[0] = NAN;
parsed.phase_power_w[1] = NAN;
parsed.phase_power_w[2] = NAN;
parsed.valid = false;
if (meter_parse_frame(frame, frame_len, parsed)) {
g_last_meter_data = parsed;
g_last_meter_valid = true;
g_last_meter_rx_ms = now_ms;
g_meter_stale_seconds = 0;
g_last_meter_seq++;
} else {
g_last_meter_valid = false;
}
}
if (g_time_acquired) {
if (now_ms - g_last_sample_ms >= METER_SAMPLE_INTERVAL_MS) { if (now_ms - g_last_sample_ms >= METER_SAMPLE_INTERVAL_MS) {
g_last_sample_ms = now_ms; g_last_sample_ms = now_ms;
MeterData data = {}; MeterData data = {};
@@ -922,19 +1034,18 @@ static void sender_loop() {
data.phase_power_w[2] = NAN; data.phase_power_w[2] = NAN;
g_build_attempts++; g_build_attempts++;
// Avoid reusing stale meter frames: only accept new parsed data once per sample. uint32_t meter_age_ms = g_last_meter_valid ? (now_ms - g_last_meter_rx_ms) : UINT32_MAX;
bool meter_ok = g_last_meter_valid && (g_last_meter_seq != g_last_meter_seq_used); // Reuse recent good samples to bridge short parser gaps without accepting stale data forever.
bool meter_ok = g_last_meter_valid && meter_age_ms <= METER_SAMPLE_MAX_AGE_MS;
if (meter_ok) { if (meter_ok) {
data.energy_total_kwh = g_last_meter_data.energy_total_kwh; data.energy_total_kwh = g_last_meter_data.energy_total_kwh;
data.total_power_w = g_last_meter_data.total_power_w; data.total_power_w = g_last_meter_data.total_power_w;
data.phase_power_w[0] = g_last_meter_data.phase_power_w[0]; data.phase_power_w[0] = g_last_meter_data.phase_power_w[0];
data.phase_power_w[1] = g_last_meter_data.phase_power_w[1]; data.phase_power_w[1] = g_last_meter_data.phase_power_w[1];
data.phase_power_w[2] = g_last_meter_data.phase_power_w[2]; data.phase_power_w[2] = g_last_meter_data.phase_power_w[2];
uint32_t age_ms = now_ms - g_last_meter_rx_ms; g_meter_stale_seconds = meter_age_ms >= 1000 ? (meter_age_ms / 1000) : 0;
g_meter_stale_seconds = age_ms >= 1000 ? (age_ms / 1000) : 0;
g_last_meter_seq_used = g_last_meter_seq;
} else { } else {
g_meter_stale_seconds++; g_meter_stale_seconds = g_last_meter_valid ? (meter_age_ms / 1000) : (g_meter_stale_seconds + 1);
} }
if (!meter_ok) { if (!meter_ok) {
note_fault(g_sender_faults, g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms, FaultType::MeterRead); note_fault(g_sender_faults, g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms, FaultType::MeterRead);

View File

@@ -4,7 +4,9 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
static constexpr uint32_t METER_FRAME_TIMEOUT_MS = 1500; // LoRa TX/RX windows can block the main loop for several seconds at SF12.
// Keep partial frame state long enough so valid telegrams are not dropped.
static constexpr uint32_t METER_FRAME_TIMEOUT_MS = 20000;
static constexpr size_t METER_FRAME_MAX = 512; static constexpr size_t METER_FRAME_MAX = 512;
enum class MeterRxState : uint8_t { enum class MeterRxState : uint8_t {
@@ -24,6 +26,10 @@ static uint32_t g_rx_timeout = 0;
static uint32_t g_last_log_ms = 0; static uint32_t g_last_log_ms = 0;
void meter_init() { void meter_init() {
#ifdef ARDUINO_ARCH_ESP32
// Buffer enough serial data to survive long LoRa blocking sections.
Serial2.setRxBufferSize(8192);
#endif
Serial2.begin(9600, SERIAL_7E1, PIN_METER_RX, -1); Serial2.begin(9600, SERIAL_7E1, PIN_METER_RX, -1);
} }