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:
@@ -220,7 +220,7 @@ Fixed header (little-endian):
|
|||||||
- `err_m` u8 (meter read failures, sender-side counter)
|
- `err_m` u8 (meter read failures, sender-side counter)
|
||||||
- `err_d` u8 (decode failures, sender-side counter)
|
- `err_d` u8 (decode failures, sender-side counter)
|
||||||
- `err_tx` u8 (LoRa TX failures, sender-side counter)
|
- `err_tx` u8 (LoRa TX failures, sender-side counter)
|
||||||
- `err_last` u8 (last error code: 0=None, 1=MeterRead, 2=Decode, 3=LoraTx)
|
- `err_last` u8 (last error code: 0=None, 1=MeterRead, 2=Decode, 3=LoraTx, 4=TimeSync)
|
||||||
|
|
||||||
Body:
|
Body:
|
||||||
- `E0` u32 (absolute energy in Wh)
|
- `E0` u32 (absolute energy in Wh)
|
||||||
@@ -303,6 +303,8 @@ inline constexpr uint16_t EXPECTED_SENDER_IDS[NUM_SENDERS] = { 0xF19C };
|
|||||||
- Receiver time sync packets set the RTC.
|
- Receiver time sync packets set the RTC.
|
||||||
- On boot, if no LoRa time sync has arrived yet, the sender uses the RTC time as the initial `ts_utc`.
|
- On boot, if no LoRa time sync has arrived yet, the sender uses the RTC time as the initial `ts_utc`.
|
||||||
- Receiver keeps sending time sync every 60 seconds.
|
- Receiver keeps sending time sync every 60 seconds.
|
||||||
|
- If a sender’s timestamps drift from receiver time by more than `TIME_SYNC_DRIFT_THRESHOLD_SEC`, the receiver enters a burst mode (every `TIME_SYNC_BURST_INTERVAL_MS` for `TIME_SYNC_BURST_DURATION_MS`).
|
||||||
|
- Sender raises a local `TimeSync` error if it has not received a time beacon for `TIME_SYNC_ERROR_TIMEOUT_MS` (default 2 days). This is shown on the sender OLED only and is not sent over LoRa.
|
||||||
|
|
||||||
## Build Environments
|
## Build Environments
|
||||||
- `lilygo-t3-v1-6-1`: production build (debug on)
|
- `lilygo-t3-v1-6-1`: production build (debug on)
|
||||||
@@ -321,7 +323,7 @@ Key timing settings in `include/config.h`:
|
|||||||
- `BATTERY_SAMPLE_INTERVAL_MS`
|
- `BATTERY_SAMPLE_INTERVAL_MS`
|
||||||
- `BATTERY_CAL`
|
- `BATTERY_CAL`
|
||||||
- `BATCH_ACK_TIMEOUT_MS`
|
- `BATCH_ACK_TIMEOUT_MS`
|
||||||
- `BATCH_MAX_RETRIES`
|
- `BATCH_MAX_RETRIES`
|
||||||
- `BATCH_QUEUE_DEPTH`
|
- `BATCH_QUEUE_DEPTH`
|
||||||
- `BATCH_RETRY_POLICY` (keep or drop on retry exhaustion)
|
- `BATCH_RETRY_POLICY` (keep or drop on retry exhaustion)
|
||||||
- `SERIAL_DEBUG_MODE_FLAG` (build flag) / `SERIAL_DEBUG_DUMP_JSON`
|
- `SERIAL_DEBUG_MODE_FLAG` (build flag) / `SERIAL_DEBUG_DUMP_JSON`
|
||||||
@@ -329,6 +331,9 @@ Key timing settings in `include/config.h`:
|
|||||||
- `ENABLE_SD_LOGGING` / `PIN_SD_CS`
|
- `ENABLE_SD_LOGGING` / `PIN_SD_CS`
|
||||||
- `SENDER_TIMESYNC_WINDOW_MS`
|
- `SENDER_TIMESYNC_WINDOW_MS`
|
||||||
- `SENDER_TIMESYNC_CHECK_SEC_FAST` / `SENDER_TIMESYNC_CHECK_SEC_SLOW`
|
- `SENDER_TIMESYNC_CHECK_SEC_FAST` / `SENDER_TIMESYNC_CHECK_SEC_SLOW`
|
||||||
|
- `TIME_SYNC_DRIFT_THRESHOLD_SEC`
|
||||||
|
- `TIME_SYNC_BURST_INTERVAL_MS` / `TIME_SYNC_BURST_DURATION_MS`
|
||||||
|
- `TIME_SYNC_ERROR_TIMEOUT_MS`
|
||||||
- `SD_HISTORY_MAX_DAYS` / `SD_HISTORY_MIN_RES_MIN`
|
- `SD_HISTORY_MAX_DAYS` / `SD_HISTORY_MIN_RES_MIN`
|
||||||
- `SD_HISTORY_MAX_BINS` / `SD_HISTORY_TIME_BUDGET_MS`
|
- `SD_HISTORY_MAX_BINS` / `SD_HISTORY_TIME_BUDGET_MS`
|
||||||
- `WEB_AUTH_REQUIRE_STA` / `WEB_AUTH_REQUIRE_AP` / `WEB_AUTH_DEFAULT_USER` / `WEB_AUTH_DEFAULT_PASS`
|
- `WEB_AUTH_REQUIRE_STA` / `WEB_AUTH_REQUIRE_AP` / `WEB_AUTH_DEFAULT_USER` / `WEB_AUTH_DEFAULT_PASS`
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ constexpr uint32_t TIME_SYNC_FAST_WINDOW_MS = 10UL * 60UL * 1000UL;
|
|||||||
constexpr uint32_t SENDER_TIMESYNC_WINDOW_MS = 300;
|
constexpr uint32_t SENDER_TIMESYNC_WINDOW_MS = 300;
|
||||||
constexpr uint32_t SENDER_TIMESYNC_CHECK_SEC_FAST = 60;
|
constexpr uint32_t SENDER_TIMESYNC_CHECK_SEC_FAST = 60;
|
||||||
constexpr uint32_t SENDER_TIMESYNC_CHECK_SEC_SLOW = 3600;
|
constexpr uint32_t SENDER_TIMESYNC_CHECK_SEC_SLOW = 3600;
|
||||||
|
constexpr uint32_t TIME_SYNC_DRIFT_THRESHOLD_SEC = 10;
|
||||||
|
constexpr uint32_t TIME_SYNC_BURST_INTERVAL_MS = 10000;
|
||||||
|
constexpr uint32_t TIME_SYNC_BURST_DURATION_MS = 10UL * 60UL * 1000UL;
|
||||||
|
constexpr uint32_t TIME_SYNC_ERROR_TIMEOUT_MS = 2UL * 24UL * 60UL * 60UL * 1000UL;
|
||||||
constexpr bool ENABLE_DS3231 = true;
|
constexpr bool ENABLE_DS3231 = true;
|
||||||
constexpr uint32_t OLED_PAGE_INTERVAL_MS = 4000;
|
constexpr uint32_t OLED_PAGE_INTERVAL_MS = 4000;
|
||||||
constexpr uint32_t OLED_AUTO_OFF_MS = 10UL * 60UL * 1000UL;
|
constexpr uint32_t OLED_AUTO_OFF_MS = 10UL * 60UL * 1000UL;
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ enum class FaultType : uint8_t {
|
|||||||
None = 0,
|
None = 0,
|
||||||
MeterRead = 1,
|
MeterRead = 1,
|
||||||
Decode = 2,
|
Decode = 2,
|
||||||
LoraTx = 3
|
LoraTx = 3,
|
||||||
|
TimeSync = 4
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FaultCounters {
|
struct FaultCounters {
|
||||||
|
|||||||
@@ -172,6 +172,8 @@ static bool render_last_error_line(uint8_t y) {
|
|||||||
label = "decode";
|
label = "decode";
|
||||||
} else if (g_last_error == FaultType::LoraTx) {
|
} else if (g_last_error == FaultType::LoraTx) {
|
||||||
label = "lora";
|
label = "lora";
|
||||||
|
} else if (g_last_error == FaultType::TimeSync) {
|
||||||
|
label = "timesync";
|
||||||
}
|
}
|
||||||
display.setCursor(0, y);
|
display.setCursor(0, y);
|
||||||
display.printf("Err: %s %lus", label, static_cast<unsigned long>(age_seconds(g_last_error_ts, g_last_error_ms)));
|
display.printf("Err: %s %lus", label, static_cast<unsigned long>(age_seconds(g_last_error_ts, g_last_error_ms)));
|
||||||
|
|||||||
74
src/main.cpp
74
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_sender_discovery_sent[NUM_SENDERS] = {};
|
||||||
static bool g_receiver_discovery_sent = false;
|
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_HEADER_SIZE = 6;
|
||||||
static constexpr size_t BATCH_CHUNK_PAYLOAD = LORA_MAX_PAYLOAD - BATCH_HEADER_SIZE;
|
static constexpr size_t BATCH_CHUNK_PAYLOAD = LORA_MAX_PAYLOAD - BATCH_HEADER_SIZE;
|
||||||
static constexpr size_t BATCH_MAX_COMPRESSED = 4096;
|
static constexpr size_t BATCH_MAX_COMPRESSED = 4096;
|
||||||
@@ -190,6 +202,27 @@ static bool batch_queue_drop_oldest() {
|
|||||||
return dropped_inflight;
|
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() {
|
static BatchBuffer *batch_queue_peek() {
|
||||||
if (g_batch_count == 0) {
|
if (g_batch_count == 0) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -802,9 +835,21 @@ static void sender_loop() {
|
|||||||
g_sender_rx_window_ms += rx_elapsed;
|
g_sender_rx_window_ms += rx_elapsed;
|
||||||
}
|
}
|
||||||
if (got && rx.payload_type == PayloadType::TimeSync) {
|
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) {
|
if (!g_batch_ack_pending) {
|
||||||
lora_sleep();
|
lora_sleep();
|
||||||
}
|
}
|
||||||
@@ -885,6 +930,7 @@ static void receiver_loop() {
|
|||||||
g_sender_statuses[i].has_data = true;
|
g_sender_statuses[i].has_data = true;
|
||||||
g_sender_faults_remote[i].meter_read_fail = data.err_meter_read;
|
g_sender_faults_remote[i].meter_read_fail = data.err_meter_read;
|
||||||
g_sender_faults_remote[i].lora_tx_fail = data.err_lora_tx;
|
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[i] = data.last_error;
|
||||||
g_sender_last_error_remote_utc[i] = time_get_utc();
|
g_sender_last_error_remote_utc[i] = time_get_utc();
|
||||||
g_sender_last_error_remote_ms[i] = millis();
|
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_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].meter_read_fail = samples[count - 1].err_meter_read;
|
||||||
g_sender_faults_remote[sender_idx].lora_tx_fail = samples[count - 1].err_lora_tx;
|
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[sender_idx] = samples[count - 1].last_error;
|
||||||
g_sender_last_error_remote_utc[sender_idx] = time_get_utc();
|
g_sender_last_error_remote_utc[sender_idx] = time_get_utc();
|
||||||
g_sender_last_error_remote_ms[sender_idx] = millis();
|
g_sender_last_error_remote_ms[sender_idx] = millis();
|
||||||
@@ -1005,12 +1052,33 @@ static void receiver_loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint32_t interval_sec = TIME_SYNC_INTERVAL_SEC;
|
uint32_t interval_sec = TIME_SYNC_INTERVAL_SEC;
|
||||||
if (!g_ap_mode && millis() - g_last_timesync_ms > interval_sec * 1000UL) {
|
uint32_t now_ms = millis();
|
||||||
g_last_timesync_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)) {
|
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);
|
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);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mqtt_loop();
|
mqtt_loop();
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ static const char *fault_text(FaultType fault) {
|
|||||||
return "decode";
|
return "decode";
|
||||||
case FaultType::LoraTx:
|
case FaultType::LoraTx:
|
||||||
return "loratx";
|
return "loratx";
|
||||||
|
case FaultType::TimeSync:
|
||||||
|
return "timesync";
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -613,7 +613,7 @@ static void handle_manual() {
|
|||||||
html += "<li>Battery: percent with voltage in V.</li>";
|
html += "<li>Battery: percent with voltage in V.</li>";
|
||||||
html += "<li>RSSI/SNR: LoRa link quality from last packet.</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_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 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 += "<li>faults last: last receiver-side error code (same mapping as err_last).</li>";
|
||||||
html += "</ul>";
|
html += "</ul>";
|
||||||
|
|||||||
Reference in New Issue
Block a user