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}