Skip to main content

esp_idf_svc/
sntp.rs

1//! SNTP Time Synchronization
2
3use core::cmp::min;
4use core::marker::PhantomData;
5use core::time::Duration;
6
7use ::log::*;
8
9use crate::private::cstr::to_cstring_arg;
10use crate::private::cstr::CString;
11use crate::private::mutex;
12
13#[cfg(feature = "alloc")]
14extern crate alloc;
15
16#[cfg(not(any(
17    esp_idf_version_major = "4",
18    all(esp_idf_version_major = "5", esp_idf_version_minor = "0"),
19)))] // For ESP-IDF v5.1 and later
20mod esp_sntp {
21    use super::OperatingMode;
22    pub use crate::sys::*;
23
24    impl From<esp_sntp_operatingmode_t> for OperatingMode {
25        #[allow(non_upper_case_globals)]
26        fn from(from: esp_sntp_operatingmode_t) -> Self {
27            match from {
28                esp_sntp_operatingmode_t_ESP_SNTP_OPMODE_POLL => OperatingMode::Poll,
29                esp_sntp_operatingmode_t_ESP_SNTP_OPMODE_LISTENONLY => OperatingMode::ListenOnly,
30                _ => unreachable!(),
31            }
32        }
33    }
34
35    impl From<OperatingMode> for esp_sntp_operatingmode_t {
36        #[allow(non_upper_case_globals)]
37        fn from(from: OperatingMode) -> Self {
38            match from {
39                OperatingMode::Poll => esp_sntp_operatingmode_t_ESP_SNTP_OPMODE_POLL,
40                OperatingMode::ListenOnly => esp_sntp_operatingmode_t_ESP_SNTP_OPMODE_LISTENONLY,
41            }
42        }
43    }
44
45    pub use esp_sntp_init as sntp_init;
46    pub use esp_sntp_setoperatingmode as sntp_setoperatingmode;
47    pub use esp_sntp_setservername as sntp_setservername;
48    pub use esp_sntp_stop as sntp_stop;
49}
50
51#[cfg(any(
52    esp_idf_version_major = "4",
53    all(esp_idf_version_major = "5", esp_idf_version_minor = "0"),
54))] // Up to ESP-IDF v5.0.x
55mod esp_sntp {
56    use super::OperatingMode;
57    pub use crate::sys::*;
58
59    impl From<u8_t> for OperatingMode {
60        fn from(from: u8_t) -> Self {
61            match from as u32 {
62                SNTP_OPMODE_POLL => OperatingMode::Poll,
63                SNTP_OPMODE_LISTENONLY => OperatingMode::ListenOnly,
64                _ => unreachable!(),
65            }
66        }
67    }
68
69    impl From<OperatingMode> for u8_t {
70        fn from(from: OperatingMode) -> Self {
71            match from {
72                OperatingMode::Poll => SNTP_OPMODE_POLL as u8_t,
73                OperatingMode::ListenOnly => SNTP_OPMODE_LISTENONLY as u8_t,
74            }
75        }
76    }
77}
78
79use esp_sntp::*;
80
81const SNTP_SERVER_NUM: usize = SNTP_MAX_SERVERS as usize;
82
83const DEFAULT_SERVERS: [&str; 4] = [
84    "0.pool.ntp.org",
85    "1.pool.ntp.org",
86    "2.pool.ntp.org",
87    "3.pool.ntp.org",
88];
89
90#[derive(Copy, Clone, Debug, PartialEq, Eq)]
91#[cfg_attr(feature = "std", derive(Hash))]
92pub enum OperatingMode {
93    Poll,
94    ListenOnly,
95}
96
97#[derive(Copy, Clone, Debug, PartialEq, Eq)]
98#[cfg_attr(feature = "std", derive(Hash))]
99pub enum SyncMode {
100    Smooth,
101    Immediate,
102}
103
104impl From<sntp_sync_mode_t> for SyncMode {
105    #[allow(non_upper_case_globals)]
106    fn from(from: sntp_sync_mode_t) -> Self {
107        match from {
108            sntp_sync_mode_t_SNTP_SYNC_MODE_SMOOTH => SyncMode::Smooth,
109            sntp_sync_mode_t_SNTP_SYNC_MODE_IMMED => SyncMode::Immediate,
110            _ => unreachable!(),
111        }
112    }
113}
114
115impl From<SyncMode> for sntp_sync_mode_t {
116    fn from(from: SyncMode) -> Self {
117        match from {
118            SyncMode::Smooth => sntp_sync_mode_t_SNTP_SYNC_MODE_SMOOTH,
119            SyncMode::Immediate => sntp_sync_mode_t_SNTP_SYNC_MODE_IMMED,
120        }
121    }
122}
123
124#[derive(Copy, Clone, Debug, PartialEq, Eq)]
125#[cfg_attr(feature = "std", derive(Hash))]
126pub enum SyncStatus {
127    Reset,
128    Completed,
129    InProgress,
130}
131
132impl From<sntp_sync_status_t> for SyncStatus {
133    #[allow(non_upper_case_globals)]
134    fn from(from: sntp_sync_status_t) -> Self {
135        match from {
136            sntp_sync_status_t_SNTP_SYNC_STATUS_RESET => SyncStatus::Reset,
137            sntp_sync_status_t_SNTP_SYNC_STATUS_COMPLETED => SyncStatus::Completed,
138            sntp_sync_status_t_SNTP_SYNC_STATUS_IN_PROGRESS => SyncStatus::InProgress,
139            _ => unreachable!(),
140        }
141    }
142}
143
144pub struct SntpConf<'a> {
145    pub servers: [&'a str; SNTP_SERVER_NUM],
146    pub operating_mode: OperatingMode,
147    pub sync_mode: SyncMode,
148}
149
150impl Default for SntpConf<'_> {
151    fn default() -> Self {
152        let mut servers: [&str; SNTP_SERVER_NUM] = Default::default();
153        let copy_len = min(servers.len(), DEFAULT_SERVERS.len());
154
155        servers[..copy_len].copy_from_slice(&DEFAULT_SERVERS[..copy_len]);
156
157        Self {
158            servers,
159            operating_mode: OperatingMode::Poll,
160            sync_mode: SyncMode::Immediate,
161        }
162    }
163}
164
165#[cfg(feature = "alloc")]
166type SyncCallback = alloc::boxed::Box<dyn FnMut(Duration) + Send + 'static>;
167#[cfg(feature = "alloc")]
168static SYNC_CB: mutex::Mutex<Option<SyncCallback>> = mutex::Mutex::new(None);
169static TAKEN: mutex::Mutex<bool> = mutex::Mutex::new(false);
170
171pub struct EspSntp<'a> {
172    // Needs to be kept around because the C bindings only have a pointer.
173    _sntp_servers: [CString; SNTP_SERVER_NUM],
174    _ref: PhantomData<&'a ()>,
175}
176
177impl EspSntp<'static> {
178    pub fn new_default() -> Result<Self, EspError> {
179        Self::new(&Default::default())
180    }
181
182    pub fn new(conf: &SntpConf) -> Result<Self, EspError> {
183        let mut taken = TAKEN.lock();
184
185        if *taken {
186            return Err(EspError::from_infallible::<ESP_ERR_INVALID_STATE>());
187        }
188
189        let sntp = Self::init(conf)?;
190
191        *taken = true;
192        Ok(sntp)
193    }
194
195    #[cfg(feature = "alloc")]
196    pub fn new_with_callback<F>(conf: &SntpConf, callback: F) -> Result<Self, EspError>
197    where
198        F: FnMut(Duration) + Send + 'static,
199    {
200        Self::internal_new_with_callback(conf, callback)
201    }
202}
203
204impl<'a> EspSntp<'a> {
205    /// # Safety
206    ///
207    /// This method - in contrast to method `new_with_callback` - allows the user to set
208    /// a non-static callback/closure into the returned `EspSntp` service. This enables users to borrow
209    /// - in the closure - variables that live on the stack - or more generally - in the same
210    ///   scope where the service is created.
211    ///
212    /// HOWEVER: care should be taken NOT to call `core::mem::forget()` on the service,
213    /// as that would immediately lead to an UB (crash).
214    /// Also note that forgetting the service might happen with `Rc` and `Arc`
215    /// when circular references are introduced: <https://github.com/rust-lang/rust/issues/24456>
216    ///
217    /// The reason is that the closure is actually sent to a hidden ESP IDF thread.
218    /// This means that if the service is forgotten, Rust is free to e.g. unwind the stack
219    /// and the closure now owned by this other thread will end up with references to variables that no longer exist.
220    ///
221    /// The destructor of the service takes care - prior to the service being dropped and e.g.
222    /// the stack being unwind - to remove the closure from the hidden thread and destroy it.
223    /// Unfortunately, when the service is forgotten, the un-subscription does not happen
224    /// and invalid references are left dangling.
225    ///
226    /// This "local borrowing" will only be possible to express in a safe way once/if `!Leak` types
227    /// are introduced to Rust (i.e. the impossibility to "forget" a type and thus not call its destructor).
228    #[cfg(feature = "alloc")]
229    pub unsafe fn new_nonstatic_with_callback<F>(
230        conf: &SntpConf,
231        callback: F,
232    ) -> Result<Self, EspError>
233    where
234        F: FnMut(Duration) + Send + 'a,
235    {
236        Self::internal_new_with_callback(conf, callback)
237    }
238
239    #[cfg(feature = "alloc")]
240    fn internal_new_with_callback<F>(conf: &SntpConf, callback: F) -> Result<Self, EspError>
241    where
242        F: FnMut(Duration) + Send + 'a,
243    {
244        let mut taken = TAKEN.lock();
245
246        if *taken {
247            esp!(ESP_ERR_INVALID_STATE)?;
248        }
249
250        #[allow(clippy::type_complexity)]
251        let callback: alloc::boxed::Box<dyn FnMut(Duration) + Send + 'a> =
252            alloc::boxed::Box::new(callback);
253        #[allow(clippy::type_complexity)]
254        let callback: alloc::boxed::Box<dyn FnMut(Duration) + Send + 'static> =
255            unsafe { core::mem::transmute(callback) };
256
257        *SYNC_CB.lock() = Some(callback);
258        let sntp = Self::init(conf)?;
259
260        *taken = true;
261        Ok(sntp)
262    }
263
264    fn init(conf: &SntpConf) -> Result<Self, EspError> {
265        info!("Initializing");
266
267        unsafe { sntp_setoperatingmode(conf.operating_mode.into()) };
268        unsafe { sntp_set_sync_mode(sntp_sync_mode_t::from(conf.sync_mode)) };
269
270        let mut c_servers: [CString; SNTP_SERVER_NUM] = Default::default();
271        for (i, s) in conf.servers.iter().enumerate() {
272            let c_server = to_cstring_arg(s)?;
273            unsafe { sntp_setservername(i as u8, c_server.as_ptr()) };
274            c_servers[i] = c_server;
275        }
276
277        unsafe {
278            sntp_set_time_sync_notification_cb(Some(Self::sync_cb));
279
280            sntp_init();
281        };
282
283        info!("Initialization complete");
284
285        Ok(Self {
286            _sntp_servers: c_servers,
287            _ref: PhantomData,
288        })
289    }
290
291    #[cfg(feature = "alloc")]
292    fn unsubscribe(&mut self) {
293        *SYNC_CB.lock() = None;
294    }
295
296    pub fn get_sync_status(&self) -> SyncStatus {
297        SyncStatus::from(unsafe { sntp_get_sync_status() })
298    }
299
300    unsafe extern "C" fn sync_cb(tv: *mut timeval) {
301        debug!(
302            " Sync cb called: sec: {}, usec: {}",
303            (*tv).tv_sec,
304            (*tv).tv_usec,
305        );
306
307        #[cfg(feature = "alloc")]
308        if let Some(cb) = &mut *SYNC_CB.lock() {
309            let duration = Duration::from_secs((*tv).tv_sec as u64)
310                + Duration::from_micros((*tv).tv_usec as u64);
311
312            cb(duration);
313        }
314    }
315}
316
317impl Drop for EspSntp<'_> {
318    fn drop(&mut self) {
319        {
320            let mut taken = TAKEN.lock();
321
322            unsafe { sntp_stop() };
323
324            #[cfg(feature = "alloc")]
325            self.unsubscribe();
326
327            *taken = false;
328        }
329
330        info!("Dropped");
331    }
332}