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>
254 lines
9.3 KiB
C
254 lines
9.3 KiB
C
#include <assert.h>
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "gps/gps_ephemeris_validity.h"
|
|
|
|
#define WEEK_SEC 604800.0
|
|
|
|
static gps_ephemeris_t make_ephemeris(unsigned int prn, unsigned int iodc)
|
|
{
|
|
gps_ephemeris_t eph;
|
|
memset(&eph, 0, sizeof(eph));
|
|
eph.SVID = (unsigned char)prn;
|
|
eph.ephemeris_valid = true;
|
|
eph.iodc = iodc;
|
|
eph.iode = (unsigned char)(iodc & 0xffu);
|
|
eph.sva = 2u;
|
|
eph.svhlth = 0u;
|
|
eph.update = 0u;
|
|
eph.sqrta = 5153.6;
|
|
eph.A = eph.sqrta * eph.sqrta;
|
|
eph.ecc = 0.008;
|
|
eph.toe.sec = 172800.0;
|
|
eph.toc.sec = 172800.0;
|
|
eph.WN = 430u;
|
|
eph.tow.week = 2430;
|
|
/* Resolved from WN and the handover word by gps_resolve_toe_week(). */
|
|
eph.toe.week = 2430;
|
|
eph.toe_week_valid = true;
|
|
return eph;
|
|
}
|
|
|
|
static void test_fit_interval_table(void)
|
|
{
|
|
/* Fit flag clear is always the nominal four hour curve fit. */
|
|
assert(gps_ephemeris_fit_interval_hours(0u, false) == 4.0);
|
|
assert(gps_ephemeris_fit_interval_hours(500u, false) == 4.0);
|
|
|
|
/* IS-GPS-200 Table 20-XII ranges. */
|
|
assert(gps_ephemeris_fit_interval_hours(240u, true) == 8.0);
|
|
assert(gps_ephemeris_fit_interval_hours(247u, true) == 8.0);
|
|
assert(gps_ephemeris_fit_interval_hours(248u, true) == 14.0);
|
|
assert(gps_ephemeris_fit_interval_hours(496u, true) == 14.0);
|
|
assert(gps_ephemeris_fit_interval_hours(497u, true) == 26.0);
|
|
assert(gps_ephemeris_fit_interval_hours(504u, true) == 50.0);
|
|
assert(gps_ephemeris_fit_interval_hours(511u, true) == 74.0);
|
|
|
|
/* Untabulated IODC with the flag set falls back conservatively, below
|
|
* the shortest long fit the ICD defines. */
|
|
double fallback = gps_ephemeris_fit_interval_hours(100u, true);
|
|
assert(fallback == GPS_EPHEMERIS_LONG_FIT_FALLBACK_HOURS);
|
|
assert(fallback < 8.0);
|
|
}
|
|
|
|
static void test_static_rules(void)
|
|
{
|
|
gps_ephemeris_t eph = make_ephemeris(7u, 40u);
|
|
assert(gps_ephemeris_check_static(&eph, 7u) == GPS_EPHEMERIS_OK);
|
|
|
|
assert(gps_ephemeris_check_static(NULL, 7u) ==
|
|
GPS_EPHEMERIS_REJECT_NULL);
|
|
assert(gps_ephemeris_check_static(&eph, 8u) ==
|
|
GPS_EPHEMERIS_REJECT_PRN);
|
|
|
|
gps_ephemeris_t uncommitted = eph;
|
|
uncommitted.ephemeris_valid = false;
|
|
assert(gps_ephemeris_check_static(&uncommitted, 7u) ==
|
|
GPS_EPHEMERIS_REJECT_UNCOMMITTED);
|
|
|
|
/* IODE must equal the eight LSBs of IODC. */
|
|
gps_ephemeris_t mismatched = eph;
|
|
mismatched.iode = (unsigned char)(mismatched.iode + 1u);
|
|
assert(gps_ephemeris_check_static(&mismatched, 7u) ==
|
|
GPS_EPHEMERIS_REJECT_IOD);
|
|
|
|
/* A ten bit IODC whose low byte matches is accepted. */
|
|
gps_ephemeris_t wide_iodc = make_ephemeris(7u, 0x123u);
|
|
assert(wide_iodc.iode == 0x23u);
|
|
assert(gps_ephemeris_check_static(&wide_iodc, 7u) == GPS_EPHEMERIS_OK);
|
|
|
|
gps_ephemeris_t unhealthy = eph;
|
|
unhealthy.svhlth = 0x20u;
|
|
assert(gps_ephemeris_check_static(&unhealthy, 7u) ==
|
|
GPS_EPHEMERIS_REJECT_HEALTH);
|
|
|
|
gps_ephemeris_t no_accuracy = eph;
|
|
no_accuracy.sva = 15u;
|
|
assert(gps_ephemeris_check_static(&no_accuracy, 7u) ==
|
|
GPS_EPHEMERIS_REJECT_ACCURACY);
|
|
|
|
gps_ephemeris_t bad_orbit = eph;
|
|
bad_orbit.ecc = 1.5;
|
|
assert(gps_ephemeris_check_static(&bad_orbit, 7u) ==
|
|
GPS_EPHEMERIS_REJECT_PARAMETERS);
|
|
|
|
gps_ephemeris_t bad_toe = eph;
|
|
bad_toe.toe.sec = WEEK_SEC;
|
|
assert(gps_ephemeris_check_static(&bad_toe, 7u) ==
|
|
GPS_EPHEMERIS_REJECT_PARAMETERS);
|
|
}
|
|
|
|
static void test_fit_window(void)
|
|
{
|
|
gps_ephemeris_t eph = make_ephemeris(7u, 40u);
|
|
const double toe = eph.toe.sec;
|
|
|
|
/* Nominal four hour fit: usable for two hours either side of Toe. */
|
|
assert(gps_ephemeris_max_toe_offset_sec(&eph) == 7200.0);
|
|
assert(gps_ephemeris_check(&eph, 7u, true, 2430, toe) ==
|
|
GPS_EPHEMERIS_OK);
|
|
assert(gps_ephemeris_check(&eph, 7u, true, 2430, toe + 7199.0) ==
|
|
GPS_EPHEMERIS_OK);
|
|
assert(gps_ephemeris_check(&eph, 7u, true, 2430, toe - 7199.0) ==
|
|
GPS_EPHEMERIS_OK);
|
|
assert(gps_ephemeris_check(&eph, 7u, true, 2430, toe + 7201.0) ==
|
|
GPS_EPHEMERIS_REJECT_AGE);
|
|
assert(gps_ephemeris_check(&eph, 7u, true, 2430, toe - 7201.0) ==
|
|
GPS_EPHEMERIS_REJECT_AGE);
|
|
|
|
/* An extended fit interval widens the same window. */
|
|
gps_ephemeris_t extended = make_ephemeris(7u, 250u);
|
|
extended.fit_flag = true;
|
|
assert(gps_ephemeris_max_toe_offset_sec(&extended) == 14.0 * 1800.0);
|
|
assert(gps_ephemeris_check(&extended, 7u, true, 2430, toe + 20000.0) ==
|
|
GPS_EPHEMERIS_OK);
|
|
|
|
/* Without receiver time only the static rules apply, so a boot-time
|
|
* flash record stays a usable candidate. */
|
|
assert(gps_ephemeris_check(&eph, 7u, false, 0, 0.0) ==
|
|
GPS_EPHEMERIS_OK);
|
|
}
|
|
|
|
static void test_week_rollover(void)
|
|
{
|
|
/* Toe 600 s before the end of the week, receiver time 600 s after the
|
|
* rollover: the true age is 1200 s, not almost a full week. */
|
|
gps_ephemeris_t eph = make_ephemeris(11u, 60u);
|
|
eph.toe.sec = WEEK_SEC - 600.0;
|
|
|
|
double offset = 0.0;
|
|
assert(gps_ephemeris_toe_offset_sec(&eph, 2431, 600.0, &offset));
|
|
assert(fabs(offset - 1200.0) < 1e-6);
|
|
assert(gps_ephemeris_check(&eph, 11u, true, 2431, 600.0) ==
|
|
GPS_EPHEMERIS_OK);
|
|
|
|
/* The same record is stale eight hours into the new week. */
|
|
assert(gps_ephemeris_check(&eph, 11u, true, 2431, 28800.0) ==
|
|
GPS_EPHEMERIS_REJECT_AGE);
|
|
|
|
/* Toe just after a rollover with receiver time just before it gives a
|
|
* small negative age rather than a week-long one. */
|
|
gps_ephemeris_t fresh = make_ephemeris(11u, 60u);
|
|
fresh.toe.sec = 600.0;
|
|
/* Toe belongs to the week after the one it was broadcast in. */
|
|
fresh.toe.week = 2431;
|
|
assert(gps_ephemeris_toe_offset_sec(&fresh, 2430, WEEK_SEC - 600.0,
|
|
&offset));
|
|
assert(fabs(offset + 1200.0) < 1e-6);
|
|
assert(gps_ephemeris_check(&fresh, 11u, true, 2430,
|
|
WEEK_SEC - 600.0) == GPS_EPHEMERIS_OK);
|
|
}
|
|
|
|
/*
|
|
* Toe is a second-of-week, so before its week was resolved the age check could
|
|
* only work modulo one week: a record exactly N weeks old, evaluated at a
|
|
* similar time of week, read as brand new. While ephemeris lives in RAM and
|
|
* expires in hours that is unreachable. Persisting it to flash makes it
|
|
* directly reachable - switch the receiver off and on again a week later at a
|
|
* similar hour and it would accept its own week-old orbits as current, which
|
|
* is worth kilometres of position error with nothing to flag it.
|
|
*/
|
|
static void test_whole_week_staleness_is_not_invisible(void)
|
|
{
|
|
gps_ephemeris_t eph = make_ephemeris(11u, 60u);
|
|
double offset = 0.0;
|
|
|
|
/* Same second of week, one week on. */
|
|
assert(gps_ephemeris_toe_offset_sec(&eph, 2431, eph.toe.sec, &offset));
|
|
assert(fabs(offset - WEEK_SEC) < 1e-6);
|
|
assert(gps_ephemeris_check(&eph, 11u, true, 2431, eph.toe.sec) ==
|
|
GPS_EPHEMERIS_REJECT_AGE);
|
|
|
|
/* And four weeks on, which is what a shelved receiver comes back to. */
|
|
assert(gps_ephemeris_check(&eph, 11u, true, 2434, eph.toe.sec) ==
|
|
GPS_EPHEMERIS_REJECT_AGE);
|
|
|
|
/* A record whose week could not be resolved has an unknown vintage and
|
|
* must be refused rather than assumed current. */
|
|
gps_ephemeris_t unresolved = make_ephemeris(11u, 60u);
|
|
unresolved.toe_week_valid = false;
|
|
assert(!gps_ephemeris_toe_offset_sec(&unresolved, 2430,
|
|
unresolved.toe.sec + 60.0, &offset));
|
|
assert(gps_ephemeris_check(&unresolved, 11u, true, 2430,
|
|
unresolved.toe.sec + 60.0) ==
|
|
GPS_EPHEMERIS_REJECT_AGE);
|
|
/* Without receiver time it is still a usable cold-start candidate; the
|
|
* epoch gate re-checks it the moment time exists. */
|
|
assert(gps_ephemeris_check(&unresolved, 11u, false, 0, 0.0) ==
|
|
GPS_EPHEMERIS_OK);
|
|
printf(" whole-week staleness is visible to the age check\n");
|
|
}
|
|
|
|
static void test_supersede(void)
|
|
{
|
|
gps_ephemeris_t current = make_ephemeris(19u, 40u);
|
|
gps_ephemeris_t repeat = current;
|
|
|
|
/* The same data set decoded again is not new information. */
|
|
assert(!gps_ephemeris_supersedes(&repeat, ¤t));
|
|
|
|
/* Health or transmitted accuracy can change inside one issue of data. */
|
|
gps_ephemeris_t health_change = current;
|
|
health_change.svhlth = 0x3fu;
|
|
assert(gps_ephemeris_supersedes(&health_change, ¤t));
|
|
|
|
/* A newer upload with a later Toe replaces it. */
|
|
gps_ephemeris_t newer = make_ephemeris(19u, 41u);
|
|
newer.toe.sec = current.toe.sec + 7200.0;
|
|
assert(gps_ephemeris_supersedes(&newer, ¤t));
|
|
|
|
/* An older Toe with a different IODE does not: that is a replayed or
|
|
* out-of-order decode. */
|
|
gps_ephemeris_t older = make_ephemeris(19u, 39u);
|
|
older.toe.sec = current.toe.sec - 7200.0;
|
|
assert(!gps_ephemeris_supersedes(&older, ¤t));
|
|
|
|
/* Toe advancing across the week boundary still counts as newer. */
|
|
gps_ephemeris_t before_rollover = make_ephemeris(19u, 42u);
|
|
before_rollover.toe.sec = WEEK_SEC - 1800.0;
|
|
gps_ephemeris_t after_rollover = make_ephemeris(19u, 43u);
|
|
after_rollover.toe.sec = 1800.0;
|
|
assert(gps_ephemeris_supersedes(&after_rollover, &before_rollover));
|
|
assert(!gps_ephemeris_supersedes(&before_rollover, &after_rollover));
|
|
|
|
/* Nothing supersedes across PRNs, and anything replaces an empty slot. */
|
|
gps_ephemeris_t other_prn = make_ephemeris(20u, 41u);
|
|
assert(!gps_ephemeris_supersedes(&other_prn, ¤t));
|
|
assert(gps_ephemeris_supersedes(¤t, NULL));
|
|
assert(!gps_ephemeris_supersedes(NULL, ¤t));
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
test_fit_interval_table();
|
|
test_static_rules();
|
|
test_fit_window();
|
|
test_week_rollover();
|
|
test_whole_week_staleness_is_not_invisible();
|
|
test_supersede();
|
|
puts("gps ephemeris validity tests passed");
|
|
return 0;
|
|
}
|