device_envoy/
time_sync.rs1#![allow(clippy::future_not_send, reason = "single-threaded")]
8#![allow(dead_code, unused_imports)]
9
10use time::{OffsetDateTime, UtcOffset};
11
12#[repr(transparent)]
14#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, defmt::Format)]
15pub struct UnixSeconds(pub i64);
16
17impl UnixSeconds {
18 #[must_use]
20 pub const fn as_i64(self) -> i64 {
21 self.0
22 }
23
24 #[must_use]
26 pub const fn from_ntp_seconds(ntp: u32) -> Option<Self> {
27 const NTP_TO_UNIX_SECONDS: i64 = 2_208_988_800;
28 let seconds = (ntp as i64) - NTP_TO_UNIX_SECONDS;
29 if seconds >= 0 {
30 Some(Self(seconds))
31 } else {
32 None
33 }
34 }
35
36 #[must_use]
38 pub fn to_offset_datetime(self, offset: UtcOffset) -> Option<OffsetDateTime> {
39 OffsetDateTime::from_unix_timestamp(self.as_i64())
40 .ok()
41 .map(|datetime| datetime.to_offset(offset))
42 }
43}
44
45#[cfg(feature = "wifi")]
46mod wifi_impl {
47 use core::convert::Infallible;
48 use defmt::*;
49 use core::panic;
51 use embassy_executor::Spawner;
52 use embassy_net::{Stack, dns, udp};
53 use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
54 use embassy_sync::signal::Signal;
55 use embassy_time::{Duration, Timer};
56 use static_cell::StaticCell;
57
58 use crate::time_sync::UnixSeconds;
59 use crate::{Error, Result};
60
61 #[derive(Debug, defmt::Format)]
67 pub enum TimeSyncEvent {
68 Ok(UnixSeconds),
70 Err(&'static str),
72 }
73
74 type TimeSyncEvents = Signal<CriticalSectionRawMutex, TimeSyncEvent>;
76
77 pub struct TimeSyncStatic {
79 events: TimeSyncEvents,
80 time_sync_cell: StaticCell<TimeSync>,
81 }
82
83 pub struct TimeSync {
98 events: &'static TimeSyncEvents,
99 }
100
101 impl TimeSync {
102 #[must_use]
104 pub const fn new_static() -> TimeSyncStatic {
105 TimeSyncStatic {
106 events: Signal::new(),
107 time_sync_cell: StaticCell::new(),
108 }
109 }
110
111 pub fn new(
116 time_sync_static: &'static TimeSyncStatic,
117 stack: &'static Stack<'static>,
118 spawner: Spawner,
119 ) -> &'static Self {
120 unwrap!(spawner.spawn(time_sync_stack_loop(stack, &time_sync_static.events,)));
121
122 time_sync_static.time_sync_cell.init(Self {
123 events: &time_sync_static.events,
124 })
125 }
126
127 pub async fn wait_for_sync(&self) -> TimeSyncEvent {
129 self.events.wait().await
130 }
131 }
132
133 #[embassy_executor::task]
134 async fn time_sync_stack_loop(
135 stack: &'static Stack<'static>,
136 sync_events: &'static TimeSyncEvents,
137 ) -> ! {
138 let err = run_time_sync_loop(stack, sync_events).await.unwrap_err();
139 panic!("{err}");
140 }
141
142 async fn run_time_sync_loop(
143 stack: &'static Stack<'static>,
144 sync_events: &'static TimeSyncEvents,
145 ) -> Result<Infallible> {
146 info!("TimeSync received network stack");
147 info!("TimeSync device started");
148
149 let mut attempt = 0;
151 loop {
152 attempt += 1;
153 info!("Sync attempt {}", attempt);
154 match fetch_ntp_time(stack).await {
155 Ok(unix_seconds) => {
156 info!(
157 "Initial sync successful: unix_seconds={}",
158 unix_seconds.as_i64()
159 );
160
161 sync_events.signal(TimeSyncEvent::Ok(unix_seconds));
162 break;
163 }
164 Err(e) => {
165 if let Error::Ntp(msg) = e {
166 info!("Sync failed: {}", msg);
167 sync_events.signal(TimeSyncEvent::Err(msg));
168 }
169 let delay_secs = if attempt == 1 {
171 10
172 } else if attempt == 2 {
173 30
174 } else if attempt == 3 {
175 60
176 } else {
177 300 };
179 info!("Sync failed, retrying in {}s...", delay_secs);
180 Timer::after_secs(delay_secs).await;
181 }
182 }
183 }
184
185 let mut last_success_elapsed = 0_u64;
187 loop {
188 let wait_secs = if last_success_elapsed == 0 { 3600 } else { 300 };
190 Timer::after_secs(wait_secs).await;
191 last_success_elapsed = last_success_elapsed.saturating_add(wait_secs);
192
193 info!(
194 "Periodic sync ({}s since last success)...",
195 last_success_elapsed
196 );
197 match fetch_ntp_time(stack).await {
198 Ok(unix_seconds) => {
199 info!(
200 "Periodic sync successful: unix_seconds={}",
201 unix_seconds.as_i64()
202 );
203
204 sync_events.signal(TimeSyncEvent::Ok(unix_seconds));
205 last_success_elapsed = 0; }
207 Err(e) => {
208 if let Error::Ntp(msg) = e {
209 info!("Periodic sync failed: {}", msg);
210 sync_events.signal(TimeSyncEvent::Err(msg));
211 }
212 info!("Sync failed, will retry in 5 minutes");
213 }
214 }
215 }
216 }
217
218 async fn fetch_ntp_time(stack: &Stack<'static>) -> Result<UnixSeconds> {
223 use dns::DnsQueryType;
224 use udp::UdpSocket;
225
226 const NTP_SERVER: &str = "pool.ntp.org";
228 const NTP_PORT: u16 = 123;
229
230 info!(
232 "Resolving Network Time Protocol (NTP) host {}...",
233 NTP_SERVER
234 );
235 let dns_result = stack
236 .dns_query(NTP_SERVER, DnsQueryType::A)
237 .await
238 .map_err(|e| {
239 warn!("DNS lookup failed: {:?}", e);
240 Error::Ntp("DNS lookup failed")
241 })?;
242 let server_addr = dns_result.first().ok_or(Error::Ntp("No DNS results"))?;
243
244 info!("Network Time Protocol (NTP) server IP: {}", server_addr);
245
246 let mut rx_meta = [udp::PacketMetadata::EMPTY; 1];
248 let mut rx_buffer = [0; 128];
249 let mut tx_meta = [udp::PacketMetadata::EMPTY; 1];
250 let mut tx_buffer = [0; 128];
251 let mut socket = UdpSocket::new(
252 *stack,
253 &mut rx_meta,
254 &mut rx_buffer,
255 &mut tx_meta,
256 &mut tx_buffer,
257 );
258
259 socket.bind(0).map_err(|e| {
260 warn!("Socket bind failed: {:?}", e);
261 Error::Ntp("Socket bind failed")
262 })?;
263
264 let mut ntp_request = [0u8; 48];
266 ntp_request[0] = 0x1B; info!(
270 "Sending Network Time Protocol (NTP) request to {}...",
271 server_addr
272 );
273 socket
274 .send_to(&ntp_request, (*server_addr, NTP_PORT))
275 .await
276 .map_err(|e| {
277 warn!("Network Time Protocol (NTP) send failed: {:?}", e);
278 Error::Ntp("Network Time Protocol (NTP) send failed")
279 })?;
280
281 let mut response = [0u8; 48];
283 let (n, _from) =
284 embassy_time::with_timeout(Duration::from_secs(5), socket.recv_from(&mut response))
285 .await
286 .map_err(|_| {
287 warn!("Network Time Protocol (NTP) receive timeout");
288 Error::Ntp("Network Time Protocol (NTP) receive timeout")
289 })?
290 .map_err(|e| {
291 warn!("Network Time Protocol (NTP) receive failed: {:?}", e);
292 Error::Ntp("Network Time Protocol (NTP) receive failed")
293 })?;
294
295 if n < 48 {
296 warn!(
297 "Network Time Protocol (NTP) response too short: {} bytes",
298 n
299 );
300 return Err(Error::Ntp("Network Time Protocol (NTP) response too short"));
301 }
302
303 let ntp_seconds =
305 u32::from_be_bytes([response[40], response[41], response[42], response[43]]);
306
307 let unix_time = UnixSeconds::from_ntp_seconds(ntp_seconds)
309 .ok_or(Error::Ntp("Invalid Network Time Protocol (NTP) timestamp"))?;
310
311 info!(
312 "Network Time Protocol (NTP) time: {} (unix timestamp)",
313 unix_time.as_i64()
314 );
315 Ok(unix_time)
316 }
317} #[cfg(feature = "wifi")]
321pub use wifi_impl::{TimeSync, TimeSyncEvent, TimeSyncStatic};
322
323#[cfg(not(feature = "wifi"))]
328mod stub {
329 use crate::time_sync::UnixSeconds;
330 use embassy_executor::Spawner;
331 use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
332 use embassy_sync::signal::Signal;
333 use static_cell::StaticCell;
334
335 #[derive(Debug, defmt::Format)]
338 pub enum TimeSyncEvent {
339 Ok(UnixSeconds),
341 Err(&'static str),
343 }
344
345 type TimeSyncEvents = Signal<CriticalSectionRawMutex, TimeSyncEvent>;
347
348 pub struct TimeSyncStatic {
350 events: TimeSyncEvents,
351 time_sync_cell: StaticCell<TimeSync>,
352 }
353
354 pub struct TimeSync {
356 events: &'static TimeSyncEvents,
357 }
358
359 impl TimeSync {
360 #[must_use]
362 pub const fn new_static() -> TimeSyncStatic {
363 TimeSyncStatic {
364 events: Signal::new(),
365 time_sync_cell: StaticCell::new(),
366 }
367 }
368
369 pub fn new(time_sync_static: &'static TimeSyncStatic, _spawner: Spawner) -> &'static Self {
371 time_sync_static.time_sync_cell.init(Self {
372 events: &time_sync_static.events,
373 })
374 }
375
376 pub async fn wait_for_sync(&self) -> TimeSyncEvent {
378 self.events.wait().await
379 }
380 }
381}
382
383#[cfg(not(feature = "wifi"))]
384pub use stub::{TimeSync, TimeSyncEvent, TimeSyncStatic};