Repo of no-std crates for my personal embedded projects
1#![no_std]
2
3#[derive(Debug, PartialEq, Eq)]
4#[cfg_attr(feature = "defmt", derive(defmt::Format))]
5pub enum SntpError {
6 InvalidPacket,
7 InvalidVersion,
8 InvalidMode,
9 KissOfDeath,
10 InvalidTime,
11 NetFailure,
12 NetTimeout,
13}
14
15pub struct SntpRequest;
16
17#[derive(Debug, PartialEq, Eq, Clone, Copy)]
18#[cfg_attr(feature = "defmt", derive(defmt::Format))]
19/// SNTP timestamp.
20pub struct SntpTimestamp(u64);
21
22impl SntpTimestamp {
23 pub fn microseconds(&self) -> u64 {
24 (self.0 >> 32) * 1_000_000 + ((self.0 & 0xFFFFFFFF) * 1_000_000 / 0x100000000)
25 }
26
27 /// Returns true if the most significant bit is set.
28 ///
29 /// Relevant documentation from RFC 2030:
30 ///
31 /// ```text
32 /// Note that, since some time in 1968 (second 2,147,483,648) the most
33 /// significant bit (bit 0 of the integer part) has been set and that the
34 /// 64-bit field will overflow some time in 2036 (second 4,294,967,296).
35 /// Should NTP or SNTP be in use in 2036, some external means will be
36 /// necessary to qualify time relative to 1900 and time relative to 2036
37 /// (and other multiples of 136 years). There will exist a 200-picosecond
38 /// interval, henceforth ignored, every 136 years when the 64-bit field
39 /// will be 0, which by convention is interpreted as an invalid or
40 /// unavailable timestamp.
41 /// As the NTP timestamp format has been in use for the last 17 years,
42 /// it remains a possibility that it will be in use 40 years from now
43 /// when the seconds field overflows. As it is probably inappropriate
44 /// to archive NTP timestamps before bit 0 was set in 1968, a
45 /// convenient way to extend the useful life of NTP timestamps is the
46 /// following convention: If bit 0 is set, the UTC time is in the
47 /// range 1968-2036 and UTC time is reckoned from 0h 0m 0s UTC on 1
48 /// January 1900. If bit 0 is not set, the time is in the range 2036-
49 /// 2104 and UTC time is reckoned from 6h 28m 16s UTC on 7 February
50 /// 2036. Note that when calculating the correspondence, 2000 is not a
51 /// leap year. Note also that leap seconds are not counted in the
52 /// reckoning.
53 ///```
54 pub fn msb_set(&self) -> bool {
55 self.0 & (1 << 63) != 0
56 }
57
58 /// Microseconds since the UNIX epoch.
59 pub fn utc_micros(&self) -> i64 {
60 let ntp_epoch_micros = self.microseconds() as i64;
61 let offset: i64 = if self.msb_set() {
62 -2208988800000000
63 } else {
64 2085978496000000
65 };
66
67 ntp_epoch_micros + offset
68 }
69
70 #[cfg(feature = "jiff")]
71 pub fn try_to_unix_timestamp(self) -> Result<jiff::Timestamp, SntpError> {
72 self.try_into()
73 }
74}
75
76impl SntpRequest {
77 pub const SNTP_PACKET_SIZE: usize = 48;
78
79 pub const fn create_packet() -> [u8; Self::SNTP_PACKET_SIZE] {
80 let mut packet = [0u8; Self::SNTP_PACKET_SIZE];
81 packet[0] = (3 << 6) | (4 << 3) | 3;
82 packet
83 }
84
85 pub fn create_packet_from_buffer(packet: &mut [u8]) -> (usize, Result<(), SntpError>) {
86 if packet.len() != Self::SNTP_PACKET_SIZE {
87 return (0, Err(SntpError::InvalidPacket));
88 }
89
90 packet[0] = (3 << 6) | (4 << 3) | 3;
91
92 (Self::SNTP_PACKET_SIZE, Ok(()))
93 }
94
95 pub fn read_timestamp(packet: &[u8]) -> Result<SntpTimestamp, SntpError> {
96 if packet.len() == Self::SNTP_PACKET_SIZE {
97 let header = packet[0];
98 let version = (header & 0x38) >> 3;
99
100 if version != 4 {
101 return Err(SntpError::InvalidVersion);
102 }
103
104 let mode = header & 0x7;
105
106 if !(4..=5).contains(&mode) {
107 return Err(SntpError::InvalidMode);
108 }
109
110 let kiss_of_death = packet[1] == 0;
111
112 if kiss_of_death {
113 return Err(SntpError::KissOfDeath);
114 }
115
116 let timestamp = SntpTimestamp(read_be_u64(&packet[40..48]));
117
118 return Ok(timestamp);
119 }
120
121 Err(SntpError::InvalidPacket)
122 }
123}
124
125#[cfg(feature = "jiff")]
126impl TryFrom<SntpTimestamp> for jiff::Timestamp {
127 type Error = SntpError;
128
129 fn try_from(timestamp: SntpTimestamp) -> Result<Self, Self::Error> {
130 jiff::Timestamp::from_microsecond(timestamp.utc_micros())
131 .map_err(|_| SntpError::InvalidTime)
132 }
133}
134
135#[inline]
136fn read_be_u64(input: &[u8]) -> u64 {
137 let (int_bytes, _) = input.split_at(core::mem::size_of::<u64>());
138 u64::from_be_bytes(int_bytes.try_into().unwrap())
139}
140
141#[cfg(feature = "embassy-net")]
142pub trait SntpSocket {
143 fn resolve_time(
144 &mut self,
145 addrs: &[embassy_net::IpAddress],
146 ) -> impl Future<Output = Result<SntpTimestamp, SntpError>>;
147}
148
149#[cfg(feature = "embassy-net")]
150impl SntpSocket for embassy_net::udp::UdpSocket<'_> {
151 async fn resolve_time(
152 &mut self,
153 addrs: &[embassy_net::IpAddress],
154 ) -> Result<SntpTimestamp, SntpError> {
155 use embassy_time::{Duration, WithTimeout};
156 use sachy_fmt::{debug, error};
157
158 if addrs.is_empty() {
159 return Err(SntpError::NetFailure);
160 }
161
162 debug!("SNTP address list: {}", addrs);
163
164 let addr = addrs[0];
165
166 debug!("Binding to port 123");
167 self.bind(123).map_err(|_| SntpError::NetFailure)?;
168
169 debug!("Sending SNTP request");
170 self.send_to_with(
171 SntpRequest::SNTP_PACKET_SIZE,
172 embassy_net::IpEndpoint::new(addr, 123),
173 SntpRequest::create_packet_from_buffer,
174 )
175 .await
176 .map_err(|e| {
177 error!("Failed to send: {}", e);
178 self.close();
179 SntpError::NetFailure
180 })??;
181
182 debug!("Waiting for a response...");
183 let res = self
184 .recv_from_with(|buf, _from| SntpRequest::read_timestamp(buf))
185 .with_timeout(Duration::from_secs(5))
186 .await
187 .map_err(|_| SntpError::NetTimeout)
188 .flatten();
189
190 debug!("Received: {}", &res);
191 debug!("Closing SNTP port...");
192 self.close();
193
194 res
195 }
196}