Skip to main content

device_envoy/
clock_sync.rs

1//! A device abstraction that combines time sync with a local clock.
2//! See [`ClockSync`] for the full usage example.
3
4#![cfg(feature = "wifi")]
5#![allow(clippy::future_not_send, reason = "single-threaded")]
6
7use embassy_executor::Spawner;
8use embassy_net::Stack;
9use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
10use embassy_sync::signal::Signal;
11use embassy_time::{Duration, Instant};
12use portable_atomic::{AtomicBool, AtomicU64, Ordering};
13use time::OffsetDateTime;
14
15use crate::clock::{Clock, ClockStatic};
16pub use crate::time_sync::UnixSeconds;
17use crate::time_sync::{TimeSync, TimeSyncEvent, TimeSyncStatic};
18
19/// Duration representing one second.
20pub const ONE_SECOND: Duration = Duration::from_secs(1);
21/// Duration representing one minute (60 seconds).
22pub const ONE_MINUTE: Duration = Duration::from_secs(60);
23/// Duration representing one day (24 hours).
24pub const ONE_DAY: Duration = Duration::from_secs(86_400);
25
26/// Extract hour (12-hour format), minute, second from `OffsetDateTime`.
27pub fn h12_m_s(dt: &OffsetDateTime) -> (u8, u8, u8) {
28    let hour_24 = dt.hour() as u8;
29    let hour_12 = match hour_24 {
30        0 => 12,
31        1..=12 => hour_24,
32        _ => hour_24 - 12,
33    };
34    let minute = dt.minute() as u8;
35    let second = dt.second() as u8;
36    (hour_12, minute, second)
37}
38
39/// Tick event emitted by [`ClockSync`].
40///
41/// See the [ClockSync struct example](ClockSync) for usage.
42pub struct ClockSyncTick {
43    /// The current local time (adjusted by timezone offset if set).
44    pub local_time: OffsetDateTime,
45    /// Duration since the last successful NTP synchronization.
46    pub since_last_sync: Duration,
47}
48
49type SyncReadySignal = Signal<CriticalSectionRawMutex, ()>;
50
51/// Resources needed to construct [`ClockSync`].
52pub struct ClockSyncStatic {
53    clock_static: ClockStatic,
54    clock_cell: static_cell::StaticCell<Clock>,
55    time_sync_static: TimeSyncStatic,
56    sync_ready: SyncReadySignal,
57    last_sync_ticks: AtomicU64,
58    synced: AtomicBool,
59}
60
61/// Combines NTP synchronization with a local clock and tick events.
62///
63/// `ClockSync` does not emit ticks until the first successful sync (or a manual
64/// call to [`ClockSync::set_utc_time`]). Each tick includes how long it has been
65/// since the last successful sync.
66///
67/// # Example: WiFi + ClockSync logging
68///
69/// ```rust,no_run
70/// # #![no_std]
71/// # #![no_main]
72/// # use defmt_rtt as _;
73/// # use panic_probe as _;
74/// use device_envoy::{
75///     Error,
76///     Result,
77///     button::PressedTo,
78///     clock_sync::{ClockSync, ClockSyncStatic, ONE_SECOND, h12_m_s},
79///     flash_array::FlashArray,
80///     wifi_auto::fields::{TimezoneField, TimezoneFieldStatic},
81///     wifi_auto::{WifiAuto, WifiAutoEvent},
82/// };
83/// use defmt::info;
84///
85/// async fn run(
86///     spawner: embassy_executor::Spawner,
87///     p: embassy_rp::Peripherals,
88/// ) -> Result<(), device_envoy::Error> {
89///     let [wifi_credentials_flash_block, timezone_flash_block] = FlashArray::<2>::new(p.FLASH)?;
90///
91///     static TIMEZONE_STATIC: TimezoneFieldStatic = TimezoneField::new_static();
92///     let timezone_field = TimezoneField::new(&TIMEZONE_STATIC, timezone_flash_block);
93///
94///     let wifi_auto = WifiAuto::new(
95///         p.PIN_23,
96///         p.PIN_24,
97///         p.PIN_25,
98///         p.PIN_29,
99///         p.PIO0,
100///         p.DMA_CH0,
101///         wifi_credentials_flash_block,
102///         p.PIN_13,
103///         PressedTo::Ground,
104///         "ClockSync",
105///         [timezone_field],
106///         spawner,
107///     )?;
108///
109///     let (stack, _button) = wifi_auto
110///         .connect(|event| async move {
111///             match event {
112///                 WifiAutoEvent::CaptivePortalReady => {
113///                     info!("WifiAuto: setup mode ready");
114///                 }
115///                 WifiAutoEvent::Connecting { .. } => {
116///                     info!("WifiAuto: connecting");
117///                 }
118///                 WifiAutoEvent::ConnectionFailed => {
119///                     info!("WifiAuto: connection failed");
120///                 }
121///             }
122///             Ok(())
123///         })
124///         .await?;
125///
126///     let offset_minutes = timezone_field
127///         .offset_minutes()?
128///         .ok_or(Error::MissingCustomWifiAutoField)?;
129///     static CLOCK_SYNC_STATIC: ClockSyncStatic = ClockSync::new_static();
130///     let clock_sync = ClockSync::new(
131///         &CLOCK_SYNC_STATIC,
132///         stack,
133///         offset_minutes,
134///         Some(ONE_SECOND),
135///         spawner,
136///     );
137///
138///     loop {
139///         let tick = clock_sync.wait_for_tick().await;
140///         let (hours, minutes, seconds) = h12_m_s(&tick.local_time);
141///         info!(
142///             "Time {:02}:{:02}:{:02}, since sync {}s",
143///             hours,
144///             minutes,
145///             seconds,
146///             tick.since_last_sync.as_secs()
147///         );
148///     }
149/// }
150/// ```
151pub struct ClockSync {
152    clock: &'static Clock,
153    time_sync: &'static TimeSync,
154    sync_ready: &'static SyncReadySignal,
155    last_sync_ticks: &'static AtomicU64,
156    synced: &'static AtomicBool,
157}
158
159impl ClockSyncStatic {
160    /// Creates static resources for the ClockSync device.
161    #[must_use]
162    pub(crate) const fn new() -> Self {
163        Self {
164            clock_static: Clock::new_static(),
165            clock_cell: static_cell::StaticCell::new(),
166            time_sync_static: TimeSync::new_static(),
167            sync_ready: Signal::new(),
168            last_sync_ticks: AtomicU64::new(0),
169            synced: AtomicBool::new(false),
170        }
171    }
172}
173
174impl ClockSync {
175    /// Create [`ClockSync`] resources.
176    #[must_use]
177    pub const fn new_static() -> ClockSyncStatic {
178        ClockSyncStatic::new()
179    }
180
181    /// Create a [`ClockSync`] using an existing network stack.
182    ///
183    /// See the [ClockSync struct example](Self) for usage.
184    pub fn new(
185        clock_sync_static: &'static ClockSyncStatic,
186        stack: &'static Stack<'static>,
187        offset_minutes: i32,
188        tick_interval: Option<Duration>,
189        spawner: Spawner,
190    ) -> Self {
191        let clock = Clock::new(
192            &clock_sync_static.clock_static,
193            offset_minutes,
194            tick_interval,
195            spawner,
196        );
197        let clock = clock_sync_static.clock_cell.init(clock);
198        let time_sync = TimeSync::new(&clock_sync_static.time_sync_static, stack, spawner);
199
200        let clock_sync = Self {
201            clock,
202            time_sync,
203            sync_ready: &clock_sync_static.sync_ready,
204            last_sync_ticks: &clock_sync_static.last_sync_ticks,
205            synced: &clock_sync_static.synced,
206        };
207
208        defmt::unwrap!(spawner.spawn(clock_sync_loop(
209            clock_sync.clock,
210            clock_sync.time_sync,
211            clock_sync.sync_ready,
212            clock_sync.last_sync_ticks,
213            clock_sync.synced,
214        )));
215
216        clock_sync
217    }
218
219    /// Wait for and return the next tick after sync.
220    ///
221    /// See the [ClockSync struct example](Self) for usage.
222    pub async fn wait_for_tick(&self) -> ClockSyncTick {
223        self.wait_for_first_sync().await;
224        let local_time = self.clock.wait_for_tick().await;
225        ClockSyncTick {
226            local_time,
227            since_last_sync: self.since_last_sync(),
228        }
229    }
230
231    /// Get the current local time without waiting for a tick.
232    pub fn now_local(&self) -> OffsetDateTime {
233        self.clock.now_local()
234    }
235
236    /// Update the UTC offset used for local time.
237    pub async fn set_offset_minutes(&self, minutes: i32) {
238        self.clock.set_offset_minutes(minutes).await;
239    }
240
241    /// Get the current UTC offset in minutes.
242    pub fn offset_minutes(&self) -> i32 {
243        self.clock.offset_minutes()
244    }
245
246    /// Set the tick interval. Use `None` to disable periodic ticks.
247    pub async fn set_tick_interval(&self, interval: Option<Duration>) {
248        self.clock.set_tick_interval(interval).await;
249    }
250
251    /// Update the speed multiplier (1.0 = real time).
252    pub async fn set_speed(&self, speed_multiplier: f32) {
253        self.clock.set_speed(speed_multiplier).await;
254    }
255
256    /// Manually set the current UTC time and mark the clock as synced.
257    pub async fn set_utc_time(&self, unix_seconds: UnixSeconds) {
258        self.clock.set_utc_time(unix_seconds).await;
259        self.mark_synced();
260    }
261
262    fn since_last_sync(&self) -> Duration {
263        let last_sync_ticks = self.last_sync_ticks.load(Ordering::Acquire);
264        if last_sync_ticks == 0 {
265            return Duration::from_secs(0);
266        }
267        let now_ticks = Instant::now().as_ticks();
268        assert!(now_ticks >= last_sync_ticks);
269        let elapsed_ticks = now_ticks - last_sync_ticks;
270        Duration::from_micros(elapsed_ticks)
271    }
272
273    async fn wait_for_first_sync(&self) {
274        if self.synced.load(Ordering::Acquire) {
275            return;
276        }
277        self.sync_ready.wait().await;
278    }
279
280    fn mark_synced(&self) {
281        let now_ticks = Instant::now().as_ticks();
282        self.last_sync_ticks.store(now_ticks, Ordering::Release);
283        self.synced.store(true, Ordering::Release);
284        self.sync_ready.signal(());
285    }
286}
287
288#[embassy_executor::task]
289async fn clock_sync_loop(
290    clock: &'static Clock,
291    time_sync: &'static TimeSync,
292    sync_ready: &'static SyncReadySignal,
293    last_sync_ticks: &'static AtomicU64,
294    synced: &'static AtomicBool,
295) -> ! {
296    loop {
297        match time_sync.wait_for_sync().await {
298            TimeSyncEvent::Ok(unix_seconds) => {
299                clock.set_utc_time(unix_seconds).await;
300                let now_ticks = Instant::now().as_ticks();
301                last_sync_ticks.store(now_ticks, Ordering::Release);
302                synced.store(true, Ordering::Release);
303                sync_ready.signal(());
304            }
305            TimeSyncEvent::Err(message) => {
306                defmt::info!("ClockSync time sync failed: {}", message);
307            }
308        }
309    }
310}