From a0e2947b5717e25d6c1b0293f8386b281faba20a Mon Sep 17 00:00:00 2001 From: acidburns Date: Fri, 13 Feb 2026 23:31:46 +0100 Subject: [PATCH] Add meter Sekundenindex anchoring for epoch timestamps Parse 0-0:96.8.0*255 meter seconds, derive sample epoch from anchored offset, and detect meter-time jumps via monotonic/delta checks. --- include/data_model.h | 2 + src/main.cpp | 104 ++++++++++++++++++++++++++++++++++++++++--- src/meter_driver.cpp | 53 ++++++++++++++++++++++ 3 files changed, 153 insertions(+), 6 deletions(-) diff --git a/include/data_model.h b/include/data_model.h index 4f0568d..5b825f8 100644 --- a/include/data_model.h +++ b/include/data_model.h @@ -26,6 +26,7 @@ struct FaultCounters { struct MeterData { uint32_t ts_utc; + uint32_t meter_seconds; uint16_t short_id; char device_id[16]; float energy_total_kwh; @@ -33,6 +34,7 @@ struct MeterData { float total_power_w; float battery_voltage_v; uint8_t battery_percent; + bool meter_seconds_valid; bool valid; int16_t link_rssi_dbm; float link_snr_db; diff --git a/src/main.cpp b/src/main.cpp index 187985b..fbceb70 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -100,12 +100,20 @@ 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 bool g_meter_time_anchor_valid = false; +static int64_t g_meter_epoch_offset = 0; +static bool g_meter_time_prev_valid = false; +static uint32_t g_meter_time_prev_seconds = 0; +static uint32_t g_meter_time_prev_rx_ms = 0; +static bool g_meter_time_jump_pending = false; static bool g_time_acquired = false; static uint32_t g_last_sync_request_ms = 0; static uint32_t g_build_attempts = 0; static uint32_t g_build_valid = 0; static uint32_t g_build_invalid = 0; static constexpr uint32_t METER_SAMPLE_MAX_AGE_MS = 15000; +static constexpr uint32_t METER_TIME_DELTA_TOLERANCE_S = 2; +static constexpr int64_t METER_TIME_ANCHOR_DRIFT_TOLERANCE_S = 2; struct MeterSampleEvent { MeterData data; uint32_t rx_ms; @@ -151,7 +159,79 @@ static uint8_t bit_count32(uint32_t value) { return count; } -static void set_last_meter_sample(const MeterData &parsed, uint32_t rx_ms) { +static uint32_t abs_diff_u32(uint32_t a, uint32_t b) { + return a >= b ? (a - b) : (b - a); +} + +static void meter_time_update_snapshot(MeterData &parsed, uint32_t rx_ms) { + if (!parsed.meter_seconds_valid) { + return; + } + + bool jump = false; + const char *jump_reason = nullptr; + uint32_t delta_meter_s = 0; + uint32_t delta_wall_s = 0; + if (g_meter_time_prev_valid) { + if (parsed.meter_seconds < g_meter_time_prev_seconds) { + jump = true; + jump_reason = "rollback"; + } else { + delta_meter_s = parsed.meter_seconds - g_meter_time_prev_seconds; + uint32_t delta_wall_ms = rx_ms - g_meter_time_prev_rx_ms; + delta_wall_s = (delta_wall_ms + 500) / 1000; + if (abs_diff_u32(delta_meter_s, delta_wall_s) > METER_TIME_DELTA_TOLERANCE_S) { + jump = true; + jump_reason = "delta"; + } + } + } + + if (time_is_synced()) { + uint32_t epoch_now = time_get_utc(); + if (epoch_now >= MIN_ACCEPTED_EPOCH_UTC) { + int64_t new_offset = static_cast(epoch_now) - static_cast(parsed.meter_seconds); + if (!g_meter_time_anchor_valid || jump) { + g_meter_epoch_offset = new_offset; + g_meter_time_anchor_valid = true; + } else { + int64_t drift_s = new_offset - g_meter_epoch_offset; + if (drift_s > METER_TIME_ANCHOR_DRIFT_TOLERANCE_S || drift_s < -METER_TIME_ANCHOR_DRIFT_TOLERANCE_S) { + jump = true; + jump_reason = jump_reason ? jump_reason : "anchor"; + g_meter_epoch_offset = new_offset; + } + } + } + } + + if (g_meter_time_anchor_valid) { + int64_t epoch64 = static_cast(parsed.meter_seconds) + g_meter_epoch_offset; + if (epoch64 > 0 && epoch64 <= static_cast(UINT32_MAX)) { + parsed.ts_utc = static_cast(epoch64); + } + } + + if (jump) { + g_meter_time_jump_pending = true; + if (SERIAL_DEBUG_MODE) { + serial_debug_printf("meter_time: jump reason=%s sec=%lu prev=%lu d_meter=%lu d_wall=%lu", + jump_reason ? jump_reason : "unknown", + static_cast(parsed.meter_seconds), + static_cast(g_meter_time_prev_seconds), + static_cast(delta_meter_s), + static_cast(delta_wall_s)); + } + } + + g_meter_time_prev_seconds = parsed.meter_seconds; + g_meter_time_prev_rx_ms = rx_ms; + g_meter_time_prev_valid = true; +} + +static void set_last_meter_sample(const MeterData &parsed_in, uint32_t rx_ms) { + MeterData parsed = parsed_in; + meter_time_update_snapshot(parsed, rx_ms); g_last_meter_data = parsed; g_last_meter_valid = true; g_last_meter_rx_ms = rx_ms; @@ -1095,6 +1175,8 @@ static void sender_loop() { bool has_snapshot = g_last_meter_valid; bool meter_ok = has_snapshot && meter_age_ms <= METER_SAMPLE_MAX_AGE_MS; if (has_snapshot) { + data.meter_seconds = g_last_meter_data.meter_seconds; + data.meter_seconds_valid = g_last_meter_data.meter_seconds_valid; 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]; @@ -1108,17 +1190,27 @@ static void sender_loop() { 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); } + if (g_meter_time_jump_pending) { + g_meter_time_jump_pending = false; + 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); + } if (g_build_count == 0 && battery_sample_due(now_ms)) { update_battery_cache(); } data.battery_voltage_v = g_last_battery_voltage_v; data.battery_percent = g_last_battery_percent; data.rx_reject_reason = static_cast(g_sender_rx_reject_reason); - uint32_t sample_ts_utc = time_get_utc(); - if (sample_ts_utc > 0 && now_ms > g_last_sample_ms) { - uint32_t lag_s = (now_ms - g_last_sample_ms) / 1000; - if (sample_ts_utc > lag_s) { - sample_ts_utc -= lag_s; + uint32_t sample_ts_utc = 0; + if (has_snapshot && g_last_meter_data.meter_seconds_valid && g_last_meter_data.ts_utc >= MIN_ACCEPTED_EPOCH_UTC) { + sample_ts_utc = g_last_meter_data.ts_utc; + } else { + sample_ts_utc = time_get_utc(); + if (sample_ts_utc > 0 && now_ms > g_last_sample_ms) { + uint32_t lag_s = (now_ms - g_last_sample_ms) / 1000; + if (sample_ts_utc > lag_s) { + sample_ts_utc -= lag_s; + } } } data.ts_utc = sample_ts_utc; diff --git a/src/meter_driver.cpp b/src/meter_driver.cpp index 0626e89..bc34210 100644 --- a/src/meter_driver.cpp +++ b/src/meter_driver.cpp @@ -100,6 +100,52 @@ static bool parse_obis_ascii_unit_scale(const char *line, const char *obis, floa 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_u32(const char *line, const char *obis, uint32_t &out_value) { + const char *p = strstr(line, obis); + if (!p) { + return false; + } + const char *lparen = strchr(p, '('); + 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; @@ -242,6 +288,11 @@ bool meter_parse_frame(const char *frame, size_t len, MeterData &data) { p3_ok = true; got_any = true; } + uint32_t meter_seconds = 0; + if (parse_obis_hex_u32(line, "0-0:96.8.0*255", meter_seconds)) { + data.meter_seconds = meter_seconds; + data.meter_seconds_valid = true; + } line_len = 0; continue; @@ -261,6 +312,8 @@ bool meter_parse_frame(const char *frame, size_t len, MeterData &data) { } 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;