feat(example): Add SNTP example to show how to update Rtc to the current time (#3995)

* feat(example): Add SNTP example to show how to update Rtc

* Bump embassy-net to 0.7.0 for compatibility

* Revert back to task-arena to build on stable.
This commit is contained in:
Anthony Grondin 2025-08-29 03:45:04 -04:00 committed by GitHub
parent 5f1c1feed9
commit 3f29f0571c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 374 additions and 0 deletions

View File

@ -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"]

View File

@ -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

View File

@ -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
}