diff --git a/docs/cli_commands.md b/docs/cli_commands.md index eb325c90..62cb9504 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -590,6 +590,7 @@ send text.flood checking ridge link - `get clock.sync.status` - `get clock.sync.mesh` - `set clock.sync.mesh ` +- `clock.sync.mesh now` - `get clock.sync.internet` - `set clock.sync.internet ` - `get clock.sync.drift` @@ -598,7 +599,7 @@ send text.flood checking ridge link - `set clock.sync.samples <3-16>` **Defaults:** -- `clock.sync.mesh`: `off` +- `clock.sync.mesh`: `on` for nRF52 repeaters; `off` for other builds - `clock.sync.internet`: `off` - `clock.sync.drift`: `3600` seconds - `clock.sync.samples`: `9` @@ -607,8 +608,22 @@ 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 -completes the one-time check without changing the clock. If no source is -available yet, the repeater retries every 30 minutes. +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. + +`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 +source is also enabled. It uses any currently fresh samples without clearing the +16-slot table. If there is not yet enough evidence, mesh collection remains open +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. `clock.sync.mesh` collects signature-verified advert timestamps and MAC-valid, decrypted Public-channel plain-text timestamps, but only after the packet passes @@ -626,6 +641,9 @@ succeed but an 8-vs-8 split cannot. The median is used. `clock.sync.samples` accepts `3` through `16`; samples older than two hours are ignored. Status reports `collecting` when fewer than the configured number of paths exist, and `no consensus` when enough paths exist but the effective quorum does not agree. +Mesh collection begins immediately after boot. Following a successful estimate, +it resumes two hours before the next seven-day deadline so only evidence that +can still be fresh at evaluation time is processed. A timestamp is eligible for a clock-sync sample only when it falls between the UTC build epoch embedded by `build.sh` and that time plus ten calendar years. @@ -664,15 +682,16 @@ prove that the advertising node's own clock is correct. Mesh time is therefore a consensus estimate, not an authoritative time service. `clock.sync.internet` is available on WiFi MQTT repeater-observer builds. Its -30-minute query runs on the MQTT/WiFi task and is read-only until the repeater -applies the configured drift test. On other repeater builds, the preference can -be stored but status reports that internet time is unavailable. MQTT builds -retain their existing startup NTP behavior required for MQTT/TLS/JWT operation; -this setting controls the additional delayed drift check. Startup NTP is always -preferred when it succeeds, regardless of this setting. +initial and seven-day queries run on the MQTT/WiFi task and are read-only until +the repeater applies the configured drift test. Failed queries retry after 30 +minutes. On other repeater builds, the preference can be stored but status +reports that internet time is unavailable. MQTT builds retain their existing +startup NTP behavior required for MQTT/TLS/JWT operation; this setting controls +the additional delayed drift checks. Startup NTP is always preferred when it +succeeds, regardless of this setting. Changing any `clock.sync.*` setting starts a new attempt for the current boot. -Settings are persistent in `/clock_sync`. +Settings are persistent in `/clock_sync`; samples and schedule state are not. A backward correction is intentionally allowed, but peers that already recorded a later timestamp from this node may temporarily reject its lower timestamps as diff --git a/docs/halo_keymind_settings.md b/docs/halo_keymind_settings.md index 686b40c4..2baa8048 100644 --- a/docs/halo_keymind_settings.md +++ b/docs/halo_keymind_settings.md @@ -86,8 +86,8 @@ 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] `, `del flood.channel.scope.|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] [N|N+|N-M|all] [suspend=tempradio]`, `del flood.filter.|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] [path=...]`, `del flood.moderation.|all` | `set flood.moderation public "Noisy User" rate=5/min hops=4` | -| `clock.sync.mesh` | After 30 minutes of uptime, estimates UTC from a configurable consensus of fresh signed-advert or valid Public-channel sources. 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. | `get clock.sync.mesh`, `set clock.sync.mesh `, `get clock.sync.status` | `set clock.sync.mesh on` | -| `clock.sync.internet` | Adds a read-only internet/NTP estimate at the same delayed check on WiFi MQTT repeater-observer builds. Other builds retain the setting but report internet unavailable. | `get clock.sync.internet`, `set clock.sync.internet ` | `set clock.sync.internet on` | +| `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. | `get clock.sync.mesh`, `set clock.sync.mesh `, `clock.sync.mesh now`, `get clock.sync.status` | `set clock.sync.mesh 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 ` | `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` | | `clock.sync.samples` | Minimum number of distinct, in-window receive paths that must agree before mesh time can be used. A strict majority of all fresh samples is also required. Range `3-16`; default `9`. | `get clock.sync.samples`, `set clock.sync.samples <3-16>` | `set clock.sync.samples 9` | | `outpath` | Overrides the primary direct route used for replies to the current remote client. | `get outpath`, `set outpath `, `set outpath direct`, `set outpath clear`, `set outpath flood` | `set outpath A1B2C3,D4E5F6` | diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 093ac2d1..b97e59e8 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -2457,10 +2457,11 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc memset(flood_channel_scopes, 0, sizeof(flood_channel_scopes)); memset(flood_group_moderation, 0, sizeof(flood_group_moderation)); memset(clock_sync_samples, 0, sizeof(clock_sync_samples)); - clock_sync_mesh_enabled = false; + clock_sync_mesh_enabled = CLOCK_SYNC_MESH_DEFAULT_ENABLED != 0; clock_sync_internet_enabled = false; clock_sync_complete = false; clock_sync_internet_pending = false; + clock_sync_force_mesh_pending = false; clock_sync_mesh_suppressed_by = CLOCK_SYNC_MESH_SUPPRESS_NONE; clock_sync_last_result = CLOCK_SYNC_RESULT_WAITING; clock_sync_last_source = CLOCK_SYNC_SOURCE_NONE; @@ -5326,7 +5327,7 @@ void MyMesh::deleteFloodGroupModeration(const char* args, char* reply) { } void MyMesh::loadClockSyncPrefs() { - clock_sync_mesh_enabled = false; + clock_sync_mesh_enabled = CLOCK_SYNC_MESH_DEFAULT_ENABLED != 0; clock_sync_internet_enabled = false; clock_sync_drift_seconds = CLOCK_SYNC_DRIFT_DEFAULT_SECONDS; clock_sync_required_samples = CLOCK_SYNC_REQUIRED_SAMPLES_DEFAULT; @@ -5383,6 +5384,7 @@ bool MyMesh::saveClockSyncPrefs() { void MyMesh::resetClockSyncAttempt() { clock_sync_complete = false; clock_sync_internet_pending = false; + clock_sync_force_mesh_pending = false; clock_sync_last_result = CLOCK_SYNC_RESULT_WAITING; clock_sync_last_source = CLOCK_SYNC_SOURCE_NONE; clock_sync_last_sample_count = 0; @@ -5409,6 +5411,7 @@ void MyMesh::suppressMeshClockSyncForBoot(uint8_t source) { || clock_sync_mesh_suppressed_by != CLOCK_SYNC_MESH_SUPPRESS_NONE) return; clock_sync_mesh_suppressed_by = source; + clock_sync_force_mesh_pending = false; memset(clock_sync_samples, 0, sizeof(clock_sync_samples)); MESH_DEBUG_PRINTLN("Clock sync: LoRa estimate suppressed by %s until reboot", clockSyncMeshSuppressionName(source)); @@ -5425,6 +5428,20 @@ void MyMesh::checkGpsClockSyncOverride() { } } +bool MyMesh::isClockSyncCollectionActive() const { + if (!clock_sync_mesh_enabled + || clock_sync_mesh_suppressed_by != CLOCK_SYNC_MESH_SUPPRESS_NONE) return false; + if (!clock_sync_complete) return true; + if (clock_sync_next_attempt_uptime == 0) return false; + if (uptime_millis >= clock_sync_next_attempt_uptime) return true; + + // Keep only the evidence that could still be fresh at the next scheduled + // evaluation. This opens a two-hour rolling collection window before each + // seven-day re-sync without decrypting Public traffic for the entire week. + return clock_sync_next_attempt_uptime - uptime_millis + <= (uint64_t)CLOCK_SYNC_SAMPLE_MAX_AGE_MILLIS; +} + static void deriveClockSyncPathId(const mesh::Packet* packet, uint8_t path_id[CLOCK_SYNC_PATH_ID_SIZE]) { uint8_t material[2 + MAX_PATH_SIZE]; @@ -5466,8 +5483,7 @@ uint32_t MyMesh::estimateClockTransitMillis(const mesh::Packet* packet) const { void MyMesh::recordClockSyncSample(uint8_t source_kind, const uint8_t source_id[4], uint32_t epoch, const mesh::Packet* packet) { - if (!clock_sync_mesh_enabled || clock_sync_complete - || clock_sync_mesh_suppressed_by != CLOCK_SYNC_MESH_SUPPRESS_NONE || source_id == NULL + if (!isClockSyncCollectionActive() || source_id == NULL || packet == NULL || !packet->isRouteFlood() || !clockSyncEpochIsValid(epoch)) return; uint32_t transit_millis = estimateClockTransitMillis(packet); @@ -5536,8 +5552,7 @@ void MyMesh::recordClockSyncSample(uint8_t source_kind, const uint8_t source_id[ } void MyMesh::recordAcceptedFloodClockSample(const mesh::Packet* packet) { - if (!clock_sync_mesh_enabled || clock_sync_complete - || clock_sync_mesh_suppressed_by != CLOCK_SYNC_MESH_SUPPRESS_NONE || packet == NULL) return; + if (!isClockSyncCollectionActive() || packet == NULL) return; if (packet->getPayloadType() == PAYLOAD_TYPE_GRP_TXT) { recordPublicChannelClockSample(packet); @@ -5555,8 +5570,7 @@ void MyMesh::recordAcceptedFloodClockSample(const mesh::Packet* packet) { } void MyMesh::recordPublicChannelClockSample(const mesh::Packet* packet) { - if (!clock_sync_mesh_enabled || clock_sync_complete - || clock_sync_mesh_suppressed_by != CLOCK_SYNC_MESH_SUPPRESS_NONE) return; + if (!isClockSyncCollectionActive()) return; uint32_t timestamp = 0; char sender[FLOOD_GROUP_MODERATION_NAME_LEN]; if (!decodeFloodGroupPlainText(packet, FLOOD_PUBLIC_CHANNEL_SECRET, CIPHER_KEY_SIZE, @@ -5620,7 +5634,7 @@ bool MyMesh::applyClockEstimate(uint32_t estimate, uint8_t source, uint8_t sampl clock_sync_last_estimate = estimate; clock_sync_last_abs_drift = magnitude > UINT32_MAX ? UINT32_MAX : (uint32_t)magnitude; clock_sync_complete = true; - clock_sync_next_attempt_uptime = 0; + clock_sync_next_attempt_uptime = uptime_millis + CLOCK_SYNC_RESYNC_INTERVAL_MILLIS; if (magnitude <= clock_sync_drift_seconds) { clock_sync_last_result = CLOCK_SYNC_RESULT_WITHIN_DRIFT; @@ -5667,37 +5681,49 @@ void MyMesh::checkClockSync() { bool mesh_available = clock_sync_mesh_enabled && clock_sync_mesh_suppressed_by == CLOCK_SYNC_MESH_SUPPRESS_NONE; - if (clock_sync_complete || (!mesh_available && !clock_sync_internet_enabled) - || uptime_millis < clock_sync_next_attempt_uptime) return; + bool force_mesh = clock_sync_force_mesh_pending && mesh_available; + if ((!mesh_available && !clock_sync_internet_enabled) + || (!force_mesh && uptime_millis < clock_sync_next_attempt_uptime)) return; + // A manual mesh request bypasses both the normal deadline and the optional + // internet-first probe once. It never bypasses the source safety latch. + if (force_mesh) clock_sync_force_mesh_pending = false; + + // A successful estimate closes the current attempt but leaves a seven-day + // deadline behind. Reopen the attempt when that deadline arrives; failures + // below retain the existing 30-minute retry cadence until the next success. + if (clock_sync_complete) clock_sync_complete = false; + + if (!force_mesh) { #ifdef WITH_MQTT_BRIDGE - if (clock_sync_internet_enabled && mqtt_bridge != NULL && mqtt_bridge->isRunning()) { - if (clock_sync_internet_pending) { - uint32_t estimate = 0; - bool finished = false; - bool success = mqtt_bridge->takeNtpTimeEstimate(estimate, finished); - if (!finished && _ms->getMillis() - clock_sync_internet_requested_millis < 30000UL) return; + if (clock_sync_internet_enabled && mqtt_bridge != NULL && mqtt_bridge->isRunning()) { + if (clock_sync_internet_pending) { + uint32_t estimate = 0; + bool finished = false; + bool success = mqtt_bridge->takeNtpTimeEstimate(estimate, finished); + if (!finished && _ms->getMillis() - clock_sync_internet_requested_millis < 30000UL) return; + clock_sync_internet_pending = false; + if (success && applyClockEstimate(estimate, CLOCK_SYNC_SOURCE_INTERNET, 1)) return; + clock_sync_last_result = CLOCK_SYNC_RESULT_INTERNET_UNAVAILABLE; + } else if (mqtt_bridge->requestNtpTimeEstimate()) { + clock_sync_internet_pending = true; + clock_sync_internet_requested_millis = _ms->getMillis(); + clock_sync_last_result = CLOCK_SYNC_RESULT_INTERNET_PENDING; + return; + } else { + clock_sync_last_result = CLOCK_SYNC_RESULT_INTERNET_UNAVAILABLE; + } + } else if (clock_sync_internet_enabled) { clock_sync_internet_pending = false; - if (success && applyClockEstimate(estimate, CLOCK_SYNC_SOURCE_INTERNET, 1)) return; - clock_sync_last_result = CLOCK_SYNC_RESULT_INTERNET_UNAVAILABLE; - } else if (mqtt_bridge->requestNtpTimeEstimate()) { - clock_sync_internet_pending = true; - clock_sync_internet_requested_millis = _ms->getMillis(); - clock_sync_last_result = CLOCK_SYNC_RESULT_INTERNET_PENDING; - return; - } else { clock_sync_last_result = CLOCK_SYNC_RESULT_INTERNET_UNAVAILABLE; } - } else if (clock_sync_internet_enabled) { - clock_sync_internet_pending = false; - clock_sync_last_result = CLOCK_SYNC_RESULT_INTERNET_UNAVAILABLE; - } #else - if (clock_sync_internet_enabled) { - clock_sync_internet_pending = false; - clock_sync_last_result = CLOCK_SYNC_RESULT_INTERNET_UNAVAILABLE; - } + if (clock_sync_internet_enabled) { + clock_sync_internet_pending = false; + clock_sync_last_result = CLOCK_SYNC_RESULT_INTERNET_UNAVAILABLE; + } #endif + } if (mesh_available) { uint32_t estimate = 0; @@ -5755,23 +5781,24 @@ void MyMesh::formatClockSyncStatus(char* reply, size_t reply_len) const { } const char* source = clock_sync_last_source == CLOCK_SYNC_SOURCE_INTERNET ? "internet" : (clock_sync_last_source == CLOCK_SYNC_SOURCE_MESH ? "mesh" : "none"); + uint64_t remaining_ms = clock_sync_next_attempt_uptime > uptime_millis + ? clock_sync_next_attempt_uptime - uptime_millis : 0; + unsigned long next_seconds = (unsigned long)((remaining_ms + 999ULL) / 1000ULL); if (clock_sync_complete) { if (clock_sync_last_source == CLOCK_SYNC_SOURCE_MESH) { - snprintf(reply, reply_len, "> %s %lus via %s epoch=%lu agree=%u/%u paths=%u mesh=%s", + snprintf(reply, reply_len, "> %s %lus via %s epoch=%lu agree=%u/%u paths=%u mesh=%s next=%lus", result, (unsigned long)clock_sync_last_abs_drift, source, (unsigned long)clock_sync_last_estimate, (unsigned int)clock_sync_last_sample_count, (unsigned int)clock_sync_last_required_count, - (unsigned int)clock_sync_last_fresh_count, mesh_state); + (unsigned int)clock_sync_last_fresh_count, mesh_state, next_seconds); } else { - snprintf(reply, reply_len, "> %s %lus via %s, epoch=%lu drift=%lus mesh=%s", + snprintf(reply, reply_len, "> %s %lus via %s, epoch=%lu drift=%lus mesh=%s next=%lus", result, (unsigned long)clock_sync_last_abs_drift, source, (unsigned long)clock_sync_last_estimate, - (unsigned long)clock_sync_drift_seconds, mesh_state); + (unsigned long)clock_sync_drift_seconds, mesh_state, next_seconds); } } else { - uint64_t remaining_ms = clock_sync_next_attempt_uptime > uptime_millis - ? clock_sync_next_attempt_uptime - uptime_millis : 0; if (clock_sync_last_result == CLOCK_SYNC_RESULT_NO_CONSENSUS) { snprintf(reply, reply_len, "> no consensus, mesh=%s internet=%s agree=%u/%u paths=%u next=%lus", @@ -5779,14 +5806,14 @@ void MyMesh::formatClockSyncStatus(char* reply, size_t reply_len) const { (unsigned int)clock_sync_last_sample_count, (unsigned int)clock_sync_last_required_count, (unsigned int)clock_sync_last_fresh_count, - (unsigned long)((remaining_ms + 999ULL) / 1000ULL)); + next_seconds); } else { snprintf(reply, reply_len, "> %s, mesh=%s internet=%s drift=%lus paths=%u/%u next=%lus", result, mesh_state, clock_sync_internet_enabled ? "on" : "off", (unsigned long)clock_sync_drift_seconds, (unsigned int)fresh, (unsigned int)clock_sync_required_samples, - (unsigned long)((remaining_ms + 999ULL) / 1000ULL)); + next_seconds); } } } @@ -6564,6 +6591,18 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char * sprintf(reply, "> %lu", (unsigned long)clock_sync_drift_seconds); } else if (strcmp(command, "get clock.sync.samples") == 0) { sprintf(reply, "> %u", (unsigned int)clock_sync_required_samples); + } else if (strcmp(command, "clock.sync.mesh now") == 0) { + if (!clock_sync_mesh_enabled) { + strcpy(reply, "Err - mesh clock sync is off"); + } else if (clock_sync_mesh_suppressed_by != CLOCK_SYNC_MESH_SUPPRESS_NONE) { + sprintf(reply, "Err - mesh sync suppressed by %s until reboot", + clockSyncMeshSuppressionName(clock_sync_mesh_suppressed_by)); + } else { + resetClockSyncAttempt(); + clock_sync_force_mesh_pending = true; + clock_sync_next_attempt_uptime = uptime_millis; + strcpy(reply, "OK - mesh clock sync queued"); + } } else if (strncmp(command, "set clock.sync.mesh ", 20) == 0) { const char* value = command + 20; bool enabled; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 6ae9da90..b3b61b1a 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -151,12 +151,20 @@ struct NeighbourInfo { #ifndef CLOCK_SYNC_SAMPLE_SLOTS #define CLOCK_SYNC_SAMPLE_SLOTS 16 #endif +#ifndef CLOCK_SYNC_MESH_DEFAULT_ENABLED + #if defined(NRF52_PLATFORM) + #define CLOCK_SYNC_MESH_DEFAULT_ENABLED 1 + #else + #define CLOCK_SYNC_MESH_DEFAULT_ENABLED 0 + #endif +#endif #define CLOCK_SYNC_REQUIRED_SAMPLES_MIN 3 #define CLOCK_SYNC_REQUIRED_SAMPLES_MAX CLOCK_SYNC_SAMPLE_SLOTS #define CLOCK_SYNC_REQUIRED_SAMPLES_DEFAULT 9 #define CLOCK_SYNC_PATH_ID_SIZE 8 #define CLOCK_SYNC_STARTUP_DELAY_MILLIS (30ULL * 60ULL * 1000ULL) #define CLOCK_SYNC_RETRY_INTERVAL_MILLIS (30ULL * 60ULL * 1000ULL) +#define CLOCK_SYNC_RESYNC_INTERVAL_MILLIS (7ULL * 24ULL * 60ULL * 60ULL * 1000ULL) #define CLOCK_SYNC_SAMPLE_MAX_AGE_MILLIS (2UL * 60UL * 60UL * 1000UL) #define CLOCK_SYNC_CONSENSUS_WINDOW_SECONDS 600UL #define CLOCK_SYNC_DRIFT_MIN_SECONDS 30UL @@ -283,6 +291,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks bool clock_sync_internet_enabled; bool clock_sync_complete; bool clock_sync_internet_pending; + bool clock_sync_force_mesh_pending; uint8_t clock_sync_mesh_suppressed_by; uint8_t clock_sync_last_result; uint8_t clock_sync_last_source; @@ -451,6 +460,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks void resetClockSyncAttempt(); void suppressMeshClockSyncForBoot(uint8_t source); void checkGpsClockSyncOverride(); + bool isClockSyncCollectionActive() const; uint32_t estimateClockTransitMillis(const mesh::Packet* packet) const; void recordClockSyncSample(uint8_t source_kind, const uint8_t source_id[4], uint32_t epoch, const mesh::Packet* packet);