Add basic clock support

This commit is contained in:
jpirnay
2026-03-26 16:46:23 +01:00
parent 5b43639f98
commit dd9c9e6def
18 changed files with 484 additions and 43 deletions
+181
View File
@@ -0,0 +1,181 @@
#include "HalClock.h"
#include <Arduino.h>
#include <Logging.h>
#include <Preferences.h>
#include <esp_private/esp_clk.h>
#include <esp_sntp.h>
#include <sys/time.h>
// ---- RTC-memory state (survives deep sleep, not cold boot) ----------------
static constexpr uint32_t CLOCK_RTC_MAGIC = 0xC10C4B1D;
RTC_NOINIT_ATTR static uint32_t rtcClockMagic;
RTC_NOINIT_ATTR static time_t rtcEpoch; // last-known unix epoch
RTC_NOINIT_ATTR static uint64_t rtcLpTimeUs; // esp_clk_rtc_time() at capture
static bool clockApproximate = true;
// ---- NVS helpers ----------------------------------------------------------
static constexpr char NVS_NAMESPACE[] = "halclock";
static constexpr char NVS_KEY[] = "epoch";
static void nvsWrite(time_t epoch) {
Preferences prefs;
if (prefs.begin(NVS_NAMESPACE, false)) {
prefs.putLong64(NVS_KEY, (int64_t)epoch);
prefs.end();
}
}
static time_t nvsRead() {
Preferences prefs;
time_t epoch = 0;
if (prefs.begin(NVS_NAMESPACE, true)) {
epoch = (time_t)prefs.getLong64(NVS_KEY, 0);
prefs.end();
}
return epoch;
}
// ---- internal helpers -----------------------------------------------------
static void setSystemClock(time_t epoch) {
struct timeval tv = {};
tv.tv_sec = epoch;
settimeofday(&tv, nullptr);
}
static bool rtcValid() {
return rtcClockMagic == CLOCK_RTC_MAGIC && rtcEpoch > 0;
}
/// Capture current time + LP timer into RTC memory, and epoch into NVS.
static void capture() {
rtcEpoch = time(nullptr);
rtcLpTimeUs = esp_clk_rtc_time();
rtcClockMagic = CLOCK_RTC_MAGIC;
nvsWrite(rtcEpoch);
}
// ---- public API -----------------------------------------------------------
namespace HalClock {
bool syncNtp() {
if (esp_sntp_enabled()) {
esp_sntp_stop();
}
esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
esp_sntp_setservername(0, "pool.ntp.org");
esp_sntp_init();
int retry = 0;
constexpr int maxRetries = 50; // 5 seconds
while (sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED && retry < maxRetries) {
vTaskDelay(100 / portTICK_PERIOD_MS);
retry++;
}
if (retry >= maxRetries) {
LOG_ERR("CLK", "NTP sync timeout");
return false;
}
capture();
clockApproximate = false;
LOG_INF("CLK", "NTP synced, epoch %lld", (long long)rtcEpoch);
return true;
}
void saveBeforeSleep() {
if (!isSynced()) {
return;
}
capture();
LOG_DBG("CLK", "Saved epoch %lld before sleep", (long long)rtcEpoch);
}
void restore() {
if (rtcValid()) {
// RTC memory survived — we woke from deep sleep.
// Use the LP timer to compute how much time elapsed during sleep.
uint64_t lpNow = esp_clk_rtc_time();
time_t estimated = rtcEpoch;
if (lpNow > rtcLpTimeUs) {
estimated += (time_t)((lpNow - rtcLpTimeUs) / 1000000LL);
}
setSystemClock(estimated);
// Re-capture with current LP baseline
rtcEpoch = estimated;
rtcLpTimeUs = lpNow;
clockApproximate = true;
LOG_INF("CLK", "Restored from RTC + LP timer, epoch %lld", (long long)estimated);
return;
}
// Cold boot — try NVS. No elapsed correction possible.
time_t epoch = nvsRead();
if (epoch > 0) {
setSystemClock(epoch);
rtcEpoch = epoch;
rtcLpTimeUs = esp_clk_rtc_time();
rtcClockMagic = CLOCK_RTC_MAGIC;
clockApproximate = true;
LOG_INF("CLK", "Restored from NVS, epoch %lld (no elapsed correction)", (long long)epoch);
}
}
time_t now() {
if (!isSynced()) {
return 0;
}
return time(nullptr);
}
bool isSynced() {
return time(nullptr) > 1577836800; // > 2020-01-01
}
bool isApproximate() {
return clockApproximate;
}
void formatTime(char* buf, size_t bufSize, bool use24h) {
if (!isSynced()) {
snprintf(buf, bufSize, "--:--");
return;
}
time_t t = time(nullptr);
struct tm timeinfo;
localtime_r(&t, &timeinfo);
const char* prefix = isApproximate() ? "~" : "";
if (use24h) {
snprintf(buf, bufSize, "%s%02d:%02d", prefix, timeinfo.tm_hour, timeinfo.tm_min);
} else {
int hour = timeinfo.tm_hour % 12;
if (hour == 0) hour = 12;
const char* ampm = timeinfo.tm_hour < 12 ? "am" : "pm";
snprintf(buf, bufSize, "%s%d:%02d%s", prefix, hour, timeinfo.tm_min, ampm);
}
}
void formatLogTime(char* buf, size_t bufSize) {
if (!isSynced()) {
buf[0] = '\0';
return;
}
time_t t = time(nullptr);
struct tm timeinfo;
localtime_r(&t, &timeinfo);
snprintf(buf, bufSize, "%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
}
} // namespace HalClock
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include <cstdint>
#include <ctime>
/// Lightweight wall-clock facade.
///
/// The ESP32-C3 has no battery-backed RTC, so wall-clock time is lost on every
/// deep-sleep / power cycle. HalClock bridges this gap using three layers:
///
/// - **LP timer** (`esp_clk_rtc_time()`) — keeps running during deep sleep
/// when `keepClockAlive` is enabled (GPIO13 stays HIGH). Used to compute
/// elapsed time and correct the stored epoch on wake.
/// - **RTC memory** (`RTC_NOINIT_ATTR`) — survives deep sleep, lost on cold
/// boot. Stores the epoch + LP timer value captured before sleep.
/// - **NVS** (flash key-value store) — survives power cycles. Fallback when
/// RTC memory is unavailable (cold boot).
///
/// Usage:
/// 1. On boot, call `restore()` to seed the system clock from the best
/// available source (RTC memory + LP correction > NVS).
/// 2. After a successful NTP sync, call `onNtpSynced()`.
/// 3. Before entering deep sleep, call `saveBeforeSleep()`.
///
/// `now()` returns the best-effort epoch (0 if never synced).
namespace HalClock {
/// Perform an NTP sync (requires WiFi to be connected). Starts SNTP,
/// waits up to 5 seconds for completion, then captures the result.
/// Returns true if the sync succeeded.
bool syncNtp();
/// Call just before deep sleep. Snapshots the current system time to RTC
/// memory and NVS so it can be restored on wake / cold boot.
void saveBeforeSleep();
/// Call on boot to seed the system clock from the best available stored
/// value. When RTC memory is valid (deep-sleep wake) and the LP timer was
/// running, the restored time includes elapsed-time correction. Falls back
/// to NVS for cold boot (stale, but better than nothing).
void restore();
/// Returns the current best-effort wall-clock epoch, or 0 if the clock was
/// never set.
time_t now();
/// True if the clock has been set at least once (NTP or restore).
bool isSynced();
/// True if the last restore was from a backup (not NTP) — i.e. the clock
/// may have drifted. Cleared on NTP sync.
bool isApproximate();
/// Format the current time for display. Returns "--:--" if the clock was
/// never synced, prefixes with "~" if approximate.
/// When use24h is false, formats as "2:05pm" / "12:30am".
/// Output is written to `buf` (must be at least 16 bytes).
void formatTime(char* buf, size_t bufSize, bool use24h);
/// Format the current time for log timestamps. Returns "HH:MM:SS" if
/// synced, or an empty string if not.
void formatLogTime(char* buf, size_t bufSize);
} // namespace HalClock
+12 -8
View File
@@ -52,26 +52,30 @@ void HalPowerManager::setPowerSaving(bool enabled) {
// Otherwise, no change needed
}
void HalPowerManager::startDeepSleep(HalGPIO& gpio) const {
void HalPowerManager::startDeepSleep(HalGPIO& gpio, bool keepClockAlive) const {
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
while (gpio.isPressed(HalGPIO::BTN_POWER)) {
delay(50);
gpio.update();
}
// Pre-sleep routines from the original firmware
// GPIO13 is connected to battery latch MOSFET, we need to make sure it's low during sleep
// Note that this means the MCU will be completely powered off during sleep, including RTC
// GPIO13 is connected to the battery latch MOSFET.
// When keepClockAlive is false (default): GPIO13 goes LOW, the MCU is
// completely powered off during sleep (including the LP timer / RTC memory).
// When keepClockAlive is true: GPIO13 stays HIGH, the MCU remains powered
// at ~3-4 mA so the LP timer keeps running and RTC memory is preserved.
// This allows HalClock to accurately compute elapsed sleep time on wake.
constexpr gpio_num_t GPIO_SPIWP = GPIO_NUM_13;
gpio_set_direction(GPIO_SPIWP, GPIO_MODE_OUTPUT);
gpio_set_level(GPIO_SPIWP, 0);
gpio_set_level(GPIO_SPIWP, keepClockAlive ? 1 : 0);
esp_sleep_config_gpio_isolate();
gpio_deep_sleep_hold_en();
gpio_hold_en(GPIO_SPIWP);
pinMode(InputManager::POWER_BUTTON_PIN, INPUT_PULLUP);
// Arm the wakeup trigger *after* the button is released
// Note: this is only useful for waking up on USB power. On battery, the MCU will be completely powered off, so the
// power button is hard-wired to briefly provide power to the MCU, waking it up regardless of the wakeup source
// configuration
// Note: when keepClockAlive is false, this is only useful for waking up on USB power. On battery, the MCU will be
// completely powered off, so the power button is hard-wired to briefly provide power to the MCU, waking it up
// regardless of the wakeup source configuration.
// When keepClockAlive is true, this is the actual wakeup mechanism since the MCU stays powered.
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
// Enter Deep Sleep
esp_deep_sleep_start();
+5 -3
View File
@@ -29,9 +29,11 @@ class HalPowerManager {
// Control CPU frequency for power saving
void setPowerSaving(bool enabled);
// Setup wake up GPIO and enter deep sleep
// Should be called inside main loop() to handle the currentLockMode
void startDeepSleep(HalGPIO& gpio) const;
// Setup wake up GPIO and enter deep sleep.
// When keepClockAlive is true, GPIO13 stays HIGH so the LP timer keeps
// running during sleep (~3-4 mA extra). This allows HalClock to compute
// elapsed sleep time and restore the wall clock accurately on wake.
void startDeepSleep(HalGPIO& gpio, bool keepClockAlive = false) const;
// Get battery percentage (range 0-100)
uint16_t getBatteryPercentage() const;