#include "meter_driver.h" #include "config.h" #include #include #include // Dedicated reader task pumps UART continuously; keep timeout short so parser can // recover quickly from broken frames. static constexpr uint32_t METER_FRAME_TIMEOUT_MS = 3000; 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; static uint32_t g_last_good_frame_ms = 0; static constexpr uint32_t METER_FIXED_FRAC_MAX_DIV = 10000; 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); } enum class ObisField : uint8_t { None = 0, Energy = 1, TotalPower = 2, Phase1 = 3, Phase2 = 4, Phase3 = 5, MeterSeconds = 6 }; static ObisField detect_obis_field(const char *line) { if (!line) { return ObisField::None; } const char *p = line; while (*p == ' ' || *p == '\t') { ++p; } if (strncmp(p, "1-0:1.8.0", 9) == 0) { return ObisField::Energy; } if (strncmp(p, "1-0:16.7.0", 10) == 0) { return ObisField::TotalPower; } if (strncmp(p, "1-0:36.7.0", 10) == 0) { return ObisField::Phase1; } if (strncmp(p, "1-0:56.7.0", 10) == 0) { return ObisField::Phase2; } if (strncmp(p, "1-0:76.7.0", 10) == 0) { return ObisField::Phase3; } if (strncmp(p, "0-0:96.8.0*255", 14) == 0) { return ObisField::MeterSeconds; } return ObisField::None; } static bool parse_decimal_fixed(const char *start, const char *end, float &out_value) { if (!start || !end || end <= start) { return false; } const char *cur = start; bool started = false; bool negative = false; bool in_fraction = false; bool saw_digit = false; uint64_t int_part = 0; uint32_t frac_part = 0; uint32_t frac_div = 1; while (cur < end) { char c = *cur++; if (!started) { if (c == '+' || c == '-') { started = true; negative = (c == '-'); continue; } if (c >= '0' && c <= '9') { started = true; saw_digit = true; int_part = static_cast(c - '0'); continue; } if (c == '.' || c == ',') { started = true; in_fraction = true; continue; } continue; } if (c >= '0' && c <= '9') { saw_digit = true; uint32_t digit = static_cast(c - '0'); if (!in_fraction) { if (int_part <= (UINT64_MAX - digit) / 10ULL) { int_part = int_part * 10ULL + digit; } } else if (frac_div < METER_FIXED_FRAC_MAX_DIV) { frac_part = frac_part * 10U + digit; frac_div *= 10U; } continue; } if ((c == '.' || c == ',') && !in_fraction) { in_fraction = true; continue; } break; } if (!saw_digit) { return false; } double value = static_cast(int_part); if (frac_div > 1U) { value += static_cast(frac_part) / static_cast(frac_div); } if (negative) { value = -value; } out_value = static_cast(value); return true; } static bool parse_obis_ascii_payload_value(const char *line, float &out_value) { const char *lparen = strchr(line, '('); if (!lparen) { return false; } const char *end = lparen + 1; while (*end && *end != ')' && *end != '*') { ++end; } if (end <= lparen + 1) { return false; } return parse_decimal_fixed(lparen + 1, end, out_value); } static bool parse_obis_ascii_unit_scale(const char *line, float &value) { const char *lparen = strchr(line, '('); if (!lparen) { return false; } const char *asterisk = strchr(lparen, '*'); if (!asterisk) { return false; } const char *end = strchr(asterisk, ')'); if (!end) { return false; } char unit_buf[8]; size_t ulen = 0; for (const char *c = asterisk + 1; c < end && ulen + 1 < sizeof(unit_buf); ++c) { if (*c == ' ') { continue; } unit_buf[ulen++] = *c; } unit_buf[ulen] = '\0'; if (ulen == 0) { return false; } if (strcmp(unit_buf, "Wh") == 0) { value *= 0.001f; return true; } return false; } static int8_t hex_nibble(char c) { if (c >= '0' && c <= '9') { return static_cast(c - '0'); } if (c >= 'A' && c <= 'F') { return static_cast(10 + (c - 'A')); } if (c >= 'a' && c <= 'f') { return static_cast(10 + (c - 'a')); } return -1; } static bool parse_obis_hex_payload_u32(const char *line, uint32_t &out_value) { const char *lparen = strchr(line, '('); if (!lparen) { return false; } const char *cur = lparen + 1; uint32_t value = 0; size_t n = 0; while (*cur && *cur != ')' && *cur != '*') { int8_t nib = hex_nibble(*cur++); if (nib < 0) { if (n == 0) { continue; } break; } if (n >= 8) { return false; } value = (value << 4) | static_cast(nib); n++; } if (n == 0) { return false; } out_value = value; return true; } 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)); } void meter_get_stats(MeterDriverStats &out) { out.frames_ok = g_frames_ok; out.frames_parse_fail = g_frames_parse_fail; out.rx_overflow = g_rx_overflow; out.rx_timeout = g_rx_timeout; out.bytes_rx = g_bytes_rx; out.last_rx_ms = g_last_rx_ms; out.last_good_frame_ms = g_last_good_frame_ms; } 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; } // Fast resync if a new telegram starts before current frame completed. if (c == '/') { 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; bool p2_ok = false; bool p3_ok = false; char line[128]; size_t line_len = 0; 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++; g_last_good_frame_ms = millis(); } 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++; g_last_good_frame_ms = millis(); } else { g_frames_parse_fail++; } return data.valid; } ObisField field = detect_obis_field(line); float value = NAN; uint32_t meter_seconds = 0; switch (field) { case ObisField::Energy: if (parse_obis_ascii_payload_value(line, value)) { parse_obis_ascii_unit_scale(line, value); data.energy_total_kwh = value; energy_ok = true; got_any = true; } break; case ObisField::TotalPower: if (parse_obis_ascii_payload_value(line, value)) { data.total_power_w = value; total_p_ok = true; got_any = true; } break; case ObisField::Phase1: if (parse_obis_ascii_payload_value(line, value)) { data.phase_power_w[0] = value; p1_ok = true; got_any = true; } break; case ObisField::Phase2: if (parse_obis_ascii_payload_value(line, value)) { data.phase_power_w[1] = value; p2_ok = true; got_any = true; } break; case ObisField::Phase3: if (parse_obis_ascii_payload_value(line, value)) { data.phase_power_w[2] = value; p3_ok = true; got_any = true; } break; case ObisField::MeterSeconds: if (parse_obis_hex_payload_u32(line, meter_seconds)) { data.meter_seconds = meter_seconds; data.meter_seconds_valid = true; } break; default: break; } if (energy_ok && total_p_ok && p1_ok && p2_ok && p3_ok && data.meter_seconds_valid) { data.valid = true; g_frames_ok++; g_last_good_frame_ms = millis(); return true; } line_len = 0; continue; } if (line_len + 1 < sizeof(line)) { line[line_len++] = c; } } data.valid = got_any; if (data.valid) { g_frames_ok++; g_last_good_frame_ms = millis(); } else { g_frames_parse_fail++; } return data.valid; } bool meter_read(MeterData &data) { data.meter_seconds = 0; data.meter_seconds_valid = false; data.energy_total_kwh = NAN; data.total_power_w = NAN; data.phase_power_w[0] = NAN; data.phase_power_w[1] = NAN; data.phase_power_w[2] = NAN; data.valid = false; const char *frame = nullptr; size_t len = 0; if (!meter_poll_frame(frame, len)) { return false; } return meter_parse_frame(frame, len, data); }