diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index 78439a50a..ad0bdec04 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -31,13 +31,63 @@ static uint32_t timeStartMsec; // Once we have a GPS lock, this is where we hold the initial msec clock that corresponds to that time static uint64_t zeroOffsetSecs; // GPS based time in secs since 1970 - only updated once on initial lock +#ifdef PIO_UNIT_TESTING +// Test seam: unit tests can inject a fake system clock (e.g. the uptime seconds that +// gettimeofday() returns on boards without a real RTC, like RP2040) and force readFromRTC() +// down the no-hardware-RTC fallback even when a hardware-RTC branch is compiled in. +static bool hasMockSystemTime = false; +static bool forceSystemTimeFallback = false; +static struct timeval mockSystemTime = {}; +#endif + +// Reads the platform system clock (or the injected mock during unit tests). Used only by the +// no-hardware-RTC fallback below, so it may be unused on builds with a hardware RTC. +[[maybe_unused]] static bool readSystemTime(struct timeval *tv) +{ +#ifdef PIO_UNIT_TESTING + if (hasMockSystemTime) { + *tv = mockSystemTime; + return true; + } +#endif + return gettimeofday(tv, NULL) == 0; +} + +// Seeds the clock from the system time on boards without a hardware RTC. gettimeofday() can +// return uptime rather than wall-clock time there (e.g. RP2040), so only adopt it when we have +// nothing better yet -- never clobber a higher-quality GPS/NTP/phone source (issue #9828). +[[maybe_unused]] static RTCSetResult readFromSystemTimeFallback() +{ + struct timeval tv; + if (readSystemTime(&tv)) { + uint32_t now = millis(); + uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms + if (currentQuality == RTCQualityNone) { + LOG_DEBUG("Seed time from system clock: %lu", (unsigned long)printableEpoch); + timeStartMsec = now; + zeroOffsetSecs = tv.tv_sec; + } else { + LOG_DEBUG("Ignore system clock fallback (%lu); current RTC quality is %s", (unsigned long)printableEpoch, + RtcName(currentQuality)); + } + return RTCSetResultSuccess; + } + return RTCSetResultNotSet; +} + /** - * Reads the current date and time from the RTC module and updates the system time. - * @return True if the RTC was successfully read and the system time was updated, false otherwise. + * Reads date/time from the RTC module (or system-time fallback) and seeds internal timekeeping. + * @return RTCSetResultSuccess if a time source was read successfully (even if an existing higher-quality time is retained). */ RTCSetResult readFromRTC() { - struct timeval tv; /* btw settimeofday() is helpful here too*/ +#ifdef PIO_UNIT_TESTING + if (forceSystemTimeFallback) { + return readFromSystemTimeFallback(); + } +#endif + + [[maybe_unused]] struct timeval tv; /* btw settimeofday() is helpful here too*/ #ifdef RV3028_RTC if (rtc_found.address == RV3028_RTC) { uint32_t now = millis(); @@ -162,14 +212,7 @@ RTCSetResult readFromRTC() } } #else - if (!gettimeofday(&tv, NULL)) { - uint32_t now = millis(); - uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms - LOG_DEBUG("Read RTC time as %ld", printableEpoch); - timeStartMsec = now; - zeroOffsetSecs = tv.tv_sec; - return RTCSetResultSuccess; - } + return readFromSystemTimeFallback(); #endif return RTCSetResultNotSet; } @@ -292,7 +335,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd LOG_WARN("Failed to set time for RX8130CE"); } } -#elif defined(ARCH_ESP32) +#elif defined(ARCH_ESP32) || defined(ARCH_RP2040) settimeofday(tv, NULL); #endif @@ -423,6 +466,38 @@ void setBootRelativeTimeForUnitTest(uint32_t secondsSinceBoot) lastSetFromPhoneNtpOrGps = 0; lastTimeValidationWarning = 0; } + +void clearRTCSystemTimeForTests() +{ + hasMockSystemTime = false; + mockSystemTime = {}; +} + +void setRTCSystemTimeForTests(const struct timeval *tv) +{ + if (tv == NULL) { + clearRTCSystemTimeForTests(); + return; + } + mockSystemTime = *tv; + hasMockSystemTime = true; +} + +void setReadFromRTCUseSystemTimeForTests(bool enabled) +{ + forceSystemTimeFallback = enabled; +} + +void resetRTCStateForTests() +{ + currentQuality = RTCQualityNone; + timeStartMsec = 0; + zeroOffsetSecs = 0; + lastSetFromPhoneNtpOrGps = 0; + lastTimeValidationWarning = 0; + setReadFromRTCUseSystemTimeForTests(false); + clearRTCSystemTimeForTests(); +} #endif time_t gm_mktime(const struct tm *tm) diff --git a/src/gps/RTC.h b/src/gps/RTC.h index cd1e1d002..b69a99ec9 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -56,6 +56,10 @@ RTCSetResult readFromRTC(); #ifdef PIO_UNIT_TESTING void setBootRelativeTimeForUnitTest(uint32_t secondsSinceBoot); +void resetRTCStateForTests(); +void setRTCSystemTimeForTests(const struct timeval *tv); +void clearRTCSystemTimeForTests(); +void setReadFromRTCUseSystemTimeForTests(bool enabled); #endif time_t gm_mktime(const struct tm *tm); diff --git a/test/test_rtc/test_main.cpp b/test/test_rtc/test_main.cpp new file mode 100644 index 000000000..02cad01c4 --- /dev/null +++ b/test/test_rtc/test_main.cpp @@ -0,0 +1,82 @@ +#include "TestUtil.h" +#include "gps/RTC.h" +#include +#include +#include + +// Regression coverage for issue #9828: on boards without a hardware RTC (e.g. RP2040), +// gettimeofday() can return uptime seconds rather than wall-clock time. A later readFromRTC() +// must not overwrite a higher-quality network/GPS time with that value, but it should still seed +// the clock when nothing better exists yet. +// +// The native test build compiles the RV3028 hardware-RTC branch (variants/native/portduino +// defines RV3028_RTC), so these tests use setReadFromRTCUseSystemTimeForTests() to force the +// no-hardware-RTC fallback path and setRTCSystemTimeForTests() to inject a deterministic clock. + +static const uint32_t kAllowedDriftSeconds = 2; +static const time_t kUptimeSeconds = 21; // what gettimeofday() returns on RP2040 without a real clock + +// A clearly-valid wall-clock epoch, safely inside any BUILD_EPOCH validity window. +static time_t makeValidEpoch() +{ + return time(NULL) + SEC_PER_DAY; +} + +void setUp(void) +{ + resetRTCStateForTests(); +} + +void tearDown(void) +{ + resetRTCStateForTests(); +} + +// A higher-quality network time must survive a later system-time read that only knows uptime. +static void test_readFromRTC_preserves_better_network_time(void) +{ + const time_t networkEpoch = makeValidEpoch(); + struct timeval networkTime; + networkTime.tv_sec = networkEpoch; + networkTime.tv_usec = 0; + TEST_ASSERT_EQUAL_INT(RTCSetResultSuccess, perhapsSetRTC(RTCQualityFromNet, &networkTime)); + + // Simulate a later readFromRTC() falling back to a system clock that only knows uptime. + struct timeval uptime; + uptime.tv_sec = kUptimeSeconds; + uptime.tv_usec = 0; + setRTCSystemTimeForTests(&uptime); + setReadFromRTCUseSystemTimeForTests(true); + + TEST_ASSERT_EQUAL_INT(RTCSetResultSuccess, readFromRTC()); + TEST_ASSERT_EQUAL_INT(RTCQualityFromNet, getRTCQuality()); + TEST_ASSERT_UINT32_WITHIN(kAllowedDriftSeconds, (uint32_t)networkEpoch, getValidTime(RTCQualityFromNet)); +} + +// Before any higher-quality source exists, the fallback should still seed the clock. +static void test_readFromRTC_initializes_time_when_no_better_source(void) +{ + const time_t systemEpoch = makeValidEpoch(); + struct timeval systemTime; + systemTime.tv_sec = systemEpoch; + systemTime.tv_usec = 0; + setRTCSystemTimeForTests(&systemTime); + setReadFromRTCUseSystemTimeForTests(true); + + TEST_ASSERT_EQUAL_INT(RTCSetResultSuccess, readFromRTC()); + TEST_ASSERT_EQUAL_INT(RTCQualityNone, getRTCQuality()); + TEST_ASSERT_UINT32_WITHIN(kAllowedDriftSeconds, (uint32_t)systemEpoch, getTime()); +} + +void setup() +{ + delay(10); + initializeTestEnvironment(); + + UNITY_BEGIN(); + RUN_TEST(test_readFromRTC_preserves_better_network_time); + RUN_TEST(test_readFromRTC_initializes_time_when_no_better_source); + exit(UNITY_END()); +} + +void loop() {}