f59d6ebb43
Persisting navigation data changed its lifetime from one power cycle to weeks, and every piece of arithmetic that had only ever run over the shorter lifetime turned out to be wrong. Each fix below reproduces in a host test that fails against the previous behaviour. Almanac (gps_almanac.c, gps_lnav_parser.c) WNa was lifted against gps_ephems.toe.week, a field nothing ever writes, so any WNa above 128 underflowed to ~4.29e9. The propagation then reduced tk with a while-loop, turning one visibility refresh into 4.29e9 iterations - hours on the target, inside a 10 s periodic call. WNa is the week modulo 256, so this armed at GPS week 2433 and then for 127 weeks in every 256. Now lifted against tow.week in signed arithmetic, refused if impossible, and the propagation is closed form. The IS-GPS-200 20.3.3.5.2.3 half-week wrap was also applied to tk unconditionally. It is a week-crossover correction, not a general reduction: a GPS week is 14.0384 orbital periods, so aliasing one week moves a satellite 5161 km, enough to report one 5.1 degrees below the horizon as 5.7 above. The crossover guess now lives in gps_almanac_assumed_week(), used only before page 25 arrives, and an almanac too old to propagate yields no prediction rather than a wrong one. Ephemeris age (gps_ephemeris_validity.c, gps_lnav_parser.c) The curve-fit check compared two second-of-week values and never read a week at all, so a record exactly N weeks old read as current. Harmless while ephemeris lived in RAM; directly reachable once persisted across a power-off. gps_resolve_toe_week() now reconstructs Toe's week at commit time from the subframe 1 week number and the handover word; an unresolvable week yields no age, which reads as a rejection. A restored record could also outrank the live broadcast, because the Toe ordering inverts past half a week. Flash records are now marked restored and are candidates, never incumbents. Flash wear (gps_nav_context_store.c, gps_interrupt_handlers.c) The CRC wear policy never engaged: the payload carried the live WLS position and a receiver timestamp nothing on the restore path read, so every 60 s save was an erase - about 139 days to wear-out. The seed is now held and refreshed only on 5 km of movement, at most hourly: one erase per day instead of 1440. Measurement model (NavEquations.c) The elevation weighting was floored at 10 degrees, capping its term at 17.3 m where the model's own answer is 34.4 m at 5 degrees and 86 m at 2, so a low satellite was trusted several times more than the model allowed. Floor lowered to 3 degrees, which is where 1/sin must still be bounded because a zero weight fails the whole epoch. Measured on a six-satellite geometry, 30 m of uncorrected delay now moves the fix 4.18 m through a 4-degree satellite against 48.50 m through a 72-degree one. Both atmospheric corrections switched off below the horizon, so a satellite crossing it lost 78.8 m of tropospheric delay in one step. Elevation is now clamped at the horizon for the models, and the extrapolation is paid for in variance. Also: gps_pps_select_reference() returned early on the preferred PRN, leaving later non-finite capture times unscreened. Checked and found correct, with regressions added: the epoch assembly under satellite connect/disconnect and reassignment, coalesced captures, the 32-bit RF sample counter wrap (pseudoranges agree to 3.3 um across it), the refusal to publish a solvable subset of an incoherent epoch, and the Klobuchar model against IS-GPS-200 20.3.3.5.2.5. Tests: 22 host suites, 5 RTL testbenches, sanitizer clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
165 lines
5.2 KiB
C
165 lines
5.2 KiB
C
#include <assert.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "gps/gps_ephemeris_cache.h"
|
|
|
|
static gps_ephemeris_t make_ephemeris(
|
|
unsigned int prn,
|
|
unsigned char iode)
|
|
{
|
|
gps_ephemeris_t ephemeris;
|
|
memset(&ephemeris, 0, sizeof(ephemeris));
|
|
ephemeris.SVID = (unsigned char)prn;
|
|
ephemeris.iode = iode;
|
|
ephemeris.iodc = iode;
|
|
ephemeris.ephemeris_valid = true;
|
|
ephemeris.sva = 2u;
|
|
ephemeris.sqrta = 5153.6;
|
|
ephemeris.A = ephemeris.sqrta * ephemeris.sqrta;
|
|
ephemeris.ecc = 0.008;
|
|
ephemeris.toe.week = 2430;
|
|
ephemeris.toe_week_valid = true;
|
|
ephemeris.toe.sec = 123456.0;
|
|
ephemeris.toc.sec = 123456.0;
|
|
return ephemeris;
|
|
}
|
|
|
|
static void test_store_restore_and_expiry(void)
|
|
{
|
|
gps_ephemeris_cache_t cache;
|
|
gps_ephemeris_cache_init(&cache, 100u);
|
|
gps_ephemeris_t input = make_ephemeris(4u, 17u);
|
|
gps_ephemeris_t output;
|
|
memset(&output, 0, sizeof(output));
|
|
|
|
assert(gps_ephemeris_cache_store(
|
|
&cache, 4u, &input, 1000u));
|
|
input.iode = 99u;
|
|
assert(gps_ephemeris_cache_restore(
|
|
&cache, 4u, 1100u, &output));
|
|
assert(output.iode == 17u);
|
|
assert(output.SVID == 4u);
|
|
assert(!gps_ephemeris_cache_restore(
|
|
&cache, 4u, 1101u, &output));
|
|
/* Expiration invalidates the slot, not only this read. */
|
|
assert(!gps_ephemeris_cache_restore(
|
|
&cache, 4u, 1001u, &output));
|
|
}
|
|
|
|
static void test_validation_independence_and_invalidate(void)
|
|
{
|
|
gps_ephemeris_cache_t cache;
|
|
gps_ephemeris_cache_init(&cache, 500u);
|
|
gps_ephemeris_t prn4 = make_ephemeris(4u, 21u);
|
|
gps_ephemeris_t prn19 = make_ephemeris(19u, 44u);
|
|
gps_ephemeris_t output;
|
|
|
|
prn4.ephemeris_valid = false;
|
|
assert(!gps_ephemeris_cache_store(
|
|
&cache, 4u, &prn4, 0u));
|
|
prn4.ephemeris_valid = true;
|
|
assert(!gps_ephemeris_cache_store(
|
|
&cache, 5u, &prn4, 0u));
|
|
assert(!gps_ephemeris_cache_store(
|
|
&cache, GPS_PRN_COUNT, &prn4, 0u));
|
|
|
|
assert(gps_ephemeris_cache_store(
|
|
&cache, 4u, &prn4, 20u));
|
|
assert(gps_ephemeris_cache_store(
|
|
&cache, 19u, &prn19, 30u));
|
|
assert(gps_ephemeris_cache_restore(
|
|
&cache, 19u, 40u, &output));
|
|
assert(output.iode == 44u);
|
|
|
|
gps_ephemeris_cache_invalidate(&cache, 4u);
|
|
assert(!gps_ephemeris_cache_restore(
|
|
&cache, 4u, 40u, &output));
|
|
assert(gps_ephemeris_cache_restore(
|
|
&cache, 19u, 40u, &output));
|
|
}
|
|
|
|
static void test_store_if_new_reports_real_changes_only(void)
|
|
{
|
|
gps_ephemeris_cache_t cache;
|
|
gps_ephemeris_cache_init(&cache, 1000u);
|
|
gps_ephemeris_t eph = make_ephemeris(9u, 30u);
|
|
bool changed = true;
|
|
|
|
assert(gps_ephemeris_cache_store_if_new(&cache, 9u, &eph, 10u,
|
|
&changed));
|
|
assert(changed);
|
|
|
|
/* Re-decoding the same data set refreshes the age datum but reports no
|
|
* change, so no flash write is triggered. */
|
|
assert(gps_ephemeris_cache_store_if_new(&cache, 9u, &eph, 20u,
|
|
&changed));
|
|
assert(!changed);
|
|
|
|
gps_ephemeris_t output;
|
|
assert(gps_ephemeris_cache_restore(&cache, 9u, 1019u, &output));
|
|
assert(output.iode == 30u);
|
|
|
|
/* A newer upload is a real change. */
|
|
gps_ephemeris_t newer = make_ephemeris(9u, 31u);
|
|
newer.toe.sec = eph.toe.sec + 7200.0;
|
|
assert(gps_ephemeris_cache_store_if_new(&cache, 9u, &newer, 30u,
|
|
&changed));
|
|
assert(changed);
|
|
assert(gps_ephemeris_cache_restore(&cache, 9u, 30u, &output));
|
|
assert(output.iode == 31u);
|
|
|
|
/* An out-of-order older upload is rejected outright. */
|
|
assert(!gps_ephemeris_cache_store_if_new(&cache, 9u, &eph, 40u,
|
|
&changed));
|
|
assert(!changed);
|
|
assert(gps_ephemeris_cache_restore(&cache, 9u, 40u, &output));
|
|
assert(output.iode == 31u);
|
|
|
|
/* A structurally invalid record never enters the cache. */
|
|
gps_ephemeris_t corrupt = make_ephemeris(10u, 5u);
|
|
corrupt.iodc = 6u;
|
|
assert(!gps_ephemeris_cache_store_if_new(&cache, 10u, &corrupt, 50u,
|
|
&changed));
|
|
assert(!changed);
|
|
}
|
|
|
|
static void test_restore_at_applies_fit_window(void)
|
|
{
|
|
gps_ephemeris_cache_t cache;
|
|
gps_ephemeris_cache_init(&cache, 1000000u);
|
|
gps_ephemeris_t eph = make_ephemeris(12u, 44u);
|
|
gps_ephemeris_t output;
|
|
bool changed = false;
|
|
|
|
assert(gps_ephemeris_cache_store_if_new(&cache, 12u, &eph, 0u,
|
|
&changed));
|
|
|
|
/* Inside the four hour curve fit. */
|
|
assert(gps_ephemeris_cache_restore_at(&cache, 12u, 10u, true, 2430,
|
|
eph.toe.sec + 3600.0, &output));
|
|
assert(output.iode == 44u);
|
|
|
|
/* Without receiver time the record stays available as a candidate. */
|
|
assert(gps_ephemeris_cache_restore_at(&cache, 12u, 10u, false, 0, 0.0,
|
|
&output));
|
|
|
|
/* Past the fit interval the entry is rejected and dropped, so a later
|
|
* in-window query cannot resurrect it. */
|
|
assert(!gps_ephemeris_cache_restore_at(&cache, 12u, 10u, true, 2430,
|
|
eph.toe.sec + 9000.0, &output));
|
|
assert(!gps_ephemeris_cache_restore_at(&cache, 12u, 10u, true, 2430,
|
|
eph.toe.sec, &output));
|
|
assert(!gps_ephemeris_cache_restore(&cache, 12u, 10u, &output));
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
test_store_restore_and_expiry();
|
|
test_validation_independence_and_invalidate();
|
|
test_store_if_new_reports_real_changes_only();
|
|
test_restore_at_applies_fit_window();
|
|
puts("gps ephemeris cache tests passed");
|
|
return 0;
|
|
}
|