Add timesync burst handling and sender-only timeout

- Add TimeSync fault code and labels in UI/SD/web docs
- Trigger receiver beacon bursts on sender drift, but keep errors sender-local
- Sender flags TimeSync only after TIME_SYNC_ERROR_TIMEOUT_MS
This commit is contained in:
2026-02-03 23:40:11 +01:00
parent 43893c24d1
commit f7a2503d7a
7 changed files with 92 additions and 10 deletions

View File

@@ -50,6 +50,18 @@ static uint32_t g_sender_last_error_remote_ms[NUM_SENDERS] = {};
static bool g_sender_discovery_sent[NUM_SENDERS] = {};
static bool g_receiver_discovery_sent = false;
struct TimeSyncBurstState {
bool active;
uint32_t start_ms;
uint32_t last_send_ms;
uint32_t last_drift_check_ms;
bool last_drift_ok;
};
static TimeSyncBurstState g_timesync_burst[NUM_SENDERS] = {};
static uint32_t g_sender_last_timesync_rx_ms = 0;
static bool g_sender_timesync_error = false;
static constexpr size_t BATCH_HEADER_SIZE = 6;
static constexpr size_t BATCH_CHUNK_PAYLOAD = LORA_MAX_PAYLOAD - BATCH_HEADER_SIZE;
static constexpr size_t BATCH_MAX_COMPRESSED = 4096;
@@ -190,6 +202,27 @@ static bool batch_queue_drop_oldest() {
return dropped_inflight;
}
static void receiver_note_timesync_drift(uint8_t sender_idx, uint32_t sender_ts_utc) {
if (sender_idx >= NUM_SENDERS) {
return;
}
if (!time_is_synced() || sender_ts_utc == 0) {
return;
}
uint32_t now_utc = time_get_utc();
uint32_t diff = now_utc > sender_ts_utc ? now_utc - sender_ts_utc : sender_ts_utc - now_utc;
TimeSyncBurstState &state = g_timesync_burst[sender_idx];
state.last_drift_check_ms = millis();
state.last_drift_ok = diff <= TIME_SYNC_DRIFT_THRESHOLD_SEC;
if (!state.last_drift_ok) {
if (!state.active) {
state.active = true;
state.start_ms = millis();
state.last_send_ms = 0;
}
}
}
static BatchBuffer *batch_queue_peek() {
if (g_batch_count == 0) {
return nullptr;
@@ -802,9 +835,21 @@ static void sender_loop() {
g_sender_rx_window_ms += rx_elapsed;
}
if (got && rx.payload_type == PayloadType::TimeSync) {
time_handle_timesync_payload(rx.payload, rx.payload_len);
if (time_handle_timesync_payload(rx.payload, rx.payload_len)) {
g_sender_last_timesync_rx_ms = now_ms;
if (g_sender_timesync_error) {
g_sender_timesync_error = false;
display_set_last_error(FaultType::None, 0, 0);
}
}
}
}
uint32_t timesync_age_ms = (g_sender_last_timesync_rx_ms > 0) ? (now_ms - g_sender_last_timesync_rx_ms)
: (now_ms - g_boot_ms);
if (!g_sender_timesync_error && timesync_age_ms > TIME_SYNC_ERROR_TIMEOUT_MS) {
g_sender_timesync_error = true;
display_set_last_error(FaultType::TimeSync, time_get_utc(), now_ms);
}
if (!g_batch_ack_pending) {
lora_sleep();
}
@@ -885,6 +930,7 @@ static void receiver_loop() {
g_sender_statuses[i].has_data = true;
g_sender_faults_remote[i].meter_read_fail = data.err_meter_read;
g_sender_faults_remote[i].lora_tx_fail = data.err_lora_tx;
receiver_note_timesync_drift(i, data.ts_utc);
g_sender_last_error_remote[i] = data.last_error;
g_sender_last_error_remote_utc[i] = time_get_utc();
g_sender_last_error_remote_ms[i] = millis();
@@ -983,6 +1029,7 @@ static void receiver_loop() {
g_sender_statuses[sender_idx].has_data = true;
g_sender_faults_remote[sender_idx].meter_read_fail = samples[count - 1].err_meter_read;
g_sender_faults_remote[sender_idx].lora_tx_fail = samples[count - 1].err_lora_tx;
receiver_note_timesync_drift(static_cast<uint8_t>(sender_idx), samples[count - 1].ts_utc);
g_sender_last_error_remote[sender_idx] = samples[count - 1].last_error;
g_sender_last_error_remote_utc[sender_idx] = time_get_utc();
g_sender_last_error_remote_ms[sender_idx] = millis();
@@ -1005,11 +1052,32 @@ static void receiver_loop() {
}
uint32_t interval_sec = TIME_SYNC_INTERVAL_SEC;
if (!g_ap_mode && millis() - g_last_timesync_ms > interval_sec * 1000UL) {
g_last_timesync_ms = millis();
if (!time_send_timesync(g_short_id)) {
note_fault(g_receiver_faults, g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms, FaultType::LoraTx);
display_set_last_error(g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms);
uint32_t now_ms = millis();
if (!g_ap_mode) {
bool burst_sent = false;
for (uint8_t i = 0; i < NUM_SENDERS; ++i) {
TimeSyncBurstState &state = g_timesync_burst[i];
if (state.active) {
if (now_ms - state.start_ms >= TIME_SYNC_BURST_DURATION_MS) {
state.active = false;
} else if (state.last_send_ms == 0 || now_ms - state.last_send_ms >= TIME_SYNC_BURST_INTERVAL_MS) {
state.last_send_ms = now_ms;
burst_sent = true;
}
}
}
if (burst_sent) {
if (!time_send_timesync(g_short_id)) {
note_fault(g_receiver_faults, g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms, FaultType::LoraTx);
display_set_last_error(g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms);
}
g_last_timesync_ms = now_ms;
} else if (now_ms - g_last_timesync_ms > interval_sec * 1000UL) {
g_last_timesync_ms = now_ms;
if (!time_send_timesync(g_short_id)) {
note_fault(g_receiver_faults, g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms, FaultType::LoraTx);
display_set_last_error(g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms);
}
}
}