From afa7ada0950053ec271073cc7221fb83ee26b9be Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Thu, 15 Oct 2026 21:53:08 +0200 Subject: [PATCH] sleep fixes --- docs/ARCHITECTURE.md | 4 + docs/CLI_commands.md | 4 +- releasenotes/RELEASE_NOTES_1.18.0-zephcore.md | 15 +++ zephcore/adapters/gps/ZephyrGPSManager.cpp | 99 ++++++++++++++++--- zephcore/adapters/gps/ZephyrGPSManager.h | 3 +- zephcore/helpers/CommonCLI.cpp | 4 +- zephcore/src/main_companion.cpp | 1 + zephcore/src/server_main_common.cpp | 1 + 8 files changed, 111 insertions(+), 20 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ad0d8de..cdae774 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -842,6 +842,10 @@ Loading also range-checks `freq`/`sf`/`bw` and falls back to the compile-time de - Three files: `ZephyrGPSManager.cpp` (states, fix validation, public API), `gps_power.cpp` (power line / PMU rail / UART sleep commands, UARTE PM), `gps_module_cfg.cpp` (GNSS-API or PMTK/PCAS/UBX configuration, `get gps diag`); `gps_internal.h` carries the feature detection - **Main thread only.** The GNSS callbacks (system work queue) only validate and post: a validated fix is snapshotted and handed to the fix callback by `gps_process_event()` on the main thread, so the callback sets the clock and the node position directly. The UI's GPS toggles post an action too. - The SoC light-sleep lock is held only while ACQUIRING (`gps_hold_sleep_lock()`, tracked 1:1), never through standby +- Boot probe (upstream's GPS detection): the first acquire window since boot must parse a GGA or RMC sentence + (`zephcore_gnss_rx_count()`) within 10 s, or the module is powered off and `gps_is_available()` is false until the + next boot (`gps`: `Can't find GPS`); `prefs.gps_enabled` is kept. For boards whose optional GPS header is empty + (LLD 10 ยง21) - Boot: servers apply `prefs.gps_enabled` like upstream's `applyGpsPrefs()` (`zc.gps_set` in prefs.json marks it a real choice; older files are upgraded to on once, because older firmware ran the GPS regardless). The observer parks its GPS (`gps_park()`) **Duty cycle vs always-on** diff --git a/docs/CLI_commands.md b/docs/CLI_commands.md index b687b40..c2c49a5 100644 --- a/docs/CLI_commands.md +++ b/docs/CLI_commands.md @@ -164,8 +164,8 @@ Regions control which flood packets the repeater forwards. The region tree is hi | Command | Description | |---------|-------------| -| `gps` | GPS status in upstream's form: `on, active\|standby, fix\|no fix, N sats`, `off`, or `Can't find GPS`. `standby` (upstream: `deactivated`) means on but asleep between duty-cycle fixes | -| `get gps` | *(ZephCore only)* The detail: `> on state= sats=N fix=Ns ago lat= lon=`, or `no fix next=Ns` (seconds to the next wake) | +| `gps` | GPS status in upstream's form: `on, active\|standby, fix\|no fix, N sats`, `off`, or `Can't find GPS`. `standby` (upstream: `deactivated`) means on but asleep between duty-cycle fixes. `Can't find GPS` also when the boot probe heard no NMEA within 10 s of the first power-on (no module fitted), until the next boot; the other `gps` commands then answer `gps toggle not found` / `gps provider not found` / `error` | +| `get gps` | *(ZephCore only)* The detail: `> on state= sats=N fix=Ns ago lat= lon=`, or `no fix next=Ns` (seconds to the next wake); `> off`; `> not found` where `gps` says `Can't find GPS` | | `gps diag` | Receiver liveness, in upstream's key form: `req:<0\|1> en:<0\|1> ok: sat: fix:<0\|1> fa: bc: sc:`. `req` is the requested setting, `en` the module powered and searching (0 in duty-cycle standby), `ok` NMEA sentences the driver has parsed since boot (a count that stops rising means the module or its UART went silent, whatever `sat` says), `fa` the age of the last validated fix, `bc`/`sc` module power-on/off counts. Upstream's UART byte and bad-checksum counters are absent: the GNSS driver only passes valid sentences. Module configuration results are `get gps diag` | | `gps sync` | Take a fresh fix now (wake the GPS or extend its window); the fix sets the clock. `gps is off` / `gps provider not found` otherwise | | `gps on` | Enable GPS module | diff --git a/releasenotes/RELEASE_NOTES_1.18.0-zephcore.md b/releasenotes/RELEASE_NOTES_1.18.0-zephcore.md index 459cbb8..21aa601 100644 --- a/releasenotes/RELEASE_NOTES_1.18.0-zephcore.md +++ b/releasenotes/RELEASE_NOTES_1.18.0-zephcore.md @@ -39,6 +39,21 @@ back. Now only satellite-solved positions count, five in a row. A fix takes abou This applies to every board with a GPS. Thanks to **Innocent Killer 101** for the report and the measurements. +## No GPS fitted, no GPS searching + +Boards with an optional GPS connector (Heltec V3, V4 and V4.3, RAK4631, XIAO nRF54L15 and others) +searched for a GPS that was not there: for five minutes after boot on a repeater, and again after +every telemetry request. On an ESP32 board that kept the processor out of light sleep the whole +time. + +- **The first time the GPS powers up after a reboot, the node listens for it for 10 seconds.** + That is at boot if your GPS is on, or when you first turn it on. If nothing answers, the GPS is + treated as not fitted until the next reboot, as upstream MeshCore does: `gps` answers + `Can't find GPS`, and telemetry no longer wakes it. +- Your GPS setting is kept, so a GPS you fit later is found at the next boot. + +Thanks to **svenlange2** for the report. + ## New: KISS modem role A ZephCore node can now act as a plain LoRa modem for a program on a computer, as upstream diff --git a/zephcore/adapters/gps/ZephyrGPSManager.cpp b/zephcore/adapters/gps/ZephyrGPSManager.cpp index 4708b02..3684f66 100644 --- a/zephcore/adapters/gps/ZephyrGPSManager.cpp +++ b/zephcore/adapters/gps/ZephyrGPSManager.cpp @@ -17,6 +17,8 @@ * full power-off instead of its state-keeping one (see gps_go_to_standby) * - Full power-off also on user-disable, on boot with the GPS disabled and on * System OFF; the acquisition after any full power-off gets the long window + * - Boot probe: no NMEA sentence in the first window's first GPS_PROBE_MS = + * no module; powered off and not available until the next boot */ #include "gps_internal.h" @@ -60,6 +62,7 @@ static gps_event_callback_t gps_event_cb = NULL; #define GPS_ACTION_FIX_DONE BIT(2) /* Got enough good fixes โ†’ go to standby */ #define GPS_ACTION_FIX BIT(3) /* A validated fix for gps_fix_cb (fix_pending) */ #define GPS_ACTION_REAPPLY BIT(4) /* Next module re-send step (gps_reapply_step) */ +#define GPS_ACTION_PROBE BIT(5) /* Probe window over โ†’ check for NMEA (gps_probe_check) */ static atomic_t pending_gps_actions; /* GPS Power Management State Machine */ @@ -120,17 +123,38 @@ static struct { } fix_pending; static bool gps_repeater_mode = false; /* True = repeater (time sync only), False = companion */ -static bool gnss_activity_seen_this_cycle = false; /* Runtime-only: set by GNSS callback while acquiring */ + +/* Boot probe (upstream's GPS detection): the first acquire window after boot + * must parse a GGA or RMC sentence within GPS_PROBE_MS, or the GPS is not + * available until the next boot. The position callback is no use here: the + * driver publishes only once GGA and RMC carry a valid UTC. Why 10 s: + * devdocs/lld/10-peripherals-power.md, "Boot probe". */ +#define GPS_PROBE_MS 10000 + +/* Parsed-sentence count from the GNSS driver (patch 0005); NULL on a driver + * without it, which then gets no probe. */ +extern "C" uint32_t zephcore_gnss_rx_count(void) __attribute__((weak)); + +static bool probe_pending = true; /* The next acquire window is the first since boot */ +static uint32_t probe_rx_base; /* zephcore_gnss_rx_count() when that window started */ /* Forward declarations for work handlers and state functions */ static void gps_wake_work_fn(struct k_work *work); static void gps_timeout_work_fn(struct k_work *work); +static void gps_probe_work_fn(struct k_work *work); static void gps_go_to_standby(void); static void gps_start_acquiring(void); /* Delayable work for event-driven timers (no polling!) */ static K_WORK_DELAYABLE_DEFINE(gps_wake_work, gps_wake_work_fn); static K_WORK_DELAYABLE_DEFINE(gps_timeout_work, gps_timeout_work_fn); +static K_WORK_DELAYABLE_DEFINE(gps_probe_work, gps_probe_work_fn); + +static void gps_probe_cancel(void) +{ + k_work_cancel_delayable(&gps_probe_work); + atomic_and(&pending_gps_actions, ~GPS_ACTION_PROBE); +} #if HAS_GPS_UART && defined(CONFIG_ZEPHCORE_GPS_REAPPLY) /* CONFIG_ZEPHCORE_GPS_REAPPLY: after each power-on, wait for the module @@ -245,11 +269,6 @@ static void gnss_data_cb(const struct device *dev, const struct gnss_data *data) return; } - if (gps_current_state == GPS_STATE_ACQUIRING) { - /* Any callback means GNSS hardware/UART path is alive, even without a fix. */ - gnss_activity_seen_this_cycle = true; - } - LOG_DBG("GNSS callback: fix=%d sats=%d state=%d", data->info.fix_status, data->info.satellites_cnt, gps_current_state); @@ -518,6 +537,7 @@ static void gps_go_to_standby(void) * RTC survive the cut and re-acquisition is a warm/hot start, not cold. * Other non-GPIO boards: software sleep via UART commands (PMTK + UBX). */ gps_reapply_cancel(); + gps_probe_cancel(); /* Keep the module's state only for a short interval; past * gps_standby_max_sec use the full power-off. The same decision on every * board: gps_power_control() maps it onto the pins. */ @@ -546,7 +566,6 @@ static void gps_start_acquiring(void) LOG_INF("GPS: Waking for %s", gps_repeater_mode ? "time sync" : "position fix"); gps_current_state = GPS_STATE_ACQUIRING; consecutive_good_fixes = 0; - gnss_activity_seen_this_cycle = false; gps_hold_sleep_lock(true); /* Bring the UART back before the module powers up / the wake byte @@ -555,6 +574,12 @@ static void gps_start_acquiring(void) gps_module_power(true); gps_reapply_start(); + if (probe_pending && zephcore_gnss_rx_count != NULL) { + probe_pending = false; + probe_rx_base = zephcore_gnss_rx_count(); + k_work_reschedule(&gps_probe_work, K_MSEC(GPS_PROBE_MS)); + } + /* Schedule the standby timeout โ€” unless always-on (interval 0), where the * GPS stays in continuous acquisition and never sleeps. Every duty window * is bounded (the first cold-start window is just longer; see @@ -601,16 +626,54 @@ static void gps_timeout_work_fn(struct k_work *work) LOG_WRN("GPS: Timeout after %d/%d fixes, deferring standby to main thread", consecutive_good_fixes, GPS_GOOD_FIX_COUNT); - if (gps_repeater_mode && !gnss_activity_seen_this_cycle) { - LOG_WRN("GPS: Repeater acquire window had no GNSS callbacks; retrying on next cycle"); - } - atomic_or(&pending_gps_actions, GPS_ACTION_TIMEOUT); if (gps_event_cb) { gps_event_cb(); } } +/* Work handler: the boot probe's window is over. Main thread decides. */ +static void gps_probe_work_fn(struct k_work *work) +{ + ARG_UNUSED(work); + + if (gps_current_state != GPS_STATE_ACQUIRING) { + return; + } + + atomic_or(&pending_gps_actions, GPS_ACTION_PROBE); + if (gps_event_cb) { + gps_event_cb(); + } +} + +/* Main thread: no sentence parsed since the first window began, so no module + * (or one at the wrong baud). Power it off, release the UART and the sleep + * lock, and report the GPS as not available until the next boot. Nothing is + * persisted: prefs.gps_enabled keeps the user's choice. */ +static void gps_probe_check(void) +{ + if (zephcore_gnss_rx_count() != probe_rx_base) { + LOG_INF("GPS: Module detected"); + return; + } + + LOG_WRN("GPS: No NMEA within %u s of power-on, no module detected", + GPS_PROBE_MS / 1000U); + + k_work_cancel_delayable(&gps_wake_work); + k_work_cancel_delayable(&gps_timeout_work); + gps_reapply_cancel(); + + gps_module_power(false, false); + gps_uart_set_power(false); + gps_hold_sleep_lock(false); + + gps_current_state = GPS_STATE_OFF; + gps_enabled = false; + gps_available = false; +} + /* ========== GNSS Init ========== */ static int gnss_init(void) @@ -852,6 +915,11 @@ void gps_enable(bool enable) k_work_cancel_delayable(&gps_wake_work); k_work_cancel_delayable(&gps_timeout_work); gps_reapply_cancel(); + /* A probe cut short is run again by the next enable. */ + if (k_work_delayable_is_pending(&gps_probe_work)) { + probe_pending = true; + } + gps_probe_cancel(); /* Full power-off on every board: this off has no end, and any * state-keeping one (backup sleep, standby pin, VRTC) draws all @@ -1066,11 +1134,6 @@ void gps_get_state_info(struct gps_state_info *info) #endif } -#if HAS_GNSS -/* Parsed-sentence count from the GNSS driver (see gps_module_cfg.cpp). */ -extern "C" uint32_t zephcore_gnss_rx_count(void) __attribute__((weak)); -#endif - void gps_format_diagnostics(char *out, size_t out_size) { if (out_size == 0) { @@ -1158,6 +1221,10 @@ void gps_process_event(void) if (gps_current_state == GPS_STATE_ACQUIRING) { gps_go_to_standby(); } + } else if (actions & GPS_ACTION_PROBE) { + if (gps_current_state == GPS_STATE_ACQUIRING) { + gps_probe_check(); + } } #if HAS_GPS_UART && defined(CONFIG_ZEPHCORE_GPS_REAPPLY) diff --git a/zephcore/adapters/gps/ZephyrGPSManager.h b/zephcore/adapters/gps/ZephyrGPSManager.h index aaa1865..070ce17 100644 --- a/zephcore/adapters/gps/ZephyrGPSManager.h +++ b/zephcore/adapters/gps/ZephyrGPSManager.h @@ -79,7 +79,8 @@ void gps_process_event(void); /* Initialize GPS manager (call once at boot) */ int gps_manager_init(void); -/* GPS functions */ +/* GPS functions. Available: the GNSS device came up and, once the first acquire + * window since boot has run its probe, the module sent NMEA. */ bool gps_is_available(void); bool gps_is_enabled(void); void gps_enable(bool enable); diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index 309e91d..f81ce9e 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -1145,7 +1145,9 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, const char* command, cha } else if (memcmp(config, "gps", 3) == 0) { /* The detail behind the `gps` line: duty-cycle state, fix age, * position. Per-constellation counts are in get gps diag. */ - if (!gps_is_enabled()) { + if (!_sensors->isGPSDetected()) { + strcpy(reply, "> not found"); + } else if (!gps_is_enabled()) { strcpy(reply, "> off"); } else { static const char* const state_str[] = { "off", "standby", "acquiring" }; diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index 39dab93..ebddc6c 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -392,6 +392,7 @@ static void mesh_event_loop(void) * system work queue signals, so it would deadlock there. */ if (events & MESH_EVENT_GPS_ACTION) { gps_process_event(); + ui_set_gps_available(gps_is_available()); /* the boot probe may clear it */ } /* Inbound frames and CLI lines, before loop() drains what they diff --git a/zephcore/src/server_main_common.cpp b/zephcore/src/server_main_common.cpp index 3c1e703..b4a622f 100644 --- a/zephcore/src/server_main_common.cpp +++ b/zephcore/src/server_main_common.cpp @@ -505,6 +505,7 @@ static void server_event_loop(void) * modem_chat blocks on system work queue semaphore). */ if (events & MESH_EVENT_GPS_ACTION) { gps_process_event(); + ui_set_gps_available(gps_is_available()); /* the boot probe may clear it */ } /* Console input: assemble + echo, then run each finished line here