mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 22:38:19 +00:00
unify hold gate for timesync
This commit is contained in:
+6
-6
@@ -35,12 +35,12 @@ worth watching that dry-run output for a day before (and after) enabling.
|
||||
|
||||
## What it will NOT do
|
||||
|
||||
- It never fights GPS: a node whose GPS has delivered a real fix in the
|
||||
last 72 hours only observes. If GPS is on but cannot get a fix (indoors,
|
||||
dead antenna), mesh correction takes over after 72 hours — until the
|
||||
next real fix.
|
||||
- It never overrides a recent manual `time <epoch>` or `clock sync` — any
|
||||
manual set protects the clock from automatic changes for 7 days.
|
||||
- It never fights a recent clock set, whether from GPS or from you: any GPS
|
||||
fix or manual `time <epoch>` / `clock sync` protects the clock from
|
||||
automatic changes for 7 days (the same mechanism for both). A live GPS
|
||||
re-arms this on every fix, so it keeps owning the clock. If GPS goes dead
|
||||
(indoors, dead antenna), mesh correction takes over once 7 days pass
|
||||
without a fix — until the next real fix.
|
||||
- It never steps more than 1 hour at a time, and at most one step per 6 hours.
|
||||
- It does nothing unless at least 6 trustworthy senders are visible and a
|
||||
strict majority of them agree (exception: a provably dead clock after a
|
||||
|
||||
@@ -331,9 +331,8 @@ ZephCore-only divergence from Arduino MeshCore (like the Adaptive Contention Win
|
||||
|
||||
**Consensus**: Marzullo interval intersection over eligible votes, each `[skew − r, skew + r]` with `r = 150 s + 15 s × hop` (the 150 s base covers the real fleet's good-clock scatter, not just RF delay). No absolute outlier thresholds against the local clock — clustering does the rejection, so an epoch-reset clock still finds the true cluster. Stepping requires `CONFIG_ZEPHCORE_TIMESYNC_QUORUM` (default 6, floor 3, build-time security knob) eligible senders AND a strict majority inside the intersection; otherwise abstain.
|
||||
|
||||
**Correction policy** (priority: GPS > manual set > mesh consensus):
|
||||
- GPS gate: nodes whose GPS delivered a validated fix within **72 h** never step (covers the repeater's 48 h GPS duty cycle with margin). Only a real fix makes the mesh yield — a GPS that is enabled but cannot fix (indoors, dead antenna) stops gating after the window, so those units stay mesh-correctable. Sensing always continues; the dry-run marks refused steps `(gps-gated)`.
|
||||
- Manual set (`time`, `clock sync`, app time set) arms a **7-day suppression** of all stepping, bootstrap included, plus drift-envelope pedigree.
|
||||
**Correction policy** (priority: GPS / manual set > mesh consensus):
|
||||
- Any clock set — GPS fix **or** manual set (`time`, `clock sync`, app time set) — arms the same **7-day suppression** of all stepping, bootstrap included, plus drift-envelope pedigree (`noteGPSSync` and `noteManualSync` are identical). A live GPS re-arms it on every fix (so a repeater's 48 h duty cycle keeps GPS owning the clock); a GPS that cannot fix (indoors, dead antenna) becomes mesh-correctable once 7 days pass without a fix. Sensing always continues; a suppressed node shows `hold (suppressed)` in the dry-run.
|
||||
- Step trigger 10 min, dead band 5 min, step capped **±1 h**, one step per **6 h**, logged loudly. Production contains coherent wrong-time islands (+28 h × 63 repeaters at analysis time); the cap bounds capture drag to 4 h/day.
|
||||
- **Drift-envelope gate**: with a trusted sync + continuous uptime since (pedigree, RAM-only), corrections beyond `elapsed × 300 ppm + 10 min` are physically impossible for a crystal — refused regardless of quorum.
|
||||
- **Bootstrap**: local time < firmware build epoch (`FIRMWARE_BUILD_EPOCH`, CMake-injected) is provably wrong → any 3 agreeing senders, step to the cluster's **low edge** (midpoint − 150 s; undershoot so later refinement is always forward = monotonicity-safe).
|
||||
|
||||
@@ -206,7 +206,7 @@ All `set uplink.*` changes are saved immediately and only applied after reboot.
|
||||
| `get radio.rxgain` | RX gain boost: `0` or `1` |
|
||||
| `get rxduty` | RX duty cycle mode: `0` or `1` |
|
||||
| `get gps duty` | Now-effective GPS duty interval in seconds (`always on (0)` when continuous) |
|
||||
| `get meshtimesync` | Mesh time-sync state + live dry-run: on/off, eligible voter count, votes for/against, consensus skew and radius, would-be verdict (`ok`/`in-band`/`step±N`/`abstain (reason)`/`hold (reason)`; steps the policy would refuse are annotated `(gps-gated)` or `(skipped: forward-only)`), step counters, suppression countdown, and a per-sender evidence table (`prefix hops count skew E`, `E` = tenure-eligible). Sensing runs even while off, so this works as a dry-run before enabling. Over remote admin the reply is truncated to the packet size (summary always fits); the full table needs the USB CLI. |
|
||||
| `get meshtimesync` | Mesh time-sync state + live dry-run: on/off, eligible voter count, votes for/against, consensus skew and radius, would-be verdict (`ok`/`in-band`/`step±N`/`abstain (reason)`/`hold (reason)`; a recent clock set — manual or GPS — shows as `hold (suppressed)`, and a backward step a forward-only role would refuse is annotated `(skipped: forward-only)`), step counters, suppression countdown, and a per-sender evidence table (`prefix hops count skew E`, `E` = tenure-eligible). Sensing runs even while off, so this works as a dry-run before enabling. Over remote admin the reply is truncated to the packet size (summary always fits); the full table needs the USB CLI. |
|
||||
| `get dc.restarts` | Duty-cycle preamble false-positive re-arm counter (RxTimeout re-arms + parked-RX watchdog recoveries). High values mean the preamble detector is tripping on noise/interference without real packets arriving — inflates RX-on time and drains battery; packets are never lost to it. Reset by `clear stats`. |
|
||||
| `get adc.multiplier` | Battery voltage ADC calibration multiplier |
|
||||
| `get bootloader.ver` | Bootloader version string |
|
||||
@@ -251,7 +251,7 @@ Changes are persisted immediately unless noted. Some require a reboot.
|
||||
| `set radio.rxgain <0\|1\|on\|off>` | | RX gain boost, applied live. Replies `Error: unsupported` on radios without RX boost (SX127x); the pref is still saved. |
|
||||
| `set rxduty <0\|1\|on\|off>` | | RX duty cycle mode *(reboot required)*. Window timing auto-sized per SF/BW/preamble from the SX126x datasheet constraints (boot log line `rxduty:` shows the result). Zero-loss guarantee assumes senders on preamble-32 firmware (current MeshCore at SF≤8); legacy preamble-16 senders are only caught ~50% worst-phase — keep off until the local mesh has converted. Presets with 16-symbol preambles (SF≥9) fall back to continuous RX automatically. |
|
||||
| `set adc.multiplier <mult>` | (0 = use board default) | Battery voltage ADC calibration multiplier |
|
||||
| `set meshtimesync <on\|off>` | default **off** | Mesh time sync: automatically correct this node's clock from the consensus of Ed25519-signed advert timestamps heard on the mesh. Steps at most ±1 h per step, one step per 6 h; abstains without a quorum (default 6) of tenured agreeing senders; never overrides a GPS clock with a validated fix in the last 72 h, or a manual set less than 7 days old. See `MESHTIMESYNC.md`. |
|
||||
| `set meshtimesync <on\|off>` | default **off** | Mesh time sync: automatically correct this node's clock from the consensus of Ed25519-signed advert timestamps heard on the mesh. Steps at most ±1 h per step, one step per 6 h; abstains without a quorum (default 6) of tenured agreeing senders; never overrides a clock set in the last 7 days, whether from GPS (re-armed on every fix) or a manual set. See `MESHTIMESYNC.md`. |
|
||||
| `set prv.key <hex>` | 64-char hex (32-byte key) | Replace private key; derive new identity *(reboot to apply)* |
|
||||
|
||||
---
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
#include "MeshTimeSync.h"
|
||||
|
||||
#include <adapters/gps/ZephyrGPSManager.h>
|
||||
#include <adapters/clock/ZephyrRTCDiscover.h>
|
||||
#include <helpers/time_sync.h>
|
||||
#include <zephyr/kernel.h>
|
||||
@@ -24,19 +23,6 @@ static long clampl(int64_t v)
|
||||
return (long)v;
|
||||
}
|
||||
|
||||
/* GPS gate: only a validated fix younger than GPS_FIX_FRESH_SECS makes the
|
||||
* mesh yield — a GPS that is enabled but cannot fix (indoors, dead antenna)
|
||||
* stops gating after the window, so those units stay mesh-correctable. */
|
||||
static bool gps_gate_active(void)
|
||||
{
|
||||
if (!gps_is_available() || !gps_is_enabled()) {
|
||||
return false;
|
||||
}
|
||||
struct gps_state_info info;
|
||||
gps_get_state_info(&info);
|
||||
return info.last_fix_age_s < MeshTimeSync::GPS_FIX_FRESH_SECS;
|
||||
}
|
||||
|
||||
void MeshTimeSync::reset(uint32_t build_epoch, bool forward_only)
|
||||
{
|
||||
memset(_slots, 0, sizeof(_slots));
|
||||
@@ -347,11 +333,6 @@ bool MeshTimeSync::runTick(mesh::RTCClock &rtc)
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (gps_gate_active()) {
|
||||
LOG_INF("step %+ld s wanted, GPS fix is fresh - not applied",
|
||||
clampl(v.delta));
|
||||
return false;
|
||||
}
|
||||
if (_forward_only && v.delta < 0) {
|
||||
/* Forward-only roles: post timestamps feed client sync_since
|
||||
* ordering (room server) / peers hold per-sender replay high-water
|
||||
@@ -390,6 +371,13 @@ void MeshTimeSync::noteManualSync(uint32_t uptime_secs)
|
||||
|
||||
void MeshTimeSync::noteGPSSync(uint32_t uptime_secs)
|
||||
{
|
||||
/* A GPS fix owns the clock exactly like a manual set: arm the same 7-day
|
||||
* suppression window (re-armed on every fix, so a repeater's 48 h GPS
|
||||
* duty cycle keeps it owning the clock) plus the drift-envelope pedigree.
|
||||
* A unit whose GPS goes dead becomes mesh-correctable once the window
|
||||
* lapses without a new fix. */
|
||||
_suppress_uptime = uptime_secs;
|
||||
_suppressed = true;
|
||||
_pedigree_uptime = uptime_secs;
|
||||
_pedigree = true;
|
||||
}
|
||||
@@ -437,11 +425,11 @@ int MeshTimeSync::formatStatus(char *out, size_t cap, uint32_t local_time,
|
||||
char verdict[56];
|
||||
if (v.type == VERDICT_STEP) {
|
||||
/* Annotate steps the shared step policy would refuse, so the
|
||||
* dry-run never claims a step that will not happen. */
|
||||
* dry-run never claims a step that will not happen. (Suppression —
|
||||
* manual or GPS — surfaces as a hold verdict inside evaluateNow,
|
||||
* so it needs no annotation here.) */
|
||||
const char *note = "";
|
||||
if (gps_gate_active()) {
|
||||
note = " (gps-gated)";
|
||||
} else if (_forward_only && v.delta < 0) {
|
||||
if (_forward_only && v.delta < 0) {
|
||||
note = " (skipped: forward-only)";
|
||||
}
|
||||
snprintf(verdict, sizeof(verdict), "step%+ld%s%s", clampl(v.delta),
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
*
|
||||
* Role-agnostic estimator plus shared step policy. Each role feeds it
|
||||
* signature-verified adverts (onAdvertHeard) and calls runTick() from its
|
||||
* loop; runTick applies the shared policy (GPS fix-freshness gate,
|
||||
* forward-only roles skip backward steps, ±1 h cap, 6 h rate limit) and
|
||||
* steps the given clock. Role-specific bookkeeping (neighbor/ACL timestamp
|
||||
* loop; runTick applies the shared policy (7-day suppression armed by any
|
||||
* manual or GPS clock set, forward-only roles skip backward steps, ±1 h cap,
|
||||
* 6 h rate limit) and steps the given clock. Role-specific bookkeeping (neighbor/ACL timestamp
|
||||
* shifts, rate-limiter resets) happens in the role after runTick returns
|
||||
* true. ZephCore-only divergence from Arduino MeshCore — design rationale
|
||||
* in ARCHITECTURE.md, user-facing doc in MESHTIMESYNC.md.
|
||||
@@ -60,12 +60,6 @@ public:
|
||||
static constexpr int64_t PEDIGREE_PPM = 300;
|
||||
static constexpr int64_t PEDIGREE_BASE_SECS = 10 * 60;
|
||||
static constexpr uint8_t BOOTSTRAP_QUORUM = 3;
|
||||
/* GPS gate: a validated fix younger than this means GPS owns the clock
|
||||
* (it re-syncs and would step-back a wrong mesh step, poisoning our
|
||||
* advert high-water marks at peers). Covers the repeater's 48 h GPS
|
||||
* duty cycle with margin; a unit whose GPS cannot get a fix (indoors,
|
||||
* dead antenna) becomes mesh-correctable after this window. */
|
||||
static constexpr uint32_t GPS_FIX_FRESH_SECS = 72 * 3600;
|
||||
|
||||
/* 8-byte prefix is a security floor, not a tuning knob: it is the
|
||||
* sender's identity for tenure/votes while signatures verify the full
|
||||
@@ -92,7 +86,7 @@ public:
|
||||
REASON_NO_DATA, /* no eligible voters */
|
||||
REASON_NO_QUORUM,
|
||||
REASON_NO_MAJORITY,
|
||||
REASON_SUPPRESSED, /* manual clock set less than 7 days ago */
|
||||
REASON_SUPPRESSED, /* manual or GPS clock set less than 7 days ago */
|
||||
REASON_RATE_LIMITED, /* < 6 h since last applied step */
|
||||
REASON_PEDIGREE, /* drift-envelope physics veto */
|
||||
};
|
||||
@@ -145,8 +139,9 @@ public:
|
||||
* 7-day suppression window AND drift-envelope pedigree. Suppression
|
||||
* gates bootstrap too. */
|
||||
void noteManualSync(uint32_t uptime_secs);
|
||||
/* GPS time sync: arms pedigree only (stepping is gated by GPS fix
|
||||
* freshness, so no suppression needed). */
|
||||
/* GPS time sync: identical to a manual set — arms the same 7-day
|
||||
* suppression window and drift-envelope pedigree, re-armed on every fix
|
||||
* so a live GPS keeps owning the clock. */
|
||||
void noteGPSSync(uint32_t uptime_secs);
|
||||
|
||||
/* Delta of the most recent applied step (for role bookkeeping shifts). */
|
||||
@@ -154,8 +149,9 @@ public:
|
||||
|
||||
/* Compact status + evidence-table formatter shared by all role CLIs.
|
||||
* Writes at most `cap` bytes (NUL-terminated), summary first, then as
|
||||
* many per-sender entries as fit. Annotates verdicts the step policy
|
||||
* would refuse ("gps-gated", "skipped: forward-only"). */
|
||||
* many per-sender entries as fit. Annotates a backward step a
|
||||
* forward-only role would refuse ("skipped: forward-only"); a suppressed
|
||||
* clock (manual or GPS) surfaces as a "hold (suppressed)" verdict. */
|
||||
int formatStatus(char *out, size_t cap, uint32_t local_time,
|
||||
uint32_t uptime_secs, bool enabled) const;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user