w5500_sntp/
lib.rs

1//! SNTP client for the [Wiznet W5500] SPI internet offload chip.
2//!
3//! # Limitations
4//!
5//! * No support for message digests
6//!
7//! # Feature Flags
8//!
9//! All features are disabled by default.
10//!
11//! * `eh0`: Passthrough to [`w5500-hl`].
12//! * `eh1`: Passthrough to [`w5500-hl`].
13//! * `defmt`: Enable logging with `defmt`. Also a passthrough to [`w5500-hl`].
14//! * `log`: Enable logging with `log`.
15//! * `chrono`: Enable conversion to `chrono::naive::NaiveDateTime`.
16//! * `time`: Enable conversion to `time::PrimitiveDateTime`.
17//! * `num-rational`: Enable conversion to `num_rational::Ratio`.
18//!
19//! # Reference Documentation
20//!
21//! * [RFC 4330](https://www.rfc-editor.org/rfc/rfc4330.html)
22//!
23//! [`w5500-hl`]: https://crates.io/crates/w5500-hl
24//! [Wiznet W5500]: https://www.wiznet.io/product-item/w5500/
25#![cfg_attr(docsrs, feature(doc_cfg), feature(doc_auto_cfg))]
26#![cfg_attr(not(test), no_std)]
27#![deny(unsafe_code)]
28#![warn(missing_docs)]
29
30// This mod MUST go first, so that the others see its macros.
31pub(crate) mod fmt;
32
33mod fixed_point;
34mod timestamp;
35
36pub use fixed_point::FixedPoint;
37pub use timestamp::{Timestamp, TimestampError};
38pub use w5500_hl as hl;
39pub use w5500_hl::ll;
40
41use hl::{
42    io::{Read, Write},
43    Common, Error, Udp, UdpReader, UdpWriter,
44};
45use ll::{net::SocketAddrV4, Registers, Sn, SocketInterrupt, SocketInterruptMask};
46
47#[cfg(feature = "chrono")]
48pub use chrono;
49
50#[cfg(feature = "time")]
51pub use time;
52
53/// IANA SNTP destination port.
54pub const DST_PORT: u16 = 123;
55
56// 3-bit version number indicating the current protocol version
57const VERSION_NUMBER: u8 = 4;
58
59/// W5500 SNTP client.
60#[derive(Debug)]
61#[cfg_attr(feature = "defmt", derive(defmt::Format))]
62pub struct Client {
63    sn: Sn,
64    port: u16,
65    /// SNTP server address.
66    ///
67    /// Changes made to the server address will only apply after calling
68    /// [`Client::setup_socket`].
69    pub server: SocketAddrV4,
70}
71
72impl Client {
73    /// Create a new NTP client.
74    ///
75    /// # Arguments
76    ///
77    /// * `sn` - The socket number to use for DNS queries.
78    /// * `port` - SNTP source port, typically 123 is used.
79    /// * `server` - The SNTP server IPv4 address.
80    ///   Typically this is a DNS server provided by your DHCP client.
81    ///
82    /// # Example
83    ///
84    /// ```
85    /// use w5500_sntp::{
86    ///     ll::{
87    ///         net::{Ipv4Addr, SocketAddrV4},
88    ///         Sn,
89    ///     },
90    ///     Client, DST_PORT,
91    /// };
92    ///
93    /// const SNTP_SRC_PORT: u16 = 123;
94    /// const SNTP_SERVER: SocketAddrV4 = SocketAddrV4::new(Ipv4Addr::new(216, 239, 35, 4), DST_PORT);
95    ///
96    /// let sntp_client: Client = Client::new(Sn::Sn3, SNTP_SRC_PORT, SNTP_SERVER);
97    /// ```
98    pub const fn new(sn: Sn, port: u16, server: SocketAddrV4) -> Self {
99        Self { sn, port, server }
100    }
101
102    /// Setup the SNTP socket.
103    ///
104    /// This should be called once during initialization.
105    ///
106    /// This only sets up the W5500 interrupts, you must configure the W5500
107    /// interrupt pin for a falling edge trigger yourself.
108    ///
109    /// # Example
110    ///
111    /// ```no_run
112    /// # let mut w5500 = w5500_regsim::W5500::default();
113    /// use w5500_sntp::{
114    ///     ll::{
115    ///         net::{Ipv4Addr, SocketAddrV4},
116    ///         Sn,
117    ///     },
118    ///     Client, DST_PORT,
119    /// };
120    ///
121    /// const SNTP_SRC_PORT: u16 = 123;
122    /// const SNTP_SERVER: SocketAddrV4 = SocketAddrV4::new(Ipv4Addr::new(216, 239, 35, 4), DST_PORT);
123    ///
124    /// let sntp_client: Client = Client::new(Sn::Sn3, SNTP_SRC_PORT, SNTP_SERVER);
125    /// sntp_client.setup_socket(&mut w5500)?;
126    /// # Ok::<(), w5500_hl::Error<std::io::ErrorKind>>(())
127    /// ```
128    pub fn setup_socket<W5500: Registers>(&self, w5500: &mut W5500) -> Result<(), W5500::Error> {
129        let simr: u8 = w5500.simr()?;
130        w5500.set_simr(self.sn.bitmask() | simr)?;
131        const MASK: SocketInterruptMask = SocketInterruptMask::ALL_MASKED.unmask_recv();
132        w5500.set_sn_imr(self.sn, MASK)?;
133        w5500.close(self.sn)?;
134        w5500.udp_bind(self.sn, self.port)?;
135        w5500.set_sn_dest(self.sn, &self.server)
136    }
137
138    /// Send a request to the SNTP server.
139    ///
140    /// This will enable the RECV interrupt for the socket, and mask all others.
141    ///
142    /// At the moment this does not support adding a transmit timestamp.
143    ///
144    /// The result can be retrieved with [`on_recv_interrupt`] after the next
145    /// RECV interrupt.
146    ///
147    /// # Errors
148    ///
149    /// This method can only return:
150    ///
151    /// * [`Error::Other`]
152    /// * [`Error::OutOfMemory`]
153    ///   * Sending a request requires 48 bytes of memory in the socket buffers.
154    ///
155    /// # Example
156    ///
157    /// ```no_run
158    /// # let mut w5500 = w5500_regsim::W5500::default();
159    /// use w5500_sntp::{
160    ///     ll::{
161    ///         net::{Ipv4Addr, SocketAddrV4},
162    ///         Sn,
163    ///     },
164    ///     Client, DST_PORT,
165    /// };
166    ///
167    /// const SNTP_SRC_PORT: u16 = 123;
168    /// const SNTP_SERVER: SocketAddrV4 = SocketAddrV4::new(Ipv4Addr::new(216, 239, 35, 4), DST_PORT);
169    ///
170    /// let sntp_client: Client = Client::new(Sn::Sn3, SNTP_SRC_PORT, SNTP_SERVER);
171    /// sntp_client.setup_socket(&mut w5500)?;
172    /// sntp_client.request(&mut w5500)?;
173    /// # Ok::<(), w5500_hl::Error<std::io::ErrorKind>>(())
174    /// ```
175    ///
176    /// [`on_recv_interrupt`]: Self::on_recv_interrupt
177    pub fn request<W5500: Registers>(&self, w5500: &mut W5500) -> Result<(), Error<W5500::Error>> {
178        const LI: u8 = (LeapIndicator::NoWarning as u8) << 6;
179        const VN: u8 = VERSION_NUMBER << 3;
180        const MODE: u8 = Mode::Client as u8;
181
182        const STRATUM: u8 = 0;
183        const POLL: u8 = 0;
184        const PRECISION: u8 = 0;
185
186        // https://www.rfc-editor.org/rfc/rfc4330.html#section-4
187        #[rustfmt::skip]
188        const REQUEST_PKT: [u8; 48] = [
189            LI | VN | MODE, STRATUM, POLL, PRECISION,
190            // 4..8 root relay
191            0, 0, 0, 0,
192            // 8..12 root dispersion
193            0, 0, 0, 0,
194            // 12..16 reference identifier
195            0, 0, 0, 0,
196            // 16..24 reference timestamp
197            0, 0, 0, 0, 0, 0, 0, 0,
198            // 24..32 originate timestamp
199            0, 0, 0, 0, 0, 0, 0, 0,
200            // 32..40 receive timestamp
201            0, 0, 0, 0, 0, 0, 0, 0,
202            // 40..48 transmit timestamp
203            // in the future this can be provided as an argument
204            0, 0, 0, 0, 0, 0, 0, 0,
205        ];
206
207        let mut writer: UdpWriter<W5500> = w5500.udp_writer(self.sn)?;
208        writer.write_all(&REQUEST_PKT)?;
209        writer.send()?;
210
211        Ok(())
212    }
213
214    /// Read a reply from the server.
215    ///
216    /// You should only call this method after sending a [`request`] and
217    /// receiving a RECV interrupt.
218    ///
219    /// This will clear the pending RECV interrupt.
220    ///
221    /// # Errors
222    ///
223    /// This method can only return:
224    ///
225    /// * [`Error::Other`]
226    /// * [`Error::WouldBlock`]
227    ///   * In addition to being returned if there is no data to read this can
228    ///     also be returned when receiving invalid packets.
229    ///
230    /// # Example
231    ///
232    /// ```no_run
233    /// # let mut w5500 = w5500_regsim::W5500::default();
234    /// use w5500_sntp::{
235    ///     hl::Error,
236    ///     ll::{
237    ///         net::{Ipv4Addr, SocketAddrV4},
238    ///         Sn,
239    ///     },
240    ///     Client, Reply, DST_PORT,
241    /// };
242    ///
243    /// const SNTP_SRC_PORT: u16 = 123;
244    /// const SNTP_SERVER: SocketAddrV4 = SocketAddrV4::new(Ipv4Addr::new(216, 239, 35, 4), DST_PORT);
245    ///
246    /// let sntp_client: Client = Client::new(Sn::Sn3, SNTP_SRC_PORT, SNTP_SERVER);
247    /// sntp_client.setup_socket(&mut w5500)?;
248    /// sntp_client.request(&mut w5500)?;
249    ///
250    /// // ... wait for RECV interrupt with a timeout
251    ///
252    /// let reply: Reply = match sntp_client.on_recv_interrupt(&mut w5500) {
253    ///     Err(Error::WouldBlock) => todo!("implement retry logic here"),
254    ///     Err(e) => todo!("handle error: {:?}", e),
255    ///     Ok(reply) => reply,
256    /// };
257    /// # Ok::<(), w5500_hl::Error<std::io::ErrorKind>>(())
258    /// ```
259    ///
260    /// [`request`]: Self::request
261    pub fn on_recv_interrupt<W5500: Registers>(
262        &self,
263        w5500: &mut W5500,
264    ) -> Result<Reply, Error<W5500::Error>> {
265        let sn_ir: SocketInterrupt = w5500.sn_ir(self.sn)?;
266        if sn_ir.any_raised() {
267            w5500.set_sn_ir(self.sn, sn_ir)?;
268        }
269
270        let mut buf: [u8; 48] = [0; 48];
271        let mut reader: UdpReader<W5500> = w5500.udp_reader(self.sn)?;
272
273        if reader.header().origin != self.server {
274            debug!("unexpected packet from {}", reader.header().origin);
275            reader.done()?;
276            return Err(Error::WouldBlock);
277        }
278        reader.read_exact(&mut buf)?;
279        reader.done()?;
280
281        match Mode::try_from(buf[0] & 0b111) {
282            Ok(Mode::Server) => (),
283            Ok(mode) => {
284                warn!("invalid mode for reply: {:?}", mode);
285                return Err(Error::WouldBlock);
286            }
287            Err(value) => {
288                warn!("invalid value for mode: {}", value);
289                return Err(Error::WouldBlock);
290            }
291        };
292
293        let version_number: u8 = (buf[0] >> 3) & 0b111;
294        if version_number != VERSION_NUMBER {
295            warn!("unsupported version number: {}", version_number);
296            return Err(Error::WouldBlock);
297        }
298
299        let leap_indicator: LeapIndicator = LeapIndicator::from_bits(buf[0] >> 6);
300
301        let stratum: Stratum = match buf[1].try_into() {
302            Ok(stratum) => stratum,
303            Err(value) => {
304                warn!("invalid value for stratum: {}", value);
305                return Err(Error::WouldBlock);
306            }
307        };
308
309        let poll: u8 = buf[2];
310        if poll != 0 {
311            // poll is copied from request for unicast/multicast
312            warn!("poll value should be zero not {}", poll);
313            return Err(Error::WouldBlock);
314        }
315
316        Ok(Reply {
317            leap_indicator,
318            stratum,
319            precision: buf[3] as i8,
320            root_delay: FixedPoint {
321                bits: u32::from_be_bytes(buf[4..8].try_into().unwrap()),
322            },
323            root_dispersion: FixedPoint {
324                bits: u32::from_be_bytes(buf[8..12].try_into().unwrap()),
325            },
326            reference_identifier: buf[12..16].try_into().unwrap(),
327            reference_timestamp: Timestamp {
328                bits: u64::from_be_bytes(buf[16..24].try_into().unwrap()),
329            },
330            originate_timestamp: Timestamp {
331                bits: u64::from_be_bytes(buf[24..32].try_into().unwrap()),
332            },
333            receive_timestamp: Timestamp {
334                bits: u64::from_be_bytes(buf[32..40].try_into().unwrap()),
335            },
336            transmit_timestamp: Timestamp {
337                bits: u64::from_be_bytes(buf[40..48].try_into().unwrap()),
338            },
339        })
340    }
341}
342
343/// Reply from the SNTP server.
344#[derive(Debug)]
345#[cfg_attr(feature = "defmt", derive(defmt::Format))]
346pub struct Reply {
347    /// Leap indicator warning of an impending leap second to be
348    /// inserted/deleted in the last minute of the current day.
349    pub leap_indicator: LeapIndicator,
350    /// Stratum.
351    pub stratum: Stratum,
352    /// This is an eight-bit signed integer used as an exponent of
353    /// two, where the resulting value is the precision of the system clock
354    /// in seconds.  This field is significant only in server messages, where
355    /// the values range from -6 for mains-frequency clocks to -20 for
356    /// microsecond clocks found in some workstations.
357    pub precision: i8,
358    /// Total roundtrip delay to the primary reference source, in seconds.
359    ///
360    /// The values range from negative values of a few milliseconds to positive
361    /// values of several hundred milliseconds.
362    pub root_delay: FixedPoint,
363    /// The maximum error due to the clock frequency tolerance, in seconds.
364    pub root_dispersion: FixedPoint,
365    /// For [`Stratum::KoD`] and [`Stratum::Primary`] the value is a
366    /// four-character ASCII string, left justified and zero padded to 32 bits.
367    ///
368    /// For [`Stratum::Secondary`], the value is the 32-bit IPv4 address of
369    /// the synchronization source.
370    pub reference_identifier: [u8; 4],
371    /// This field is the time the system clock was last set or corrected.
372    pub reference_timestamp: Timestamp,
373    /// This is the time at which the request departed the client for the server.
374    pub originate_timestamp: Timestamp,
375    /// The time at which the request arrived at the server.
376    pub receive_timestamp: Timestamp,
377    /// The time at which the reply departed the server.
378    pub transmit_timestamp: Timestamp,
379}
380
381/// Leap indicator.
382///
383/// This is a two-bit code warning of an impending leap second to be
384/// inserted/deleted in the last minute of the current day.
385///
386/// # References
387///
388/// * [RFC 4330 Section 4](https://datatracker.ietf.org/doc/html/rfc4330#section-4)
389#[derive(Debug, Copy, Clone, Eq, PartialEq)]
390#[cfg_attr(feature = "defmt", derive(defmt::Format))]
391#[repr(u8)]
392pub enum LeapIndicator {
393    /// No warning
394    NoWarning = 0,
395    /// Last minute has 61 seconds
396    LastMin61Sec = 1,
397    /// Last minute has 59 seconds
398    LastMin59Sec = 2,
399    /// Alarm condition (clock not synchronized)
400    Alarm = 3,
401}
402
403impl LeapIndicator {
404    pub(crate) fn from_bits(bits: u8) -> Self {
405        match bits & 0b11 {
406            x if x == (Self::NoWarning as u8) => Self::NoWarning,
407            x if x == (Self::LastMin61Sec as u8) => Self::LastMin61Sec,
408            x if x == (Self::LastMin59Sec as u8) => Self::LastMin59Sec,
409            x if x == (Self::Alarm as u8) => Self::Alarm,
410            _ => unreachable!(),
411        }
412    }
413}
414
415/// SNTP modes.
416///
417/// # References
418///
419/// * [RFC 4330 Section 4](https://datatracker.ietf.org/doc/html/rfc4330#section-4)
420#[derive(Debug, Copy, Clone, Eq, PartialEq)]
421#[cfg_attr(feature = "defmt", derive(defmt::Format))]
422#[repr(u8)]
423#[non_exhaustive]
424enum Mode {
425    SymmetricActive = 1,
426    SymmetricPassive = 2,
427    Client = 3,
428    Server = 4,
429    Broadcast = 5,
430}
431
432impl TryFrom<u8> for Mode {
433    type Error = u8;
434
435    fn try_from(bits: u8) -> Result<Self, Self::Error> {
436        match bits {
437            x if x == (Self::SymmetricActive as u8) => Ok(Self::SymmetricActive),
438            x if x == (Self::SymmetricPassive as u8) => Ok(Self::SymmetricPassive),
439            x if x == (Self::Client as u8) => Ok(Self::Client),
440            x if x == (Self::Server as u8) => Ok(Self::Server),
441            x if x == (Self::Broadcast as u8) => Ok(Self::Broadcast),
442            x => Err(x),
443        }
444    }
445}
446
447/// Stratum, device's distance to the reference clock.
448#[non_exhaustive]
449#[derive(Debug, Copy, Clone, Eq, PartialEq)]
450#[cfg_attr(feature = "defmt", derive(defmt::Format))]
451pub enum Stratum {
452    /// Kiss-o'-Death
453    ///
454    /// See [RFC 4330 Section 8] for information about KoD.
455    ///
456    /// [RFC 4330 Section 8]: https://www.rfc-editor.org/rfc/rfc4330.html#section-8
457    KoD,
458    /// Primary reference (e.g., synchronized by radio clock)
459    Primary,
460    /// Secondary reference (synchronized by NTP or SNTP)
461    Secondary(u8),
462}
463
464impl TryFrom<u8> for Stratum {
465    type Error = u8;
466
467    fn try_from(bits: u8) -> Result<Self, Self::Error> {
468        match bits {
469            0 => Ok(Self::KoD),
470            1 => Ok(Self::Primary),
471            2..=15 => Ok(Self::Secondary(bits)),
472            _ => Err(bits),
473        }
474    }
475}