/ firmware / tests / sntp.rs
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  }