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 /// The `tick_interval` parameter uses [`embassy_time::Duration`].
185 pub fn new(
186 clock_sync_static: &'static ClockSyncStatic,
187 stack: &'static Stack<'static>,
188 offset_minutes: i32,
189 tick_interval: Option<embassy_time::Duration>,
190 spawner: Spawner,
191 ) -> Self {
192 let clock = Clock::new(
193 &clock_sync_static.clock_static,
194 offset_minutes,
195 tick_interval,
196 spawner,
197 );
198 let clock = clock_sync_static.clock_cell.init(clock);
199 let time_sync = TimeSync::new(&clock_sync_static.time_sync_static, stack, spawner);
200
201 let clock_sync = Self {
202 clock,
203 time_sync,
204 sync_ready: &clock_sync_static.sync_ready,
205 last_sync_ticks: &clock_sync_static.last_sync_ticks,
206 synced: &clock_sync_static.synced,
207 };
208
209 defmt::unwrap!(spawner.spawn(clock_sync_loop(
210 clock_sync.clock,
211 clock_sync.time_sync,
212 clock_sync.sync_ready,
213 clock_sync.last_sync_ticks,
214 clock_sync.synced,
215 )));
216
217 clock_sync
218 }
219
220 /// Wait for and return the next tick after sync.
221 ///
222 /// See the [ClockSync struct example](Self) for usage.
223 pub async fn wait_for_tick(&self) -> ClockSyncTick {
224 self.wait_for_first_sync().await;
225 let local_time = self.clock.wait_for_tick().await;
226 ClockSyncTick {
227 local_time,
228 since_last_sync: self.since_last_sync(),
229 }
230 }
231
232 /// Get the current local time without waiting for a tick.
233 pub fn now_local(&self) -> OffsetDateTime {
234 self.clock.now_local()
235 }
236
237 /// Update the UTC offset used for local time.
238 pub async fn set_offset_minutes(&self, minutes: i32) {
239 self.clock.set_offset_minutes(minutes).await;
240 }
241
242 /// Get the current UTC offset in minutes.
243 pub fn offset_minutes(&self) -> i32 {
244 self.clock.offset_minutes()
245 }
246
247 /// Set the tick interval. Use `None` to disable periodic ticks.
248 /// This uses [`embassy_time::Duration`] for interval timing.
249 pub async fn set_tick_interval(&self, interval: Option<embassy_time::Duration>) {
250 self.clock.set_tick_interval(interval).await;
251 }
252
253 /// Update the speed multiplier (1.0 = real time).
254 pub async fn set_speed(&self, speed_multiplier: f32) {
255 self.clock.set_speed(speed_multiplier).await;
256 }
257
258 /// Manually set the current UTC time and mark the clock as synced.
259 pub async fn set_utc_time(&self, unix_seconds: UnixSeconds) {
260 self.clock.set_utc_time(unix_seconds).await;
261 self.mark_synced();
262 }
263
264 fn since_last_sync(&self) -> Duration {
265 let last_sync_ticks = self.last_sync_ticks.load(Ordering::Acquire);
266 if last_sync_ticks == 0 {
267 return Duration::from_secs(0);
268 }
269 let now_ticks = Instant::now().as_ticks();
270 assert!(now_ticks >= last_sync_ticks);
271 let elapsed_ticks = now_ticks - last_sync_ticks;
272 Duration::from_micros(elapsed_ticks)
273 }
274
275 async fn wait_for_first_sync(&self) {
276 if self.synced.load(Ordering::Acquire) {
277 return;
278 }
279 self.sync_ready.wait().await;
280 }
281
282 fn mark_synced(&self) {
283 let now_ticks = Instant::now().as_ticks();
284 self.last_sync_ticks.store(now_ticks, Ordering::Release);
285 self.synced.store(true, Ordering::Release);
286 self.sync_ready.signal(());
287 }
288}
289
290#[embassy_executor::task]
291async fn clock_sync_loop(
292 clock: &'static Clock,
293 time_sync: &'static TimeSync,
294 sync_ready: &'static SyncReadySignal,
295 last_sync_ticks: &'static AtomicU64,
296 synced: &'static AtomicBool,
297) -> ! {
298 loop {
299 match time_sync.wait_for_sync().await {
300 TimeSyncEvent::Ok(unix_seconds) => {
301 clock.set_utc_time(unix_seconds).await;
302 let now_ticks = Instant::now().as_ticks();
303 last_sync_ticks.store(now_ticks, Ordering::Release);
304 synced.store(true, Ordering::Release);
305 sync_ready.signal(());
306 }
307 TimeSyncEvent::Err(message) => {
308 defmt::info!("ClockSync time sync failed: {}", message);
309 }
310 }
311 }
312}