1#![allow(clippy::future_not_send, reason = "single-threaded")]
13
14#[cfg(feature = "wifi")]
15use embassy_executor::Spawner;
16#[cfg(feature = "wifi")]
17use embassy_net::Stack;
18#[cfg(feature = "wifi")]
19use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
20#[cfg(feature = "wifi")]
21use embassy_sync::signal::Signal;
22use embassy_time::Duration;
23#[cfg(feature = "wifi")]
24use embassy_time::Instant;
25#[cfg(feature = "wifi")]
26use portable_atomic::{AtomicBool, AtomicU64, Ordering};
27use time::OffsetDateTime;
28
29#[cfg(feature = "wifi")]
30use crate::clock::{Clock, ClockStatic};
31#[cfg(feature = "wifi")]
32use crate::time_sync::{TimeSync, TimeSyncEvent, TimeSyncStatic};
33#[cfg(feature = "wifi")]
34use crate::{Error, Result};
35
36#[repr(transparent)]
42#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
43#[cfg_attr(feature = "defmt", derive(defmt::Format))]
44pub struct UnixSeconds(pub i64);
45
46impl UnixSeconds {
47 #[must_use]
49 pub const fn as_i64(self) -> i64 {
50 self.0
51 }
52
53 #[must_use]
55 pub const fn from_ntp_seconds(ntp: u32) -> Option<Self> {
56 let seconds = (ntp as i64) - 2_208_988_800;
57 if seconds >= 0 {
58 Some(Self(seconds))
59 } else {
60 None
61 }
62 }
63
64 #[must_use]
66 pub fn to_offset_datetime(self, offset: time::UtcOffset) -> Option<OffsetDateTime> {
67 OffsetDateTime::from_unix_timestamp(self.0)
68 .ok()
69 .map(|datetime| datetime.to_offset(offset))
70 }
71}
72
73pub const ONE_SECOND: Duration = Duration::from_secs(1);
79pub const ONE_MINUTE: Duration = Duration::from_secs(60);
81pub const ONE_DAY: Duration = Duration::from_secs(86_400);
83
84pub fn h12_m_s(dt: &OffsetDateTime) -> (u8, u8, u8) {
91 let hour_24 = dt.hour();
92 let hour_12 = match hour_24 {
93 0 => 12,
94 1..=12 => hour_24,
95 _ => hour_24 - 12,
96 };
97 (hour_12, dt.minute(), dt.second())
98}
99
100pub struct ClockSyncTick {
108 pub local_time: OffsetDateTime,
110 pub since_last_sync: Duration,
112}
113
114#[allow(async_fn_in_trait)]
150pub trait ClockSync {
151 async fn wait_for_tick(&self) -> ClockSyncTick;
155
156 fn now_local(&self) -> OffsetDateTime;
158
159 fn set_offset_minutes(&self, minutes: i32);
161
162 fn offset_minutes(&self) -> i32;
164
165 fn set_tick_interval(&self, interval: Option<embassy_time::Duration>);
169
170 fn set_speed(&self, speed_multiplier: f32);
172
173 fn set_utc_time(&self, unix_seconds: UnixSeconds);
175}
176
177#[cfg(feature = "wifi")]
178type SyncReadySignal = Signal<CriticalSectionRawMutex, ()>;
179
180#[cfg(feature = "wifi")]
181pub struct ClockSyncStatic {
182 clock_static: ClockStatic,
183 time_sync_static: TimeSyncStatic,
184 initialized: AtomicBool,
185 sync_ready: SyncReadySignal,
186 last_sync_ticks: AtomicU64,
187 synced: AtomicBool,
188}
189
190#[cfg(feature = "wifi")]
191pub struct ClockSyncRuntime {
192 clock: Clock,
193 time_sync: TimeSync,
194 sync_ready: &'static SyncReadySignal,
195 last_sync_ticks: &'static AtomicU64,
196 synced: &'static AtomicBool,
197}
198
199#[cfg(feature = "wifi")]
200impl ClockSyncStatic {
201 #[must_use]
203 pub(crate) const fn new() -> Self {
204 Self {
205 clock_static: Clock::new_static(),
206 time_sync_static: TimeSync::new_static(),
207 initialized: AtomicBool::new(false),
208 sync_ready: Signal::new(),
209 last_sync_ticks: AtomicU64::new(0),
210 synced: AtomicBool::new(false),
211 }
212 }
213}
214
215#[cfg(feature = "wifi")]
216impl ClockSyncRuntime {
217 #[must_use]
219 pub const fn new_static() -> ClockSyncStatic {
220 ClockSyncStatic::new()
221 }
222
223 pub fn new(
229 clock_sync_static: &'static ClockSyncStatic,
230 stack: &'static Stack<'static>,
231 offset_minutes: i32,
232 tick_interval: Option<embassy_time::Duration>,
233 spawner: Spawner,
234 ) -> Result<Self> {
235 let clock_sync_uninitialized = clock_sync_static
236 .initialized
237 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
238 .is_ok();
239 assert!(
240 clock_sync_uninitialized,
241 "ClockSyncRuntime::new must be called at most once per ClockSyncStatic"
242 );
243
244 let clock = Clock::new(
245 &clock_sync_static.clock_static,
246 offset_minutes,
247 tick_interval,
248 spawner,
249 )?;
250 let time_sync = TimeSync::new(&clock_sync_static.time_sync_static, stack, spawner)?;
251
252 let clock_sync = Self {
253 clock,
254 time_sync,
255 sync_ready: &clock_sync_static.sync_ready,
256 last_sync_ticks: &clock_sync_static.last_sync_ticks,
257 synced: &clock_sync_static.synced,
258 };
259
260 spawner.spawn(
261 clock_sync_loop(
262 &clock_sync_static.clock_static,
263 clock_sync.time_sync.events(),
264 clock_sync.sync_ready,
265 clock_sync.last_sync_ticks,
266 clock_sync.synced,
267 )
268 .map_err(Error::TaskSpawn)?,
269 );
270
271 Ok(clock_sync)
272 }
273
274 fn since_last_sync(&self) -> Duration {
275 let last_sync_ticks = self.last_sync_ticks.load(Ordering::Acquire);
276 if last_sync_ticks == 0 {
277 return Duration::from_secs(0);
278 }
279 let now_ticks = Instant::now().as_ticks();
280 assert!(now_ticks >= last_sync_ticks);
281 let elapsed_ticks = now_ticks - last_sync_ticks;
282 Duration::from_micros(elapsed_ticks)
283 }
284
285 async fn wait_for_first_sync(&self) {
286 if self.synced.load(Ordering::Acquire) {
287 return;
288 }
289 self.sync_ready.wait().await;
290 }
291
292 fn mark_synced(&self) {
293 let now_ticks = Instant::now().as_ticks();
294 self.last_sync_ticks.store(now_ticks, Ordering::Release);
295 self.synced.store(true, Ordering::Release);
296 self.sync_ready.signal(());
297 }
298}
299
300#[cfg(feature = "wifi")]
301impl ClockSync for ClockSyncRuntime {
302 async fn wait_for_tick(&self) -> ClockSyncTick {
303 self.wait_for_first_sync().await;
304 let local_time = self.clock.wait_for_tick().await;
305 ClockSyncTick {
306 local_time,
307 since_last_sync: self.since_last_sync(),
308 }
309 }
310
311 fn now_local(&self) -> OffsetDateTime {
312 self.clock.now_local()
313 }
314
315 fn set_offset_minutes(&self, minutes: i32) {
316 self.clock.set_offset_minutes(minutes);
317 }
318
319 fn offset_minutes(&self) -> i32 {
320 self.clock.offset_minutes()
321 }
322
323 fn set_tick_interval(&self, interval: Option<embassy_time::Duration>) {
324 self.clock.set_tick_interval(interval);
325 }
326
327 fn set_speed(&self, speed_multiplier: f32) {
328 self.clock.set_speed(speed_multiplier);
329 }
330
331 fn set_utc_time(&self, unix_seconds: UnixSeconds) {
332 self.clock.set_utc_time(unix_seconds);
333 self.mark_synced();
334 }
335}
336
337#[embassy_executor::task(pool_size = 2)]
342#[cfg(feature = "wifi")]
343async fn clock_sync_loop(
344 clock_static: &'static ClockStatic,
345 time_sync_events: &'static crate::time_sync::TimeSyncEvents,
346 sync_ready: &'static SyncReadySignal,
347 last_sync_ticks: &'static AtomicU64,
348 synced: &'static AtomicBool,
349) -> ! {
350 let clock = Clock::from_static(clock_static);
351 loop {
352 match time_sync_events.wait().await {
353 TimeSyncEvent::Ok(unix_seconds) => {
354 clock.set_utc_time(unix_seconds);
355 let now_ticks = Instant::now().as_ticks();
356 last_sync_ticks.store(now_ticks, Ordering::Release);
357 synced.store(true, Ordering::Release);
358 sync_ready.signal(());
359 }
360 TimeSyncEvent::Err(message) => {
361 #[cfg(feature = "defmt")]
362 defmt::info!("ClockSync time sync failed: {}", message);
363 #[cfg(not(feature = "defmt"))]
364 let _ = message;
365 }
366 }
367 }
368}