From ac2bf9372a88a5c865667a026d87db6574544847 Mon Sep 17 00:00:00 2001 From: Olexandr Tyshchenko Date: Thu, 12 Nov 2026 10:48:10 +0200 Subject: [PATCH 1/2] Limit the first periodic GPS acquisition after boot Before the first fix since boot, a periodic acquisition had no timeout. If no fix is accepted (no sky view, no antenna, or a guard rejecting spoofed fixes), the receiver then stayed on until a fix was accepted. Give that window its own limit, GPS_FIRST_FIX_TIMEOUT_MS (15 min by default, enough for a full almanac; 0 keeps the old behaviour). It counts as a miss and the next acquisition starts after the configured period. --- docs/cli_commands.md | 2 +- src/helpers/sensors/MicroNMEALocationProvider.h | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index bc30adbd48..f6c56ee363 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1026,7 +1026,7 @@ region save **Parameters:** - `value`: `0` for continuous tracking, or a period. The value can be postfixed with `s`, `m`, `h` or `d` to specify the unit (seconds by default). -**Note:** A periodic acquisition stops after receiving a valid fix, then resumes after the configured period. +**Note:** A periodic acquisition stops after receiving a valid fix, then resumes after the configured period. Without a fix it stops after 5 minutes, or after 15 minutes until the first fix since boot (`GPS_PERIODIC_ACQUISITION_TIMEOUT_MS`, `GPS_FIRST_FIX_TIMEOUT_MS`). --- diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index 09b4d95546..c44e1c47cc 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -12,6 +12,11 @@ #ifndef GPS_PERIODIC_ACQUISITION_TIMEOUT_MS #define GPS_PERIODIC_ACQUISITION_TIMEOUT_MS 300000UL #endif +// limit for the periodic window before the first fix since boot (cold start can be long, +// a full almanac takes 12.5 min); 0 = keep searching until a fix +#ifndef GPS_FIRST_FIX_TIMEOUT_MS + #define GPS_FIRST_FIX_TIMEOUT_MS 900000UL +#endif // in periodic mode, keep reading after the first valid fix until enough satellites // are used or the settle time has elapsed @@ -363,10 +368,13 @@ class MicroNMEALocationProvider : public LocationProvider { } } - // no timeout until a first fix has been obtained (cold start can be long) - if (_acquiringPeriodically && _lastFix.valid && - (uint32_t)(millis() - _acquisitionStarted) >= GPS_PERIODIC_ACQUISITION_TIMEOUT_MS) { - finishPeriodicAcquisition(); + // a longer limit until a first fix has been obtained (cold start can be long) + if (_acquiringPeriodically) { + const uint32_t limit = _lastFix.valid ? GPS_PERIODIC_ACQUISITION_TIMEOUT_MS + : GPS_FIRST_FIX_TIMEOUT_MS; + if (limit > 0 && (uint32_t)(millis() - _acquisitionStarted) >= limit) { + finishPeriodicAcquisition(); + } } updateTime(); From 069b9fdaf8cb5ee54b90eba73e0d7be5da79d3ad Mon Sep 17 00:00:00 2001 From: Olexandr Tyshchenko Date: Fri, 13 Nov 2026 14:29:02 +0200 Subject: [PATCH 2/2] GPS: first fix timeout as a saved setting (gps firstfix), 1 h by default A cold start can take up to ~30 min with a weak sky view, so the default limit before the first fix since boot goes from 15 min to 1 h. The limit is now a GPS preference, set with `gps firstfix ` (0 = no limit, at most 1 day) and saved with the other GPS settings, so repeaters and companions can use different values without a rebuild. GPS_FIRST_FIX_TIMEOUT_MS stays as the build default. --- docs/cli_commands.md | 16 +++++- examples/companion_radio/NodePrefs.h | 8 +++ src/helpers/CommonCLI.h | 8 +++ src/helpers/CommonGpsPrefs.cpp | 54 +++++++++++++++---- src/helpers/CommonGpsPrefs.h | 3 ++ src/helpers/sensors/LocationProvider.h | 3 ++ .../sensors/MicroNMEALocationProvider.h | 18 +++++-- 7 files changed, 95 insertions(+), 15 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index f6c56ee363..7bd0e3ace3 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1026,7 +1026,21 @@ region save **Parameters:** - `value`: `0` for continuous tracking, or a period. The value can be postfixed with `s`, `m`, `h` or `d` to specify the unit (seconds by default). -**Note:** A periodic acquisition stops after receiving a valid fix, then resumes after the configured period. Without a fix it stops after 5 minutes, or after 15 minutes until the first fix since boot (`GPS_PERIODIC_ACQUISITION_TIMEOUT_MS`, `GPS_FIRST_FIX_TIMEOUT_MS`). +**Note:** A periodic acquisition stops after receiving a valid fix, then resumes after the configured period. Without a fix it stops after 5 minutes (`GPS_PERIODIC_ACQUISITION_TIMEOUT_MS`), or, until the first fix since boot, after the first fix timeout (see `gps firstfix`). + +--- + +#### View or change the first fix timeout +**Usage:** +- `gps firstfix` +- `gps firstfix ` + +**Parameters:** +- `value`: `0` for no limit, or a duration up to `1d`. The value can be postfixed with `s`, `m`, `h` or `d` to specify the unit (seconds by default). + +**Default:** `1h` (`GPS_FIRST_FIX_TIMEOUT_MS`) + +**Note:** In periodic mode, limits an acquisition until the first valid fix since boot. A cold start can take much longer than the usual 5 minutes with a weak sky view; after the limit the GPS sleeps for the period and tries again. Saved with the other GPS settings. --- diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index c53879767b..09dbba0e8f 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -38,6 +38,7 @@ class NodePrefs : public ConfigSerializer { // persisted to file uint32_t gps_interval = 0; // GPS read interval in seconds uint32_t gps_period = 0; // periodic acquisition interval in seconds int32_t gps_fence = -1; // GPS_GUARD fence in metres, 0 = off, -1 = build default + int32_t gps_first_fix = -1; // first-fix timeout in seconds, 0 = no limit, -1 = build default uint8_t gps_guard = 1; // GPS_GUARD on/off uint8_t autoadd_config = 0; // bitmask for auto-add contacts config uint8_t rx_boosted_gain = 0; // SX126x RX boosted gain mode (0=power saving, 1=boosted) @@ -169,6 +170,7 @@ class NodePrefs : public ConfigSerializer { // persisted to file def("int", _parent->gps_interval); // interval in seconds def("period", _parent->gps_period); // periodic acquisition interval in seconds def("adv_loc", _parent->advert_loc_policy); + def("ffix", _parent->gps_first_fix); #ifdef GPS_GUARD def("fence", _parent->gps_fence); def("guard", _parent->gps_guard); @@ -180,6 +182,12 @@ class NodePrefs : public ConfigSerializer { // persisted to file void setGpsEnabled(bool enabled) override { _parent->gps_enabled = enabled; markDirty(); } uint32_t getGpsPeriod() const override { return _parent->gps_period; } void setGpsPeriod(uint32_t seconds) override { _parent->gps_period = seconds; markDirty(); } + bool getFirstFixTimeout(uint32_t& seconds) const override { + if (_parent->gps_first_fix < 0) return false; + seconds = (uint32_t)_parent->gps_first_fix; + return true; + } + void setFirstFixTimeout(uint32_t seconds) override { _parent->gps_first_fix = (int32_t)seconds; markDirty(); } bool getGuardFence(uint32_t& metres) const override { if (_parent->gps_fence < 0) return false; metres = (uint32_t)_parent->gps_fence; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index cb85f6f993..24b1fe26bd 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -64,6 +64,7 @@ class NodePrefs : public ConfigSerializer { uint32_t gps_interval = 0; // in seconds uint32_t gps_period = 0; // periodic acquisition interval in seconds int32_t gps_fence = -1; // GPS_GUARD fence in metres, 0 = off, -1 = build default + int32_t gps_first_fix = -1; // first-fix timeout in seconds, 0 = no limit, -1 = build default uint8_t gps_guard = 1; // GPS_GUARD on/off uint8_t advert_loc_policy = 0; uint32_t discovery_mod_timestamp = 0; @@ -164,6 +165,7 @@ class NodePrefs : public ConfigSerializer { def("int", _parent->gps_interval); // interval in seconds def("period", _parent->gps_period); // periodic acquisition interval in seconds def("adv_loc", _parent->advert_loc_policy); + def("ffix", _parent->gps_first_fix); #ifdef GPS_GUARD def("fence", _parent->gps_fence); def("guard", _parent->gps_guard); @@ -175,6 +177,12 @@ class NodePrefs : public ConfigSerializer { void setGpsEnabled(bool enabled) override { _parent->gps_enabled = enabled; markDirty(); } uint32_t getGpsPeriod() const override { return _parent->gps_period; } void setGpsPeriod(uint32_t seconds) override { _parent->gps_period = seconds; markDirty(); } + bool getFirstFixTimeout(uint32_t& seconds) const override { + if (_parent->gps_first_fix < 0) return false; + seconds = (uint32_t)_parent->gps_first_fix; + return true; + } + void setFirstFixTimeout(uint32_t seconds) override { _parent->gps_first_fix = (int32_t)seconds; markDirty(); } bool getGuardFence(uint32_t& metres) const override { if (_parent->gps_fence < 0) return false; metres = (uint32_t)_parent->gps_fence; diff --git a/src/helpers/CommonGpsPrefs.cpp b/src/helpers/CommonGpsPrefs.cpp index cdc889d00e..d9768fe1eb 100644 --- a/src/helpers/CommonGpsPrefs.cpp +++ b/src/helpers/CommonGpsPrefs.cpp @@ -46,6 +46,8 @@ bool CommonGpsPrefs::apply(SensorManager& sensors) const { LocationProvider* location = sensors.getLocationProvider(); if (location == nullptr) return false; applyGuardReference(sensors); + uint32_t first_fix; + if (getFirstFixTimeout(first_fix)) location->setFirstFixTimeout(first_fix); location->setGpsPeriod(getGpsPeriod()); location->setGpsOn(isGpsEnabled()); return true; @@ -62,6 +64,20 @@ static void formatDuration(char* buf, size_t size, uint32_t seconds) { } } +// "[s|m|h|d]", seconds by default +static bool parseDuration(const char* text, uint32_t& seconds) { + char* end; + unsigned long value = strtoul(text, &end, 10); + unsigned long mult = 1; + if (*end == 'm') mult = 60; + else if (*end == 'h') mult = 3600; + else if (*end == 'd') mult = 86400; + if (mult > 1 || *end == 's') end++; // skip the suffix char + if (text[0] == '\0' || *end != '\0') return false; + seconds = (uint32_t)(value * mult); + return true; +} + bool CommonGpsPrefs::handleCommand(const char* command, SensorManager& sensors, char* reply) { if (strcmp(command, "gps") == 0) { LocationProvider* location = sensors.getLocationProvider(); @@ -180,17 +196,10 @@ bool CommonGpsPrefs::handleCommand(const char* command, SensorManager& sensors, return true; } if (memcmp(command, "gps period ", 11) == 0) { - char* end; - unsigned long period = strtoul(command + 11, &end, 10); - unsigned long mult = 1; - if (*end == 'm') mult = 60; - else if (*end == 'h') mult = 3600; - else if (*end == 'd') mult = 86400; - if (mult > 1 || *end == 's') end++; // skip the suffix char - - if (command[11] == '\0' || *end != '\0') { + uint32_t period; + if (!parseDuration(command + 11, period)) { strcpy(reply, "Error, invalid gps period"); - } else if (!setGpsPeriod(sensors, (uint32_t)(period * mult)) || + } else if (!setGpsPeriod(sensors, period) || !setGpsEnabled(sensors, true)) { strcpy(reply, "Error, gps provider not found"); } else { @@ -198,6 +207,31 @@ bool CommonGpsPrefs::handleCommand(const char* command, SensorManager& sensors, } return true; } + if (strcmp(command, "gps firstfix") == 0) { + LocationProvider* location = sensors.getLocationProvider(); + if (location == nullptr) { + strcpy(reply, "Error, gps provider not found"); + } else if (location->getFirstFixTimeout() == 0) { + strcpy(reply, "> 0 (no limit)"); + } else { + sprintf(reply, "> %lu", (unsigned long)location->getFirstFixTimeout()); + } + return true; + } + if (memcmp(command, "gps firstfix ", 13) == 0) { + LocationProvider* location = sensors.getLocationProvider(); + uint32_t seconds; + if (location == nullptr) { + strcpy(reply, "Error, gps provider not found"); + } else if (!parseDuration(command + 13, seconds) || seconds > 86400UL) { // at most a day + strcpy(reply, "Error, invalid first fix timeout"); + } else { + location->setFirstFixTimeout(seconds); + setFirstFixTimeout(seconds); // saved by the caller (isDirty) + strcpy(reply, "OK"); + } + return true; + } if (memcmp(command, "gps sync", 8) == 0) { LocationProvider* location = sensors.getLocationProvider(); if (location == nullptr) { diff --git a/src/helpers/CommonGpsPrefs.h b/src/helpers/CommonGpsPrefs.h index c1e30b8eaf..5502cba802 100644 --- a/src/helpers/CommonGpsPrefs.h +++ b/src/helpers/CommonGpsPrefs.h @@ -18,6 +18,9 @@ class CommonGpsPrefs : public ConfigSerializer { virtual void setGpsEnabled(bool enabled) = 0; virtual uint32_t getGpsPeriod() const = 0; virtual void setGpsPeriod(uint32_t seconds) = 0; + // Saved first-fix timeout in seconds; false = not set, use the build default. + virtual bool getFirstFixTimeout(uint32_t& seconds) const { return false; } + virtual void setFirstFixTimeout(uint32_t seconds) { } // Reference position for the anti-spoofing guard (GPS_GUARD), e.g. the configured node position. virtual bool getGuardReference(double& lat, double& lon) const { return false; } diff --git a/src/helpers/sensors/LocationProvider.h b/src/helpers/sensors/LocationProvider.h index 5b5b69c044..2e73034399 100644 --- a/src/helpers/sensors/LocationProvider.h +++ b/src/helpers/sensors/LocationProvider.h @@ -81,6 +81,9 @@ class LocationProvider { } virtual void setGpsPeriod(uint32_t seconds) { } virtual uint32_t getGpsPeriod() const { return 0; } + // periodic mode: limit of an acquisition before the first fix since boot, 0 = no limit + virtual void setFirstFixTimeout(uint32_t seconds) { } + virtual uint32_t getFirstFixTimeout() const { return 0; } virtual uint32_t getLastFixAge() const { // in seconds, UINT32_MAX if never const LastFix f = getLastFix(); return f.valid ? (uint32_t)(millis() - f.acquiredAt) / 1000UL : UINT32_MAX; diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index c44e1c47cc..93b2dbfc4d 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -12,10 +12,11 @@ #ifndef GPS_PERIODIC_ACQUISITION_TIMEOUT_MS #define GPS_PERIODIC_ACQUISITION_TIMEOUT_MS 300000UL #endif -// limit for the periodic window before the first fix since boot (cold start can be long, -// a full almanac takes 12.5 min); 0 = keep searching until a fix +// default limit for the periodic window before the first fix since boot (a cold start +// can take up to ~30 min with a weak sky view); 0 = keep searching until a fix. +// Can be changed at runtime with `gps firstfix`. #ifndef GPS_FIRST_FIX_TIMEOUT_MS - #define GPS_FIRST_FIX_TIMEOUT_MS 900000UL + #define GPS_FIRST_FIX_TIMEOUT_MS 3600000UL #endif // in periodic mode, keep reading after the first valid fix until enough satellites @@ -41,6 +42,7 @@ class MicroNMEALocationProvider : public LocationProvider { long time_valid = 0; unsigned long _last_time_sync = 0; uint32_t _gpsPeriodSeconds = 0; + uint32_t _firstFixTimeoutSeconds = GPS_FIRST_FIX_TIMEOUT_MS / 1000UL; unsigned long _nextAcquisition = 0; unsigned long _acquisitionStarted = 0; unsigned long _firstFixInWindow = 0; @@ -204,6 +206,14 @@ class MicroNMEALocationProvider : public LocationProvider { return _gpsPeriodSeconds; } + void setFirstFixTimeout(uint32_t seconds) override { + _firstFixTimeoutSeconds = seconds; + } + + uint32_t getFirstFixTimeout() const override { + return _firstFixTimeoutSeconds; + } + uint32_t getAcquisitionAge() const override { return (uint32_t)(millis() - _acquisitionStarted) / 1000UL; } @@ -371,7 +381,7 @@ class MicroNMEALocationProvider : public LocationProvider { // a longer limit until a first fix has been obtained (cold start can be long) if (_acquiringPeriodically) { const uint32_t limit = _lastFix.valid ? GPS_PERIODIC_ACQUISITION_TIMEOUT_MS - : GPS_FIRST_FIX_TIMEOUT_MS; + : _firstFixTimeoutSeconds * 1000UL; if (limit > 0 && (uint32_t)(millis() - _acquisitionStarted) >= limit) { finishPeriodicAcquisition(); }