Files
DD3-LoRa-Bridge-MultiSender/src/meter_driver.cpp

457 lines
11 KiB
C++

#include "meter_driver.h"
#include "config.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
// 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<uint64_t>(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<uint32_t>(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<double>(int_part);
if (frac_div > 1U) {
value += static_cast<double>(frac_part) / static_cast<double>(frac_div);
}
if (negative) {
value = -value;
}
out_value = static_cast<float>(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<int8_t>(c - '0');
}
if (c >= 'A' && c <= 'F') {
return static_cast<int8_t>(10 + (c - 'A'));
}
if (c >= 'a' && c <= 'f') {
return static_cast<int8_t>(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<uint32_t>(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<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));
}
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<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;
}
// 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);
}