/ firmware / src / services / rtc.cpp
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