// 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 #include #include #include #include #include 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 reads{}; std::vector 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(1U << field); if (!(unread & mask)) (void)getPVT(max_wait); unread &= static_cast(~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(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); }