Files
MeshCore/test/fixtures/rak_gps_clock_freshness.cpp
T

242 lines
12 KiB
C++

// Compile the actual provider/base policy. Only GNSS, clock and Arduino I/O
// are adapters. This is not a full EnvironmentSensorManager or I2C test.
#include <array>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>
#include <helpers/sensors/LocationProvider.h>
void LocationProvider::sendSentence(const char*) {}
struct Pvt {
bool fix = true, date = true, time = true;
int sats = 8;
long lat = 476100000, lng = -1223300000, alt = 125000, epoch = 1792195200;
};
// SparkFun 2.2.x flushPVT clears unread bits, not the PVT payload. Each scalar
// getter implicitly polls if its bit is clear, ignores failure, then returns
// the retained payload. Only the eight fields consumed by this provider are
// modeled (UnixEpoch's calendar conversion/parser are outside this boundary).
struct Gnss {
enum Field { FIX, LAT, LNG, ALT, SATS, DATE, TIME, EPOCH, FIELDS };
Pvt incoming, cached{false, false, false, 0, 0, 0, 0, 0};
bool available = true;
uint8_t unread = 0;
unsigned polls = 0, received = 0, flushes = 0;
std::array<unsigned, FIELDS> reads{};
std::vector<uint16_t> waits;
void flushPVT() { ++flushes; unread = 0; }
bool getPVT(uint16_t max_wait) {
++polls;
waits.push_back(max_wait);
if (!available) return false;
++received;
cached = incoming;
unread = 0xff;
return true;
}
void access(Field field, uint16_t max_wait) {
++reads[field];
const uint8_t mask = static_cast<uint8_t>(1U << field);
if (!(unread & mask)) (void)getPVT(max_wait);
unread &= static_cast<uint8_t>(~mask);
}
bool getGnssFixOk(uint16_t wait) { access(FIX, wait); return cached.fix; }
long getLatitude(uint16_t wait) { access(LAT, wait); return cached.lat; }
long getLongitude(uint16_t wait) { access(LNG, wait); return cached.lng; }
long getAltitude(uint16_t wait) { access(ALT, wait); return cached.alt; }
int getSIV(uint16_t wait) { access(SATS, wait); return cached.sats; }
bool getDateValid(uint16_t wait) { access(DATE, wait); return cached.date; }
bool getTimeValid(uint16_t wait) { access(TIME, wait); return cached.time; }
long getUnixEpoch(uint16_t wait) { access(EPOCH, wait); return cached.epoch; }
} ublox_GNSS;
@PROVIDER@
static const char* scenario = "";
static void check(bool value, const char* diagnostic, int line) {
if (value) return;
std::fprintf(stderr, "FAIL %s: %s (line %d)\n", scenario, diagnostic, line);
std::exit(1);
}
#define CHECK(value, diagnostic) check((value), (diagnostic), __LINE__)
// A due loop must perform exactly one bounded PVT poll. Successful cache
// getters must not cause hidden additional polls; failed polls read no fields.
static void poll(RAK12500LocationProvider& gps, uint32_t at) {
const auto polls = ublox_GNSS.polls, flushes = ublox_GNSS.flushes;
const auto reads = ublox_GNSS.reads;
now_ms = at;
gps.loop();
CHECK(ublox_GNSS.polls == polls + 1 && ublox_GNSS.waits.back() == 8,
"due loop must use exactly one eight-millisecond PVT poll");
CHECK(ublox_GNSS.flushes == flushes + 1, "due loop must discard unread status before polling");
for (unsigned field = 0; field < Gnss::FIELDS; ++field) {
const bool position = field >= Gnss::LAT && field <= Gnss::SATS;
const unsigned count = ublox_GNSS.available && (!position || ublox_GNSS.incoming.fix);
CHECK(ublox_GNSS.reads[field] == reads[field] + count,
"PVT cache getter access budget changed");
}
}
static void good(RAK12500LocationProvider& gps, uint32_t at) {
ublox_GNSS.available = true;
ublox_GNSS.incoming.epoch = 1792195200 + at / 1000;
poll(gps, at);
}
static void qualifiedThree(RAK12500LocationProvider& gps, uint32_t first) {
for (unsigned i = 0; i < 3; ++i) good(gps, first + i * 1000U);
}
int main(int argc, char** argv) {
if (argc != 2) return 2;
scenario = argv[1];
mesh::RTCClock clock;
RAK12500LocationProvider gps;
gps.setRTCClock(&clock);
if (!std::strcmp(scenario, "startup-silence")) {
// Put the clock oracle before budget/fix assertions so the old normal
// path fails for counting retained timestamps, not an incidental metric.
now_ms = 1; gps.loop();
CHECK(gps.isValid() && !clock.writes && ublox_GNSS.received == 1,
"one new startup PVT must update position without setting clock");
ublox_GNSS.available = false;
now_ms = 1001; gps.loop(); now_ms = 2001; gps.loop();
CHECK(clock.writes == 0, "stale PVT must not update the clock");
CHECK(!gps.isValid() && gps.waitingTimeSync() && !gps.getTimeSyncGeneration(),
"failed startup polls must invalidate fix without claiming time sync");
CHECK(ublox_GNSS.received == 1 && ublox_GNSS.polls == 3
&& ublox_GNSS.flushes == 3, "startup failure poll budget changed");
qualifiedThree(gps, 3001);
CHECK(clock.writes == 1 && gps.getTimeSyncGeneration() == 1,
"three new PVTs must recover startup time acquisition");
} else if (!std::strcmp(scenario, "periodic-silence")) {
for (unsigned i = 0; i < 3; ++i) {
now_ms = 1 + i * 1000; ublox_GNSS.incoming.epoch += i; gps.loop();
}
CHECK(clock.writes == 1 && !gps.waitingTimeSync(), "initial three-PVT sync failed");
CHECK(gps.consumeTimeSyncApplied() && !gps.consumeTimeSyncApplied(),
"applied notification must be an edge");
const auto synced = clock.getCurrentTime();
const uint32_t due = now_ms + gps.periodicTimeSyncIntervalMillis();
ublox_GNSS.available = false;
now_ms = due; gps.loop();
CHECK(clock.writes == 1, "stale PVT must not update the clock");
CHECK(clock.getCurrentTime() == synced + 1800 && gps.getTimeSyncGeneration() == 1
&& !gps.consumeTimeSyncApplied(), "failed periodic poll must not rewind RTC or emit sync");
CHECK(!gps.isValid() && ublox_GNSS.received == 3 && ublox_GNSS.polls == 4,
"failed periodic acquisition must retain only old cache, not a fix");
good(gps, due + 1000); good(gps, due + 2000);
CHECK(clock.writes == 1, "periodic recovery must require three new PVTs after failure");
good(gps, due + 3000);
CHECK(clock.writes == 2 && gps.getTimeSyncGeneration() == 2,
"fresh periodic acquisition must eventually update RTC");
CHECK(clock.getCurrentTime() == static_cast<uint32_t>(gps.getTimestamp()),
"periodic recovery wrote a timestamp other than the new PVT");
} else if (!std::strcmp(scenario, "consecutive-failure")) {
good(gps, 1); good(gps, 1001);
CHECK(!clock.writes, "two samples must not set the clock");
ublox_GNSS.available = false; poll(gps, 2001);
good(gps, 3001); good(gps, 4001);
CHECK(!clock.writes, "poll failure must reset consecutive fresh-sample count");
good(gps, 5001);
CHECK(clock.writes == 1 && gps.isValid(), "third post-failure fresh PVT must restore sync");
} else if (!std::strcmp(scenario, "poll-budget")) {
good(gps, 1);
const auto reads = ublox_GNSS.reads;
for (uint32_t at : {1U, 2U, 500U, 1000U}) { now_ms = at; gps.loop(); }
CHECK(ublox_GNSS.polls == 1 && ublox_GNSS.flushes == 1 && ublox_GNSS.reads == reads,
"subsecond service must not poll or reread receiver cache");
good(gps, 1001);
const long lat = gps.getLatitude(), lng = gps.getLongitude(), alt = gps.getAltitude();
const long sats = gps.satellitesCount(), epoch = gps.getTimestamp();
ublox_GNSS.available = false; poll(gps, 2001);
CHECK(!gps.isValid() && !clock.writes, "failed poll must not count retained navigation data");
const auto polls = ublox_GNSS.polls;
CHECK(gps.getLatitude() == lat && gps.getLongitude() == lng && gps.getAltitude() == alt
&& gps.satellitesCount() == sats && gps.getTimestamp() == epoch,
"failure must preserve scalar cache for explicitly invalid position");
CHECK(ublox_GNSS.polls == polls, "provider cache getters must not perform I2C polls");
} else if (!std::strcmp(scenario, "position-request")) {
good(gps, 1);
CHECK(gps.clearPositionFix() && !gps.isValid(), "position request must invalidate old fix");
ublox_GNSS.available = false; poll(gps, 2);
CHECK(!gps.isValid() && !clock.writes, "position-only request cannot reuse retained fix");
qualifiedThree(gps, 1002);
CHECK(gps.isValid() && clock.writes == 1, "position-only request must recover with fresh PVTs");
} else if (!std::strcmp(scenario, "fresh-tracker")) {
gps.beginFreshTimeSync();
ublox_GNSS.available = false; poll(gps, 1); poll(gps, 1001);
CHECK(!gps.isValid() && !clock.writes && gps.waitingTimeSync(),
"fresh tracker request cannot use failed polls");
good(gps, 2001); good(gps, 3001);
CHECK(!clock.writes, "tracker clock must wait for three fresh samples");
good(gps, 4001);
CHECK(clock.writes == 1 && gps.getTimeSyncGeneration() == 1 && gps.consumeTimeSyncApplied(),
"fresh tracker request must finish with one applied edge");
gps.beginFreshTimeSync();
ublox_GNSS.available = false; poll(gps, 5001);
CHECK(!gps.isValid() && clock.writes == 1 && gps.getTimeSyncGeneration() == 1,
"a second tracker request must not inherit its previous valid cache");
qualifiedThree(gps, 6001);
CHECK(clock.writes == 2 && gps.getTimeSyncGeneration() == 2
&& gps.consumeTimeSyncApplied() && !gps.consumeTimeSyncApplied(),
"second tracker request must require three new samples and emit one edge");
gps.endFreshTimeSync();
} else if (!std::strcmp(scenario, "reset")) {
qualifiedThree(gps, 1);
CHECK(clock.writes == 1, "reset control needs a previous applied fix");
gps.beginFreshTimeSync(); gps.clearPositionFix(); gps.reset();
CHECK(!gps.isValid() && !gps.getLatitude() && !gps.getLongitude() && !gps.getAltitude()
&& !gps.satellitesCount() && !gps.getTimestamp() && !gps.getLastValidTimeSync()
&& gps.waitingTimeSync() && !gps.consumeTimeSyncApplied(),
"reset must clear public fix/time state before reselecting the receiver");
ublox_GNSS.available = false; poll(gps, 2002);
CHECK(clock.writes == 1 && !gps.isValid() && !gps.getTimestamp(),
"reset provider must not republish retained receiver cache after failed poll");
good(gps, 3002); good(gps, 4002);
CHECK(clock.writes == 1, "reset must discard the previous consecutive sample count");
good(gps, 5002);
CHECK(clock.writes == 2 && gps.getTimeSyncGeneration() == 2,
"reset must allow a newly qualified sync without reusing an applied edge");
} else if (!std::strcmp(scenario, "null-clock")) {
gps.setRTCClock(nullptr);
qualifiedThree(gps, 1);
CHECK(gps.isValid() && gps.getTimestamp() > 0 && !clock.writes
&& gps.waitingTimeSync() && !gps.getTimeSyncGeneration() && !gps.consumeTimeSyncApplied(),
"clockless location acquisition must not fabricate a time-sync result");
ublox_GNSS.available = false; poll(gps, 3001);
CHECK(!gps.isValid(), "clockless failed poll must also invalidate fix");
} else if (!std::strncmp(scenario, "invalid-", 8)) {
good(gps, 1); good(gps, 1001);
const char* field = scenario + 8;
if (!std::strcmp(field, "fix")) ublox_GNSS.incoming.fix = false;
else if (!std::strcmp(field, "date")) ublox_GNSS.incoming.date = false;
else if (!std::strcmp(field, "time")) ublox_GNSS.incoming.time = false;
else if (!std::strcmp(field, "sats")) ublox_GNSS.incoming.sats = 4;
else if (!std::strcmp(field, "epoch")) ublox_GNSS.incoming.epoch = 0;
else return 2;
poll(gps, 2001);
CHECK(!clock.writes, "unqualified fresh PVT must not finish time acquisition");
if (!std::strcmp(field, "fix")) CHECK(!gps.isValid(), "no-nav PVT must invalidate position");
ublox_GNSS.incoming = Pvt{};
good(gps, 3001); good(gps, 4001);
CHECK(!clock.writes, "unqualified PVT must reset consecutive time samples");
good(gps, 5001);
CHECK(clock.writes == 1 && gps.isValid(), "three newly qualified PVTs must restore time sync");
} else if (!std::strcmp(scenario, "boundary-contract")) {
CHECK(ublox_GNSS.getPVT(8), "boundary needs a received PVT");
const auto lat = ublox_GNSS.cached.lat, epoch = ublox_GNSS.cached.epoch;
ublox_GNSS.flushPVT(); ublox_GNSS.available = false;
CHECK(!ublox_GNSS.getPVT(8) && ublox_GNSS.cached.lat == lat,
"failed explicit poll must retain receiver payload");
CHECK(ublox_GNSS.getLatitude(2) == lat && ublox_GNSS.getUnixEpoch(2) == epoch
&& ublox_GNSS.polls == 4 && ublox_GNSS.received == 1,
"failed implicit getters must still return retained payload");
} else return 2;
std::printf("PASS %s\n", scenario);
}