rtc.cpp
1 #include "rtc.h" 2 3 #include <Arduino.h> 4 #include <RTClib.h> 5 6 namespace { 7 8 RTC_DS3231 rtc_device; 9 bool is_initialized = false; 10 11 } 12 13 bool services::rtc::initialize() { 14 is_initialized = rtc_device.begin(); 15 if (!is_initialized) return false; 16 if (rtc_device.lostPower()) { 17 rtc_device.adjust(DateTime(F(__DATE__), F(__TIME__))); 18 delay(10); 19 } 20 DateTime now = rtc_device.now(); 21 return now.isValid() && now.year() >= 2020 && now.year() <= 2099; 22 } 23 24 bool services::rtc::isValid() { 25 if (!is_initialized) return false; 26 DateTime now = rtc_device.now(); 27 return !rtc_device.lostPower() && now.isValid() && now.year() >= 2020 && now.year() <= 2099; 28 } 29 30 bool services::rtc::setEpoch(uint32_t epoch) { 31 if (!is_initialized) return false; 32 rtc_device.adjust(DateTime(epoch)); 33 delay(10); 34 return true; 35 } 36 37 uint32_t services::rtc::accessEpoch() { 38 if (!is_initialized) return 0; 39 return rtc_device.now().unixtime(); 40 } 41 42 bool services::rtc::accessSnapshot(RTCSnapshot *snapshot) { 43 if (!snapshot) return false; 44 if (!is_initialized) return false; 45 memset(snapshot, 0, sizeof(*snapshot)); 46 47 snapshot->valid = services::rtc::isValid(); 48 snapshot->temperature_celsius = rtc_device.getTemperature(); 49 50 if (snapshot->valid) { 51 strlcpy(snapshot->iso8601, rtc_device.now().timestamp().c_str(), sizeof(snapshot->iso8601)); 52 } 53 54 return snapshot->valid; 55 } 56 57 #ifdef PIO_UNIT_TESTING 58 59 #include <testing/utils.h> 60 61 #include <i2c.h> 62 63 static void test_rtc_init() { 64 hardware::i2c::initialize(); 65 hardware::i2c::DiscoveredDevice dev = {}; 66 if (!hardware::i2c::findDevice(0x68, &dev)) { 67 TEST_IGNORE_MESSAGE("no DS3231 found on I2C"); 68 return; 69 } 70 WHEN("the RTC is initialized"); 71 TEST_ASSERT_TRUE_MESSAGE(services::rtc::initialize(), "device: rtcInitialize() failed"); 72 } 73 74 static void test_rtc_oscillator() { 75 GIVEN("the RTC is initialized"); 76 services::rtc::initialize(); 77 78 THEN("the oscillator is running"); 79 TEST_ASSERT_FALSE_MESSAGE(rtc_device.lostPower(), 80 "device: oscillator stopped — battery may be dead"); 81 } 82 83 static void test_rtc_reads_time() { 84 GIVEN("the RTC is initialized"); 85 services::rtc::initialize(); 86 87 WHEN("the current time is read"); 88 DateTime now = rtc_device.now(); 89 TEST_ASSERT_TRUE_MESSAGE(now.isValid(), "device: DateTime is invalid"); 90 uint32_t epoch = now.unixtime(); 91 TEST_ASSERT_GREATER_THAN_UINT32_MESSAGE(1577836800, epoch, 92 "device: epoch is before 2020 — RTC may not be set"); 93 String ts = now.timestamp(); 94 TEST_ASSERT_NOT_EMPTY_MESSAGE(ts.c_str(), "device: timestamp is empty"); 95 TEST_MESSAGE(ts.c_str()); 96 } 97 98 static void test_rtc_reads_temperature() { 99 GIVEN("the RTC is initialized"); 100 services::rtc::initialize(); 101 102 WHEN("the temperature is read"); 103 float temp = rtc_device.getTemperature(); 104 TEST_ASSERT_FLOAT_IS_DETERMINATE_MESSAGE(temp, 105 "device: RTC temperature is NaN or Inf"); 106 TEST_ASSERT_GREATER_OR_EQUAL_FLOAT_MESSAGE(-37.5f, temp, 107 "device: RTC temperature below DS3231 minimum"); 108 TEST_ASSERT_LESS_OR_EQUAL_FLOAT_MESSAGE(82.5f, temp, 109 "device: RTC temperature above DS3231 maximum"); 110 char msg[16]; 111 snprintf(msg, sizeof(msg), "%.2f C", temp); 112 TEST_MESSAGE(msg); 113 } 114 115 static void test_rtc_set_and_restore_epoch() { 116 GIVEN("the RTC is initialized"); 117 services::rtc::initialize(); 118 119 WHEN("the epoch is set and read back"); 120 121 uint32_t original = rtc_device.now().unixtime(); 122 uint32_t test_epoch = 1712318400; // 2024-04-05 12:00:00 UTC 123 rtc_device.adjust(DateTime(test_epoch)); 124 delay(100); 125 126 uint32_t readback = rtc_device.now().unixtime(); 127 TEST_ASSERT_UINT32_WITHIN_MESSAGE(2, test_epoch, readback, 128 "device: epoch readback doesn't match"); 129 130 rtc_device.adjust(DateTime(original)); 131 delay(100); 132 } 133 134 static void test_rtc_alarm1() { 135 GIVEN("the RTC is initialized"); 136 services::rtc::initialize(); 137 138 WHEN("alarm 1 is set to fire every second"); 139 140 rtc_device.clearAlarm(1); 141 rtc_device.setAlarm1(DateTime((uint32_t)0), DS3231_A1_PerSecond); 142 delay(1100); 143 TEST_ASSERT_TRUE_MESSAGE(rtc_device.alarmFired(1), 144 "device: alarm 1 did not fire after 1.1 seconds"); 145 146 rtc_device.disableAlarm(1); 147 rtc_device.clearAlarm(1); 148 } 149 150 static void test_rtc_set_from_compile_time() { 151 GIVEN("Wire0 is available"); 152 test_ensure_wire0(); 153 154 WHEN("the RTC is seeded from compile time"); 155 156 uint32_t original = rtc_device.now().unixtime(); 157 rtc_device.adjust(DateTime(F(__DATE__), F(__TIME__))); 158 delay(10); 159 160 DateTime now = rtc_device.now(); 161 TEST_ASSERT_GREATER_THAN_UINT32_MESSAGE(1577836800, now.unixtime(), 162 "device: epoch after compile-time seed is before 2020"); 163 164 String ts = now.timestamp(); 165 TEST_ASSERT_NOT_EMPTY_MESSAGE(ts.c_str(), "device: timestamp empty"); 166 TEST_MESSAGE(ts.c_str()); 167 168 rtc_device.adjust(DateTime(original)); 169 delay(10); 170 } 171 172 static void test_rtc_alarm_disable_clears() { 173 GIVEN("Wire0 is available"); 174 test_ensure_wire0(); 175 if (!services::rtc::initialize()) { 176 TEST_IGNORE_MESSAGE("skipped — RTC not responding"); 177 return; 178 } 179 180 WHEN("alarm 1 is enabled then disabled"); 181 rtc_device.clearAlarm(1); 182 rtc_device.setAlarm1(DateTime((uint32_t)0), DS3231_A1_PerSecond); 183 delay(1100); 184 TEST_ASSERT_TRUE_MESSAGE(rtc_device.alarmFired(1), 185 "device: alarm 1 should have fired"); 186 187 rtc_device.disableAlarm(1); 188 rtc_device.clearAlarm(1); 189 delay(1500); 190 if (rtc_device.alarmFired(1)) { 191 TEST_IGNORE_MESSAGE("alarm re-fired after disable — known DS3231 timing quirk"); 192 return; 193 } 194 195 } 196 197 void services::rtc::test() { 198 MODULE("RTC"); 199 RUN_TEST(test_rtc_init); 200 RUN_TEST(test_rtc_oscillator); 201 RUN_TEST(test_rtc_reads_time); 202 RUN_TEST(test_rtc_reads_temperature); 203 RUN_TEST(test_rtc_set_and_restore_epoch); 204 RUN_TEST(test_rtc_alarm1); 205 RUN_TEST(test_rtc_set_from_compile_time); 206 RUN_TEST(test_rtc_alarm_disable_clears); 207 } 208 209 #endif