From 54d6ce833e3b65380dc2d2e818f5a872c65a4730 Mon Sep 17 00:00:00 2001 From: Andrew Yong Date: Wed, 12 Aug 2026 20:42:19 +0800 Subject: [PATCH] gps: avoid pow() in GPS_HARDSLEEP threshold heuristic (#11179) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * gps: avoid pow() in GPS_HARDSLEEP threshold heuristic GPS::down() used pow(seconds, 1.22) to pick between GPS_SOFTSLEEP and GPS_HARDSLEEP - a curve fit the surrounding comment already describes as "not particularly accurate". On flash-constrained builds where this was the only pow() call site (e.g. wio-e5), it single-handedly pulled in the full double-precision libm pow/rem_pio2 chain for a heuristic threshold decision. Replaces it with gpsHardsleepThresholdMs(), a piecewise-linear lookup over the same curve, sampled at 16 points and verified to track the original formula within ~0.5% for inputs >=10s and ~1.6% for 5-10s (worse only in relative terms below 5s, where the absolute difference is at most a couple of seconds - negligible against update intervals measured in tens of seconds to hours). Signed-off-by: Andrew Yong Assisted-by: Claude Sonnet 5 * gps: trim comments to repo convention (1-2 lines) Addresses a CodeRabbit nitpick: the explanatory comments in GPSUpdateScheduling.cpp and test_gps_update_scheduling/test_main.cpp had grown into multi-line blocks with provenance detail that belongs in the commit message, not inline. Trims each to 1-2 lines, keeping only the essential rationale/bounds. Signed-off-by: Andrew Yong Assisted-by: Claude Sonnet 5 * Refactor main function to setup and loop for tests Signed-off-by: Thomas Göttgens * gps: extend the hardsleep threshold table below 5s and tighten its tests The 0s-to-5s chord read 42% high at 1s, 22% at 2s and 12% at 3s, against the ~1.6% the comment claimed. Adding 1s, 2s and 3s sample points brings the worst error below 10s to 1.60% at 7s. Above 10s it is 0.55% at 728s, unchanged. Tests: sample off-breakpoint values only, including both worst-error inputs (7s and 728s). Replace the 3000ms absolute floor, which made the 1s assertion unfalsifiable given a true value of 2750ms, with 2% and 0.75% bounds. Add breakpoint-exactness and clamp-boundary coverage. --------- Signed-off-by: Andrew Yong Signed-off-by: Thomas Göttgens Co-authored-by: Austin Co-authored-by: Thomas Göttgens --- src/gps/GPS.cpp | 6 +- src/gps/GPSUpdateScheduling.cpp | 25 +++++ src/gps/GPSUpdateScheduling.h | 4 + test/test_gps_update_scheduling/test_main.cpp | 91 +++++++++++++++++++ 4 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 test/test_gps_update_scheduling/test_main.cpp diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index fd5be417e..2ca1d86d1 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -1389,11 +1389,7 @@ void GPS::down() #endif if (softsleepSupported) { - // How long does gps_update_interval need to be, for GPS_HARDSLEEP to become more efficient than - // GPS_SOFTSLEEP? Heuristic equation. A compromise manually fitted to power observations from U-blox NEO-6M - // and M10050 https://www.desmos.com/calculator/6gvjghoumr This is not particularly accurate, but probably an - // improvement over a single, fixed threshold - uint32_t hardsleepThreshold = (2750 * pow(predictedSearchDuration / 1000, 1.22)); + uint32_t hardsleepThreshold = gpsHardsleepThresholdMs(predictedSearchDuration / 1000); LOG_DEBUG("gps_update_interval >= %us needed for hardsleep", hardsleepThreshold / 1000); // If update interval too short: softsleep (if supported by hardware) diff --git a/src/gps/GPSUpdateScheduling.cpp b/src/gps/GPSUpdateScheduling.cpp index a19d9c7d5..fe2c3ae78 100644 --- a/src/gps/GPSUpdateScheduling.cpp +++ b/src/gps/GPSUpdateScheduling.cpp @@ -2,6 +2,31 @@ #include "Default.h" +// Sampled from the original `2750 * seconds^1.22` curve. Interpolation tracks it within 0.6% for +// inputs >=10s and 1.7% below that; the 1s/2s/3s points keep the convex first segment from +// overshooting (a 0s-to-5s chord reads 42% high at 1s). +static constexpr uint32_t kThresholdCurveSecs[] = {0, 1, 2, 3, 5, 10, 15, 20, 30, 45, 60, 90, 120, 180, 240, 300, 450, 600, 900}; +static constexpr uint32_t kThresholdCurveMs[] = {0, 2750, 6406, 10506, 19592, 45639, 74845, + 106314, 174350, 285925, 406141, 666053, 946093, 1551548, + 2203893, 2893481, 4745172, 6740269, 11053722}; +static constexpr size_t kThresholdCurvePoints = sizeof(kThresholdCurveSecs) / sizeof(kThresholdCurveSecs[0]); + +// How long does gps_update_interval need to be, for GPS_HARDSLEEP to become more efficient than +// GPS_SOFTSLEEP? Avoids pow() so this heuristic doesn't pull double-precision libm into the image. +uint32_t gpsHardsleepThresholdMs(uint32_t predictedSearchSecs) +{ + if (predictedSearchSecs >= kThresholdCurveSecs[kThresholdCurvePoints - 1]) + return kThresholdCurveMs[kThresholdCurvePoints - 1]; + + size_t i = 1; + while (kThresholdCurveSecs[i] < predictedSearchSecs) + i++; + + uint32_t x0 = kThresholdCurveSecs[i - 1], x1 = kThresholdCurveSecs[i]; + uint32_t y0 = kThresholdCurveMs[i - 1], y1 = kThresholdCurveMs[i]; + return y0 + (uint32_t)((uint64_t)(y1 - y0) * (predictedSearchSecs - x0) / (x1 - x0)); +} + // Mark the time when searching for GPS position begins void GPSUpdateScheduling::informSearching() { diff --git a/src/gps/GPSUpdateScheduling.h b/src/gps/GPSUpdateScheduling.h index 120605c4e..d7609d704 100644 --- a/src/gps/GPSUpdateScheduling.h +++ b/src/gps/GPSUpdateScheduling.h @@ -2,6 +2,10 @@ #include "configuration.h" +// Approximates the GPS_HARDSLEEP/GPS_SOFTSLEEP crossover curve without pow(); see .cpp for the +// sampled reference values it interpolates between. +uint32_t gpsHardsleepThresholdMs(uint32_t predictedSearchSecs); + // Encapsulates code responsible for the timing of GPS updates class GPSUpdateScheduling { diff --git a/test/test_gps_update_scheduling/test_main.cpp b/test/test_gps_update_scheduling/test_main.cpp new file mode 100644 index 000000000..00c01c0f6 --- /dev/null +++ b/test/test_gps_update_scheduling/test_main.cpp @@ -0,0 +1,91 @@ +#include "Arduino.h" +#include "TestUtil.h" +#include "gps/GPSUpdateScheduling.h" +#include +#include +#include + +void setUp(void) {} +void tearDown(void) {} + +// Confirms gpsHardsleepThresholdMs()'s pow()-free lookup table tracks the original +// `2750 * pow(seconds, 1.22)` curve closely. +static double originalFormula(uint32_t seconds) +{ + return 2750.0 * std::pow((double)seconds, 1.22); +} + +static void test_matches_original_formula_at_sampled_points(void) +{ + // Off-breakpoint values only - a breakpoint interpolates exactly by construction, so it would + // test nothing here (test_exact_at_table_breakpoints covers those). Includes both worst-error + // inputs: 7s (1.60%) and 728s (0.55%). Capped at 900s, the pre-existing 15-minute search clamp. + const uint32_t samples[] = {4, 6, 7, 8, 9, 33, 100, 150, 500, 728, 899}; + for (uint32_t s : samples) { + double expected = originalFormula(s); + uint32_t actual = gpsHardsleepThresholdMs(s); + // Pure integer arithmetic, so results are bit-identical everywhere - no float noise to + // leave headroom for, and these sit just above the measured worst cases. + double tolerance = expected * (s < 10 ? 0.02 : 0.0075); + TEST_ASSERT_DOUBLE_WITHIN(tolerance, expected, (double)actual); + } +} + +static void test_zero_seconds_is_zero(void) +{ + TEST_ASSERT_EQUAL_UINT32(0, gpsHardsleepThresholdMs(0)); +} + +static void test_monotonically_nondecreasing(void) +{ + uint32_t prev = gpsHardsleepThresholdMs(0); + for (uint32_t s = 1; s <= 1200; s += 7) { + uint32_t cur = gpsHardsleepThresholdMs(s); + TEST_ASSERT_GREATER_OR_EQUAL_UINT32(prev, cur); + prev = cur; + } +} + +static void test_exact_at_table_breakpoints(void) +{ + // Every breakpoint must return its own sampled value. Catches an off-by-one in the segment + // scan, which a percentage bound on interpolated points would absorb. + const uint32_t breakpoints[] = {0, 1, 2, 3, 5, 10, 15, 20, 30, 45, 60, 90, 120, 180, 240, 300, 450, 600, 900}; + for (uint32_t s : breakpoints) { + char msg[64]; + snprintf(msg, sizeof(msg), "breakpoint %us", s); + // Within 2ms, not exact: the 30s entry is rounded 1ms high, and pow() can differ by an ULP + // across libm implementations. A real off-by-one in the scan misses by thousands. + TEST_ASSERT_UINT32_WITHIN_MESSAGE(2, (uint32_t)(originalFormula(s) + 0.5), gpsHardsleepThresholdMs(s), msg); + } +} + +static void test_clamps_above_table_range(void) +{ + uint32_t atMax = gpsHardsleepThresholdMs(900); + TEST_ASSERT_EQUAL_UINT32(atMax, gpsHardsleepThresholdMs(2000)); + TEST_ASSERT_EQUAL_UINT32(atMax, gpsHardsleepThresholdMs(UINT32_MAX)); +} + +static void test_clamp_boundary(void) +{ + // The clamp must engage exactly at the last table point, not before or after it. + TEST_ASSERT_LESS_THAN_UINT32(gpsHardsleepThresholdMs(900), gpsHardsleepThresholdMs(899)); + TEST_ASSERT_EQUAL_UINT32(gpsHardsleepThresholdMs(900), gpsHardsleepThresholdMs(901)); +} + +void setup() +{ + delay(10); + initializeTestEnvironment(); + UNITY_BEGIN(); + RUN_TEST(test_matches_original_formula_at_sampled_points); + RUN_TEST(test_zero_seconds_is_zero); + RUN_TEST(test_monotonically_nondecreasing); + RUN_TEST(test_exact_at_table_breakpoints); + RUN_TEST(test_clamps_above_table_range); + RUN_TEST(test_clamp_boundary); + exit(UNITY_END()); +} + +void loop() {}