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:
@@ -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;
|
||||
|
||||
104
src/main.cpp
104
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<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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user