From c4a1777956cdc1f0c9f6fa07e7ea452bf0da6e91 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Wed, 17 Jun 2026 15:42:50 +0200 Subject: [PATCH] refactor gps manager --- zephcore/Kconfig | 11 ++ zephcore/Repeater_CLI_commands.md | 3 + zephcore/adapters/gps/ZephyrGPSManager.cpp | 172 +++++++++++++++------ zephcore/app/RepeaterDataStore.cpp | 13 ++ zephcore/app/RepeaterMesh.h | 1 + zephcore/app/RoomServerMesh.h | 1 + zephcore/helpers/CommonCLI.cpp | 33 ++++ zephcore/helpers/CommonCLI.h | 3 + zephcore/src/main_companion.cpp | 4 + zephcore/src/main_repeater.cpp | 2 + zephcore/src/main_room_server.cpp | 2 + 11 files changed, 202 insertions(+), 43 deletions(-) diff --git a/zephcore/Kconfig b/zephcore/Kconfig index 2b667e3..4e119c3 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -600,6 +600,17 @@ config ZEPHCORE_GPS_FIRST_FIX_TIMEOUT_SEC Default 300s (5 minutes). Companion mode only; repeaters use a fixed 5-minute time-sync window. +config ZEPHCORE_REPEATER_GPS_INTERVAL_SEC + int "Repeater GPS duty interval in seconds (boot default)" + default 172800 + range 0 604800 + help + Default GPS standby interval for repeaters/room servers: how often + the GPS wakes for a time-sync fix. Default 172800s (48 hours). + 0 = always-on (never sleeps). Applied at boot until the operator + overrides it with "set gps duty " (persisted to flash). + Companions default to ZEPHCORE_GPS_POLL_INTERVAL_SEC (300s) instead. + endmenu menu "WiFi OTA Update" diff --git a/zephcore/Repeater_CLI_commands.md b/zephcore/Repeater_CLI_commands.md index 50e12b1..8662ce6 100644 --- a/zephcore/Repeater_CLI_commands.md +++ b/zephcore/Repeater_CLI_commands.md @@ -121,6 +121,8 @@ Regions control which flood packets the repeater forwards. The region tree is hi | `gps advert none` | Do not include location in advertisements | | `gps advert share` | Include live GPS location in advertisements | | `gps advert prefs` | Include stored lat/lon from prefs in advertisements | +| `set gps duty ` | GPS duty interval (standby seconds between fixes). `0` = always-on (continuous; streams fresh fixes, can download a full almanac). Floor 10s, cap 604800 (1 week). Persists to flash, applied live. | +| `set gps duty default` | Reset GPS duty to the role default (repeater/room 48h, companion 300s) | --- @@ -203,6 +205,7 @@ All `set uplink.*` changes are saved immediately and only applied after reboot. | `get loop.detect` | Loop detection level: `off`, `minimal`, `moderate`, or `strict` | | `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 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 | diff --git a/zephcore/adapters/gps/ZephyrGPSManager.cpp b/zephcore/adapters/gps/ZephyrGPSManager.cpp index d13023e..88beca9 100644 --- a/zephcore/adapters/gps/ZephyrGPSManager.cpp +++ b/zephcore/adapters/gps/ZephyrGPSManager.cpp @@ -103,8 +103,21 @@ static uint32_t gps_acquire_timeout_ms = CONFIG_ZEPHCORE_GPS_FIX_TIMEOUT_SEC * static uint32_t gps_first_fix_timeout_ms = CONFIG_ZEPHCORE_GPS_FIRST_FIX_TIMEOUT_SEC * 1000U; static uint32_t gps_wake_interval_ms = CONFIG_ZEPHCORE_GPS_POLL_INTERVAL_SEC * 1000U; -/* Repeater mode intervals - GPS only for time sync */ -#define GPS_REPEATER_SYNC_INTERVAL_MS (48ULL * 60 * 60 * 1000) /* 48 hours */ +/* Duty cycle vs always-on: a non-zero standby interval duty-cycles; interval 0 + * keeps the GPS in continuous acquisition (never sleeps) so it streams fresh + * fixes for telemetry and can download a full almanac. */ +static inline bool gps_duty_cycling(void) +{ + return gps_wake_interval_ms != 0; +} + +/* k_uptime of the last always-on position/clock refresh — rate-limits the + * flash write + RTC sync to gps_acquire_timeout_ms while streaming (see + * gnss_data_cb), so continuous operation doesn't hammer flash. */ +static int64_t last_promote_ms = 0; + +/* Repeater acquire window — GPS only for time sync. The standby interval is + * unified with companions via gps_wake_interval_ms (prefs.gps_interval). */ #define GPS_REPEATER_SYNC_TIMEOUT_MS (5 * 60 * 1000) /* 5 minutes */ static bool gps_repeater_mode = false; /* True = repeater (time sync only), False = companion */ @@ -275,6 +288,9 @@ static void gnss_data_cb(const struct device *dev, const struct gnss_data *data) current_pos.longitude_ndeg / 1000000); if (consecutive_good_fixes >= GPS_GOOD_FIX_COUNT) { + bool duty = gps_duty_cycling(); + bool first_ever = !first_fix_acquired; + LOG_INF("GPS: Got %d good fixes, updating location/time", GPS_GOOD_FIX_COUNT); @@ -285,29 +301,56 @@ static void gnss_data_cb(const struct device *dev, const struct gnss_data *data) gps_time_synced = true; last_fix_uptime_ms = k_uptime_get(); - /* Persist position to flash for reboot survival */ - gps_save_position(¤t_pos); + /* Promote (persist position + sync clock via the fix + * callback) every duty-cycle wake; in always-on, only on + * the first fix and then once per gps_acquire_timeout_ms, + * so streaming doesn't hammer flash. current_pos (the + * telemetry source) is updated on every fix above either way. */ + bool promote = duty || first_ever || + (k_uptime_get() - last_promote_ms >= + (int64_t)gps_acquire_timeout_ms); + if (promote) { + last_promote_ms = k_uptime_get(); + /* Persist position to flash for reboot survival */ + gps_save_position(¤t_pos); + } - /* Cancel timeout */ - k_work_cancel_delayable(&gps_timeout_work); + if (duty) { + /* Cancel timeout */ + k_work_cancel_delayable(&gps_timeout_work); - /* Notify fix callback with validated position */ - if (gps_fix_cb) { + /* Notify fix callback with validated position */ + if (gps_fix_cb) { + double lat = (double)data->nav_data.latitude / 1000000000.0; + double lon = (double)data->nav_data.longitude / 1000000000.0; + k_mutex_unlock(&gps_mutex); + gps_fix_cb(lat, lon, gps_get_utc_time()); + } else { + k_mutex_unlock(&gps_mutex); + } + + /* Defer standby to main thread — we're on the system + * workqueue here (GNSS callback), can't call PM suspend + * (modem_chat_run_script deadlocks on same workqueue). */ + atomic_or(&pending_gps_actions, GPS_ACTION_FIX_DONE); + if (gps_event_cb) { + gps_event_cb(); + } + return; + } + + /* Always-on: keep streaming (no standby). Reset the gate so + * we don't re-promote every fix; refresh persisted position + * + RTC on the cadence captured by `promote` above. */ + consecutive_good_fixes = 0; + if (promote && gps_fix_cb) { double lat = (double)data->nav_data.latitude / 1000000000.0; double lon = (double)data->nav_data.longitude / 1000000000.0; k_mutex_unlock(&gps_mutex); gps_fix_cb(lat, lon, gps_get_utc_time()); - } else { - k_mutex_unlock(&gps_mutex); - } - - /* Defer standby to main thread — we're on the system - * workqueue here (GNSS callback), can't call PM suspend - * (modem_chat_run_script deadlocks on same workqueue). */ - atomic_or(&pending_gps_actions, GPS_ACTION_FIX_DONE); - if (gps_event_cb) { - gps_event_cb(); + return; } + k_mutex_unlock(&gps_mutex); return; } } else { @@ -985,14 +1028,14 @@ static uint32_t gps_acquire_window_ms(void) * FORCE_ON pin LOW for L76K hardware standby. */ static void gps_go_to_standby(void) { - uint64_t wake_interval = gps_repeater_mode ? - GPS_REPEATER_SYNC_INTERVAL_MS : gps_wake_interval_ms; + /* Unified standby interval for both roles — set from prefs.gps_interval + * at boot (companion default 300s, repeater default 48h). Always-on + * (interval 0) never reaches here. */ + uint64_t wake_interval = gps_wake_interval_ms; - if (gps_repeater_mode) { - LOG_INF("GPS: Going to standby for 48 hours (repeater mode)"); - } else { - LOG_INF("GPS: Going to standby for %d minutes", (int)(wake_interval / 60000)); - } + LOG_INF("GPS: Going to standby for %llu s%s", + (unsigned long long)(wake_interval / 1000), + gps_repeater_mode ? " (repeater time sync)" : ""); gps_current_state = GPS_STATE_STANDBY; consecutive_good_fixes = 0; /* The one-time long cold-start window (if any) is now spent — later @@ -1047,11 +1090,17 @@ static void gps_start_acquiring(void) gps_software_wake(); #endif - /* Start timeout timer — every window is now bounded (the first - * cold-start window is just longer; see gps_acquire_window_ms). */ - uint32_t timeout_ms = gps_acquire_window_ms(); - LOG_INF("GPS: Acquire window %u s", timeout_ms / 1000U); - k_work_schedule(&gps_timeout_work, K_MSEC(timeout_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 + * gps_acquire_window_ms). */ + if (gps_duty_cycling()) { + uint32_t timeout_ms = gps_acquire_window_ms(); + LOG_INF("GPS: Acquire window %u s", timeout_ms / 1000U); + k_work_schedule(&gps_timeout_work, K_MSEC(timeout_ms)); + } else { + LOG_INF("GPS: Always-on (continuous, no standby)"); + } } /* Work handler: wake GPS for fix. @@ -1415,10 +1464,15 @@ void gps_enable(bool enable) * ~300ms to boot after GPIO power restore and calling it * immediately deadlocks the main thread. */ - /* Bounded first-acquisition window, then the normal duty cycle */ - uint32_t timeout_ms = gps_acquire_window_ms(); - LOG_INF("GPS: Acquire window %u s", timeout_ms / 1000U); - k_work_schedule(&gps_timeout_work, K_MSEC(timeout_ms)); + /* Bounded first-acquisition window, then the normal duty cycle — + * unless always-on (interval 0), where GPS never sleeps. */ + if (gps_duty_cycling()) { + uint32_t timeout_ms = gps_acquire_window_ms(); + LOG_INF("GPS: Acquire window %u s", timeout_ms / 1000U); + k_work_schedule(&gps_timeout_work, K_MSEC(timeout_ms)); + } else { + LOG_INF("GPS: Always-on (continuous, no standby)"); + } } else { LOG_INF("GPS disabled - canceling timers and powering off"); @@ -1479,10 +1533,39 @@ uint32_t gps_get_poll_interval_sec(void) void gps_set_poll_interval_sec(uint32_t interval) { #if HAS_GNSS - if (interval < 10) interval = 10; - if (interval > 86400) interval = 86400; + /* 0 = always-on (no standby); otherwise floor 10s. Cap at 1 week — sane + * for time-sync and safely below the interval*1000 uint32 overflow (~49d). */ + if (interval != 0 && interval < 10) interval = 10; + if (interval > 604800) interval = 604800; gps_wake_interval_ms = interval * 1000U; - LOG_INF("GPS poll interval set to %u seconds", interval); + LOG_INF("GPS poll interval set to %u seconds%s", interval, + interval == 0 ? " (always on)" : ""); + + /* Live re-arm so a runtime change takes effect without a reboot. */ + if (!gps_enabled) { + return; + } + if (gps_current_state == GPS_STATE_ACQUIRING) { + if (interval == 0) { + /* Switch to always-on: drop the standby timeout so it won't sleep. */ + k_work_cancel_delayable(&gps_timeout_work); + } else if (!k_work_delayable_is_pending(&gps_timeout_work)) { + /* Was always-on: arm a timeout so it starts duty cycling. */ + k_work_reschedule(&gps_timeout_work, K_MSEC(gps_acquire_window_ms())); + } + } else if (gps_current_state == GPS_STATE_STANDBY) { + k_work_cancel_delayable(&gps_wake_work); + if (interval == 0) { + /* Wake now and stay on. */ + atomic_or(&pending_gps_actions, GPS_ACTION_WAKE); + if (gps_event_cb) { + gps_event_cb(); + } + } else { + standby_interval_ms = gps_wake_interval_ms; + k_work_reschedule(&gps_wake_work, K_MSEC(gps_wake_interval_ms)); + } + } #else ARG_UNUSED(interval); #endif @@ -1591,12 +1674,15 @@ void gps_request_fresh_fix(void) * telemetry request that arrives 25s into a 30s acquire window * only has 5s left, which in marginal signal usually means the * chip goes to standby before producing a fix the requester - * could use. Every phase now has a bounded window - * (gps_acquire_window_ms), so always reschedule from now. */ - uint32_t timeout_ms = gps_acquire_window_ms(); - LOG_INF("GPS: Fresh fix requested, extending acquire timeout to %u s", - timeout_ms / 1000U); - k_work_reschedule(&gps_timeout_work, K_MSEC(timeout_ms)); + * could use. Each duty phase has a bounded window + * (gps_acquire_window_ms); in always-on there's no timeout to extend + * (GPS is continuously acquiring and current_pos is always fresh). */ + if (gps_duty_cycling()) { + uint32_t timeout_ms = gps_acquire_window_ms(); + LOG_INF("GPS: Fresh fix requested, extending acquire timeout to %u s", + timeout_ms / 1000U); + k_work_reschedule(&gps_timeout_work, K_MSEC(timeout_ms)); + } } #endif } diff --git a/zephcore/app/RepeaterDataStore.cpp b/zephcore/app/RepeaterDataStore.cpp index a617303..692b1a8 100644 --- a/zephcore/app/RepeaterDataStore.cpp +++ b/zephcore/app/RepeaterDataStore.cpp @@ -137,6 +137,7 @@ bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) { prefs.advert_loc_policy = ADVERT_LOC_PREFS; prefs.loop_detect = LOOP_DETECT_MODERATE; prefs.path_hash_mode = 1; + prefs.gps_interval = CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC; // repeater default (48h) /* Persist defaults so flash always has a prefs file from boot 1. * Lets later code (e.g. tempradio revert) trust that flash is * authoritative without a "first run" special case. */ @@ -242,6 +243,18 @@ bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) { LOG_INF("loadPrefs: upgraded prefs format (%d -> 296 bytes)", (int)entry.size); } + /* Repeater GPS-interval unification migration: before this firmware the + * repeater ignored gps_interval (hardcoded 48h), so a stored companion + * default (300) was never a deliberate choice. Bump it to the repeater + * default once, so now-honoring the field doesn't silently switch existing + * units to 5-min GPS polling. (Triggers only on exactly 300; after the + * one-time rewrite it won't re-fire. A deliberate 300 on a repeater isn't + * reachable via the CLI — use 299/301 if you really want ~5 min.) */ + if (prefs.gps_interval == CONFIG_ZEPHCORE_GPS_POLL_INTERVAL_SEC) { + prefs.gps_interval = CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC; + savePrefs(prefs); + } + return true; } diff --git a/zephcore/app/RepeaterMesh.h b/zephcore/app/RepeaterMesh.h index 20a22cf..a25e378 100644 --- a/zephcore/app/RepeaterMesh.h +++ b/zephcore/app/RepeaterMesh.h @@ -218,6 +218,7 @@ public: bool setGpsEnabled(bool enabled) override; bool isGpsEnabled() const override; void formatGpsStatsReply(char* reply) override; + uint32_t getDefaultGpsIntervalSec() const override { return CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC; } const char* getNodeName() { return _prefs.node_name; } NodePrefs* getNodePrefs() { return &_prefs; } diff --git a/zephcore/app/RoomServerMesh.h b/zephcore/app/RoomServerMesh.h index 3edfda8..3dc21a1 100644 --- a/zephcore/app/RoomServerMesh.h +++ b/zephcore/app/RoomServerMesh.h @@ -167,6 +167,7 @@ public: bool setGpsEnabled(bool enabled) override; bool isGpsEnabled() const override; void formatGpsStatsReply(char* reply) override; + uint32_t getDefaultGpsIntervalSec() const override { return CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC; } const char* getNodeName() { return _prefs.node_name; } NodePrefs* getNodePrefs() { return &_prefs; } diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index a3f98aa..02ce179 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -530,6 +531,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } } else if (memcmp(config, "rxduty", 6) == 0) { snprintf(reply, CLI_REPLY_SIZE, "> %d", (int)_prefs->rx_duty_cycle); + } else if (memcmp(config, "gps duty", 8) == 0) { + uint32_t s = gps_get_poll_interval_sec(); // now-effective value + if (s == 0) strcpy(reply, "> always on (0)"); + else snprintf(reply, CLI_REPLY_SIZE, "> %u", (unsigned)s); } else if (memcmp(config, "dc.restarts", 11) == 0) { snprintf(reply, CLI_REPLY_SIZE, "> %u", (uint32_t)_callbacks->getDutyCycleTimeoutRestarts()); @@ -909,6 +914,34 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } else { strcpy(reply, "Error: must be 0, 1, on, or off"); } + } else if (memcmp(config, "gps duty", 8) == 0) { + // set gps duty | default (0 = always on) + const char* arg = config + 8; + while (*arg == ' ') arg++; + uint32_t val = 0; + bool ok = true; + if (*arg == '\0') { + ok = false; + } else if (strcmp(arg, "default") == 0) { + val = _callbacks->getDefaultGpsIntervalSec(); + } else { + char* end = NULL; + unsigned long parsed = strtoul(arg, &end, 10); + // reject non-numeric, fractions, or trailing garbage + if (end == arg || *end != '\0') ok = false; + else val = (uint32_t)parsed; + } + if (!ok) { + strcpy(reply, "usage: set gps duty | default (0 = always on)"); + } else { + if (val > 604800UL) val = 604800UL; // cap at 1 week + else if (val != 0 && val < 10) val = 10; // floor 10s (0 = always on) + _prefs->gps_interval = val; + gps_set_poll_interval_sec(val); // apply live + savePrefs(); + if (val == 0) strcpy(reply, "OK - gps duty=0 (always on)"); + else snprintf(reply, CLI_REPLY_SIZE, "OK - gps duty=%u s", (unsigned)val); + } } else { snprintf(reply, CLI_REPLY_SIZE, "unknown config: %s", config); } diff --git a/zephcore/helpers/CommonCLI.h b/zephcore/helpers/CommonCLI.h index 25a834d..33b2e38 100644 --- a/zephcore/helpers/CommonCLI.h +++ b/zephcore/helpers/CommonCLI.h @@ -77,6 +77,9 @@ public: virtual bool setGpsEnabled(bool enabled) { return false; } virtual bool isGpsEnabled() const { return false; } virtual void formatGpsStatsReply(char* reply) { strcpy(reply, "off"); } + /* Role default for "set gps duty default". Companion default (300s); the + * repeater/room overrides return ZEPHCORE_REPEATER_GPS_INTERVAL_SEC. */ + virtual uint32_t getDefaultGpsIntervalSec() const { return CONFIG_ZEPHCORE_GPS_POLL_INTERVAL_SEC; } virtual int getNumSensorSettings() const { return 0; } virtual const char* getSensorSettingName(int idx) const { return nullptr; } virtual const char* getSensorSettingValue(int idx) const { return nullptr; } diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index c7ec1b5..444138d 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -1109,6 +1109,10 @@ int main(void) * First, ensure power state matches prefs (powers off if disabled). * Then, if prefs say enabled, start the GPS state machine. */ if (gps_is_available()) { + /* Apply persisted GPS duty interval (0 = always on) before the + * state machine starts. */ + gps_set_poll_interval_sec(companion_mesh.prefs.gps_interval); + /* Explicitly set power state at boot (handles disabled case) */ gps_ensure_power_state(companion_mesh.prefs.gps_enabled); diff --git a/zephcore/src/main_repeater.cpp b/zephcore/src/main_repeater.cpp index 85f0bc0..f7af7f7 100644 --- a/zephcore/src/main_repeater.cpp +++ b/zephcore/src/main_repeater.cpp @@ -511,6 +511,8 @@ int main(void) if (gps_is_available()) { gps_set_fix_callback(gps_fix_callback); gps_set_event_callback(gps_event_callback); + /* Apply persisted GPS duty interval (repeater default 48h; 0 = always on) */ + gps_set_poll_interval_sec(repeater_mesh.getNodePrefs()->gps_interval); gps_set_repeater_mode(true); } diff --git a/zephcore/src/main_room_server.cpp b/zephcore/src/main_room_server.cpp index 3f82cad..7f7eea5 100644 --- a/zephcore/src/main_room_server.cpp +++ b/zephcore/src/main_room_server.cpp @@ -529,6 +529,8 @@ int main(void) if (gps_is_available()) { gps_set_fix_callback(gps_fix_callback); gps_set_event_callback(gps_event_callback); + /* Apply persisted GPS duty interval (repeater default 48h; 0 = always on) */ + gps_set_poll_interval_sec(room_mesh.getNodePrefs()->gps_interval); gps_set_repeater_mode(true); }