From ec55eb0f4c466c6f9e470e5863659b4eed0b8165 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:51:56 +0200 Subject: [PATCH] gps time sync improvement --- docs/ARCHITECTURE.md | 8 ++++--- releasenotes/RELEASE_NOTES_1.17.8-zephcore.md | 23 +++++++++++++++++++ zephcore/Kconfig | 2 +- zephcore/adapters/gps/ZephyrGPSManager.cpp | 21 +++++++++++++---- zephcore/adapters/gps/ZephyrGPSManager.h | 2 +- 5 files changed, 46 insertions(+), 10 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index bfdcfb0..0aeb5f9 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -802,7 +802,9 @@ Loading also range-checks `freq`/`sf`/`bw` and falls back to the compile-time de ### 7.3 GPS (`adapters/gps/`) - State machine: OFF → ACQUIRING → STANDBY (with warm standby on supported hardware) -- 3 consecutive good fixes (≥4 satellites) required before reporting +- 5 consecutive good fixes required before reporting. A good fix is a satellite solution (GGA quality + GNSS or DGNSS) with ≥4 satellites; an estimated fix (quality 6, dead reckoning) counts as no fix, + because its time is the module's own clock and its position the last one - Multi-constellation: GPS+GLONASS+Galileo+BeiDou with fallback - T1000-E / MeshTracker X1 (AG3335): multi-GPIO power sequencing; standby is the module's RTC backup sleep (`$PAIR650,0` before `GPS_EN` is cut, VRTC kept, a `GPS_RTC_INT` pulse wakes it) @@ -820,8 +822,8 @@ Loading also range-checks `freq`/`sf`/`bw` and falls back to the compile-time de `gps_wake_interval_ms` (initialised from `prefs.gps_interval`) controls the mode: -- **Duty cycling** (`gps_wake_interval_ms > 0`): after acquiring 3 good fixes the GPS powers down; the state machine wakes it again after the configured standby interval. The fix callback fires and then the GPS sleeps. -- **Always-on** (`gps_wake_interval_ms == 0`): the GPS never powers down. `consecutive_good_fixes` is reset after each promotion so the 3-fix gate cycles continuously, streaming fresh positions. Flash writes and fix callbacks are rate-limited to once per `gps_acquire_timeout_ms` to avoid hammering storage. +- **Duty cycling** (`gps_wake_interval_ms > 0`): after acquiring 5 good fixes the GPS powers down; the state machine wakes it again after the configured standby interval. The fix callback fires and then the GPS sleeps. +- **Always-on** (`gps_wake_interval_ms == 0`): the GPS never powers down. `consecutive_good_fixes` is reset after each promotion so the 5-fix gate cycles continuously, streaming fresh positions. Flash writes and fix callbacks are rate-limited to once per `gps_acquire_timeout_ms` to avoid hammering storage. `gps_set_poll_interval_sec(0)` switches to always-on live; persisted via `prefs.gps_interval` (set by `set gps duty 0`). diff --git a/releasenotes/RELEASE_NOTES_1.17.8-zephcore.md b/releasenotes/RELEASE_NOTES_1.17.8-zephcore.md index ef9d7eb..b237f11 100644 --- a/releasenotes/RELEASE_NOTES_1.17.8-zephcore.md +++ b/releasenotes/RELEASE_NOTES_1.17.8-zephcore.md @@ -55,3 +55,26 @@ first, which a long press cannot meet: every long press asked for the confirmati The confirmation is a second **hold**. A short press pages on instead, which on the Power Off page goes back to the first page. + +## GPS: a fix could set the clock to an old time + +On a repeater with a GPS, a fix could set the clock hours back. The next fix usually put it right +again, but until then the node's adverts and replies carried the old time, and mesh time sync was not +allowed to correct it. + +Right after waking, a GPS receiver can report an estimated position for a few seconds: its last +position and its own clock, before any satellite has confirmed them. These were accepted as a fix. +If the receiver's clock had not kept time while it slept, that old time was written to the node. + +- **Only positions solved from satellites count as a fix**, and five in a row are needed (was three). + A fix takes about two seconds longer. +- This applies to every board with a GPS. + +It was reported on a SenseCAP Solar P1 with `set gps standby` raised to 48 hours, which keeps the +receiver powered while it sleeps. With the default (one hour) a repeater switches the receiver off +between its 48-hour fixes. + +Good to know: a telemetry request from a logged-in client (anything above guest) wakes a repeater's +GPS for a fresh fix, so the GPS can run more often than the `gps duty` interval. + +Thanks to **Innocent Killer 101** for the report and the measurements. diff --git a/zephcore/Kconfig b/zephcore/Kconfig index ace48d7..6460781 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -950,7 +950,7 @@ config ZEPHCORE_GPS_FIX_TIMEOUT_SEC GPS will be powered off after this timeout. Default 120s — 30s was too aggressive for Air530Z / MAX-7Q warm starts in marginal sky conditions; the acquire loop would time out - before the 3-consecutive-good-fix gate could promote. Applies to + before the consecutive-good-fix gate could promote. Applies to wake cycles where the module kept its state; the first acquisition and every wake after a full power-off use ZEPHCORE_GPS_FIRST_FIX_TIMEOUT_SEC. diff --git a/zephcore/adapters/gps/ZephyrGPSManager.cpp b/zephcore/adapters/gps/ZephyrGPSManager.cpp index 0e298c2..4708b02 100644 --- a/zephcore/adapters/gps/ZephyrGPSManager.cpp +++ b/zephcore/adapters/gps/ZephyrGPSManager.cpp @@ -71,17 +71,20 @@ enum gps_state { static enum gps_state gps_current_state = GPS_STATE_OFF; static uint8_t consecutive_good_fixes = 0; -static bool first_fix_acquired = false; /* True after first 3-good-fix cycle since enable. Cleared on gps_enable(false) and at boot. */ +static bool first_fix_acquired = false; /* True after the first validated fix since enable. Cleared on gps_enable(false) and at boot. */ static bool first_acquire_used = false; /* True once the one-time long cold-start window has ended (fix or timeout). Cleared on gps_enable(false) and at boot. */ static bool standby_powered_off = false; /* The last standby was the full power-off (interval past gps_standby_max_sec), so the wake after it is a cold start. Set on every standby, cleared on gps_enable(false). */ static bool gps_time_synced = false; /* True after GPS syncs RTC. Starts false at boot (RTC reset), - * set true after 3 good fixes, cleared when GPS disabled. */ + * set true by a validated fix, cleared when GPS disabled. */ static int64_t last_fix_uptime_ms = 0; /* k_uptime when last validated fix was acquired */ static int64_t standby_start_ms = 0; /* k_uptime when standby started (for next-wake calc) */ static uint64_t standby_interval_ms = 0; /* How long standby lasts (for next-wake calc) */ -#define GPS_GOOD_FIX_COUNT 3 /* Need 3 consecutive good fixes */ -#define GPS_MIN_SATELLITES 4 /* Minimum satellites for valid fix */ +/* A validated fix is GPS_GOOD_FIX_COUNT consecutive good fixes. A good fix is + * a satellite solution (see gps_fix_is_real()) with GPS_MIN_SATELLITES. Why: + * devdocs/lld/10-peripherals-power.md, "Fix validation". */ +#define GPS_GOOD_FIX_COUNT 5 +#define GPS_MIN_SATELLITES 4 /* Acquire windows (Kconfig) and the duty interval (prefs, set at boot). */ static const uint32_t gps_acquire_timeout_ms = CONFIG_ZEPHCORE_GPS_FIX_TIMEOUT_SEC * 1000U; @@ -223,6 +226,14 @@ static void gps_post_fix_locked(const struct gnss_data *data) atomic_or(&pending_gps_actions, GPS_ACTION_FIX); } +/* True for a position and time solved from satellites. An estimated fix (GGA + * quality 6, dead reckoning) is the module's last position and its own clock, + * so it counts as no fix. */ +static bool gps_fix_is_real(enum gnss_fix_status status) +{ + return status == GNSS_FIX_STATUS_GNSS_FIX || status == GNSS_FIX_STATUS_DGNSS_FIX; +} + /* GNSS callback - called when new fix data is available */ static void gnss_data_cb(const struct device *dev, const struct gnss_data *data) { @@ -244,7 +255,7 @@ static void gnss_data_cb(const struct device *dev, const struct gnss_data *data) k_mutex_lock(&gps_mutex, K_FOREVER); - if (data->info.fix_status >= GNSS_FIX_STATUS_GNSS_FIX) { + if (gps_fix_is_real(data->info.fix_status)) { /* Reject (0,0): the parser can publish a valid fix status with lat/lon not * yet filled (GGA ahead of RMC). It is never a real fix. */ if (data->nav_data.latitude == 0 && data->nav_data.longitude == 0) { diff --git a/zephcore/adapters/gps/ZephyrGPSManager.h b/zephcore/adapters/gps/ZephyrGPSManager.h index 76d3d89..aaa1865 100644 --- a/zephcore/adapters/gps/ZephyrGPSManager.h +++ b/zephcore/adapters/gps/ZephyrGPSManager.h @@ -57,7 +57,7 @@ void gps_format_diagnostics(char *out, size_t out_size); typedef void (*gps_enable_callback_t)(bool enabled); void gps_set_enable_callback(gps_enable_callback_t cb); -/* GPS fix callback - called when GPS acquires a validated fix (3 consecutive good fixes) +/* GPS fix callback - called when GPS acquires a validated fix (5 consecutive satellite fixes) * Parameters: latitude (degrees), longitude (degrees), utc_time (Unix timestamp, * 0 if the fix carried no usable date). Runs on the main thread, from * gps_process_event(), so it may set the clock and prefs directly. */