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:
@@ -172,6 +172,8 @@ static bool render_last_error_line(uint8_t y) {
|
||||
label = "decode";
|
||||
} else if (g_last_error == FaultType::LoraTx) {
|
||||
label = "lora";
|
||||
} else if (g_last_error == FaultType::TimeSync) {
|
||||
label = "timesync";
|
||||
}
|
||||
display.setCursor(0, y);
|
||||
display.printf("Err: %s %lus", label, static_cast<unsigned long>(age_seconds(g_last_error_ts, g_last_error_ms)));
|
||||
|
||||
80
src/main.cpp
80
src/main.cpp
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ static const char *fault_text(FaultType fault) {
|
||||
return "decode";
|
||||
case FaultType::LoraTx:
|
||||
return "loratx";
|
||||
case FaultType::TimeSync:
|
||||
return "timesync";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -613,7 +613,7 @@ static void handle_manual() {
|
||||
html += "<li>Battery: percent with voltage in V.</li>";
|
||||
html += "<li>RSSI/SNR: LoRa link quality from last packet.</li>";
|
||||
html += "<li>err_tx: sender-side LoRa TX error counter.</li>";
|
||||
html += "<li>err_last: last error code (0=None, 1=MeterRead, 2=Decode, 3=LoraTx).</li>";
|
||||
html += "<li>err_last: last error code (0=None, 1=MeterRead, 2=Decode, 3=LoraTx, 4=TimeSync).</li>";
|
||||
html += "<li>faults m/d/tx: receiver-side counters (meter read fails, decode fails, LoRa TX fails).</li>";
|
||||
html += "<li>faults last: last receiver-side error code (same mapping as err_last).</li>";
|
||||
html += "</ul>";
|
||||
|
||||
Reference in New Issue
Block a user