sntp.rs
1 //! `describe("SNTP Sync")` 2 //! 3 //! The device joins home WiFi, resolves `pool.ntp.org`, asks an NTP 4 //! server for the current epoch, and writes the result into the ESP32 5 //! LPWR RTC. `#[ignore]` because it needs an external AP + internet. 6 7 #![no_std] 8 #![no_main] 9 10 extern crate alloc; 11 12 #[path = "common/mod.rs"] 13 mod common; 14 15 use core::net::{IpAddr, SocketAddr}; 16 17 use defmt::info; 18 use embassy_net::{dns::DnsQueryType, udp::PacketMetadata}; 19 use esp_hal::rtc_cntl::Rtc; 20 use sntpc::{NtpContext, NtpTimestampGenerator, NtpUdpSocket, Result as SntpResult, get_time}; 21 22 use common::{Device, tasks}; 23 24 esp_bootloader_esp_idf::esp_app_desc!(); 25 26 const NTP_SERVER_HOSTNAME: &str = "pool.ntp.org"; 27 const MICROSECONDS_PER_SECOND: u64 = 1_000_000; 28 const MIN_PLAUSIBLE_EPOCH_SECONDS: u32 = 1_700_000_000; 29 30 #[derive(Copy, Clone)] 31 struct RtcBackedTimestampGenerator<'rtc> { 32 rtc: &'rtc Rtc<'rtc>, 33 captured_time_microseconds: u64, 34 } 35 36 impl NtpTimestampGenerator for RtcBackedTimestampGenerator<'_> { 37 fn init(&mut self) { 38 self.captured_time_microseconds = self.rtc.current_time_us(); 39 } 40 41 fn timestamp_sec(&self) -> u64 { 42 self.captured_time_microseconds / MICROSECONDS_PER_SECOND 43 } 44 45 fn timestamp_subsec_micros(&self) -> u32 { 46 (self.captured_time_microseconds % MICROSECONDS_PER_SECOND) as u32 47 } 48 } 49 50 struct EmbassyNetUdpSocket<'socket> { 51 socket: embassy_net::udp::UdpSocket<'socket>, 52 } 53 54 impl NtpUdpSocket for EmbassyNetUdpSocket<'_> { 55 async fn send_to( 56 &self, 57 buffer: &[u8], 58 destination_address: core::net::SocketAddr, 59 ) -> SntpResult<usize> { 60 let core::net::SocketAddr::V4(destination_address_v4) = destination_address else { 61 return Err(sntpc::Error::Network); 62 }; 63 let ip_endpoint = embassy_net::IpEndpoint::from(destination_address_v4); 64 self.socket 65 .send_to(buffer, ip_endpoint) 66 .await 67 .map(|()| buffer.len()) 68 .map_err(|_| sntpc::Error::Network) 69 } 70 71 async fn recv_from( 72 &self, 73 buffer: &mut [u8], 74 ) -> SntpResult<(usize, core::net::SocketAddr)> { 75 let (bytes_read, udp_metadata) = self 76 .socket 77 .recv_from(buffer) 78 .await 79 .map_err(|_| sntpc::Error::Network)?; 80 81 let embassy_net::IpEndpoint { 82 addr: embassy_net::IpAddress::Ipv4(addr_v4), 83 port, 84 } = udp_metadata.endpoint; 85 86 let octets = addr_v4.octets(); 87 let core_address = core::net::SocketAddr::V4(core::net::SocketAddrV4::new( 88 core::net::Ipv4Addr::new(octets[0], octets[1], octets[2], octets[3]), 89 port, 90 )); 91 Ok((bytes_read, core_address)) 92 } 93 } 94 95 #[cfg(test)] 96 #[embedded_test::setup] 97 fn setup() { 98 rtt_target::rtt_init_defmt!(); 99 } 100 101 #[cfg(test)] 102 #[embedded_test::tests(default_timeout = 60, executor = esp_rtos::embassy::Executor::new())] 103 mod tests { 104 use super::*; 105 106 #[init] 107 fn init() -> Device { 108 info!("=== SNTP Sync — describe block ==="); 109 common::setup::boot_device() 110 } 111 112 /// `it("user syncs the device RTC from pool.ntp.org")` 113 #[test] 114 #[timeout(60)] 115 async fn user_syncs_device_rtc_from_pool_ntp_org( 116 mut device: Device, 117 ) -> Result<(), &'static str> { 118 // SAFETY: every embedded-test runs inside an `esp_rtos::embassy::Executor`. 119 let embassy_spawner = 120 unsafe { embassy_executor::Spawner::for_current_executor() }.await; 121 122 tasks::wifi::connect_to_home_access_point(&mut device, embassy_spawner).await?; 123 124 let embassy_network_stack = device 125 .embassy_network_stack 126 .ok_or("device: embassy-net stack missing after WiFi bring-up")?; 127 128 info!("user resolves NTP server hostname={=str}", NTP_SERVER_HOSTNAME); 129 let ntp_server_addresses = embassy_network_stack 130 .dns_query(NTP_SERVER_HOSTNAME, DnsQueryType::A) 131 .await 132 .map_err(|_| "device: DNS lookup for pool.ntp.org failed")?; 133 134 if ntp_server_addresses.is_empty() { 135 return Err("device: DNS returned zero A records for pool.ntp.org"); 136 } 137 138 let ntp_server_address: IpAddr = ntp_server_addresses[0].into(); 139 info!( 140 "device resolved NTP server address={=[u8]:?}", 141 match ntp_server_address { 142 IpAddr::V4(ipv4) => ipv4.octets(), 143 IpAddr::V6(_) => [0u8; 4], 144 } 145 ); 146 147 let mut udp_rx_metadata = [PacketMetadata::EMPTY; 16]; 148 let mut udp_rx_buffer = [0u8; 4096]; 149 let mut udp_tx_metadata = [PacketMetadata::EMPTY; 16]; 150 let mut udp_tx_buffer = [0u8; 4096]; 151 let mut udp_socket = embassy_net::udp::UdpSocket::new( 152 embassy_network_stack, 153 &mut udp_rx_metadata, 154 &mut udp_rx_buffer, 155 &mut udp_tx_metadata, 156 &mut udp_tx_buffer, 157 ); 158 udp_socket 159 .bind(123) 160 .map_err(|_| "device: failed to bind UDP socket to port 123")?; 161 162 let lpwr_rtc = Rtc::new(unsafe { esp_hal::peripherals::LPWR::steal() }); 163 let rtc_time_before_sync_microseconds = lpwr_rtc.current_time_us(); 164 info!( 165 "device LPWR RTC before SNTP sync us={=u64}", 166 rtc_time_before_sync_microseconds 167 ); 168 169 let ntp_context = NtpContext::new(RtcBackedTimestampGenerator { 170 rtc: &lpwr_rtc, 171 captured_time_microseconds: 0, 172 }); 173 174 let udp_socket_wrapper = EmbassyNetUdpSocket { socket: udp_socket }; 175 176 let ntp_response = get_time( 177 SocketAddr::from((ntp_server_address, 123)), 178 &udp_socket_wrapper, 179 ntp_context, 180 ) 181 .await 182 .map_err(|_| "device: SNTP get_time request failed")?; 183 184 let synced_epoch_seconds = ntp_response.sec(); 185 defmt::assert!( 186 synced_epoch_seconds > MIN_PLAUSIBLE_EPOCH_SECONDS, 187 "NTP epoch {=u32} looks implausibly old (< 2023)", 188 synced_epoch_seconds 189 ); 190 191 let synced_epoch_microseconds = (synced_epoch_seconds as u64 * MICROSECONDS_PER_SECOND) 192 + ((ntp_response.sec_fraction() as u64 * MICROSECONDS_PER_SECOND) >> 32); 193 lpwr_rtc.set_current_time_us(synced_epoch_microseconds); 194 195 info!( 196 "device LPWR RTC synced epoch_seconds={=u32} fraction={=u32}", 197 synced_epoch_seconds, 198 ntp_response.sec_fraction() 199 ); 200 Ok(()) 201 } 202 }