mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-08-28 02:04:09 +00:00
Evaluate mesh clock when quorum arrives
This commit is contained in:
+16
-11
@@ -610,17 +610,20 @@ send text.flood checking ridge link
|
||||
- `clock.sync.samples`: `9`
|
||||
|
||||
When either source is enabled, the repeater makes its first clock-bootstrap
|
||||
attempt after 30 minutes of uptime. A successful estimate changes the RTC only
|
||||
when the absolute difference is **greater than** `clock.sync.drift`; correction
|
||||
can move the clock forward or backward. A valid estimate within the threshold
|
||||
counts as a successful sync without changing the clock. Seven days after each
|
||||
successful estimate, the repeater evaluates time again; the seven-day deadline
|
||||
therefore starts from the last successful estimate rather than from boot. This
|
||||
is a lazy uptime deadline: the check runs on the first normal loop/wake after it
|
||||
becomes due and does not wake the device by itself. If no source or consensus is
|
||||
available, the repeater retries every 30 minutes until one succeeds, then starts
|
||||
a new seven-day interval. Every reboot starts with the initial 30-minute attempt.
|
||||
An existing saved setting always overrides the platform default.
|
||||
attempt after 30 minutes of uptime, or immediately when the configured number
|
||||
of fresh evidence sources has been collected, whichever comes first. A
|
||||
successful estimate changes the RTC only when the absolute difference is
|
||||
**greater than** `clock.sync.drift`; correction can move the clock forward or
|
||||
backward. A valid estimate within the threshold counts as a successful sync
|
||||
without changing the clock. Seven days after each successful estimate, the
|
||||
repeater evaluates time again; the seven-day deadline therefore starts from the
|
||||
last successful estimate rather than from boot. This is a lazy uptime deadline:
|
||||
the check runs on the first normal loop/wake after it becomes due and does not
|
||||
wake the device by itself. If no source or consensus is available, the repeater
|
||||
retries every 30 minutes, and newly collected evidence triggers another
|
||||
immediate evaluation once the configured source count is present. Every reboot
|
||||
starts with the initial bootstrap attempt. An existing saved setting always
|
||||
overrides the platform default.
|
||||
|
||||
`clock.sync.mesh now` bypasses the startup/seven-day deadline and queues a
|
||||
LoRa-only consensus evaluation on the next normal loop, even when the internet
|
||||
@@ -629,6 +632,8 @@ source is also enabled. It uses any currently fresh samples without clearing the
|
||||
and the next attempt follows the normal 30-minute retry. The command requires
|
||||
`clock.sync.mesh` to be on and does not bypass CLI, GPS, or NTP suppression; it
|
||||
also retains the normal quorum, timestamp-validity, and drift checks.
|
||||
The separate `clock` command only displays the current RTC and does not request
|
||||
a synchronization attempt.
|
||||
|
||||
`clock.sync.mesh` collects signature-verified advert timestamps and MAC-valid,
|
||||
decrypted Public-channel plain-text timestamps. In normal path mode, collection
|
||||
|
||||
@@ -86,7 +86,7 @@ set flood.retry.ignore none
|
||||
| `flood.channel.scope` | Assigns a transport-region scope to received unscoped floods. Exact channel keys beat `txt:*`; `login:*` covers the remote-login family, and `other:*` covers every remaining flood type except TRACE, including OTA. TRACE remains unchanged across scope boundaries. ACL permission `4` can manage the table. | `get flood.channel.scope[.n]`, `set flood.channel.scope[.n] <channel|txt:*|login:*|other:*> <region>`, `del flood.channel.scope.<n>|all` | `set flood.channel.scope login:* west` |
|
||||
| `flood.filter` | Persistent repeater-only forwarding rules for flood routes `0x00`/`0x01`, selected by payload type and optional received hop count/range (omitted means `all`). New tables seed slot 1 with `ota all suspend=tempradio`; only rows marked `suspend=tempradio` are skipped during temporary-radio operation. Login-capable `anon_req`/`path`/`response` types become filterable at hop `7`; flood `txt_msg` becomes filterable at hop `5`. Standard direct traceroute, other direct routing, and local receive/logging are unchanged. | `get flood.filter[.n]`, `set flood.filter[.n] <type> [N|N+|N-M|all] [suspend=tempradio]`, `del flood.filter.<n>|all` | `set flood.filter.1 0x0C all suspend=tempradio` |
|
||||
| `flood.moderation` | Decrypts keyed `GRP_TXT` channels and applies drop, per-username messages/minute, and maximum-hop controls, optionally matched against the first 1-3 path hashes. Supports `public`, `#channel`, and 128/256-bit channel keys. Sender names and truncated path hashes are moderation hints, not authenticated identities. | `get flood.moderation[.n]`, `set flood.moderation[.n] <channel> <sender> <drop|rate=X/min|hops=N> [path=...]`, `del flood.moderation.<n>|all` | `set flood.moderation public "Noisy User" rate=5/min hops=4` |
|
||||
| `clock.sync.mesh` | Defaults on for nRF52 repeaters and off for other builds; a saved setting overrides that default. After 30 minutes of uptime, it estimates UTC from a configurable consensus of fresh signed-advert or valid Public-channel sources, then repeats lazily seven days after each successful estimate. Failed attempts retry every 30 minutes; `clock.sync.mesh now` queues an immediate LoRa-only attempt without bypassing quorum or source suppression. Only timestamps from firmware build time through build time plus ten years are recorded. Successful CLI, GPS, or WiFi/NTP clock updates suppress LoRa time collection until reboot; after reboot LoRa is the fallback if NTP cannot sync. Status reports the reason a clock was not set; its `.table` and `.1` through `.16` forms inspect collected samples. | `get clock.sync.mesh`, `set clock.sync.mesh <on|off>`, `clock.sync.mesh now`, `get clock.sync.status[.table|.1-.16]` | `set clock.sync.mesh on` |
|
||||
| `clock.sync.mesh` | Defaults on for nRF52 repeaters and off for other builds; a saved setting overrides that default. It estimates UTC as soon as the configured number of fresh signed-advert or valid Public-channel sources is collected, with a 30-minute bootstrap/retry timer when evidence is still insufficient, then repeats lazily seven days after each successful estimate. New evidence retriggers evaluation after a no-consensus result. `clock.sync.mesh now` queues an immediate LoRa-only attempt without bypassing quorum or source suppression. Only timestamps from firmware build time through build time plus ten years are recorded. Successful CLI, GPS, or WiFi/NTP clock updates suppress LoRa time collection until reboot; after reboot LoRa is the fallback if NTP cannot sync. Status reports the reason a clock was not set; its `.table` and `.1` through `.16` forms inspect collected samples. | `get clock.sync.mesh`, `set clock.sync.mesh <on|off>`, `clock.sync.mesh now`, `get clock.sync.status[.table|.1-.16]` | `set clock.sync.mesh on` |
|
||||
| `clock.sync.mesh.edge` | Defaults on so edge repeaters can collect clock evidence when all packets arrive through one relay path. Verified evidence is observed before the forwarding decision, so `repeat off` and forwarding filters do not prevent collection. Signed adverts are deduplicated by public key and Public-channel timestamps by case-insensitive display name; all may share one receive path. Public display names are unauthenticated and can be spoofed. Changing this setting clears current clock samples. | `get clock.sync.mesh.edge`, `set clock.sync.mesh.edge <on|off>` | `set clock.sync.mesh.edge on` |
|
||||
| `clock.sync.internet` | Adds a read-only internet/NTP estimate at the same initial and seven-day checks on WiFi MQTT repeater-observer builds. Other builds retain the setting but report internet unavailable. | `get clock.sync.internet`, `set clock.sync.internet <on|off>` | `set clock.sync.internet on` |
|
||||
| `clock.sync.drift` | Absolute correction threshold in seconds. The clock is moved forward or backward only when the estimate differs by more than this value. | `get clock.sync.drift`, `set clock.sync.drift <30-86400>` | `set clock.sync.drift 3600` |
|
||||
|
||||
@@ -5600,6 +5600,23 @@ void MyMesh::recordClockSyncSample(uint8_t source_kind, const uint8_t source_id[
|
||||
memcpy(sample.path_id, path_id, sizeof(sample.path_id));
|
||||
sample.epoch = epoch;
|
||||
sample.received_millis = received_millis;
|
||||
|
||||
// Do not leave a complete quorum waiting behind the startup or retry timer.
|
||||
// Evaluate on the next normal loop once enough fresh evidence exists. This
|
||||
// also refreshes a prior no-consensus result whenever a source changes.
|
||||
if (!clock_sync_complete) {
|
||||
uint32_t estimate = 0;
|
||||
uint8_t fresh = 0;
|
||||
uint8_t agreeing = 0;
|
||||
uint8_t required = clock_sync_required_samples;
|
||||
estimateMeshClock(estimate, fresh, agreeing, required);
|
||||
if (fresh >= clock_sync_required_samples) {
|
||||
clock_sync_last_fresh_count = fresh;
|
||||
clock_sync_last_sample_count = agreeing;
|
||||
clock_sync_last_required_count = required;
|
||||
clock_sync_next_attempt_uptime = uptime_millis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::recordAcceptedFloodClockSample(const mesh::Packet* packet) {
|
||||
@@ -5897,14 +5914,16 @@ void MyMesh::formatClockSyncStatus(const char* args, char* reply, size_t reply_l
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t fresh = 0;
|
||||
uint8_t active = 0;
|
||||
uint32_t now = _ms->getMillis();
|
||||
for (int i = 0; i < CLOCK_SYNC_SAMPLE_SLOTS; i++) {
|
||||
if (!clock_sync_samples[i].active) continue;
|
||||
active++;
|
||||
if (now - clock_sync_samples[i].received_millis <= CLOCK_SYNC_SAMPLE_MAX_AGE_MILLIS) fresh++;
|
||||
}
|
||||
uint32_t live_estimate = 0;
|
||||
uint8_t fresh = 0;
|
||||
uint8_t live_agreeing = 0;
|
||||
uint8_t live_required = clock_sync_required_samples;
|
||||
bool live_consensus = estimateMeshClock(live_estimate, fresh, live_agreeing, live_required);
|
||||
bool mesh_available = clock_sync_mesh_enabled
|
||||
&& clock_sync_mesh_suppressed_by == CLOCK_SYNC_MESH_SUPPRESS_NONE;
|
||||
const char* mesh_state = !clock_sync_mesh_enabled ? "off"
|
||||
@@ -5952,14 +5971,21 @@ void MyMesh::formatClockSyncStatus(const char* args, char* reply, size_t reply_l
|
||||
collection_active ? "active" : "inactive", mesh_mode,
|
||||
(unsigned int)active, next_seconds);
|
||||
} else {
|
||||
if (clock_sync_last_result == CLOCK_SYNC_RESULT_NO_CONSENSUS) {
|
||||
if (clock_sync_last_result == CLOCK_SYNC_RESULT_NO_CONSENSUS
|
||||
|| (fresh >= clock_sync_required_samples && !live_consensus)) {
|
||||
snprintf(reply, reply_len,
|
||||
"> not-set reason=no-consensus collect=%s mode=%s %s=%u agree=%u/%u table=%u next=%lus",
|
||||
collection_active ? "active" : "inactive", mesh_mode, evidence_name,
|
||||
(unsigned int)fresh,
|
||||
(unsigned int)clock_sync_last_sample_count,
|
||||
(unsigned int)clock_sync_last_required_count,
|
||||
(unsigned int)live_agreeing,
|
||||
(unsigned int)live_required,
|
||||
(unsigned int)active, next_seconds);
|
||||
} else if (live_consensus) {
|
||||
snprintf(reply, reply_len,
|
||||
"> not-set reason=ready collect=%s mode=%s %s=%u agree=%u/%u table=%u next=%lus",
|
||||
collection_active ? "active" : "inactive", mesh_mode, evidence_name,
|
||||
(unsigned int)fresh, (unsigned int)live_agreeing,
|
||||
(unsigned int)live_required, (unsigned int)active, next_seconds);
|
||||
} else {
|
||||
const char* reason = result;
|
||||
if (clock_sync_last_result == CLOCK_SYNC_RESULT_WAITING) reason = "waiting-deadline";
|
||||
|
||||
Reference in New Issue
Block a user