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
|
- Energy total: 1-0:1.8.0*255
|
||||||
- Total power: 1-0:16.7.0*255
|
- Total power: 1-0:16.7.0*255
|
||||||
- Phase power: 36.7 / 56.7 / 76.7
|
- 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.
|
- Reads battery voltage and estimates SoC.
|
||||||
- Builds compact binary batch payload, wraps in LoRa packet, transmits.
|
- Builds compact binary batch payload, wraps in LoRa packet, transmits.
|
||||||
- Light sleeps between meter reads; batches are sent every 30s.
|
- Light sleeps between meter reads; batches are sent every 30s.
|
||||||
|
|||||||
@@ -5,3 +5,5 @@
|
|||||||
|
|
||||||
void meter_init();
|
void meter_init();
|
||||||
bool meter_read(MeterData &data);
|
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_rx_window_ms = 0;
|
||||||
static uint32_t g_sender_sleep_ms = 0;
|
static uint32_t g_sender_sleep_ms = 0;
|
||||||
static uint32_t g_sender_power_log_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();
|
static void watchdog_kick();
|
||||||
|
|
||||||
@@ -696,13 +700,42 @@ static void sender_loop() {
|
|||||||
g_batch_retry_count);
|
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) {
|
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 = {};
|
||||||
data.short_id = g_short_id;
|
data.short_id = g_short_id;
|
||||||
strncpy(data.device_id, g_device_id, sizeof(data.device_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) {
|
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);
|
||||||
display_set_last_error(g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms);
|
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 <stdlib.h>
|
||||||
#include <string.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() {
|
void meter_init() {
|
||||||
Serial2.begin(9600, SERIAL_7E1, PIN_METER_RX, -1);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool meter_read_ascii(MeterData &data) {
|
static void meter_debug_log() {
|
||||||
const uint32_t start_ms = millis();
|
if (!SERIAL_DEBUG_MODE) {
|
||||||
bool in_telegram = false;
|
return;
|
||||||
bool got_any = false;
|
}
|
||||||
|
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 energy_ok = false;
|
||||||
bool total_p_ok = false;
|
bool total_p_ok = false;
|
||||||
bool p1_ok = false;
|
bool p1_ok = false;
|
||||||
@@ -90,66 +172,78 @@ static bool meter_read_ascii(MeterData &data) {
|
|||||||
char line[128];
|
char line[128];
|
||||||
size_t line_len = 0;
|
size_t line_len = 0;
|
||||||
|
|
||||||
while (millis() - start_ms < METER_READ_TIMEOUT_MS) {
|
for (size_t i = 0; i < len; ++i) {
|
||||||
while (Serial2.available()) {
|
char c = frame[i];
|
||||||
char c = static_cast<char>(Serial2.read());
|
if (c == '\r') {
|
||||||
if (!in_telegram) {
|
continue;
|
||||||
if (c == '/') {
|
}
|
||||||
in_telegram = true;
|
if (c == '!') {
|
||||||
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;
|
|
||||||
}
|
|
||||||
if (line_len + 1 < sizeof(line)) {
|
if (line_len + 1 < sizeof(line)) {
|
||||||
line[line_len++] = c;
|
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;
|
return data.valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,5 +255,10 @@ bool meter_read(MeterData &data) {
|
|||||||
data.phase_power_w[2] = NAN;
|
data.phase_power_w[2] = NAN;
|
||||||
data.valid = false;
|
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