lilkaoxide/
keira_rtc.rs

1//! Inherit a UTC wall clock left by KeiraOS/ESP-IDF in the ESP32-S3 RTC.
2//!
3//! Requires the opt-in **`keira-rtc`** feature. It enables no allocator, radio,
4//! network stack or timezone database. Keira must synchronize its clock before
5//! launching the application through MultiBoot (a software reset).
6//!
7//! ESP-IDF keeps the Unix time base in RTC STORE2/STORE3. `esp-hal` reads those
8//! registers through [`esp_hal::rtc_cntl::Rtc::current_time_us`]. This API samples that value once
9//! and uses the monotonic HAL timer afterwards, without changing RTC registers.
10//!
11//! A plausible RTC value does not prove NTP synchronization or identify Keira as
12//! its source. Power loss, different ESP-IDF time settings or an incorrect clock
13//! can make inheritance unavailable or inaccurate. Test the handoff on hardware.
14//!
15//! ```no_run
16//! use lilkaoxide::{hal::rtc_cntl::Rtc, keira_rtc::KeiraClock, prelude::*};
17//!
18//! let lilka = Lilka::init(Configuration::default()).unwrap();
19//! let rtc = Rtc::new(lilka.peripherals.LPWR);
20//! match KeiraClock::inherit(&rtc) {
21//!     Ok(clock) => {
22//!         let utc_seconds = clock.now_unix_seconds().unwrap();
23//!         // Convert UTC to the timezone / file timestamp your app needs.
24//!     }
25//!     Err(_) => { /* Show "time unavailable", or offer synchronization. */ }
26//! }
27//! ```
28//!
29//! The example uses blocking initialization; in an async app use
30//! `Lilka::init(...).await`. No changes to Keira or direct calls into its process
31//! are needed. A full power-off requires synchronizing the clock again.
32
33mod time;
34use esp_hal::{rtc_cntl::Rtc, time::Instant};
35pub use time::RtcTimeError;
36use time::TimeAnchor;
37
38/// A UTC clock sampled from RTC, then advanced by the monotonic HAL timer.
39///
40/// Values are checked against 2020-01-01..2108-01-01 (exclusive upper bound).
41/// This rejects an unset clock and obvious garbage; it is not proof of accuracy.
42/// Dropping this handle does not reset RTC. No timezone conversion is performed.
43pub struct KeiraClock {
44    anchor: TimeAnchor,
45}
46
47impl KeiraClock {
48    /// Capture the RTC time after [`crate::Lilka::init`].
49    ///
50    /// Only borrows `rtc` while sampling. The handle need not stay alive later.
51    /// Returns [`RtcTimeError::NotSynchronized`] for an implausible RTC date.
52    pub fn inherit(rtc: &Rtc<'_>) -> Result<Self, RtcTimeError> {
53        let unix_us = rtc.current_time_us();
54        let uptime_us = monotonic_us();
55        Ok(Self {
56            anchor: TimeAnchor::new(unix_us, uptime_us)?,
57        })
58    }
59
60    /// Current UTC Unix timestamp in microseconds, without heap allocations.
61    pub fn now_unix_micros(&self) -> Result<u64, RtcTimeError> {
62        self.anchor.at(monotonic_us())
63    }
64
65    /// Current UTC Unix timestamp in whole seconds (fraction truncated).
66    pub fn now_unix_seconds(&self) -> Result<u64, RtcTimeError> {
67        self.now_unix_micros().map(|us| us / 1_000_000)
68    }
69}
70
71fn monotonic_us() -> u64 {
72    Instant::now().duration_since_epoch().as_micros()
73}