feat(gps): add timer-based assisted reacquisition
Remove the fixed pseudorange start offset, preserve per-PRN navigation assistance across RF loss and power cycles, and seed coherent navigation measurement phase on reacquisition. Add full LNAV almanac visibility scheduling, power-safe QSPI journaling, RF-domain reset isolation, and PPS Gray CDC hardening with Linux, sanitizer, RTL, and end-to-end coverage.
This commit is contained in:
@@ -5,6 +5,7 @@ LDLIBS ?= -lm
|
||||
|
||||
GPS_TIME_SOURCE := ../../gps_system/src/gps/gps_time_anchor.c
|
||||
GPS_EPOCH_SOURCE := ../../gps_system/src/gps/gps_nav_epoch.c
|
||||
GPS_EPHEMERIS_CACHE_SOURCE := ../../gps_system/src/gps/gps_ephemeris_cache.c
|
||||
ESKF_SOURCES := ../../gps_system/src/fusion/eskf.c ../../gps_system/src/fusion/fusion_motion.c ../../gps_system/src/fusion/fusion_rest_detector.c ../../gps_system/src/fusion/fusion_engine.c ../../gps_system/src/fusion/fusion_gps_adapter.c ../../gps_system/src/fusion/fusion_bus.c
|
||||
IMU_PROFILE_SOURCE := ../../gps_system/src/adapters/imu_fusion_profile.c
|
||||
|
||||
@@ -12,8 +13,8 @@ IMU_PROFILE_SOURCE := ../../gps_system/src/adapters/imu_fusion_profile.c
|
||||
|
||||
all: standalone_gps_integrity_test eskf_motion_test fusion_rest_detector_test fusion_synthetic_test imu_quality_synthetic_test async_tilt_recovery_test async_rate_matrix_test gps_reacquisition_test platform_scenario_matrix_test gps_failure_scenario_test
|
||||
|
||||
standalone_gps_integrity_test: standalone_gps_integrity_test.c $(GPS_TIME_SOURCE) $(GPS_EPOCH_SOURCE)
|
||||
$(CC) $(CFLAGS) $^ -o $@ $(LDLIBS)
|
||||
standalone_gps_integrity_test: standalone_gps_integrity_test.c $(GPS_TIME_SOURCE) $(GPS_EPOCH_SOURCE) $(GPS_EPHEMERIS_CACHE_SOURCE)
|
||||
$(CC) $(CFLAGS) -Wno-comment -I../../gps_system/tests/fake_bsp $^ -o $@ $(LDLIBS)
|
||||
|
||||
eskf_motion_test: eskf_motion_test.c $(ESKF_SOURCES)
|
||||
$(CC) $(CFLAGS) $^ -o $@ $(LDLIBS)
|
||||
@@ -72,8 +73,10 @@ scenario-logs: platform_scenario_matrix_test
|
||||
sanitize:
|
||||
$(CC) -std=c11 -O1 -g -Wall -Wextra -Wpedantic -Werror \
|
||||
-fsanitize=address,undefined -fno-omit-frame-pointer \
|
||||
-Wno-comment \
|
||||
-I../../gps_system/header standalone_gps_integrity_test.c \
|
||||
$(GPS_TIME_SOURCE) $(GPS_EPOCH_SOURCE) \
|
||||
-I../../gps_system/tests/fake_bsp \
|
||||
$(GPS_TIME_SOURCE) $(GPS_EPOCH_SOURCE) $(GPS_EPHEMERIS_CACHE_SOURCE) \
|
||||
-o standalone_gps_integrity_test_san $(LDLIBS)
|
||||
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
|
||||
UBSAN_OPTIONS=halt_on_error=1 ./standalone_gps_integrity_test_san
|
||||
|
||||
@@ -10,12 +10,15 @@
|
||||
#include <time.h>
|
||||
|
||||
#include "gps/gps_nav_epoch.h"
|
||||
#include "gps/gps_ephemeris_cache.h"
|
||||
#include "gps/gps_time_anchor.h"
|
||||
|
||||
#define MAX_SATELLITES 9
|
||||
#define STATE_DIM 4
|
||||
#define PI 3.14159265358979323846
|
||||
#define TEST_PI 3.14159265358979323846
|
||||
#define RECEIVER_SAMPLES_PER_SECOND 16368000.0
|
||||
#define RECEIVER_COUNTER_PPM 0.20
|
||||
#define SPEED_OF_LIGHT_MPS 299792458.0
|
||||
#define EPOCH_SAMPLE_WINDOW 409200u
|
||||
#define EPOCH_TOW_TOLERANCE_SEC 0.001
|
||||
|
||||
@@ -229,8 +232,8 @@ static void create_measurements(measurement_t measurements[MAX_SATELLITES],
|
||||
enu_axes(latitude_rad, longitude_rad, axes);
|
||||
|
||||
for (unsigned index = 0; index < count; index++) {
|
||||
double azimuth = azimuth_deg[index] * PI / 180.0;
|
||||
double elevation = elevation_deg[index] * PI / 180.0;
|
||||
double azimuth = azimuth_deg[index] * TEST_PI / 180.0;
|
||||
double elevation = elevation_deg[index] * TEST_PI / 180.0;
|
||||
double los_enu[3] = {
|
||||
cos(elevation) * sin(azimuth),
|
||||
cos(elevation) * cos(azimuth),
|
||||
@@ -572,6 +575,7 @@ typedef struct {
|
||||
bool have_sf[3];
|
||||
unsigned char iod[3];
|
||||
bool ephemeris_valid;
|
||||
unsigned ephemeris_decode_count;
|
||||
} linux_nav_channel_t;
|
||||
|
||||
typedef struct {
|
||||
@@ -582,6 +586,9 @@ typedef struct {
|
||||
unsigned fixes;
|
||||
unsigned fde_fixes;
|
||||
unsigned epoch_exclusions;
|
||||
unsigned sample_observations;
|
||||
unsigned ephemeris_cache_restores;
|
||||
unsigned ephemeris_decodes;
|
||||
double east_sum_m;
|
||||
double north_sum_m;
|
||||
double up_sum_m;
|
||||
@@ -627,7 +634,7 @@ static double synthetic_rng_gaussian(synthetic_rng_t* rng)
|
||||
{
|
||||
double u1 = synthetic_rng_uniform(rng);
|
||||
double u2 = synthetic_rng_uniform(rng);
|
||||
return sqrt(-2.0 * log(u1)) * cos(2.0 * PI * u2);
|
||||
return sqrt(-2.0 * log(u1)) * cos(2.0 * TEST_PI * u2);
|
||||
}
|
||||
|
||||
static void linux_nav_receive_ephemeris_subframe(linux_nav_channel_t* channel,
|
||||
@@ -640,13 +647,161 @@ static void linux_nav_receive_ephemeris_subframe(linux_nav_channel_t* channel,
|
||||
unsigned slot = subframe_id - 1u;
|
||||
channel->have_sf[slot] = true;
|
||||
channel->iod[slot] = iod;
|
||||
if (channel->have_sf[0] && channel->have_sf[1] && channel->have_sf[2] &&
|
||||
if (!channel->ephemeris_valid &&
|
||||
channel->have_sf[0] && channel->have_sf[1] && channel->have_sf[2] &&
|
||||
channel->iod[0] == channel->iod[1] &&
|
||||
channel->iod[1] == channel->iod[2]) {
|
||||
channel->ephemeris_valid = true;
|
||||
channel->ephemeris_decode_count++;
|
||||
}
|
||||
}
|
||||
|
||||
static bool linux_nav_cache_ephemeris(gps_ephemeris_cache_t* cache,
|
||||
unsigned satellite,
|
||||
unsigned char iod,
|
||||
uint64_t tick)
|
||||
{
|
||||
gps_ephemeris_t ephemeris;
|
||||
memset(&ephemeris, 0, sizeof(ephemeris));
|
||||
ephemeris.SVID = (unsigned char)satellite;
|
||||
ephemeris.iode = iod;
|
||||
ephemeris.ephemeris_valid = true;
|
||||
ephemeris.toe.week = 2300;
|
||||
ephemeris.toe.sec = 604000.0;
|
||||
return gps_ephemeris_cache_store(
|
||||
cache, satellite, &ephemeris, tick);
|
||||
}
|
||||
|
||||
static bool linux_nav_restore_ephemeris(gps_ephemeris_cache_t* cache,
|
||||
linux_nav_channel_t* channel,
|
||||
unsigned satellite,
|
||||
uint64_t tick)
|
||||
{
|
||||
gps_ephemeris_t ephemeris;
|
||||
if (!gps_ephemeris_cache_restore(
|
||||
cache, satellite, tick, &ephemeris)) {
|
||||
return false;
|
||||
}
|
||||
channel->ephemeris_valid = ephemeris.ephemeris_valid;
|
||||
memset(channel->have_sf, 0, sizeof(channel->have_sf));
|
||||
memset(channel->iod, 0, sizeof(channel->iod));
|
||||
return channel->ephemeris_valid;
|
||||
}
|
||||
|
||||
static bool synthetic_apply_receiver_sample_datum(
|
||||
measurement_t measurements[MAX_SATELLITES],
|
||||
unsigned count,
|
||||
unsigned epoch,
|
||||
int week,
|
||||
double tow_sec,
|
||||
double absolute_clock_bias_m,
|
||||
double receiver_counter_ppm)
|
||||
{
|
||||
if (count == 0u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const double actual_sample_hz =
|
||||
RECEIVER_SAMPLES_PER_SECOND *
|
||||
(1.0 + receiver_counter_ppm * 1.0e-6);
|
||||
/* Production unwraps every channel's shared 32-bit RF sample latch around
|
||||
* the epoch reference. Keep an arbitrary moving origin here to prove that
|
||||
* neither boot time nor a counter rollover is a range datum. */
|
||||
const double epoch_sample = 1000000000.375 +
|
||||
fmod((double)epoch * 0.2 * actual_sample_hz, 4294967296.0);
|
||||
double capture_sample[MAX_SATELLITES];
|
||||
for (unsigned satellite = 0u; satellite < count; satellite++) {
|
||||
double propagation_sec =
|
||||
(measurements[satellite].pseudorange_m - absolute_clock_bias_m) /
|
||||
SPEED_OF_LIGHT_MPS;
|
||||
if (!isfinite(propagation_sec) || propagation_sec < 0.0) {
|
||||
return false;
|
||||
}
|
||||
capture_sample[satellite] = epoch_sample +
|
||||
propagation_sec * actual_sample_hz;
|
||||
}
|
||||
|
||||
for (unsigned satellite = 0u; satellite < count; satellite++) {
|
||||
double relative_observation_sec = 0.0;
|
||||
if (!gps_time_sample_differential_observation(
|
||||
week, tow_sec, capture_sample[0],
|
||||
week, tow_sec, capture_sample[satellite],
|
||||
RECEIVER_SAMPLES_PER_SECOND,
|
||||
&relative_observation_sec)) {
|
||||
return false;
|
||||
}
|
||||
measurements[satellite].pseudorange_m =
|
||||
relative_observation_sec * SPEED_OF_LIGHT_MPS;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void verify_common_start_offset_cannot_change_position(
|
||||
const measurement_t absolute_measurements[MAX_SATELLITES],
|
||||
unsigned count,
|
||||
const double receiver_truth[3],
|
||||
double latitude_rad,
|
||||
double longitude_rad)
|
||||
{
|
||||
measurement_t differential[MAX_SATELLITES];
|
||||
measurement_t shifted[MAX_SATELLITES];
|
||||
const double old_start_offset_m =
|
||||
0.068802 * SPEED_OF_LIGHT_MPS;
|
||||
const double reference_observation_m =
|
||||
absolute_measurements[0].pseudorange_m;
|
||||
for (unsigned row = 0u; row < count; row++) {
|
||||
differential[row] = absolute_measurements[row];
|
||||
shifted[row] = absolute_measurements[row];
|
||||
differential[row].pseudorange_m -= reference_observation_m;
|
||||
shifted[row].pseudorange_m =
|
||||
differential[row].pseudorange_m + old_start_offset_m;
|
||||
}
|
||||
|
||||
double differential_seed[STATE_DIM] = {
|
||||
receiver_truth[0] + 120.0,
|
||||
receiver_truth[1] - 90.0,
|
||||
receiver_truth[2] + 60.0,
|
||||
-20200000.0
|
||||
};
|
||||
double shifted_seed[STATE_DIM] = {
|
||||
receiver_truth[0] + 120.0,
|
||||
receiver_truth[1] - 90.0,
|
||||
receiver_truth[2] + 60.0,
|
||||
400000.0
|
||||
};
|
||||
solution_t differential_solution;
|
||||
solution_t differential_full;
|
||||
solution_t shifted_solution;
|
||||
solution_t shifted_full;
|
||||
bool differential_ok = solve_with_fde(
|
||||
differential, count, differential_seed,
|
||||
latitude_rad, longitude_rad,
|
||||
&differential_solution, &differential_full);
|
||||
bool shifted_ok = solve_with_fde(
|
||||
shifted, count, shifted_seed,
|
||||
latitude_rad, longitude_rad,
|
||||
&shifted_solution, &shifted_full);
|
||||
CHECK(differential_ok && shifted_ok &&
|
||||
differential_solution.solution_valid &&
|
||||
shifted_solution.solution_valid,
|
||||
"common-datum WLS invariance solutions must be valid");
|
||||
if (!differential_ok || !shifted_ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
double position_delta[3] = {
|
||||
shifted_solution.state[0] - differential_solution.state[0],
|
||||
shifted_solution.state[1] - differential_solution.state[1],
|
||||
shifted_solution.state[2] - differential_solution.state[2]
|
||||
};
|
||||
CHECK(vector_norm3(position_delta) < 1.0e-4,
|
||||
"68.802 ms common offset changed WLS XYZ");
|
||||
CHECK(fabs((shifted_solution.state[3] -
|
||||
differential_solution.state[3]) -
|
||||
old_start_offset_m) < 1.0e-4,
|
||||
"68.802 ms common offset was not confined to WLS clock bias");
|
||||
}
|
||||
|
||||
static unsigned synthetic_stage_for_epoch(unsigned epoch)
|
||||
{
|
||||
static const unsigned ends[SYNTHETIC_STAGE_COUNT] =
|
||||
@@ -773,6 +928,7 @@ static void synthetic_print_metrics(synthetic_stage_metrics_t metrics)
|
||||
}
|
||||
|
||||
printf("%-21s epochs=%3u fixes=%3u avg=%4.1f->%4.1f "
|
||||
"sample=%4u cache=%2u decode=%2u "
|
||||
"epoch_excl=%3u fde=%3u hRMSE=%6.2f hSTD=%6.2f "
|
||||
"hP95=%6.2f hMAX=%6.2f vRMSE=%6.2f vSTD=%6.2f\n",
|
||||
metrics.name, metrics.epochs, metrics.fixes,
|
||||
@@ -780,6 +936,9 @@ static void synthetic_print_metrics(synthetic_stage_metrics_t metrics)
|
||||
? (double)metrics.candidate_total / (double)metrics.epochs : 0.0,
|
||||
metrics.epochs != 0u
|
||||
? (double)metrics.selected_total / (double)metrics.epochs : 0.0,
|
||||
metrics.sample_observations,
|
||||
metrics.ephemeris_cache_restores,
|
||||
metrics.ephemeris_decodes,
|
||||
metrics.epoch_exclusions, metrics.fde_fixes,
|
||||
horizontal_rmse, horizontal_spread, p95, metrics.horizontal_max_m,
|
||||
vertical_rmse, vertical_spread);
|
||||
@@ -809,9 +968,17 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
|
||||
|
||||
linux_nav_channel_t channels[MAX_SATELLITES];
|
||||
memset(channels, 0, sizeof(channels));
|
||||
gps_ephemeris_cache_t ephemeris_cache;
|
||||
gps_ephemeris_cache_init(&ephemeris_cache, 1000u);
|
||||
for (unsigned satellite = 0u; satellite < 8u; satellite++) {
|
||||
channels[satellite].ephemeris_valid = true;
|
||||
channels[satellite].ephemeris_decode_count = 1u;
|
||||
CHECK(linux_nav_cache_ephemeris(
|
||||
&ephemeris_cache, satellite,
|
||||
(unsigned char)(17u + satellite), 0u),
|
||||
"initial per-PRN ephemeris cache store failed");
|
||||
}
|
||||
metrics[STAGE_BASELINE].ephemeris_decodes = 8u;
|
||||
|
||||
linux_nav_channel_t iod_regression = {0};
|
||||
linux_nav_receive_ephemeris_subframe(&iod_regression, 1u, 17u);
|
||||
@@ -830,7 +997,7 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
|
||||
receiver_truth[0] + 120.0,
|
||||
receiver_truth[1] - 90.0,
|
||||
receiver_truth[2] + 60.0,
|
||||
clock_bias_m - 200.0
|
||||
-20200000.0
|
||||
};
|
||||
|
||||
unsigned delayed_selection_failures = 0u;
|
||||
@@ -838,14 +1005,40 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
|
||||
unsigned precommit_join_count_failures = 0u;
|
||||
unsigned postcommit_join_count_failures = 0u;
|
||||
unsigned fde_wrong_exclusion = 0u;
|
||||
unsigned sample_datum_failures = 0u;
|
||||
unsigned ephemeris_cache_restore_total = 0u;
|
||||
bool saw_week_rollover = false;
|
||||
bool saw_sample_rollover = false;
|
||||
uint32_t previous_raw_sample = 0u;
|
||||
uint32_t previous_active_mask = 0x0ffu;
|
||||
|
||||
for (unsigned epoch = 0u; epoch < 540u; epoch++) {
|
||||
unsigned stage = synthetic_stage_for_epoch(epoch);
|
||||
synthetic_stage_metrics_t* stage_metrics = &metrics[stage];
|
||||
stage_metrics->epochs++;
|
||||
uint32_t active_mask = synthetic_active_mask(stage);
|
||||
|
||||
/* A tracking slot is volatile; the per-PRN cache is not. Model a
|
||||
* building/bridge obstruction by clearing the channel copy on loss and
|
||||
* restoring it from RAM exactly once when acquisition returns. */
|
||||
for (unsigned satellite = 0u; satellite < MAX_SATELLITES; satellite++) {
|
||||
bool was_active = (previous_active_mask & (1u << satellite)) != 0u;
|
||||
bool is_active = (active_mask & (1u << satellite)) != 0u;
|
||||
if (was_active && !is_active) {
|
||||
channels[satellite].ephemeris_valid = false;
|
||||
memset(channels[satellite].have_sf, 0,
|
||||
sizeof(channels[satellite].have_sf));
|
||||
memset(channels[satellite].iod, 0,
|
||||
sizeof(channels[satellite].iod));
|
||||
} else if (!was_active && is_active &&
|
||||
linux_nav_restore_ephemeris(
|
||||
&ephemeris_cache, &channels[satellite],
|
||||
satellite, epoch)) {
|
||||
stage_metrics->ephemeris_cache_restores++;
|
||||
ephemeris_cache_restore_total++;
|
||||
}
|
||||
}
|
||||
previous_active_mask = active_mask;
|
||||
|
||||
if (stage == STAGE_EPHEMERIS_JOIN && epoch == 420u) {
|
||||
linux_nav_receive_ephemeris_subframe(&channels[8], 1u, 29u);
|
||||
@@ -853,6 +1046,12 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
|
||||
}
|
||||
if (stage == STAGE_EPHEMERIS_JOIN && epoch == 450u) {
|
||||
linux_nav_receive_ephemeris_subframe(&channels[8], 3u, 29u);
|
||||
if (channels[8].ephemeris_valid) {
|
||||
CHECK(linux_nav_cache_ephemeris(
|
||||
&ephemeris_cache, 8u, 29u, epoch),
|
||||
"newly decoded PRN ephemeris was not cached");
|
||||
stage_metrics->ephemeris_decodes++;
|
||||
}
|
||||
}
|
||||
|
||||
const double zero_noise[MAX_SATELLITES] = {0.0};
|
||||
@@ -875,6 +1074,11 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
|
||||
if (week > 2300) {
|
||||
saw_week_rollover = true;
|
||||
}
|
||||
if (!synthetic_apply_receiver_sample_datum(
|
||||
master, MAX_SATELLITES, epoch, week, tow_sec,
|
||||
clock_bias_m, RECEIVER_COUNTER_PPM)) {
|
||||
sample_datum_failures++;
|
||||
}
|
||||
uint32_t raw_sample =
|
||||
(UINT32_MAX - 100000u) + epoch * 3273600u;
|
||||
if (epoch != 0u && raw_sample < previous_raw_sample) {
|
||||
@@ -885,7 +1089,6 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
|
||||
gps_nav_epoch_key_t candidates[GPS_NAV_EPOCH_MAX_CANDIDATES];
|
||||
unsigned candidate_to_satellite[GPS_NAV_EPOCH_MAX_CANDIDATES];
|
||||
unsigned candidate_count = 0u;
|
||||
uint32_t active_mask = synthetic_active_mask(stage);
|
||||
for (unsigned satellite = 0u; satellite < MAX_SATELLITES; satellite++) {
|
||||
if ((active_mask & (1u << satellite)) == 0u ||
|
||||
!channels[satellite].ephemeris_valid) {
|
||||
@@ -907,6 +1110,7 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
|
||||
candidate_count++;
|
||||
}
|
||||
stage_metrics->candidate_total += candidate_count;
|
||||
stage_metrics->sample_observations += candidate_count;
|
||||
|
||||
gps_nav_epoch_selection_t selection;
|
||||
bool selected = gps_nav_epoch_validate_complete(
|
||||
@@ -970,13 +1174,23 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
|
||||
}
|
||||
|
||||
puts("\nGeneral synthetic GPS channel/epoch scenario");
|
||||
puts("stage epochs/fixes candidates->selected and position scatter");
|
||||
puts("stage epochs/fixes sample/cache/decode telemetry and position scatter");
|
||||
for (unsigned stage = 0u; stage < SYNTHETIC_STAGE_COUNT; stage++) {
|
||||
synthetic_print_metrics(metrics[stage]);
|
||||
}
|
||||
|
||||
CHECK(saw_week_rollover, "general scenario must cross a GPS week boundary");
|
||||
CHECK(saw_sample_rollover, "general scenario must cross uint32 ABS_SAMPLE rollover");
|
||||
CHECK(sample_datum_failures == 0u,
|
||||
"free-running RF sample counter failed to form an end-to-end observation");
|
||||
CHECK(ephemeris_cache_restore_total == 5u,
|
||||
"every returning cached PRN must restore exactly once");
|
||||
CHECK(metrics[STAGE_REACQUIRE].ephemeris_cache_restores == 4u &&
|
||||
metrics[STAGE_REACQUIRE].ephemeris_decodes == 0u,
|
||||
"obstruction recovery must restore RAM ephemeris without SF1/SF2/SF3 decode");
|
||||
CHECK(channels[8].ephemeris_decode_count == 1u &&
|
||||
metrics[STAGE_EPHEMERIS_JOIN].ephemeris_decodes == 1u,
|
||||
"an uncached PRN must join only after one matching ephemeris decode");
|
||||
CHECK(metrics[STAGE_BASELINE].fixes >= 98u,
|
||||
"baseline must produce stable fixes across time/sample rollover");
|
||||
CHECK(metrics[STAGE_ONE_DROPOUT].fixes >= 58u,
|
||||
@@ -1010,11 +1224,198 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
|
||||
"minimum four-satellite geometry must stay within the synthetic bound");
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const char* name;
|
||||
unsigned outage_sec;
|
||||
uint32_t dropped_mask;
|
||||
uint64_t cache_max_age_sec;
|
||||
unsigned expected_cache_restores;
|
||||
unsigned expected_reparses;
|
||||
double receiver_counter_ppm;
|
||||
} counter_reacquisition_case_t;
|
||||
|
||||
static unsigned synthetic_mask_count(uint32_t mask)
|
||||
{
|
||||
unsigned count = 0u;
|
||||
while (mask != 0u) {
|
||||
count += mask & 1u;
|
||||
mask >>= 1u;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static bool synthetic_solve_counter_epoch(
|
||||
unsigned epoch,
|
||||
uint32_t active_mask,
|
||||
const double receiver_truth[3],
|
||||
double latitude_rad,
|
||||
double longitude_rad,
|
||||
double receiver_counter_ppm,
|
||||
double* position_error_out)
|
||||
{
|
||||
const double no_noise[MAX_SATELLITES] = {0.0};
|
||||
measurement_t all_measurements[MAX_SATELLITES];
|
||||
create_measurements(all_measurements, 7u, receiver_truth,
|
||||
latitude_rad, longitude_rad, 74000.0, no_noise);
|
||||
int week = 0;
|
||||
double tow_sec = 0.0;
|
||||
synthetic_epoch_time(epoch, &week, &tow_sec);
|
||||
if (!synthetic_apply_receiver_sample_datum(
|
||||
all_measurements, 7u, epoch, week, tow_sec, 74000.0,
|
||||
receiver_counter_ppm)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
measurement_t selected[MAX_SATELLITES];
|
||||
unsigned count = 0u;
|
||||
for (unsigned satellite = 0u; satellite < 7u; satellite++) {
|
||||
if ((active_mask & (1u << satellite)) != 0u) {
|
||||
selected[count++] = all_measurements[satellite];
|
||||
}
|
||||
}
|
||||
if (count < STATE_DIM) {
|
||||
return false;
|
||||
}
|
||||
|
||||
double seed[STATE_DIM] = {
|
||||
receiver_truth[0] + 120.0,
|
||||
receiver_truth[1] - 90.0,
|
||||
receiver_truth[2] + 60.0,
|
||||
-20200000.0
|
||||
};
|
||||
solution_t solution;
|
||||
solution_t full_solution;
|
||||
if (!solve_with_fde(selected, count, seed,
|
||||
latitude_rad, longitude_rad,
|
||||
&solution, &full_solution) ||
|
||||
!solution.solution_valid) {
|
||||
return false;
|
||||
}
|
||||
if (position_error_out != NULL) {
|
||||
*position_error_out = position_error_m(&solution, receiver_truth);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void run_counter_reacquisition_matrix(const double receiver_truth[3],
|
||||
double latitude_rad,
|
||||
double longitude_rad)
|
||||
{
|
||||
static const counter_reacquisition_case_t cases[] = {
|
||||
{"tunnel-all-30s", 30u, 0x07fu, 7200u, 7u, 0u, 0.2},
|
||||
{"drop-6of7-15s", 15u, 0x07eu, 7200u, 6u, 0u, 0.2},
|
||||
{"drop-6of7-2s", 2u, 0x07eu, 7200u, 6u, 0u, 0.2},
|
||||
{"drop-1of7-15s", 15u, 0x040u, 7200u, 1u, 0u, 0.2},
|
||||
{"stale-all-7201s", 7201u, 0x07fu, 7200u, 0u, 7u, 0.2},
|
||||
{"tunnel-30s-15ppm", 30u, 0x07fu, 7200u, 7u, 0u, 15.0}
|
||||
};
|
||||
const uint32_t full_mask = 0x07fu;
|
||||
|
||||
puts("\nRF free-counter + PPS epoch + per-PRN RAM reacquisition matrix");
|
||||
puts("scenario outage drop remain ppm outage_fix cache reparse nav_wait recovery_error");
|
||||
for (unsigned case_index = 0u;
|
||||
case_index < sizeof(cases) / sizeof(cases[0]);
|
||||
case_index++) {
|
||||
const counter_reacquisition_case_t* scenario = &cases[case_index];
|
||||
gps_ephemeris_cache_t cache;
|
||||
gps_ephemeris_cache_init(&cache, scenario->cache_max_age_sec);
|
||||
linux_nav_channel_t channels[7];
|
||||
memset(channels, 0, sizeof(channels));
|
||||
for (unsigned satellite = 0u; satellite < 7u; satellite++) {
|
||||
channels[satellite].ephemeris_valid = true;
|
||||
channels[satellite].ephemeris_decode_count = 1u;
|
||||
CHECK(linux_nav_cache_ephemeris(
|
||||
&cache, satellite,
|
||||
(unsigned char)(40u + satellite), 0u),
|
||||
"reacquisition matrix cache initialization failed");
|
||||
}
|
||||
|
||||
uint32_t outage_active_mask =
|
||||
full_mask & ~scenario->dropped_mask;
|
||||
for (unsigned satellite = 0u; satellite < 7u; satellite++) {
|
||||
if ((scenario->dropped_mask & (1u << satellite)) != 0u) {
|
||||
channels[satellite].ephemeris_valid = false;
|
||||
}
|
||||
}
|
||||
unsigned remain = synthetic_mask_count(outage_active_mask);
|
||||
double outage_error_m = NAN;
|
||||
bool outage_fix = synthetic_solve_counter_epoch(
|
||||
(scenario->outage_sec * 5u) / 2u,
|
||||
outage_active_mask, receiver_truth,
|
||||
latitude_rad, longitude_rad,
|
||||
scenario->receiver_counter_ppm, &outage_error_m);
|
||||
CHECK(outage_fix == (remain >= STATE_DIM),
|
||||
"outage fix availability did not match the four-satellite minimum");
|
||||
if (outage_fix) {
|
||||
CHECK(outage_error_m < 25.0,
|
||||
"partial-outage RF-counter solution exceeded the recovery bound");
|
||||
}
|
||||
|
||||
unsigned cache_restores = 0u;
|
||||
unsigned reparses = 0u;
|
||||
for (unsigned satellite = 0u; satellite < 7u; satellite++) {
|
||||
if ((scenario->dropped_mask & (1u << satellite)) == 0u) {
|
||||
continue;
|
||||
}
|
||||
if (linux_nav_restore_ephemeris(
|
||||
&cache, &channels[satellite], satellite,
|
||||
scenario->outage_sec)) {
|
||||
cache_restores++;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Expired/missing RAM data follows the real matching-IOD commit
|
||||
* rule; a partial or mismatched set is never made usable. */
|
||||
linux_nav_receive_ephemeris_subframe(
|
||||
&channels[satellite], 1u, (unsigned char)(70u + satellite));
|
||||
linux_nav_receive_ephemeris_subframe(
|
||||
&channels[satellite], 2u, (unsigned char)(70u + satellite));
|
||||
linux_nav_receive_ephemeris_subframe(
|
||||
&channels[satellite], 3u, (unsigned char)(70u + satellite));
|
||||
if (channels[satellite].ephemeris_valid) {
|
||||
reparses++;
|
||||
CHECK(linux_nav_cache_ephemeris(
|
||||
&cache, satellite,
|
||||
(unsigned char)(70u + satellite),
|
||||
scenario->outage_sec + 18u),
|
||||
"fresh post-expiry ephemeris was not returned to RAM");
|
||||
}
|
||||
}
|
||||
CHECK(cache_restores == scenario->expected_cache_restores,
|
||||
"reacquisition matrix restored the wrong number of cached PRNs");
|
||||
CHECK(reparses == scenario->expected_reparses,
|
||||
"reacquisition matrix reparsed the wrong number of PRNs");
|
||||
|
||||
unsigned nav_wait_sec = (reparses == 0u) ? 6u : 18u;
|
||||
unsigned recovery_epoch =
|
||||
(scenario->outage_sec + nav_wait_sec) * 5u;
|
||||
double recovery_error_m = NAN;
|
||||
bool recovery_fix = synthetic_solve_counter_epoch(
|
||||
recovery_epoch, full_mask, receiver_truth,
|
||||
latitude_rad, longitude_rad,
|
||||
scenario->receiver_counter_ppm, &recovery_error_m);
|
||||
CHECK(recovery_fix && recovery_error_m < 25.0,
|
||||
"post-reacquisition RF-counter position did not recover correctly");
|
||||
|
||||
printf("%-20s %6u %4u %6u %5.1f %10s %5u %7u %8u %14.3f m\n",
|
||||
scenario->name,
|
||||
scenario->outage_sec,
|
||||
synthetic_mask_count(scenario->dropped_mask),
|
||||
remain,
|
||||
scenario->receiver_counter_ppm,
|
||||
outage_fix ? "valid" : "suppressed",
|
||||
cache_restores,
|
||||
reparses,
|
||||
nav_wait_sec,
|
||||
recovery_error_m);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
const unsigned satellite_count = 7;
|
||||
const double latitude_rad = 39.925 * PI / 180.0;
|
||||
const double longitude_rad = 32.837 * PI / 180.0;
|
||||
const double latitude_rad = 39.925 * TEST_PI / 180.0;
|
||||
const double longitude_rad = 32.837 * TEST_PI / 180.0;
|
||||
const double receiver_height_m = 890.0;
|
||||
const double clock_bias_m = 74000.0;
|
||||
double receiver_truth[3];
|
||||
@@ -1040,6 +1441,10 @@ int main(int argc, char** argv)
|
||||
seed, latitude_rad, longitude_rad);
|
||||
}
|
||||
|
||||
verify_common_start_offset_cannot_change_position(
|
||||
clean, satellite_count, receiver_truth,
|
||||
latitude_rad, longitude_rad);
|
||||
|
||||
puts("Synthetic standalone GPS integrity/FDE comparison");
|
||||
puts("quality = 100/(1 + chi_square/threshold); 50 is the 99% reject boundary\n");
|
||||
|
||||
@@ -1286,6 +1691,8 @@ int main(int argc, char** argv)
|
||||
|
||||
run_general_synthetic_scenario(receiver_truth, latitude_rad,
|
||||
longitude_rad, clock_bias_m);
|
||||
run_counter_reacquisition_matrix(receiver_truth,
|
||||
latitude_rad, longitude_rad);
|
||||
|
||||
if (failures == 0) {
|
||||
puts("\nPASS: all synthetic integrity checks passed");
|
||||
|
||||
Reference in New Issue
Block a user