diff --git a/examples/wifi/sntp/.cargo/config.toml b/examples/wifi/sntp/.cargo/config.toml new file mode 100644 index 000000000..4dd1cd432 --- /dev/null +++ b/examples/wifi/sntp/.cargo/config.toml @@ -0,0 +1,26 @@ +[target.'cfg(target_arch = "riscv32")'] +runner = "espflash flash --monitor" +rustflags = [ + "-C", "link-arg=-Tlinkall.x", + "-C", "force-frame-pointers", +] + +[target.'cfg(target_arch = "xtensa")'] +runner = "espflash flash --monitor" +rustflags = [ + # GNU LD + "-C", "link-arg=-Wl,-Tlinkall.x", + "-C", "link-arg=-nostartfiles", + + # LLD + # "-C", "link-arg=-Tlinkall.x", + # "-C", "linker=rust-lld", +] + +[env] +ESP_LOG = "info" +SSID = "SSID" +PASSWORD = "PASSWORD" + +[unstable] +build-std = ["alloc", "core"] diff --git a/examples/wifi/sntp/Cargo.toml b/examples/wifi/sntp/Cargo.toml new file mode 100644 index 000000000..178d99a4d --- /dev/null +++ b/examples/wifi/sntp/Cargo.toml @@ -0,0 +1,94 @@ +[package] +name = "sntp" +version = "0.0.0" +edition = "2024" +publish = false + +[dependencies] +cfg-if = "1.0.0" +embassy-executor = { version = "0.7.0", features = ["task-arena-size-10240"] } +embassy-net = { version = "0.7.0", features = [ + "dhcpv4", + "medium-ethernet", + "udp", + "dns", +] } +embassy-time = "0.4.0" +embedded-io-async = "0.6.1" +esp-alloc = { path = "../../../esp-alloc" } +esp-backtrace = { path = "../../../esp-backtrace", features = [ + "panic-handler", + "println", +] } +esp-bootloader-esp-idf = { path = "../../../esp-bootloader-esp-idf" } +esp-hal-embassy = { path = "../../../esp-hal-embassy" } +esp-hal = { path = "../../../esp-hal", features = ["log-04", "unstable"] } +esp-println = { path = "../../../esp-println", features = ["log-04"] } +esp-preempt = { path = "../../../esp-preempt", features = ["log-04"] } +log = "0.4.17" +esp-radio = { path = "../../../esp-radio", features = [ + "log-04", + "unstable", + "wifi", +] } +static_cell = "2.1.0" +sntpc = { version = "0.6.0", default-features = false, features = [ + "embassy-socket", +] } +jiff = { version = "0.2.10", default-features = false, features = ["static"] } + +[features] +esp32 = [ + "esp-backtrace/esp32", + "esp-bootloader-esp-idf/esp32", + "esp-hal-embassy/esp32", + "esp-hal/esp32", + "esp-preempt/esp32", + "esp-radio/esp32", +] +esp32c2 = [ + "esp-backtrace/esp32c2", + "esp-bootloader-esp-idf/esp32c2", + "esp-hal-embassy/esp32c2", + "esp-hal/esp32c2", + "esp-preempt/esp32c2", + "esp-radio/esp32c2", +] +esp32c3 = [ + "esp-backtrace/esp32c3", + "esp-bootloader-esp-idf/esp32c3", + "esp-hal-embassy/esp32c3", + "esp-hal/esp32c3", + "esp-preempt/esp32c3", + "esp-radio/esp32c3", +] +esp32c6 = [ + "esp-backtrace/esp32c6", + "esp-bootloader-esp-idf/esp32c6", + "esp-hal-embassy/esp32c6", + "esp-hal/esp32c6", + "esp-preempt/esp32c6", + "esp-radio/esp32c6", +] +esp32s2 = [ + "esp-backtrace/esp32s2", + "esp-bootloader-esp-idf/esp32s2", + "esp-hal-embassy/esp32s2", + "esp-hal/esp32s2", + "esp-preempt/esp32s2", + "esp-radio/esp32s2", +] +esp32s3 = [ + "esp-backtrace/esp32s3", + "esp-bootloader-esp-idf/esp32s3", + "esp-hal-embassy/esp32s3", + "esp-hal/esp32s3", + "esp-preempt/esp32s3", + "esp-radio/esp32s3", +] + +[profile.release] +debug = true +debug-assertions = true +lto = "fat" +codegen-units = 1 diff --git a/examples/wifi/sntp/src/main.rs b/examples/wifi/sntp/src/main.rs new file mode 100644 index 000000000..ef528fe61 --- /dev/null +++ b/examples/wifi/sntp/src/main.rs @@ -0,0 +1,254 @@ +//! Embassy SNTP example +//! +//! +//! Set SSID and PASSWORD env variable before running this example. +//! +//! This gets an ip address via DHCP then performs an SNTP request to update the RTC time with the +//! response. The RTC time is then compared with the received data parsed with jiff. +//! You can change the timezone to your local timezone. + +#![no_std] +#![no_main] + +use core::net::{IpAddr, SocketAddr}; + +use embassy_executor::Spawner; +use embassy_net::{ + Runner, + StackResources, + dns::DnsQueryType, + udp::{PacketMetadata, UdpSocket}, +}; +use embassy_time::{Duration, Timer}; +use esp_alloc as _; +use esp_backtrace as _; +use esp_hal::{clock::CpuClock, rng::Rng, rtc_cntl::Rtc, timer::timg::TimerGroup}; +use esp_println::println; +use esp_radio::{ + Controller, + wifi::{ClientConfig, Config, ScanConfig, WifiController, WifiDevice, WifiEvent, WifiState}, +}; +use log::{error, info}; +use sntpc::{NtpContext, NtpTimestampGenerator, get_time}; + +esp_bootloader_esp_idf::esp_app_desc!(); + +// When you are okay with using a nightly compiler it's better to use https://docs.rs/static_cell/2.1.0/static_cell/macro.make_static.html +macro_rules! mk_static { + ($t:ty,$val:expr) => {{ + static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new(); + #[deny(unused_attributes)] + let x = STATIC_CELL.uninit().write(($val)); + x + }}; +} + +const SSID: &str = env!("SSID"); +const PASSWORD: &str = env!("PASSWORD"); +const TIMEZONE: jiff::tz::TimeZone = jiff::tz::get!("UTC"); +const NTP_SERVER: &str = "pool.ntp.org"; + +/// Microseconds in a second +const USEC_IN_SEC: u64 = 1_000_000; + +#[derive(Clone, Copy)] +struct Timestamp<'a> { + rtc: &'a Rtc<'a>, + current_time_us: u64, +} + +impl NtpTimestampGenerator for Timestamp<'_> { + fn init(&mut self) { + self.current_time_us = self.rtc.current_time_us(); + } + + fn timestamp_sec(&self) -> u64 { + self.current_time_us / 1_000_000 + } + + fn timestamp_subsec_micros(&self) -> u32 { + (self.current_time_us % 1_000_000) as u32 + } +} + +#[esp_hal_embassy::main] +async fn main(spawner: Spawner) -> ! { + esp_println::logger::init_logger_from_env(); + let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); + let peripherals = esp_hal::init(config); + let rtc = Rtc::new(peripherals.LPWR); + + esp_alloc::heap_allocator!(size: 72 * 1024); + + let timg0 = TimerGroup::new(peripherals.TIMG0); + esp_preempt::init(timg0.timer0); + + let esp_radio_ctrl = &*mk_static!(Controller<'static>, esp_radio::init().unwrap()); + + let (controller, interfaces) = esp_radio::wifi::new(esp_radio_ctrl, peripherals.WIFI).unwrap(); + + let wifi_interface = interfaces.sta; + + cfg_if::cfg_if! { + if #[cfg(feature = "esp32")] { + let timg1 = TimerGroup::new(peripherals.TIMG1); + esp_hal_embassy::init(timg1.timer0); + } else { + use esp_hal::timer::systimer::SystemTimer; + let systimer = SystemTimer::new(peripherals.SYSTIMER); + esp_hal_embassy::init(systimer.alarm0); + } + } + + let config = embassy_net::Config::dhcpv4(Default::default()); + + let rng = Rng::new(); + let seed = (rng.random() as u64) << 32 | rng.random() as u64; + + // Init network stack + let (stack, runner) = embassy_net::new( + wifi_interface, + config, + mk_static!(StackResources<3>, StackResources::<3>::new()), + seed, + ); + + spawner.spawn(connection(controller)).ok(); + spawner.spawn(net_task(runner)).ok(); + + let mut rx_meta = [PacketMetadata::EMPTY; 16]; + let mut rx_buffer = [0; 4096]; + let mut tx_meta = [PacketMetadata::EMPTY; 16]; + let mut tx_buffer = [0; 4096]; + + loop { + if stack.is_link_up() { + break; + } + Timer::after(Duration::from_millis(500)).await; + } + + println!("Waiting to get IP address..."); + loop { + if let Some(config) = stack.config_v4() { + println!("Got IP: {}", config.address); + break; + } + Timer::after(Duration::from_millis(500)).await; + } + + let ntp_addrs = stack.dns_query(NTP_SERVER, DnsQueryType::A).await.unwrap(); + + if ntp_addrs.is_empty() { + panic!("Failed to resolve DNS. Empty result"); + } + + let mut socket = UdpSocket::new( + stack, + &mut rx_meta, + &mut rx_buffer, + &mut tx_meta, + &mut tx_buffer, + ); + + socket.bind(123).unwrap(); + + // Display initial Rtc time before synchronization + let now = jiff::Timestamp::from_microsecond(rtc.current_time_us() as i64).unwrap(); + info!("Rtc: {now}"); + + loop { + let addr: IpAddr = ntp_addrs[0].into(); + let result = get_time( + SocketAddr::from((addr, 123)), + &socket, + NtpContext::new(Timestamp { + rtc: &rtc, + current_time_us: 0, + }), + ) + .await; + + match result { + Ok(time) => { + // Set time immediately after receiving to reduce time offset. + rtc.set_current_time_us( + (time.sec() as u64 * USEC_IN_SEC) + + ((time.sec_fraction() as u64 * USEC_IN_SEC) >> 32), + ); + + // Compare RTC to parsed time + info!( + "Response: {:?}\nTime: {}\nRtc : {}", + time, + // Create a Jiff Timestamp from seconds and nanoseconds + jiff::Timestamp::from_second(time.sec() as i64) + .unwrap() + .checked_add( + jiff::Span::new() + .nanoseconds((time.seconds_fraction as i64 * 1_000_000_000) >> 32), + ) + .unwrap() + .to_zoned(TIMEZONE), + jiff::Timestamp::from_microsecond(rtc.current_time_us() as i64) + .unwrap() + .to_zoned(TIMEZONE) + ); + } + Err(e) => { + error!("Error getting time: {e:?}"); + } + } + + Timer::after(Duration::from_secs(10)).await; + } +} + +#[embassy_executor::task] +async fn connection(mut controller: WifiController<'static>) { + println!("start connection task"); + println!("Device capabilities: {:?}", controller.capabilities()); + loop { + if esp_radio::wifi::wifi_state() == WifiState::StaConnected { + // wait until we're no longer connected + controller.wait_for_event(WifiEvent::StaDisconnected).await; + Timer::after(Duration::from_millis(5000)).await + } + if !matches!(controller.is_started(), Ok(true)) { + let client_config = Config::Client({ + let mut config = ClientConfig::default(); + config.ssid = SSID.into(); + config.password = PASSWORD.into(); + config + }); + controller.set_configuration(&client_config).unwrap(); + println!("Starting wifi"); + controller.start_async().await.unwrap(); + println!("Wifi started!"); + + println!("Scan"); + let scan_config = ScanConfig::default().with_max(10); + let result = controller + .scan_with_config_async(scan_config) + .await + .unwrap(); + for ap in result { + println!("{:?}", ap); + } + } + println!("About to connect..."); + + match controller.connect_async().await { + Ok(_) => println!("Wifi connected!"), + Err(e) => { + println!("Failed to connect to wifi: {e:?}"); + Timer::after(Duration::from_millis(5000)).await + } + } + } +} + +#[embassy_executor::task] +async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) { + runner.run().await +}