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 {
|
struct MeterData {
|
||||||
uint32_t ts_utc;
|
uint32_t ts_utc;
|
||||||
|
uint32_t meter_seconds;
|
||||||
uint16_t short_id;
|
uint16_t short_id;
|
||||||
char device_id[16];
|
char device_id[16];
|
||||||
float energy_total_kwh;
|
float energy_total_kwh;
|
||||||
@@ -33,6 +34,7 @@ struct MeterData {
|
|||||||
float total_power_w;
|
float total_power_w;
|
||||||
float battery_voltage_v;
|
float battery_voltage_v;
|
||||||
uint8_t battery_percent;
|
uint8_t battery_percent;
|
||||||
|
bool meter_seconds_valid;
|
||||||
bool valid;
|
bool valid;
|
||||||
int16_t link_rssi_dbm;
|
int16_t link_rssi_dbm;
|
||||||
float link_snr_db;
|
float link_snr_db;
|
||||||
|
|||||||
96
src/main.cpp
96
src/main.cpp
@@ -100,12 +100,20 @@ static MeterData g_last_meter_data = {};
|
|||||||
static bool g_last_meter_valid = false;
|
static bool g_last_meter_valid = false;
|
||||||
static uint32_t g_last_meter_rx_ms = 0;
|
static uint32_t g_last_meter_rx_ms = 0;
|
||||||
static uint32_t g_meter_stale_seconds = 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 bool g_time_acquired = false;
|
||||||
static uint32_t g_last_sync_request_ms = 0;
|
static uint32_t g_last_sync_request_ms = 0;
|
||||||
static uint32_t g_build_attempts = 0;
|
static uint32_t g_build_attempts = 0;
|
||||||
static uint32_t g_build_valid = 0;
|
static uint32_t g_build_valid = 0;
|
||||||
static uint32_t g_build_invalid = 0;
|
static uint32_t g_build_invalid = 0;
|
||||||
static constexpr uint32_t METER_SAMPLE_MAX_AGE_MS = 15000;
|
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 {
|
struct MeterSampleEvent {
|
||||||
MeterData data;
|
MeterData data;
|
||||||
uint32_t rx_ms;
|
uint32_t rx_ms;
|
||||||
@@ -151,7 +159,79 @@ static uint8_t bit_count32(uint32_t value) {
|
|||||||
return count;
|
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_data = parsed;
|
||||||
g_last_meter_valid = true;
|
g_last_meter_valid = true;
|
||||||
g_last_meter_rx_ms = rx_ms;
|
g_last_meter_rx_ms = rx_ms;
|
||||||
@@ -1095,6 +1175,8 @@ static void sender_loop() {
|
|||||||
bool has_snapshot = g_last_meter_valid;
|
bool has_snapshot = g_last_meter_valid;
|
||||||
bool meter_ok = has_snapshot && meter_age_ms <= METER_SAMPLE_MAX_AGE_MS;
|
bool meter_ok = has_snapshot && meter_age_ms <= METER_SAMPLE_MAX_AGE_MS;
|
||||||
if (has_snapshot) {
|
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.energy_total_kwh = g_last_meter_data.energy_total_kwh;
|
||||||
data.total_power_w = g_last_meter_data.total_power_w;
|
data.total_power_w = g_last_meter_data.total_power_w;
|
||||||
data.phase_power_w[0] = g_last_meter_data.phase_power_w[0];
|
data.phase_power_w[0] = g_last_meter_data.phase_power_w[0];
|
||||||
@@ -1108,19 +1190,29 @@ 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);
|
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);
|
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)) {
|
if (g_build_count == 0 && battery_sample_due(now_ms)) {
|
||||||
update_battery_cache();
|
update_battery_cache();
|
||||||
}
|
}
|
||||||
data.battery_voltage_v = g_last_battery_voltage_v;
|
data.battery_voltage_v = g_last_battery_voltage_v;
|
||||||
data.battery_percent = g_last_battery_percent;
|
data.battery_percent = g_last_battery_percent;
|
||||||
data.rx_reject_reason = static_cast<uint8_t>(g_sender_rx_reject_reason);
|
data.rx_reject_reason = static_cast<uint8_t>(g_sender_rx_reject_reason);
|
||||||
uint32_t sample_ts_utc = time_get_utc();
|
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) {
|
if (sample_ts_utc > 0 && now_ms > g_last_sample_ms) {
|
||||||
uint32_t lag_s = (now_ms - g_last_sample_ms) / 1000;
|
uint32_t lag_s = (now_ms - g_last_sample_ms) / 1000;
|
||||||
if (sample_ts_utc > lag_s) {
|
if (sample_ts_utc > lag_s) {
|
||||||
sample_ts_utc -= lag_s;
|
sample_ts_utc -= lag_s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
data.ts_utc = sample_ts_utc;
|
data.ts_utc = sample_ts_utc;
|
||||||
data.valid = has_snapshot;
|
data.valid = has_snapshot;
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,52 @@ static bool parse_obis_ascii_unit_scale(const char *line, const char *obis, floa
|
|||||||
return false;
|
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() {
|
static void meter_debug_log() {
|
||||||
if (!SERIAL_DEBUG_MODE) {
|
if (!SERIAL_DEBUG_MODE) {
|
||||||
return;
|
return;
|
||||||
@@ -242,6 +288,11 @@ bool meter_parse_frame(const char *frame, size_t len, MeterData &data) {
|
|||||||
p3_ok = true;
|
p3_ok = true;
|
||||||
got_any = 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;
|
line_len = 0;
|
||||||
continue;
|
continue;
|
||||||
@@ -261,6 +312,8 @@ bool meter_parse_frame(const char *frame, size_t len, MeterData &data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool meter_read(MeterData &data) {
|
bool meter_read(MeterData &data) {
|
||||||
|
data.meter_seconds = 0;
|
||||||
|
data.meter_seconds_valid = false;
|
||||||
data.energy_total_kwh = NAN;
|
data.energy_total_kwh = NAN;
|
||||||
data.total_power_w = NAN;
|
data.total_power_w = NAN;
|
||||||
data.phase_power_w[0] = NAN;
|
data.phase_power_w[0] = NAN;
|
||||||
|
|||||||
Reference in New Issue
Block a user