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:
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
35
src/main.cpp
35
src/main.cpp
@@ -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);
|
||||
|
||||
@@ -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,25 +172,34 @@ 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;
|
||||
}
|
||||
|
||||
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] == '!') {
|
||||
return got_any;
|
||||
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;
|
||||
@@ -146,10 +237,13 @@ static bool meter_read_ascii(MeterData &data) {
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user