Integrate SDK-based RTC and IMU backends as alternatives to direct hardware access. HalClock now attempts SDK RTC initialization and caches time values for reliability. HalTiltSensor adds SDK IMU backend with fallback logic. ClockOffsetActivity gains touch and swipe gesture support for field navigation.
244 lines
7.0 KiB
C++
244 lines
7.0 KiB
C++
#include "HalClock.h"
|
|
|
|
#include <Logging.h>
|
|
#include <WiFi.h>
|
|
#include <esp_sntp.h>
|
|
#include <time.h>
|
|
|
|
#include <cassert>
|
|
|
|
HalClock halClock; // Singleton instance
|
|
|
|
// DS3231 register layout (BCD encoded):
|
|
// 0x00: Seconds (bits 6-4 = tens, bits 3-0 = ones)
|
|
// 0x01: Minutes (bits 6-4 = tens, bits 3-0 = ones)
|
|
// 0x02: Hours (bit 6 = 12/24 mode, bits 5-4 = tens, bits 3-0 = ones)
|
|
|
|
static uint8_t bcdToDec(uint8_t bcd) { return ((bcd >> 4) * 10) + (bcd & 0x0F); }
|
|
static uint8_t decToBcd(uint8_t dec) { return ((dec / 10) << 4) | (dec % 10); }
|
|
|
|
void HalClock::begin() {
|
|
_usesSdkRtc = false;
|
|
if (!gpio.deviceIsX3()) {
|
|
_available = _sdkRtc.begin();
|
|
_usesSdkRtc = _available;
|
|
LOG_INF("CLK", _available ? "SDK RTC found" : "RTC not found");
|
|
return;
|
|
}
|
|
|
|
// I2C is already initialised by HalPowerManager::begin() for X3.
|
|
// Probe the DS3231 by reading the seconds register.
|
|
Wire.beginTransmission(I2C_ADDR_DS3231);
|
|
Wire.write(DS3231_SEC_REG);
|
|
if (Wire.endTransmission(false) != 0) {
|
|
LOG_INF("CLK", "DS3231 RTC not found");
|
|
_available = false;
|
|
return;
|
|
}
|
|
Wire.requestFrom(I2C_ADDR_DS3231, (uint8_t)1);
|
|
if (Wire.available() < 1) {
|
|
_available = false;
|
|
return;
|
|
}
|
|
Wire.read(); // discard — just testing connectivity
|
|
|
|
_available = true;
|
|
LOG_INF("CLK", "DS3231 RTC found");
|
|
|
|
// Prime the cache with an initial read
|
|
uint8_t h, m;
|
|
getTime(h, m);
|
|
}
|
|
|
|
bool HalClock::getTime(uint8_t& hour, uint8_t& minute) const {
|
|
if (!_available) return false;
|
|
|
|
const unsigned long now = millis();
|
|
if (_lastPollMs != 0 && (now - _lastPollMs) < CLOCK_POLL_MS) {
|
|
hour = _cachedHour;
|
|
minute = _cachedMinute;
|
|
return true;
|
|
}
|
|
|
|
if (_usesSdkRtc) {
|
|
Rtc::DateTime dt;
|
|
if (!_sdkRtc.now(dt)) {
|
|
if (!_hasCachedTime) return false;
|
|
_lastPollMs = now;
|
|
hour = _cachedHour;
|
|
minute = _cachedMinute;
|
|
return true;
|
|
}
|
|
_cachedHour = dt.hour;
|
|
_cachedMinute = dt.minute;
|
|
_lastPollMs = now;
|
|
_hasCachedTime = true;
|
|
hour = _cachedHour;
|
|
minute = _cachedMinute;
|
|
return true;
|
|
}
|
|
|
|
// Read 3 bytes starting at register 0x00: seconds, minutes, hours
|
|
Wire.beginTransmission(I2C_ADDR_DS3231);
|
|
Wire.write(DS3231_SEC_REG);
|
|
if (Wire.endTransmission(false) != 0) {
|
|
if (!_hasCachedTime) return false;
|
|
_lastPollMs = now;
|
|
hour = _cachedHour;
|
|
minute = _cachedMinute;
|
|
return true;
|
|
}
|
|
Wire.requestFrom(I2C_ADDR_DS3231, (uint8_t)3);
|
|
if (Wire.available() < 3) {
|
|
if (!_hasCachedTime) return false;
|
|
_lastPollMs = now;
|
|
hour = _cachedHour;
|
|
minute = _cachedMinute;
|
|
return true;
|
|
}
|
|
|
|
Wire.read(); // seconds — not needed
|
|
const uint8_t rawMin = Wire.read();
|
|
const uint8_t rawHour = Wire.read();
|
|
|
|
_cachedMinute = bcdToDec(rawMin & 0x7F);
|
|
// Handle 12/24h mode: bit 6 high = 12h mode
|
|
if (rawHour & 0x40) {
|
|
// 12h mode: bit 5 = PM, bits 4-0 = hours (1-12)
|
|
uint8_t h12 = bcdToDec(rawHour & 0x1F);
|
|
bool pm = rawHour & 0x20;
|
|
if (h12 == 12) h12 = 0;
|
|
_cachedHour = pm ? (h12 + 12) : h12;
|
|
} else {
|
|
// 24h mode: bits 5-0 = hours (0-23)
|
|
_cachedHour = bcdToDec(rawHour & 0x3F);
|
|
}
|
|
_lastPollMs = now;
|
|
_hasCachedTime = true;
|
|
|
|
hour = _cachedHour;
|
|
minute = _cachedMinute;
|
|
return true;
|
|
}
|
|
|
|
bool HalClock::formatTime(char* buf, size_t bufSize, uint8_t utcOffsetQuarterHoursBiased, bool use12Hour) const {
|
|
if (bufSize < (use12Hour ? 9u : 6u)) return false;
|
|
uint8_t h, m;
|
|
if (!getTime(h, m)) return false;
|
|
|
|
// Apply UTC offset: convert biased value to signed quarter-hours.
|
|
// Clamp against corrupted persisted values so display time can't drift outside [-12:00, +14:00].
|
|
if (utcOffsetQuarterHoursBiased > 104) utcOffsetQuarterHoursBiased = 104;
|
|
int offsetQuarterHours = static_cast<int>(utcOffsetQuarterHoursBiased) - 48;
|
|
int totalMinutes = static_cast<int>(h) * 60 + static_cast<int>(m) + offsetQuarterHours * 15;
|
|
|
|
// Wrap around 24 hours
|
|
totalMinutes = ((totalMinutes % 1440) + 1440) % 1440;
|
|
|
|
const int hour24 = totalMinutes / 60;
|
|
const int min = totalMinutes % 60;
|
|
if (use12Hour) {
|
|
const bool pm = hour24 >= 12;
|
|
int hour12 = hour24 % 12;
|
|
if (hour12 == 0) hour12 = 12;
|
|
snprintf(buf, bufSize, "%d:%02d %s", hour12, min, pm ? "PM" : "AM");
|
|
} else {
|
|
snprintf(buf, bufSize, "%02d:%02d", hour24, min);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool HalClock::writeTimeToRTC(uint8_t hour, uint8_t minute, uint8_t second) {
|
|
assert(hour < 24);
|
|
assert(minute < 60);
|
|
assert(second < 60);
|
|
if (_usesSdkRtc) {
|
|
Rtc::DateTime dt;
|
|
dt.hour = hour;
|
|
dt.minute = minute;
|
|
dt.second = second;
|
|
dt.year = 2000;
|
|
dt.month = 1;
|
|
dt.day = 1;
|
|
if (!_sdkRtc.set(dt)) {
|
|
LOG_ERR("CLK", "Failed to write time to SDK RTC");
|
|
return false;
|
|
}
|
|
_lastPollMs = 0;
|
|
_cachedHour = hour;
|
|
_cachedMinute = minute;
|
|
_hasCachedTime = true;
|
|
return true;
|
|
}
|
|
|
|
Wire.beginTransmission(I2C_ADDR_DS3231);
|
|
Wire.write(DS3231_SEC_REG); // Start at register 0x00
|
|
Wire.write(decToBcd(second)); // 0x00: Seconds
|
|
Wire.write(decToBcd(minute)); // 0x01: Minutes
|
|
Wire.write(decToBcd(hour)); // 0x02: Hours (24h mode, bit 6 = 0)
|
|
if (Wire.endTransmission() != 0) {
|
|
LOG_ERR("CLK", "Failed to write time to DS3231");
|
|
return false;
|
|
}
|
|
|
|
// Invalidate cache so next read fetches fresh data
|
|
_lastPollMs = 0;
|
|
_cachedHour = hour;
|
|
_cachedMinute = minute;
|
|
_hasCachedTime = true;
|
|
return true;
|
|
}
|
|
|
|
bool HalClock::syncFromNTP() {
|
|
if (!_available) return false;
|
|
|
|
if (WiFi.status() != WL_CONNECTED) {
|
|
LOG_ERR("CLK", "WiFi not connected, cannot sync NTP");
|
|
return false;
|
|
}
|
|
|
|
LOG_INF("CLK", "Starting NTP sync...");
|
|
configTzTime("UTC0", "pool.ntp.org", "time.nist.gov");
|
|
|
|
// Wait for SNTP sync to complete (up to 5 seconds)
|
|
constexpr int maxAttempts = 50;
|
|
for (int i = 0; i < maxAttempts; i++) {
|
|
if (sntp_get_sync_status() == SNTP_SYNC_STATUS_COMPLETED) {
|
|
time_t now = time(nullptr);
|
|
struct tm timeinfo;
|
|
gmtime_r(&now, &timeinfo);
|
|
|
|
if (_usesSdkRtc) {
|
|
Rtc::DateTime dt;
|
|
dt.year = static_cast<uint16_t>(timeinfo.tm_year + 1900);
|
|
dt.month = static_cast<uint8_t>(timeinfo.tm_mon + 1);
|
|
dt.day = static_cast<uint8_t>(timeinfo.tm_mday);
|
|
dt.hour = static_cast<uint8_t>(timeinfo.tm_hour);
|
|
dt.minute = static_cast<uint8_t>(timeinfo.tm_min);
|
|
dt.second = static_cast<uint8_t>(timeinfo.tm_sec);
|
|
dt.weekday = static_cast<uint8_t>(timeinfo.tm_wday);
|
|
if (_sdkRtc.set(dt)) {
|
|
_lastPollMs = 0;
|
|
_cachedHour = dt.hour;
|
|
_cachedMinute = dt.minute;
|
|
_hasCachedTime = true;
|
|
LOG_INF("CLK", "RTC set to %04u-%02u-%02u %02u:%02u:%02u UTC", dt.year, dt.month, dt.day, dt.hour,
|
|
dt.minute, dt.second);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (writeTimeToRTC(timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec)) {
|
|
LOG_INF("CLK", "RTC set to %02d:%02d:%02d UTC", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
delay(100);
|
|
}
|
|
|
|
LOG_ERR("CLK", "NTP sync timed out");
|
|
return false;
|
|
}
|