#![allow(clippy::future_not_send, reason = "single-threaded")]
use embassy_executor::Spawner;
use embassy_net::Stack;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::signal::Signal;
use embassy_time::{Duration, Instant};
use portable_atomic::{AtomicBool, AtomicU64, Ordering};
use time::OffsetDateTime;
use crate::clock::{Clock, ClockStatic};
use crate::time_sync::{TimeSync, TimeSyncEvent, TimeSyncStatic};
use crate::{Error, Result};
pub use crate::clock::UnixSeconds;
pub const ONE_SECOND: Duration = Duration::from_secs(1);
pub const ONE_MINUTE: Duration = Duration::from_secs(60);
pub const ONE_DAY: Duration = Duration::from_secs(86_400);
pub fn h12_m_s(dt: &OffsetDateTime) -> (u8, u8, u8) {
let hour_24 = dt.hour() as u8;
let hour_12 = match hour_24 {
0 => 12,
1..=12 => hour_24,
_ => hour_24 - 12,
};
(hour_12, dt.minute() as u8, dt.second() as u8)
}
pub struct ClockSyncTick {
pub local_time: OffsetDateTime,
pub since_last_sync: Duration,
}
#[allow(async_fn_in_trait)]
pub trait ClockSync {
async fn wait_for_tick(&self) -> ClockSyncTick;
fn now_local(&self) -> OffsetDateTime;
fn set_offset_minutes(&self, minutes: i32);
fn offset_minutes(&self) -> i32;
fn set_tick_interval(&self, interval: Option<embassy_time::Duration>);
fn set_speed(&self, speed_multiplier: f32);
fn set_utc_time(&self, unix_seconds: UnixSeconds);
}
type SyncReadySignal = Signal<CriticalSectionRawMutex, ()>;
pub struct ClockSyncStatic {
clock_static: ClockStatic,
time_sync_static: TimeSyncStatic,
initialized: AtomicBool,
sync_ready: SyncReadySignal,
last_sync_ticks: AtomicU64,
synced: AtomicBool,
}
pub struct ClockSyncRuntime {
clock: Clock,
time_sync: TimeSync,
sync_ready: &'static SyncReadySignal,
last_sync_ticks: &'static AtomicU64,
synced: &'static AtomicBool,
}
impl ClockSyncStatic {
#[must_use]
pub(crate) const fn new() -> Self {
Self {
clock_static: Clock::new_static(),
time_sync_static: TimeSync::new_static(),
initialized: AtomicBool::new(false),
sync_ready: Signal::new(),
last_sync_ticks: AtomicU64::new(0),
synced: AtomicBool::new(false),
}
}
}
impl ClockSyncRuntime {
#[must_use]
pub const fn new_static() -> ClockSyncStatic {
ClockSyncStatic::new()
}
pub fn new(
clock_sync_static: &'static ClockSyncStatic,
stack: &'static Stack<'static>,
offset_minutes: i32,
tick_interval: Option<embassy_time::Duration>,
spawner: Spawner,
) -> Result<Self> {
let clock_sync_uninitialized = clock_sync_static
.initialized
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok();
assert!(
clock_sync_uninitialized,
"ClockSyncRuntime::new must be called at most once per ClockSyncStatic"
);
let clock = Clock::new(
&clock_sync_static.clock_static,
offset_minutes,
tick_interval,
spawner,
)?;
let time_sync = TimeSync::new(&clock_sync_static.time_sync_static, stack, spawner)?;
let clock_sync = Self {
clock,
time_sync,
sync_ready: &clock_sync_static.sync_ready,
last_sync_ticks: &clock_sync_static.last_sync_ticks,
synced: &clock_sync_static.synced,
};
spawner.spawn(
clock_sync_loop(
&clock_sync_static.clock_static,
clock_sync.time_sync.events(),
clock_sync.sync_ready,
clock_sync.last_sync_ticks,
clock_sync.synced,
)
.map_err(Error::TaskSpawn)?,
);
Ok(clock_sync)
}
fn since_last_sync(&self) -> Duration {
let last_sync_ticks = self.last_sync_ticks.load(Ordering::Acquire);
if last_sync_ticks == 0 {
return Duration::from_secs(0);
}
let now_ticks = Instant::now().as_ticks();
assert!(now_ticks >= last_sync_ticks);
let elapsed_ticks = now_ticks - last_sync_ticks;
Duration::from_micros(elapsed_ticks)
}
async fn wait_for_first_sync(&self) {
if self.synced.load(Ordering::Acquire) {
return;
}
self.sync_ready.wait().await;
}
fn mark_synced(&self) {
let now_ticks = Instant::now().as_ticks();
self.last_sync_ticks.store(now_ticks, Ordering::Release);
self.synced.store(true, Ordering::Release);
self.sync_ready.signal(());
}
}
impl ClockSync for ClockSyncRuntime {
async fn wait_for_tick(&self) -> ClockSyncTick {
self.wait_for_first_sync().await;
let local_time = self.clock.wait_for_tick().await;
ClockSyncTick {
local_time,
since_last_sync: self.since_last_sync(),
}
}
fn now_local(&self) -> OffsetDateTime {
self.clock.now_local()
}
fn set_offset_minutes(&self, minutes: i32) {
self.clock.set_offset_minutes(minutes);
}
fn offset_minutes(&self) -> i32 {
self.clock.offset_minutes()
}
fn set_tick_interval(&self, interval: Option<embassy_time::Duration>) {
self.clock.set_tick_interval(interval);
}
fn set_speed(&self, speed_multiplier: f32) {
self.clock.set_speed(speed_multiplier);
}
fn set_utc_time(&self, unix_seconds: UnixSeconds) {
self.clock.set_utc_time(unix_seconds);
self.mark_synced();
}
}
#[embassy_executor::task(pool_size = 2)]
async fn clock_sync_loop(
clock_static: &'static ClockStatic,
time_sync_events: &'static crate::time_sync::TimeSyncEvents,
sync_ready: &'static SyncReadySignal,
last_sync_ticks: &'static AtomicU64,
synced: &'static AtomicBool,
) -> ! {
let clock = Clock::from_static(clock_static);
loop {
match time_sync_events.wait().await {
TimeSyncEvent::Ok(unix_seconds) => {
clock.set_utc_time(unix_seconds);
let now_ticks = Instant::now().as_ticks();
last_sync_ticks.store(now_ticks, Ordering::Release);
synced.store(true, Ordering::Release);
sync_ready.signal(());
}
TimeSyncEvent::Err(message) => {
#[cfg(feature = "defmt")]
defmt::info!("ClockSync time sync failed: {}", message);
#[cfg(not(feature = "defmt"))]
let _ = message;
}
}
}
}