refactor gps manager

This commit is contained in:
liquidraver
2026-06-17 15:42:50 +02:00
parent 533a2a7e4e
commit c4a1777956
11 changed files with 202 additions and 43 deletions
+11
View File
@@ -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 <sec>" (persisted to flash).
Companions default to ZEPHCORE_GPS_POLL_INTERVAL_SEC (300s) instead.
endmenu
menu "WiFi OTA Update"
+3
View File
@@ -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 <sec>` | 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 |
+129 -43
View File
@@ -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(&current_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(&current_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
}
+13
View File
@@ -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;
}
+1
View File
@@ -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; }
+1
View File
@@ -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; }
+33
View File
@@ -10,6 +10,7 @@
#include <helpers/TxtDataHelpers.h>
#include <helpers/AdvertDataHelpers.h>
#include <adapters/board/ZephyrBoard.h>
#include <adapters/gps/ZephyrGPSManager.h>
#include <zephyr/fs/fs.h>
#include <zephyr/logging/log.h>
#include <stdlib.h>
@@ -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 <seconds> | 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 <seconds> | 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);
}
+3
View File
@@ -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; }
+4
View File
@@ -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);
+2
View File
@@ -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);
}
+2
View File
@@ -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);
}