diff --git a/Cargo.toml b/Cargo.toml index 2f0ef25..f25f7b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,11 @@ esp-radio = { version = "0.18.0", features = [ "wifi", ] } +sntpc = { version = "0.11", default-features = false } +sntpc-net-embassy = { version = "0.11", default-features = false } +sntpc-time-embassy = { version = "0.6" } +portable-atomic = "1.15.0" + picoserve = { version = "0.19", features = ["defmt", "embassy", "json"] } diff --git a/src/bin/main.rs b/src/bin/main.rs index 664929f..9e721d0 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -19,7 +19,7 @@ use esp_println as _; use esp_radio::wifi::sta::StationConfig; use esp_radio::wifi::{ControllerConfig, Interface, WifiController}; -use cover_theif::http_api::{self, HttpApiProps, HttpApiService}; +use cover_theif::{http_api::{self, HttpApiProps, HttpApiService}, clock}; extern crate alloc; @@ -99,6 +99,8 @@ async fn main(spawner: Spawner) -> ! { info!("Got IP: {}", config.address); } + spawner.spawn(clock::ntp_upkeep_service(net_stack).unwrap()); + spawner.spawn(http_api_serve_task(HttpApiService::new(net_stack, Default::default(), Default::default())).unwrap()); loop { diff --git a/src/clock.rs b/src/clock.rs new file mode 100644 index 0000000..221de06 --- /dev/null +++ b/src/clock.rs @@ -0,0 +1,69 @@ +use core::net::SocketAddr; +use embassy_net::udp::{PacketMetadata, UdpSocket}; +use embassy_time::{Duration, Timer}; +use sntpc::{NtpContext, get_time}; +use sntpc_net_embassy::UdpSocketWrapper; +use sntpc_time_embassy::EmbassyTimestampGenerator; +use portable_atomic::{AtomicU64, Ordering}; + +// NTP server (pool.ntp.org is the usual choice) +const NTP_SERVER: &str = "pool.ntp.org"; +const NTP_PORT: u16 = 123; + +static NTP_BOOT_OFFSET: AtomicU64 = AtomicU64::new(0); + +pub async fn fetch_ntp_time(stack: embassy_net::Stack<'static>) -> Result { + // Resolve the NTP server hostname + let server_addr = stack + .dns_query(NTP_SERVER, embassy_net::dns::DnsQueryType::A) + .await + .map_err(|_| "DNS resolution failed")? + .first() + .copied() + .ok_or("No DNS result")?; + + let socket_addr = SocketAddr::new(server_addr.into(), NTP_PORT); + + // Allocate UDP buffers (can be small — SNTP packets are tiny) + let mut rx_meta = [PacketMetadata::EMPTY; 16]; + let mut rx_buf = [0u8; 48]; + let mut tx_meta = [PacketMetadata::EMPTY; 16]; + let mut tx_buf = [0u8; 48]; + + let mut socket = UdpSocket::new(stack, &mut rx_meta, &mut rx_buf, &mut tx_meta, &mut tx_buf); + socket.bind(0).map_err(|_| "bind failed")?; + + let wrapped = UdpSocketWrapper::from(socket); + let ctx = NtpContext::new(EmbassyTimestampGenerator::default()); + + let result = get_time(socket_addr, &wrapped, ctx) + .await + .map_err(|_| "SNTP request failed")?; + + // result.sec is the NTP timestamp in seconds since 1900-01-01. + // Unix epoch is 1900-01-01 + 70 years = 2_208_988_800 seconds. + const NTP_UNIX_OFFSET: u64 = 2_208_988_800; + let unix_secs = result.sec().saturating_sub(NTP_UNIX_OFFSET); + + Ok(unix_secs) +} + +pub fn now() -> embassy_time::Instant { + embassy_time::Instant::now() + Duration::from_secs(NTP_BOOT_OFFSET.load(Ordering::Relaxed)) +} + +#[embassy_executor::task] +pub async fn ntp_upkeep_service(stack: embassy_net::Stack<'static>) { + loop { + match fetch_ntp_time(stack).await { + Ok(unix_secs) => { + NTP_BOOT_OFFSET.store(unix_secs - embassy_time::Instant::now().as_secs(), portable_atomic::Ordering::Relaxed); + Timer::after(Duration::from_secs(6 * 60 * 60)).await + } + Err(err) => { + defmt::error!("Failed to sync the clock from NTP: {}", err); + Timer::after(Duration::from_secs(10)).await + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 6164f43..34783b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,3 +8,5 @@ #![feature(impl_trait_in_assoc_type)] pub mod http_api; + +pub mod clock;