diff --git a/README.md b/README.md index f53a09f..66a9303 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/include/meter_driver.h b/include/meter_driver.h index 25d4761..a38cf73 100644 --- a/include/meter_driver.h +++ b/include/meter_driver.h @@ -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); diff --git a/src/main.cpp b/src/main.cpp index 882d42e..ad44f86 100644 --- a/src/main.cpp +++ b/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); diff --git a/src/meter_driver.cpp b/src/meter_driver.cpp index 507f3bc..665c4a1 100644 --- a/src/meter_driver.cpp +++ b/src/meter_driver.cpp @@ -4,7 +4,24 @@ #include #include -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(g_frames_ok), + static_cast(g_frames_parse_fail), + static_cast(g_rx_overflow), + static_cast(g_rx_timeout), + static_cast(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(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(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); }