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}