Make IEC 62056-21 meter input non-blocking

- Add RX state machine with frame buffer, timeouts, and debug counters

- Expose meter_poll_frame/meter_parse_frame and reuse existing OBIS parsing

- Use cached last-valid frame at 1 Hz sampling to avoid blocking

- Document non-blocking meter handling in README
This commit is contained in:
2026-02-02 22:03:58 +01:00
parent 8e6c64a18e
commit 237e392c02
4 changed files with 196 additions and 61 deletions

View File

@@ -54,6 +54,7 @@ Variants:
- Energy total: 1-0:1.8.0*255
- Total power: 1-0:16.7.0*255
- Phase power: 36.7 / 56.7 / 76.7
- Meter input is parsed via a non-blocking RX state machine; the last valid frame is reused for 1 Hz sampling.
- Reads battery voltage and estimates SoC.
- Builds compact binary batch payload, wraps in LoRa packet, transmits.
- Light sleeps between meter reads; batches are sent every 30s.

View File

@@ -5,3 +5,5 @@
void meter_init();
bool meter_read(MeterData &data);
bool meter_poll_frame(const char *&frame, size_t &len);
bool meter_parse_frame(const char *frame, size_t len, MeterData &data);

View File

@@ -92,6 +92,10 @@ static uint32_t g_sender_last_timesync_check_ms = 0;
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 MeterData g_last_meter_data = {};
static bool g_last_meter_valid = false;
static uint32_t g_last_meter_rx_ms = 0;
static uint32_t g_meter_stale_seconds = 0;
static void watchdog_kick();
@@ -696,13 +700,42 @@ static void sender_loop() {
g_batch_retry_count);
}
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;
}
}
if (now_ms - g_last_sample_ms >= METER_SAMPLE_INTERVAL_MS) {
g_last_sample_ms = now_ms;
MeterData data = {};
data.short_id = g_short_id;
strncpy(data.device_id, g_device_id, sizeof(data.device_id));
bool meter_ok = meter_read(data);
bool meter_ok = g_last_meter_valid;
if (meter_ok) {
data.energy_total_kwh = g_last_meter_data.energy_total_kwh;
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[1] = g_last_meter_data.phase_power_w[1];
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 = age_ms >= 1000 ? (age_ms / 1000) : 0;
} else {
g_meter_stale_seconds++;
}
if (!meter_ok) {
note_fault(g_sender_faults, g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms, FaultType::MeterRead);
display_set_last_error(g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms);

View File

@@ -4,7 +4,24 @@
#include <stdlib.h>
#include <string.h>
static constexpr uint32_t METER_READ_TIMEOUT_MS = 2000;
static constexpr uint32_t METER_FRAME_TIMEOUT_MS = 1500;
static constexpr size_t METER_FRAME_MAX = 512;
enum class MeterRxState : uint8_t {
WaitStart = 0,
InFrame = 1
};
static MeterRxState g_rx_state = MeterRxState::WaitStart;
static char g_frame_buf[METER_FRAME_MAX + 1];
static size_t g_frame_len = 0;
static uint32_t g_last_rx_ms = 0;
static uint32_t g_bytes_rx = 0;
static uint32_t g_frames_ok = 0;
static uint32_t g_frames_parse_fail = 0;
static uint32_t g_rx_overflow = 0;
static uint32_t g_rx_timeout = 0;
static uint32_t g_last_log_ms = 0;
void meter_init() {
Serial2.begin(9600, SERIAL_7E1, PIN_METER_RX, -1);
@@ -77,11 +94,76 @@ static bool parse_obis_ascii_unit_scale(const char *line, const char *obis, floa
return false;
}
static bool meter_read_ascii(MeterData &data) {
const uint32_t start_ms = millis();
bool in_telegram = false;
bool got_any = false;
static void meter_debug_log() {
if (!SERIAL_DEBUG_MODE) {
return;
}
uint32_t now_ms = millis();
if (now_ms - g_last_log_ms < 60000) {
return;
}
g_last_log_ms = now_ms;
Serial.printf("meter: ok=%lu parse_fail=%lu overflow=%lu timeout=%lu bytes=%lu\n",
static_cast<unsigned long>(g_frames_ok),
static_cast<unsigned long>(g_frames_parse_fail),
static_cast<unsigned long>(g_rx_overflow),
static_cast<unsigned long>(g_rx_timeout),
static_cast<unsigned long>(g_bytes_rx));
}
bool meter_poll_frame(const char *&frame, size_t &len) {
frame = nullptr;
len = 0;
uint32_t now_ms = millis();
if (g_rx_state == MeterRxState::InFrame && (now_ms - g_last_rx_ms > METER_FRAME_TIMEOUT_MS)) {
g_rx_timeout++;
g_rx_state = MeterRxState::WaitStart;
g_frame_len = 0;
}
while (Serial2.available()) {
char c = static_cast<char>(Serial2.read());
g_bytes_rx++;
g_last_rx_ms = now_ms;
if (g_rx_state == MeterRxState::WaitStart) {
if (c == '/') {
g_rx_state = MeterRxState::InFrame;
g_frame_len = 0;
g_frame_buf[g_frame_len++] = c;
}
continue;
}
if (g_frame_len + 1 >= sizeof(g_frame_buf)) {
g_rx_overflow++;
g_rx_state = MeterRxState::WaitStart;
g_frame_len = 0;
continue;
}
g_frame_buf[g_frame_len++] = c;
if (c == '!') {
g_frame_buf[g_frame_len] = '\0';
frame = g_frame_buf;
len = g_frame_len;
g_rx_state = MeterRxState::WaitStart;
g_frame_len = 0;
meter_debug_log();
return true;
}
}
meter_debug_log();
return false;
}
bool meter_parse_frame(const char *frame, size_t len, MeterData &data) {
if (!frame || len == 0) {
return false;
}
bool got_any = false;
bool energy_ok = false;
bool total_p_ok = false;
bool p1_ok = false;
@@ -90,66 +172,78 @@ static bool meter_read_ascii(MeterData &data) {
char line[128];
size_t line_len = 0;
while (millis() - start_ms < METER_READ_TIMEOUT_MS) {
while (Serial2.available()) {
char c = static_cast<char>(Serial2.read());
if (!in_telegram) {
if (c == '/') {
in_telegram = true;
line_len = 0;
line[line_len++] = c;
}
continue;
}
if (c == '\r') {
continue;
}
if (c == '\n') {
line[line_len] = '\0';
if (line[0] == '!') {
return got_any;
}
float value = NAN;
if (parse_obis_ascii_value(line, "1-0:1.8.0", value)) {
parse_obis_ascii_unit_scale(line, "1-0:1.8.0", value);
data.energy_total_kwh = value;
energy_ok = true;
got_any = true;
}
if (parse_obis_ascii_value(line, "1-0:16.7.0", value)) {
data.total_power_w = value;
total_p_ok = true;
got_any = true;
}
if (parse_obis_ascii_value(line, "1-0:36.7.0", value)) {
data.phase_power_w[0] = value;
p1_ok = true;
got_any = true;
}
if (parse_obis_ascii_value(line, "1-0:56.7.0", value)) {
data.phase_power_w[1] = value;
p2_ok = true;
got_any = true;
}
if (parse_obis_ascii_value(line, "1-0:76.7.0", value)) {
data.phase_power_w[2] = value;
p3_ok = true;
got_any = true;
}
line_len = 0;
continue;
}
for (size_t i = 0; i < len; ++i) {
char c = frame[i];
if (c == '\r') {
continue;
}
if (c == '!') {
if (line_len + 1 < sizeof(line)) {
line[line_len++] = c;
}
line[line_len] = '\0';
data.valid = energy_ok || total_p_ok || p1_ok || p2_ok || p3_ok;
if (data.valid) {
g_frames_ok++;
} else {
g_frames_parse_fail++;
}
return data.valid;
}
if (c == '\n') {
line[line_len] = '\0';
if (line[0] == '!') {
data.valid = energy_ok || total_p_ok || p1_ok || p2_ok || p3_ok;
if (data.valid) {
g_frames_ok++;
} else {
g_frames_parse_fail++;
}
return data.valid;
}
float value = NAN;
if (parse_obis_ascii_value(line, "1-0:1.8.0", value)) {
parse_obis_ascii_unit_scale(line, "1-0:1.8.0", value);
data.energy_total_kwh = value;
energy_ok = true;
got_any = true;
}
if (parse_obis_ascii_value(line, "1-0:16.7.0", value)) {
data.total_power_w = value;
total_p_ok = true;
got_any = true;
}
if (parse_obis_ascii_value(line, "1-0:36.7.0", value)) {
data.phase_power_w[0] = value;
p1_ok = true;
got_any = true;
}
if (parse_obis_ascii_value(line, "1-0:56.7.0", value)) {
data.phase_power_w[1] = value;
p2_ok = true;
got_any = true;
}
if (parse_obis_ascii_value(line, "1-0:76.7.0", value)) {
data.phase_power_w[2] = value;
p3_ok = true;
got_any = true;
}
line_len = 0;
continue;
}
if (line_len + 1 < sizeof(line)) {
line[line_len++] = c;
}
delay(5);
}
data.valid = energy_ok || total_p_ok || p1_ok || p2_ok || p3_ok;
data.valid = got_any;
if (data.valid) {
g_frames_ok++;
} else {
g_frames_parse_fail++;
}
return data.valid;
}
@@ -161,5 +255,10 @@ bool meter_read(MeterData &data) {
data.phase_power_w[2] = NAN;
data.valid = false;
return meter_read_ascii(data);
const char *frame = nullptr;
size_t len = 0;
if (!meter_poll_frame(frame, len)) {
return false;
}
return meter_parse_frame(frame, len, data);
}