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 = "chrono")]
71 pub fn try_to_naive_datetime(self) -> Result<chrono::NaiveDateTime, 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]) -> Result<(), SntpError> {
86 if packet.len() != Self::SNTP_PACKET_SIZE {
87 return Err(SntpError::InvalidPacket);
88 }
89
90 packet[0] = (3 << 6) | (4 << 3) | 3;
91
92 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 #[cfg(feature = "chrono")]
125 pub fn as_naive_datetime(raw_time: SntpTimestamp) -> Result<chrono::NaiveDateTime, SntpError> {
126 raw_time.try_into()
127 }
128}
129
130#[cfg(feature = "chrono")]
131impl TryFrom<SntpTimestamp> for chrono::NaiveDateTime {
132 type Error = SntpError;
133
134 fn try_from(timestamp: SntpTimestamp) -> Result<Self, Self::Error> {
135 Ok(chrono::DateTime::<chrono::Utc>::try_from(timestamp)?.naive_utc())
136 }
137}
138
139#[cfg(feature = "chrono")]
140impl TryFrom<SntpTimestamp> for chrono::DateTime<chrono::Utc> {
141 type Error = SntpError;
142
143 fn try_from(timestamp: SntpTimestamp) -> Result<Self, Self::Error> {
144 chrono::DateTime::<chrono::Utc>::from_timestamp_micros(timestamp.utc_micros())
145 .ok_or(SntpError::InvalidTime)
146 }
147}
148
149#[inline]
150fn read_be_u64(input: &[u8]) -> u64 {
151 let (int_bytes, _) = input.split_at(core::mem::size_of::<u64>());
152 u64::from_be_bytes(int_bytes.try_into().unwrap())
153}
154
155#[cfg(feature = "embassy-net")]
156pub trait SntpSocket {
157 fn resolve_time(
158 &mut self,
159 addrs: &[embassy_net::IpAddress],
160 ) -> impl Future<Output = Result<SntpTimestamp, SntpError>>;
161}
162
163#[cfg(feature = "embassy-net")]
164impl SntpSocket for embassy_net::udp::UdpSocket<'_> {
165 async fn resolve_time(
166 &mut self,
167 addrs: &[embassy_net::IpAddress],
168 ) -> Result<SntpTimestamp, SntpError> {
169 use embassy_time::{Duration, WithTimeout};
170 use sachy_fmt::{debug, error};
171
172 if addrs.is_empty() {
173 return Err(SntpError::NetFailure);
174 }
175
176 debug!("SNTP address list: {}", addrs);
177
178 let addr = addrs[0];
179
180 debug!("Binding to port 123");
181 self.bind(123).map_err(|_| SntpError::NetFailure)?;
182
183 debug!("Sending SNTP request");
184 self.send_to_with(
185 SntpRequest::SNTP_PACKET_SIZE,
186 embassy_net::IpEndpoint::new(addr, 123),
187 SntpRequest::create_packet_from_buffer,
188 )
189 .await
190 .map_err(|e| {
191 error!("Failed to send: {}", e);
192 SntpError::NetFailure
193 })??;
194
195 debug!("Waiting for a response...");
196 let res = self
197 .recv_from_with(|buf, _from| SntpRequest::read_timestamp(buf))
198 .with_timeout(Duration::from_secs(5))
199 .await
200 .map_err(|_| SntpError::NetTimeout)
201 .flatten();
202
203 debug!("Received: {}", &res);
204 debug!("Closing SNTP port...");
205 self.close();
206
207 res
208 }
209}