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.
This commit is contained in:
2026-02-13 23:31:46 +01:00
parent 2a6ef0eb5c
commit a0e2947b57
3 changed files with 153 additions and 6 deletions

View File

@@ -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;

View File

@@ -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<int64_t>(epoch_now) - static_cast<int64_t>(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<int64_t>(parsed.meter_seconds) + g_meter_epoch_offset;
if (epoch64 > 0 && epoch64 <= static_cast<int64_t>(UINT32_MAX)) {
parsed.ts_utc = static_cast<uint32_t>(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<unsigned long>(parsed.meter_seconds),
static_cast<unsigned long>(g_meter_time_prev_seconds),
static_cast<unsigned long>(delta_meter_s),
static_cast<unsigned long>(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<uint8_t>(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;

View File

@@ -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<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_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<uint32_t>(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;