From 734971d40ce1a602b2aa0c560a61e7be0687ba39 Mon Sep 17 00:00:00 2001 From: "main()" Date: Tue, 12 May 2026 18:12:30 +0200 Subject: [PATCH 1/9] Fix modem-sleep clock gating while Wi-Fi is active While Wi-Fi is active, ESP-IDF blocks wifi_clock_disable calls from the blob: https://github.com/espressif/esp-idf/blob/release/v5.5/components/esp_hw_support/modem_clock.c#L77-L85 This is necessary because otherwise hardware-provided timers such as TWT do not fire. --- esp-radio/src/wifi/os_adapter/mod.rs | 8 ++++++-- esp-radio/src/wifi/state.rs | 7 +++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/esp-radio/src/wifi/os_adapter/mod.rs b/esp-radio/src/wifi/os_adapter/mod.rs index 78496100a3a..80d47e7d9c9 100644 --- a/esp-radio/src/wifi/os_adapter/mod.rs +++ b/esp-radio/src/wifi/os_adapter/mod.rs @@ -881,8 +881,12 @@ pub unsafe extern "C" fn wifi_clock_enable() { /// /// ************************************************************************* pub unsafe extern "C" fn wifi_clock_disable() { - trace!("wifi_clock_disable"); - crate::radio_clocks::clocks_ll::enable_wifi(false); + if super::state::is_wifi_initialized() { + trace!("wifi_clock_disable - no-op (wifi active, modem sleep)"); + } else { + trace!("wifi_clock_disable - gating clocks (wifi deinit)"); + crate::radio_clocks::clocks_ll::enable_wifi(false); + } } /// ************************************************************************** diff --git a/esp-radio/src/wifi/state.rs b/esp-radio/src/wifi/state.rs index a90597d720d..21f50a5daa4 100644 --- a/esp-radio/src/wifi/state.rs +++ b/esp-radio/src/wifi/state.rs @@ -112,6 +112,13 @@ pub(crate) fn set_station_state(state: WifiStationState) { STATION_STATE.store(state, Ordering::Relaxed) } +/// Returns `true` if Wi-Fi is currently initialized (not in the +/// `Uninitialized` state for either interface). +pub(crate) fn is_wifi_initialized() -> bool { + station_state() != WifiStationState::Uninitialized + || access_point_state() != WifiAccessPointState::Uninitialized +} + pub(crate) fn locked(f: impl FnOnce() -> R) -> R { static LOCK: NonReentrantMutex> = NonReentrantMutex::new(None); From 6e3d843c578d7774829d2ca1abb5639d25370a84 Mon Sep 17 00:00:00 2001 From: "main()" Date: Tue, 12 May 2026 18:12:50 +0200 Subject: [PATCH 2/9] Add iTWT (Individual Target Wake Time) support for Wi-Fi 6 Add individual Target Wake Time (iTWT) support for Wi-Fi 6 (802.11ax) chips (ESP32-C5, C6, C61), enabling significant power savings for periodic traffic patterns. Examples: - embassy_twt: comprehensively demonstrates usage of iTWT APIs - embassy_twt_udp: voice-over-WiFi (160B UDP on 20ms TWT wakeup) --- esp-radio/src/wifi/event.rs | 174 ++++- esp-radio/src/wifi/mod.rs | 464 +++++++++++++- esp-radio/src/wifi/twt.rs | 594 ++++++++++++++++++ examples/wifi/embassy_twt/.cargo/config.toml | 14 + examples/wifi/embassy_twt/Cargo.toml | 53 ++ examples/wifi/embassy_twt/src/main.rs | 246 ++++++++ .../wifi/embassy_twt_udp/.cargo/config.toml | 15 + examples/wifi/embassy_twt_udp/Cargo.toml | 58 ++ examples/wifi/embassy_twt_udp/src/main.rs | 184 ++++++ 9 files changed, 1784 insertions(+), 18 deletions(-) create mode 100644 esp-radio/src/wifi/twt.rs create mode 100644 examples/wifi/embassy_twt/.cargo/config.toml create mode 100644 examples/wifi/embassy_twt/Cargo.toml create mode 100644 examples/wifi/embassy_twt/src/main.rs create mode 100644 examples/wifi/embassy_twt_udp/.cargo/config.toml create mode 100644 examples/wifi/embassy_twt_udp/Cargo.toml create mode 100644 examples/wifi/embassy_twt_udp/src/main.rs diff --git a/esp-radio/src/wifi/event.rs b/esp-radio/src/wifi/event.rs index 36b3fc24b9c..c08d2f4dfb1 100644 --- a/esp-radio/src/wifi/event.rs +++ b/esp-radio/src/wifi/event.rs @@ -222,13 +222,95 @@ impl_wifi_event!( wifi_event_ap_wps_rg_pin_t ); impl_wifi_event!(AccessPointWifiProtectedStatusRegistrarPushButtonConfigurationOverlap); -impl_wifi_event!(IndividualTargetWakeTimeSetup); -impl_wifi_event!(IndividualTargetWakeTimeTeardown); -impl_wifi_event!(IndividualTargetWakeTimeProbe); -impl_wifi_event!(IndividualTargetWakeTimeSuspend); -impl_wifi_event!(TargetWakeTimeWakeup); +impl_wifi_event!(IndividualTargetWakeTimeSetup, wifi_event_sta_itwt_setup_t); +impl_wifi_event!( + IndividualTargetWakeTimeTeardown, + wifi_event_sta_itwt_teardown_t +); +impl_wifi_event!(IndividualTargetWakeTimeProbe, wifi_event_sta_itwt_probe_t); +impl_wifi_event!( + IndividualTargetWakeTimeSuspend, + wifi_event_sta_itwt_suspend_t +); +impl_wifi_event!(TargetWakeTimeWakeup, wifi_event_sta_twt_wakeup_t); impl_wifi_event!(BroadcastTargetWakeTimeSetup); impl_wifi_event!(BroadcastTargetWakeTimeTeardown); + +impl IndividualTargetWakeTimeSetup<'_> { + /// Get the setup status. 1 indicates success, other values indicate failure. + pub fn status(&self) -> i32 { + self.0.status + } + + /// Get the setup failure reason code. + pub fn reason(&self) -> u8 { + self.0.reason + } + + /// Get the TWT service period start time. + pub fn target_wake_time(&self) -> u64 { + self.0.target_wake_time + } + + /// Get the negotiated iTWT setup configuration. + pub fn config(&self) -> crate::wifi::twt::ITwtSetupConfig { + crate::wifi::twt::ITwtSetupConfig::from_raw(&self.0.config) + } +} + +impl IndividualTargetWakeTimeTeardown<'_> { + /// Get the flow ID that was torn down. + pub fn flow_id(&self) -> u8 { + self.0.flow_id + } + + /// Get the teardown status. + pub fn status(&self) -> crate::wifi::twt::ITwtTeardownStatus { + crate::wifi::twt::ITwtTeardownStatus::from_raw(self.0.status) + } +} + +impl IndividualTargetWakeTimeProbe<'_> { + /// Get the probe status. + pub fn status(&self) -> crate::wifi::twt::ITwtProbeStatus { + crate::wifi::twt::ITwtProbeStatus::from_raw(self.0.status) + } + + /// Get the failure reason code. + pub fn reason(&self) -> u8 { + self.0.reason + } +} + +impl IndividualTargetWakeTimeSuspend<'_> { + /// Get the suspend status. 0 (`ESP_OK`) indicates success. + pub fn status(&self) -> i32 { + self.0.status + } + + /// Get the bitmap of suspended flow IDs. + pub fn flow_id_bitmap(&self) -> u8 { + self.0.flow_id_bitmap + } + + /// Get the actual suspend time for each flow ID, in milliseconds. + pub fn actual_suspend_time_ms(&self) -> [u32; 8] { + self.0.actual_suspend_time_ms + } +} + +impl TargetWakeTimeWakeup<'_> { + /// Get the TWT type (individual or broadcast). + pub fn twt_type(&self) -> crate::wifi::twt::TwtType { + crate::wifi::twt::TwtType::from_raw(self.0.twt_type) + } + + /// Get the flow ID. + pub fn flow_id(&self) -> u8 { + self.0.flow_id + } +} + impl_wifi_event!(NeighborAwarenessNetworkingStarted); impl_wifi_event!(NeighborAwarenessNetworkingStopped); impl_wifi_event!( @@ -1018,19 +1100,50 @@ pub enum EventInfo { AccessPointWifiProtectedStatusRegistrarPushButtonConfigurationOverlap, /// Individual Target-Wake-Time setup. - IndividualTargetWakeTimeSetup, + IndividualTargetWakeTimeSetup { + /// The negotiated iTWT setup configuration. + config: crate::wifi::twt::ITwtSetupConfig, + /// Setup status. 1 indicates success, other values indicate failure. + status: i32, + /// Setup failure reason code. + reason: u8, + /// TWT service period start time. + target_wake_time: u64, + }, /// Individual Target-Wake-Time teardown. - IndividualTargetWakeTimeTeardown, + IndividualTargetWakeTimeTeardown { + /// Flow ID that was torn down. + flow_id: crate::wifi::twt::FlowId, + /// Teardown status. + status: crate::wifi::twt::ITwtTeardownStatus, + }, /// Individual Target-Wake-Time probe. - IndividualTargetWakeTimeProbe, + IndividualTargetWakeTimeProbe { + /// Probe status. + status: crate::wifi::twt::ITwtProbeStatus, + /// Failure reason code. + reason: u8, + }, /// Individual Target-Wake-Time suspended. - IndividualTargetWakeTimeSuspend, + IndividualTargetWakeTimeSuspend { + /// Suspend status. 0 (`ESP_OK`) indicates success. + status: i32, + /// The set of suspended flow IDs. + suspended_flows: EnumSet, + /// Actual suspend time for each flow ID, in milliseconds. + actual_suspend_time_ms: [u32; 8], + }, - /// Target-Wake-Wakeup event. - TargetWakeTimeWakeup, + /// Target-Wake-Time wakeup event. + TargetWakeTimeWakeup { + /// TWT type (individual or broadcast). + twt_type: crate::wifi::twt::TwtType, + /// Flow ID. + flow_id: crate::wifi::twt::FlowId, + }, /// Broadcast-Target-Wake-Time setup. BroadcastTargetWakeTimeSetup, @@ -1239,19 +1352,46 @@ impl EventInfo { Some(EventInfo::AccessPointWifiProtectedStatusRegistrarPushButtonConfigurationOverlap) } WifiEvent::IndividualTargetWakeTimeSetup => { - Some(EventInfo::IndividualTargetWakeTimeSetup) + let ev = + unsafe { IndividualTargetWakeTimeSetup::from_raw_event_data(payload) }; + Some(EventInfo::IndividualTargetWakeTimeSetup { + config: ev.config(), + status: ev.status(), + reason: ev.reason(), + target_wake_time: ev.target_wake_time(), + }) } - WifiEvent::IndividualTargetWakeTimeTeardown => { - Some(EventInfo::IndividualTargetWakeTimeTeardown) + WifiEvent::IndividualTargetWakeTimeTeardown => { + let ev = + unsafe { IndividualTargetWakeTimeTeardown::from_raw_event_data(payload) }; + Some(EventInfo::IndividualTargetWakeTimeTeardown { + flow_id: crate::wifi::twt::FlowId::from_raw(ev.flow_id()), + status: ev.status(), + }) } WifiEvent::IndividualTargetWakeTimeProbe => { - Some(EventInfo::IndividualTargetWakeTimeProbe) + let ev = + unsafe { IndividualTargetWakeTimeProbe::from_raw_event_data(payload) }; + Some(EventInfo::IndividualTargetWakeTimeProbe { + status: ev.status(), + reason: ev.reason(), + }) } WifiEvent::IndividualTargetWakeTimeSuspend => { - Some(EventInfo::IndividualTargetWakeTimeSuspend) + let ev = + unsafe { IndividualTargetWakeTimeSuspend::from_raw_event_data(payload) }; + Some(EventInfo::IndividualTargetWakeTimeSuspend { + status: ev.status(), + suspended_flows: EnumSet::from_repr(ev.flow_id_bitmap()), + actual_suspend_time_ms: ev.actual_suspend_time_ms(), + }) } WifiEvent::TargetWakeTimeWakeup => { - Some(EventInfo::TargetWakeTimeWakeup) + let ev = unsafe { TargetWakeTimeWakeup::from_raw_event_data(payload) }; + Some(EventInfo::TargetWakeTimeWakeup { + twt_type: ev.twt_type(), + flow_id: crate::wifi::twt::FlowId::from_raw(ev.flow_id()), + }) } WifiEvent::BroadcastTargetWakeTimeSetup => { Some(EventInfo::BroadcastTargetWakeTimeSetup) diff --git a/esp-radio/src/wifi/mod.rs b/esp-radio/src/wifi/mod.rs index 69c3313b79e..63b3fee6dec 100644 --- a/esp-radio/src/wifi/mod.rs +++ b/esp-radio/src/wifi/mod.rs @@ -47,6 +47,22 @@ //! `WifiController::set_max_tx_power` (requires the `unstable` feature) using a value in the //! range [8, 84]. Note that values above roughly 65 (~16dBm) have been reported to cause //! authentication failures on some hardware, so setting it to the maximum is not always better. +//! +//! ## Power saving +//! +//! The following options are available to reduce Wi-Fi power consumption: +//! +//! **Power Save Mode (PSM)** -- Using [`WifiController::set_power_saving`] to activate a +//! [`PowerSaveMode`] allows the modem to turn off between beacon intervals. This is also known as +//! modem sleep. Applications that send packets frequently may not see any benefit from this +//! however. +#![cfg_attr( + wifi_has_wifi6, + doc = r#" +**Target Wake Time (TWT)** -- On Wi-Fi 6 (802.11ax) networks using [`WifiController::itwt_setup`] to negotiate +an individual TWT can significantly lower power consumption, even at high transmit rates. +"# +)] use alloc::{borrow::ToOwned, collections::vec_deque::VecDeque, str, vec::Vec}; use core::{ @@ -61,8 +77,13 @@ use embassy_sync::{blocking_mutex::raw::NoopRawMutex, waitqueue::GenericAtomicWa use enumset::{EnumSet, EnumSetType}; use esp_config::esp_config_int; use esp_hal::system::Cpu; +#[cfg(any( + wifi_has_wifi6, + all(any(feature = "esp-now", feature = "sniffer"), feature = "unstable") +))] +use esp_hal::time::Duration; #[cfg(all(any(feature = "esp-now", feature = "sniffer"), feature = "unstable"))] -use esp_hal::time::{Duration, Instant}; +use esp_hal::time::Instant; use esp_sync::NonReentrantMutex; use event::EVENT_CHANNEL; use portable_atomic::{AtomicU8, AtomicUsize, Ordering}; @@ -101,6 +122,8 @@ unstable_module!( #[cfg(feature = "sniffer")] #[cfg_attr(docsrs, doc(cfg(feature = "sniffer")))] pub mod sniffer; + #[cfg_attr(not(wifi_has_wifi6), doc(hidden))] + pub mod twt; ); pub mod scan; @@ -809,6 +832,31 @@ impl From<&[u8]> for Ssid { } } +/// Information about a successfully negotiated iTWT agreement. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +#[instability::unstable] +pub struct ITwtSetupInfo { + /// The negotiated iTWT setup configuration (may differ from requested). + pub config: twt::ITwtSetupConfig, + /// TWT service period start time. + pub target_wake_time: u64, +} + +/// Information about a failed iTWT setup. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +#[instability::unstable] +pub struct ITwtSetupFailedInfo { + /// The configuration returned in the failure event. + pub config: twt::ITwtSetupConfig, + /// Setup status code (non-1 value indicates failure). + pub status: i32, + /// Failure reason code. + pub reason: u8, +} static TX_QUEUE_SIZE: AtomicUsize = AtomicUsize::new(0); /// A receive packet queue. @@ -901,6 +949,21 @@ pub enum WifiError { /// Station still in disconnect status. NotConnected, + + /// All TWT flow IDs are in use. + TwtFull, + + /// TWT setup action frame response timed out. + TwtSetupTimeout, + + /// TWT setup action frame transmission failed. + TwtSetupTxFail, + + /// TWT setup was rejected by the AP. + TwtSetupRejected, + + /// iTWT setup failed (the AP responded with a non-success status). + TwtSetupFailed(ITwtSetupFailedInfo), } impl WifiError { @@ -915,6 +978,10 @@ impl WifiError { crate::sys::include::ESP_ERR_WIFI_SSID => WifiError::InvalidSsid, crate::sys::include::ESP_ERR_WIFI_PASSWORD => WifiError::InvalidPassword, crate::sys::include::ESP_ERR_WIFI_NOT_CONNECT => WifiError::NotConnected, + crate::sys::include::ESP_ERR_WIFI_TWT_FULL => WifiError::TwtFull, + crate::sys::include::ESP_ERR_WIFI_TWT_SETUP_TIMEOUT => WifiError::TwtSetupTimeout, + crate::sys::include::ESP_ERR_WIFI_TWT_SETUP_TXFAIL => WifiError::TwtSetupTxFail, + crate::sys::include::ESP_ERR_WIFI_TWT_SETUP_REJECT => WifiError::TwtSetupRejected, _ => panic!("Unknown error code: {}", code), } } @@ -3426,3 +3493,398 @@ ignored." } } } + +#[cfg(wifi_has_wifi6)] +impl WifiController<'_> { + #[procmacros::doc_replace] + /// Negotiate an individual TWT (Target Wake Time) agreement with the AP. + /// + /// The AP may accept, modify, or reject the requested parameters. On + /// success, returns [`ITwtSetupInfo`] with the negotiated configuration. + /// Up to 8 simultaneous agreements are supported. + /// + /// Concurrent calls are safe — each is assigned a unique `twt_id` for + /// correlating the response. + /// + /// ## Example + /// + /// ```rust,no_run + /// # {before_snippet} + /// # use esp_hal::time::Duration; + /// # use esp_radio::wifi::twt::ITwtSetupConfig; + /// # let controller = + /// # esp_radio::wifi::WifiController::new(peripherals.WIFI, Default::default())?; + /// let setup = ITwtSetupConfig::default() + /// .with_wake_interval(Duration::from_millis(500)) + /// .with_min_wake_duration(Duration::from_millis(65)); + /// + /// let info = controller.itwt_setup(setup).await?; + /// println!( + /// "flow_id={:?}, interval={:?}", + /// info.config.flow_id, + /// info.config.wake_interval() + /// ); + /// # {after_snippet} + /// ``` + #[instability::unstable] + pub async fn itwt_setup( + &self, + mut config: twt::ITwtSetupConfig, + ) -> Result { + use portable_atomic::AtomicU16; + /// Monotonic counter for assigning unique `twt_id` values to iTWT setup + /// requests. + static NEXT_TWT_ID: AtomicU16 = AtomicU16::new(0); + + event::enable_wifi_events(WifiEvent::IndividualTargetWakeTimeSetup.into()); + + let mut subscriber = EVENT_CHANNEL + .subscriber() + .expect("Unable to subscribe to events - consider increasing the internal event channel subscriber count"); + + let twt_id = NEXT_TWT_ID.fetch_add(1, Ordering::Relaxed); + config.twt_id = twt_id; + + let mut raw = config.to_raw(); + esp_wifi_result!(unsafe { crate::sys::include::esp_wifi_sta_itwt_setup(&mut raw) })?; + + loop { + let event = subscriber.next_message_pure().await; + if let event::EventInfo::IndividualTargetWakeTimeSetup { + config: negotiated_config, + status, + reason, + target_wake_time, + } = event + { + if negotiated_config.twt_id != twt_id { + continue; + } + if status == 1 { + break Ok(ITwtSetupInfo { + config: negotiated_config, + target_wake_time, + }); + } else { + break Err(WifiError::TwtSetupFailed(ITwtSetupFailedInfo { + config: negotiated_config, + status, + reason, + })); + } + } + } + } + + #[procmacros::doc_replace] + /// Tear down an individual TWT agreement. + /// + /// Use [`twt::FlowTarget::All`] to tear down all active agreements. + /// + /// Concurrent calls must use different flow IDs. Otherwise incorrect + /// results may be returned. + /// + /// ## Example + /// + /// ```rust,no_run + /// # {before_snippet} + /// # use esp_radio::wifi::twt::FlowId; + /// # let controller = + /// # esp_radio::wifi::WifiController::new(peripherals.WIFI, Default::default())?; + /// let status = controller.itwt_teardown(FlowId::Flow0).await?; + /// println!("teardown status: {status:?}"); + /// # {after_snippet} + /// ``` + #[instability::unstable] + pub async fn itwt_teardown( + &self, + target: impl Into, + ) -> Result { + let target = target.into(); + + event::enable_wifi_events(WifiEvent::IndividualTargetWakeTimeTeardown.into()); + + let mut subscriber = EVENT_CHANNEL + .subscriber() + .expect("Unable to subscribe to events - consider increasing the internal event channel subscriber count"); + + esp_wifi_result!(unsafe { + crate::sys::include::esp_wifi_sta_itwt_teardown(target.as_raw() as i32) + })?; + + loop { + let event = subscriber.next_message_pure().await; + if let event::EventInfo::IndividualTargetWakeTimeTeardown { + flow_id: event_flow_id, + status, + } = event + { + match target { + twt::FlowTarget::All => {} + twt::FlowTarget::Id(id) => { + if event_flow_id != id { + continue; + } + } + } + break Ok(status); + } + } + } + + #[procmacros::doc_replace] + /// Suspend an individual TWT agreement for the given duration. + /// + /// The station temporarily stops following the TWT schedule, then + /// automatically resumes. + /// Use [`twt::FlowTarget::All`] to suspend all active agreements. + /// + /// Concurrent calls must use different flow IDs. Otherwise incorrect + /// results may be returned. + /// + /// ## Example + /// + /// ```rust,no_run + /// # {before_snippet} + /// # use esp_hal::time::Duration; + /// # use esp_radio::wifi::twt::FlowId; + /// # let controller = + /// # esp_radio::wifi::WifiController::new(peripherals.WIFI, Default::default())?; + /// // Suspend flow 0 for 5 seconds + /// controller + /// .itwt_suspend(FlowId::Flow0, Duration::from_secs(5)) + /// .await?; + /// # {after_snippet} + /// ``` + #[instability::unstable] + pub async fn itwt_suspend( + &self, + target: impl Into, + suspend_time: Duration, + ) -> Result<(), WifiError> { + let target = target.into(); + + event::enable_wifi_events(WifiEvent::IndividualTargetWakeTimeSuspend.into()); + + let mut subscriber = EVENT_CHANNEL + .subscriber() + .expect("Unable to subscribe to events - consider increasing the internal event channel subscriber count"); + + esp_wifi_result!(unsafe { + crate::sys::include::esp_wifi_sta_itwt_suspend( + target.as_raw() as i32, + suspend_time.as_millis() as i32, + ) + })?; + + loop { + let event = subscriber.next_message_pure().await; + if let event::EventInfo::IndividualTargetWakeTimeSuspend { + status, + suspended_flows, + actual_suspend_time_ms: _, + } = event + { + match target { + twt::FlowTarget::All => {} + twt::FlowTarget::Id(id) => { + if !suspended_flows.contains(id) { + continue; + } + } + } + if status == 0 { + break Ok(()); + } else { + break Err(WifiError::Failed); + } + } + } + } + + #[procmacros::doc_replace] + /// Send a probe request to resynchronize the station's TSF clock with the + /// AP. + /// + /// During iTWT the station misses beacons while sleeping, causing its + /// local clock to drift. Periodic probes keep the clocks aligned and + /// prevent the AP from tearing down the agreement. + /// + /// ## Example + /// + /// ```rust,no_run + /// # {before_snippet} + /// # use esp_hal::time::Duration; + /// # let controller = + /// # esp_radio::wifi::WifiController::new(peripherals.WIFI, Default::default())?; + /// // Send a probe with a 50ms timeout + /// let status = controller + /// .itwt_send_probe(Duration::from_millis(50)) + /// .await?; + /// println!("probe: {status:?}"); + /// # {after_snippet} + /// ``` + #[instability::unstable] + pub async fn itwt_send_probe( + &self, + timeout: Duration, + ) -> Result { + event::enable_wifi_events(WifiEvent::IndividualTargetWakeTimeProbe.into()); + + let mut subscriber = EVENT_CHANNEL + .subscriber() + .expect("Unable to subscribe to events - consider increasing the internal event channel subscriber count"); + + esp_wifi_result!(unsafe { + crate::sys::include::esp_wifi_sta_itwt_send_probe_req(timeout.as_millis() as i32) + })?; + + loop { + let event = subscriber.next_message_pure().await; + if let event::EventInfo::IndividualTargetWakeTimeProbe { status, reason: _ } = event { + break Ok(status); + } + } + } + + #[procmacros::doc_replace] + /// Wait for the next TWT wakeup event on the given flow(s). + /// + /// Returns when the station wakes up for a TWT service period on one + /// of the specified flows, or returns an error if a watched flow is + /// torn down or the station disconnects. + /// + /// [`TwtConfig::post_wakeup_event`](twt::TwtConfig::post_wakeup_event) + /// must be `true` for wakeup events to be generated. + /// + /// For latency-sensitive applications, send outgoing packets in + /// response to this event rather than on an independent timer. The + /// WiFi firmware buffers TX until the next wake window, so a packet + /// queued just after a window closes must wait for the next one. + /// + /// Multiple concurrent callers will each receive every wakeup event. + /// + /// ## Example + /// + /// ```rust,no_run + /// # {before_snippet} + /// # use esp_radio::wifi::twt::FlowId; + /// # let controller = + /// # esp_radio::wifi::WifiController::new(peripherals.WIFI, Default::default())?; + /// loop { + /// match controller.wait_for_next_twt_wakeup(FlowId::Flow0).await { + /// Ok(wakeup) => { + /// println!( + /// "woke up: type={:?}, flow={:?}", + /// wakeup.twt_type, wakeup.flow_id + /// ); + /// // ... do work during the service period ... + /// } + /// Err(e) => { + /// println!("TWT wait failed: {e:?}"); + /// break; + /// } + /// } + /// } + /// # {after_snippet} + /// ``` + #[instability::unstable] + pub async fn wait_for_next_twt_wakeup( + &self, + flows: impl Into>, + ) -> Result { + let flows = flows.into(); + + event::enable_wifi_events( + WifiEvent::TargetWakeTimeWakeup + | WifiEvent::IndividualTargetWakeTimeTeardown + | WifiEvent::StationDisconnected, + ); + + let mut subscriber = EVENT_CHANNEL + .subscriber() + .expect("Unable to subscribe to events - consider increasing the internal event channel subscriber count"); + + if !self.is_connected() { + return Err(twt::TwtWaitError::Disconnected); + } + + if let Ok(active) = self.itwt_flow_id_status() + && active.intersection(flows).is_empty() + { + return Err(twt::TwtWaitError::FlowTornDown { + flow_id: flows.iter().next().unwrap(), + status: twt::ITwtTeardownStatus::Success, + }); + } + + loop { + let event = subscriber.next_message_pure().await; + match event { + event::EventInfo::TargetWakeTimeWakeup { twt_type, flow_id } + if flows.contains(flow_id) => + { + break Ok(twt::TwtWakeupInfo { twt_type, flow_id }); + } + event::EventInfo::IndividualTargetWakeTimeTeardown { flow_id, status } + if flows.contains(flow_id) => + { + break Err(twt::TwtWaitError::FlowTornDown { flow_id, status }); + } + event::EventInfo::StationDisconnected { .. } => { + break Err(twt::TwtWaitError::Disconnected); + } + _ => {} + } + } + } + + #[procmacros::doc_replace] + /// Get the set of active iTWT flow IDs. + /// + /// ## Example + /// + /// ```rust,no_run + /// # {before_snippet} + /// # let controller = + /// # esp_radio::wifi::WifiController::new(peripherals.WIFI, Default::default())?; + /// let active = controller.itwt_flow_id_status()?; + /// println!("active flows: {:?}", active); + /// # {after_snippet} + /// ``` + #[instability::unstable] + pub fn itwt_flow_id_status(&self) -> Result, WifiError> { + let mut bitmap: i32 = 0; + esp_wifi_result!(unsafe { + crate::sys::include::esp_wifi_sta_itwt_get_flow_id_status(&mut bitmap) + })?; + Ok(EnumSet::from_repr(bitmap as u8)) + } + + /// Adjust the wake-up time relative to the negotiated target wake time. + #[instability::unstable] + pub fn itwt_set_target_wake_time_offset(&self, offset_us: u32) -> Result<(), WifiError> { + esp_wifi_result!(unsafe { + crate::sys::include::esp_wifi_sta_itwt_set_target_wake_time_offset(offset_us as i32) + }) + } + + #[procmacros::doc_replace] + /// Apply general TWT configuration. + /// + /// ## Example + /// + /// ```rust,no_run + /// # {before_snippet} + /// # use esp_radio::wifi::twt::TwtConfig; + /// # let controller = + /// # esp_radio::wifi::WifiController::new(peripherals.WIFI, Default::default())?; + /// controller.twt_config(&TwtConfig::default().with_post_wakeup_event(true))?; + /// # {after_snippet} + /// ``` + #[instability::unstable] + pub fn twt_config(&self, config: &twt::TwtConfig) -> Result<(), WifiError> { + let mut raw = config.to_raw(); + esp_wifi_result!(unsafe { crate::sys::include::esp_wifi_sta_twt_config(&mut raw) }) + } +} diff --git a/esp-radio/src/wifi/twt.rs b/esp-radio/src/wifi/twt.rs new file mode 100644 index 00000000000..8a41cb78aef --- /dev/null +++ b/esp-radio/src/wifi/twt.rs @@ -0,0 +1,594 @@ +//! Target Wake Time (TWT) types and configuration. +//! +//! TWT is a Wi-Fi 6 (802.11ax) feature that allows a station to negotiate +//! wake intervals with an access point, enabling significant power savings. +//! +//! This module provides types for configuring individual TWT (iTWT) agreements. +//! +//! # Packet buffering +//! +//! When TWT is active, WiFi firmware buffers outgoing packets and only transmits +//! them during TWT wake windows. Sending packets outside a wake window does +//! **not** immediately wake up the modem -- the packet is held until the next +//! scheduled window. +//! +//! For best latency, send packets in response to +//! [`WifiController::wait_for_next_twt_wakeup`](crate::wifi::WifiController::wait_for_next_twt_wakeup) +//! events. An independent timer can desync from TWT wakeups, causing +//! packets to miss their window. +//! +//! # Multiple flows +//! +//! Up to 8 simultaneous iTWT flows (flow IDs 0–7) can be active. +//! Whenever any flow's wake window opens, **all** buffered packets are flushed. + +use core::fmt; + +use enumset::EnumSetType; +use esp_hal::time::Duration; +use procmacros::BuilderLite; + +#[cfg(wifi_has_wifi6)] +use crate::sys::include::wifi_twt_config_t; +use crate::sys::include::{ + wifi_itwt_probe_status_t, + wifi_itwt_probe_status_t_ITWT_PROBE_FAIL, + wifi_itwt_probe_status_t_ITWT_PROBE_STA_DISCONNECTED, + wifi_itwt_probe_status_t_ITWT_PROBE_SUCCESS, + wifi_itwt_probe_status_t_ITWT_PROBE_TIMEOUT, + wifi_itwt_teardown_status_t, + wifi_itwt_teardown_status_t_ITWT_TEARDOWN_FAIL, + wifi_itwt_teardown_status_t_ITWT_TEARDOWN_SUCCESS, + wifi_twt_setup_cmds_t, + wifi_twt_setup_cmds_t_TWT_ACCEPT, + wifi_twt_setup_cmds_t_TWT_ALTERNATE, + wifi_twt_setup_cmds_t_TWT_DEMAND, + wifi_twt_setup_cmds_t_TWT_DICTATE, + wifi_twt_setup_cmds_t_TWT_GROUPING, + wifi_twt_setup_cmds_t_TWT_REJECT, + wifi_twt_setup_cmds_t_TWT_REQUEST, + wifi_twt_setup_cmds_t_TWT_SUGGEST, + wifi_twt_setup_config_t, + wifi_twt_type_t, + wifi_twt_type_t_TWT_TYPE_BROADCAST, + wifi_twt_type_t_TWT_TYPE_INDIVIDUAL, +}; + +/// TWT setup command type. +/// +/// Indicates the type of TWT command used during negotiation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[instability::unstable] +pub enum TwtSetupCommand { + /// Station requests a TWT agreement. + Request = 0, + /// Station suggests TWT parameters. + Suggest = 1, + /// Station demands specific TWT parameters. + Demand = 2, + /// TWT grouping. + Grouping = 3, + /// AP accepts the TWT agreement. + Accept = 4, + /// AP suggests alternate TWT parameters. + Alternate = 5, + /// AP dictates TWT parameters. + Dictate = 6, + /// AP rejects the TWT agreement. + Reject = 7, +} + +impl TwtSetupCommand { + #[allow(non_upper_case_globals)] + fn from_raw(val: wifi_twt_setup_cmds_t) -> Self { + match val { + wifi_twt_setup_cmds_t_TWT_REQUEST => Self::Request, + wifi_twt_setup_cmds_t_TWT_SUGGEST => Self::Suggest, + wifi_twt_setup_cmds_t_TWT_DEMAND => Self::Demand, + wifi_twt_setup_cmds_t_TWT_GROUPING => Self::Grouping, + wifi_twt_setup_cmds_t_TWT_ACCEPT => Self::Accept, + wifi_twt_setup_cmds_t_TWT_ALTERNATE => Self::Alternate, + wifi_twt_setup_cmds_t_TWT_DICTATE => Self::Dictate, + wifi_twt_setup_cmds_t_TWT_REJECT => Self::Reject, + _ => panic!("Invalid TWT setup command: {}", val), + } + } +} + +/// TWT flow type. +/// +/// Determines whether the station must announce its wakeup to the AP. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[instability::unstable] +pub enum TwtFlowType { + /// The station sends a trigger frame to the AP when it wakes up. + Announced = 0, + /// The station does not need to announce its wakeup. + Unannounced = 1, +} + +/// Unit for the minimum wake duration field. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[instability::unstable] +pub enum TwtWakeDurationUnit { + /// 256 microseconds per unit. + Us256 = 0, + /// 1 TU (1024 microseconds) per unit. + Tu = 1, +} + +/// A TWT flow identifier (0-7). +/// +/// Each individual TWT agreement is identified by a flow ID in the range 0-7. +#[derive(Debug, EnumSetType, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[instability::unstable] +#[enumset(repr = "u8")] +pub enum FlowId { + /// Flow 0. + Flow0 = 0, + /// Flow 1. + Flow1 = 1, + /// Flow 2. + Flow2 = 2, + /// Flow 3. + Flow3 = 3, + /// Flow 4. + Flow4 = 4, + /// Flow 5. + Flow5 = 5, + /// Flow 6. + Flow6 = 6, + /// Flow 7. + Flow7 = 7, +} + +impl FlowId { + /// Return the numeric flow ID (0-7). + #[instability::unstable] + pub fn as_u8(self) -> u8 { + self as u8 + } + + pub(crate) fn from_raw(val: u8) -> Self { + match val { + 0 => Self::Flow0, + 1 => Self::Flow1, + 2 => Self::Flow2, + 3 => Self::Flow3, + 4 => Self::Flow4, + 5 => Self::Flow5, + 6 => Self::Flow6, + 7 => Self::Flow7, + _ => panic!("Invalid TWT flow ID: {}", val), + } + } +} + +/// Target for a TWT operation — either a specific flow or all active flows. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[instability::unstable] +pub enum FlowTarget { + /// A single flow identified by its [`FlowId`]. + Id(FlowId), + /// All active flows. + All, +} + +impl From for FlowTarget { + fn from(id: FlowId) -> Self { + FlowTarget::Id(id) + } +} + +impl FlowTarget { + #[cfg(wifi_has_wifi6)] + pub(crate) fn as_raw(self) -> u8 { + match self { + FlowTarget::Id(id) => id.as_u8(), + FlowTarget::All => 8, + } + } +} + +#[procmacros::doc_replace] +/// Configuration for an individual TWT (iTWT) setup. +/// +/// The recommended way to create a configuration is to start from +/// [`ITwtSetupConfig::default`] and use the builder methods: +/// +/// ```rust,no_run +/// # {before_snippet} +/// # use core::default::Default; +/// # use esp_hal::time::Duration; +/// # use esp_radio::wifi::twt::*; +/// let config = ITwtSetupConfig::default() +/// .with_setup_cmd(TwtSetupCommand::Request) +/// .with_trigger(true) +/// .with_flow_type(TwtFlowType::Announced) +/// .with_wake_interval(Duration::from_millis(20)) +/// .with_min_wake_duration(Duration::from_micros(2048)) +/// .with_timeout(Duration::from_secs(5)); +/// # {after_snippet} +/// ``` +#[derive(BuilderLite, Clone, Copy, PartialEq, Eq, Hash)] +#[instability::unstable] +pub struct ITwtSetupConfig { + /// The type of TWT command. + pub setup_cmd: TwtSetupCommand, + /// Whether this is a trigger-enabled TWT. + pub trigger: bool, + /// The flow type (announced or unannounced). + pub flow_type: TwtFlowType, + /// The flow ID. + /// + /// The value in the request is typically ignored. + /// The actual flow ID assigned by the AP must be obtained from the response. + #[builder_lite(skip_setter)] + pub flow_id: FlowId, + /// Internal correlation ID used to match setup responses to requests. + /// + /// Automatically assigned by + /// [`WifiController::itwt_setup`](crate::wifi::WifiController::itwt_setup). + #[builder_lite(skip_setter)] + pub twt_id: u16, + /// The wake interval exponent (set via + /// [`with_wake_interval`](Self::with_wake_interval)). + #[builder_lite(skip_setter)] + pub wake_interval_exponent: u8, + /// The unit for the minimum wake duration (set via + /// [`with_min_wake_duration`](Self::with_min_wake_duration)). + #[builder_lite(skip_setter)] + pub wake_duration_unit: TwtWakeDurationUnit, + /// Nominal minimum wake duration in units of + /// [`wake_duration_unit`](Self::wake_duration_unit) (set via + /// [`with_min_wake_duration`](Self::with_min_wake_duration)). + #[builder_lite(skip_setter)] + pub min_wake_duration: u8, + /// Individual TWT wake interval mantissa (set via + /// [`with_wake_interval`](Self::with_wake_interval)). + #[builder_lite(skip_setter)] + pub wake_interval_mantissa: u16, + /// Timeout for receiving the setup action frame response. + pub timeout: Duration, +} + +impl Default for ITwtSetupConfig { + fn default() -> Self { + Self { + setup_cmd: TwtSetupCommand::Request, + trigger: true, + flow_type: TwtFlowType::Announced, + flow_id: FlowId::Flow0, + twt_id: 0, + wake_interval_exponent: 0, + wake_duration_unit: TwtWakeDurationUnit::Us256, + min_wake_duration: 1, + wake_interval_mantissa: 1, + timeout: Duration::from_millis(5000), + } + } +} + +impl fmt::Debug for ITwtSetupConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ITwtSetupConfig") + .field("setup_cmd", &self.setup_cmd) + .field("trigger", &self.trigger) + .field("flow_type", &self.flow_type) + .field("flow_id", &self.flow_id) + .field("wake_interval", &self.wake_interval()) + .field("wake_duration", &self.wake_duration()) + .field("timeout", &self.timeout) + .finish() + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for ITwtSetupConfig { + fn format(&self, fmt: defmt::Formatter<'_>) { + defmt::write!( + fmt, + "ITwtSetupConfig {{ \ + setup_cmd: {}, trigger: {}, flow_type: {}, flow_id: {}, \ + wake_interval: {}, wake_duration: {}, \ + timeout: {} }}", + self.setup_cmd, + self.trigger, + self.flow_type, + self.flow_id, + self.wake_interval(), + self.wake_duration(), + self.timeout, + ); + } +} + +impl ITwtSetupConfig { + /// Set the TWT wake interval. + /// + /// This is how often the station wakes up. The protocol encodes this as + /// `mantissa * 2^exponent` microseconds; this method picks the optimal + /// encoding automatically. + /// + /// The interval must be nonzero. Furthermore, very small or large values + /// are rejected by the firmware as well. + /// Some APs have also been found to silently mangle very large (hours) values. + #[instability::unstable] + pub fn with_wake_interval(mut self, interval: Duration) -> Self { + let us = interval.as_micros(); + debug_assert!(us > 0, "wake interval must be > 0"); + + let mut exponent = 0u8; + let mut mantissa = us; + while mantissa > 65535 && exponent < 31 { + exponent += 1; + mantissa = us >> exponent; + } + + self.wake_interval_mantissa = mantissa as u16; + self.wake_interval_exponent = exponent; + self + } + + /// Set the nominal minimum wake duration. + /// + /// This is the minimum time the station stays awake during each TWT + /// service period. The protocol encodes this in 256µs or 1024µs units; + /// this method picks the best unit automatically. + /// + /// The duration must be nonzero. Furthermore, very small or large values + /// are rejected by the firmware as well. + /// Some APs have also been found to silently mangle the unit, effectively limiting + /// this to a maximum duration of approximately 65ms. + #[instability::unstable] + pub fn with_min_wake_duration(mut self, duration: Duration) -> Self { + let us = duration.as_micros(); + debug_assert!(us > 0, "wake duration must be > 0"); + + // Try 256µs units first (finer granularity) + let units_256 = us.div_ceil(256); + if units_256 <= 255 { + self.wake_duration_unit = TwtWakeDurationUnit::Us256; + self.min_wake_duration = units_256 as u8; + } else { + // Fall back to TU (1024µs) units + let units_tu = us.div_ceil(1024); + self.wake_duration_unit = TwtWakeDurationUnit::Tu; + debug_assert!( + units_tu <= 255, + "wake duration exceeds maximum possible value" + ); + self.min_wake_duration = units_tu.min(255) as u8; + } + self + } + + /// The negotiated wake interval as a [`Duration`]. + #[instability::unstable] + pub fn wake_interval(&self) -> Duration { + let us = (self.wake_interval_mantissa as u64) << self.wake_interval_exponent; + Duration::from_micros(us) + } + + /// The nominal minimum wake duration as a [`Duration`]. + #[instability::unstable] + pub fn wake_duration(&self) -> Duration { + let us = match self.wake_duration_unit { + TwtWakeDurationUnit::Us256 => self.min_wake_duration as u64 * 256, + TwtWakeDurationUnit::Tu => self.min_wake_duration as u64 * 1024, + }; + Duration::from_micros(us) + } + + #[cfg(wifi_has_wifi6)] + pub(crate) fn to_raw(self) -> wifi_twt_setup_config_t { + debug_assert!(self.wake_interval_exponent <= 31); + debug_assert!(self.min_wake_duration >= 1); + debug_assert!(self.wake_interval_mantissa >= 1); + let raw = wifi_twt_setup_config_t { + setup_cmd: self.setup_cmd as wifi_twt_setup_cmds_t, + _bitfield_align_1: Default::default(), + _bitfield_1: wifi_twt_setup_config_t::new_bitfield_1( + self.trigger as u16, + self.flow_type as u16, + self.flow_id.as_u8() as u16, + self.wake_interval_exponent as u16, + self.wake_duration_unit as u16, + 0, // reserved + ), + min_wake_dura: self.min_wake_duration, + wake_invl_mant: self.wake_interval_mantissa, + twt_id: self.twt_id, + timeout_time_ms: self.timeout.as_millis() as u16, + }; + trace!( + "to_raw: setup_cmd={} trigger={} flow_type={} flow_id={} wake_invl_expn={} wake_duration_unit={} min_wake_dura={} wake_invl_mant={} twt_id={} timeout_time_ms={}", + raw.setup_cmd, + raw.trigger(), + raw.flow_type(), + raw.flow_id(), + raw.wake_invl_expn(), + raw.wake_duration_unit(), + raw.min_wake_dura, + raw.wake_invl_mant, + raw.twt_id, + raw.timeout_time_ms + ); + raw + } + + pub(crate) fn from_raw(raw: &wifi_twt_setup_config_t) -> Self { + trace!( + "from_raw: setup_cmd={} trigger={} flow_type={} flow_id={} wake_invl_expn={} wake_duration_unit={} min_wake_dura={} wake_invl_mant={} twt_id={} timeout_time_ms={}", + raw.setup_cmd, + raw.trigger(), + raw.flow_type(), + raw.flow_id(), + raw.wake_invl_expn(), + raw.wake_duration_unit(), + raw.min_wake_dura, + raw.wake_invl_mant, + raw.twt_id, + raw.timeout_time_ms + ); + Self { + setup_cmd: TwtSetupCommand::from_raw(raw.setup_cmd), + trigger: raw.trigger() != 0, + flow_type: if raw.flow_type() != 0 { + TwtFlowType::Unannounced + } else { + TwtFlowType::Announced + }, + flow_id: FlowId::from_raw(raw.flow_id() as u8), + twt_id: raw.twt_id, + wake_interval_exponent: raw.wake_invl_expn() as u8, + wake_duration_unit: if raw.wake_duration_unit() != 0 { + TwtWakeDurationUnit::Tu + } else { + TwtWakeDurationUnit::Us256 + }, + min_wake_duration: raw.min_wake_dura, + wake_interval_mantissa: raw.wake_invl_mant, + timeout: Duration::from_millis(raw.timeout_time_ms as u64), + } + } +} + +/// Status of an iTWT teardown operation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[instability::unstable] +pub enum ITwtTeardownStatus { + /// Teardown failed (station failed to send teardown frame). + Fail = 0, + /// Teardown succeeded (station sent teardown frame or received one from AP). + Success = 1, +} + +impl ITwtTeardownStatus { + #[allow(non_upper_case_globals)] + pub(crate) fn from_raw(val: wifi_itwt_teardown_status_t) -> Self { + match val { + wifi_itwt_teardown_status_t_ITWT_TEARDOWN_SUCCESS => Self::Success, + wifi_itwt_teardown_status_t_ITWT_TEARDOWN_FAIL => Self::Fail, + _ => panic!("Invalid iTWT teardown status: {}", val), + } + } +} + +/// Status of an iTWT probe operation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[instability::unstable] +pub enum ITwtProbeStatus { + /// Probe failed. + Fail = 0, + /// Probe succeeded. + Success = 1, + /// Probe timed out. + Timeout = 2, + /// Station disconnected during probe. + Disconnected = 3, +} + +impl ITwtProbeStatus { + #[allow(non_upper_case_globals)] + pub(crate) fn from_raw(val: wifi_itwt_probe_status_t) -> Self { + match val { + wifi_itwt_probe_status_t_ITWT_PROBE_SUCCESS => Self::Success, + wifi_itwt_probe_status_t_ITWT_PROBE_TIMEOUT => Self::Timeout, + wifi_itwt_probe_status_t_ITWT_PROBE_STA_DISCONNECTED => Self::Disconnected, + wifi_itwt_probe_status_t_ITWT_PROBE_FAIL => Self::Fail, + _ => panic!("Invalid iTWT probe status: {}", val), + } + } +} + +/// TWT type (individual or broadcast). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[instability::unstable] +pub enum TwtType { + /// Individual TWT agreement. + Individual = 0, + /// Broadcast TWT agreement. + Broadcast = 1, +} + +impl TwtType { + #[allow(non_upper_case_globals)] + pub(crate) fn from_raw(val: wifi_twt_type_t) -> Self { + match val { + wifi_twt_type_t_TWT_TYPE_BROADCAST => Self::Broadcast, + wifi_twt_type_t_TWT_TYPE_INDIVIDUAL => Self::Individual, + _ => panic!("Invalid TWT type: {}", val), + } + } +} + +/// General TWT configuration options. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, BuilderLite)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[instability::unstable] +pub struct TwtConfig { + /// Whether to post a wakeup event when TWT wakes up. + post_wakeup_event: bool, + /// Whether to send QoS Null frames to keep the connection alive. + enable_keep_alive: bool, +} + +#[cfg(wifi_has_wifi6)] +impl TwtConfig { + pub(crate) fn to_raw(self) -> wifi_twt_config_t { + wifi_twt_config_t { + post_wakeup_event: self.post_wakeup_event, + twt_enable_keep_alive: self.enable_keep_alive, + } + } +} + +/// Information about a TWT wakeup event. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[instability::unstable] +pub struct TwtWakeupInfo { + /// The TWT type (individual or broadcast). + pub twt_type: TwtType, + /// The flow ID that woke up. + pub flow_id: FlowId, +} + +/// Error returned by +/// [`WifiController::wait_for_next_twt_wakeup`](crate::wifi::WifiController::wait_for_next_twt_wakeup). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[instability::unstable] +pub enum TwtWaitError { + /// An active TWT flow was torn down (by the station or the AP). + FlowTornDown { + /// The flow that was torn down. + flow_id: FlowId, + /// Teardown status. + status: ITwtTeardownStatus, + }, + /// The station disconnected from the AP. + Disconnected, +} + +impl fmt::Display for TwtWaitError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::FlowTornDown { flow_id, .. } => { + write!(f, "TWT flow {:?} torn down", flow_id) + } + Self::Disconnected => write!(f, "station disconnected"), + } + } +} + +impl core::error::Error for TwtWaitError {} diff --git a/examples/wifi/embassy_twt/.cargo/config.toml b/examples/wifi/embassy_twt/.cargo/config.toml new file mode 100644 index 00000000000..30a70482c08 --- /dev/null +++ b/examples/wifi/embassy_twt/.cargo/config.toml @@ -0,0 +1,14 @@ +[target.'cfg(target_arch = "riscv32")'] +runner = "espflash flash --monitor" +rustflags = [ + "-C", "link-arg=-Tlinkall.x", + "-C", "force-frame-pointers", +] + +[env] +ESP_LOG = "info" +SSID = "SSID" +PASSWORD = "PASSWORD" + +[unstable] +build-std = ["alloc", "core"] diff --git a/examples/wifi/embassy_twt/Cargo.toml b/examples/wifi/embassy_twt/Cargo.toml new file mode 100644 index 00000000000..97bd7c57a8f --- /dev/null +++ b/examples/wifi/embassy_twt/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "embassy-twt" +version = "0.0.0" +edition = "2024" +publish = false + +[dependencies] +embassy-executor = "0.10.0" +embassy-time = "0.5.0" +enumset = "1.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 = { path = "../../../esp-hal", features = ["log-04", "unstable"] } +esp-println = { path = "../../../esp-println", features = ["log-04"] } +esp-rtos = { path = "../../../esp-rtos", features = ["esp-radio", "embassy", "log-04"] } +esp-radio = { path = "../../../esp-radio", features = [ + "log-04", + "wifi", + "unstable", +] } + +[features] +esp32c5 = [ + "esp-backtrace/esp32c5", + "esp-bootloader-esp-idf/esp32c5", + "esp-hal/esp32c5", + "esp-rtos/esp32c5", + "esp-radio/esp32c5", +] +esp32c6 = [ + "esp-backtrace/esp32c6", + "esp-bootloader-esp-idf/esp32c6", + "esp-hal/esp32c6", + "esp-rtos/esp32c6", + "esp-radio/esp32c6", +] +esp32c61 = [ + "esp-backtrace/esp32c61", + "esp-bootloader-esp-idf/esp32c61", + "esp-hal/esp32c61", + "esp-rtos/esp32c61", + "esp-radio/esp32c61", +] + +[profile.release] +debug = true +debug-assertions = true +lto = "fat" +codegen-units = 1 diff --git a/examples/wifi/embassy_twt/src/main.rs b/examples/wifi/embassy_twt/src/main.rs new file mode 100644 index 00000000000..e7dff74d950 --- /dev/null +++ b/examples/wifi/embassy_twt/src/main.rs @@ -0,0 +1,246 @@ +//% CHIP_FILTER: wifi_has_wifi6 +//! Embassy iTWT (Individual Target Wake Time) Example +//! +//! Set SSID and PASSWORD env variable before running this example. +//! +//! Demonstrates multiple concurrent TWT flows: sets up three flows with +//! different intervals, tears one down, suspends another, then settles +//! into a single-flow loop with periodic TSF probes. +//! +//! The AP must support 802.11ax (Wi-Fi 6) for TWT to work. + +#![no_std] +#![no_main] + +use embassy_time::Timer; +use esp_alloc as _; +use esp_backtrace as _; +use esp_hal::{ + clock::CpuClock, + interrupt::software::SoftwareInterruptControl, + ram, + time::{Duration, Instant}, + timer::timg::TimerGroup, +}; + +macro_rules! tprintln { + ($($arg:tt)*) => {{ + let t = Instant::now().duration_since_epoch(); + let secs = t.as_millis() / 1000; + let ms = t.as_millis() % 1000; + esp_println::println!("[{:>5}.{:03}] {}", secs, ms, format_args!($($arg)*)); + }}; +} +use enumset::EnumSet; +use esp_radio::wifi::{ + Config, + ControllerConfig, + PowerSaveMode, + Protocol, + Protocols, + sta::StationConfig, + twt::{FlowId, ITwtSetupConfig, TwtConfig}, +}; + +esp_bootloader_esp_idf::esp_app_desc!(); + +const SSID: &str = env!("SSID"); +const PASSWORD: &str = env!("PASSWORD"); + +fn print_flow_status(controller: &esp_radio::wifi::WifiController) { + match controller.itwt_flow_id_status() { + Ok(active) => tprintln!("Active flows: {:?}", active), + Err(e) => tprintln!("Failed to get flow status: {e:?}"), + } +} + +/// Wait for a few wakeup events across all active flows. +async fn collect_wakeups(controller: &esp_radio::wifi::WifiController<'_>, count: u32) { + for i in 0..count { + let wakeup = controller + .wait_for_next_twt_wakeup(EnumSet::::all()) + .await + .unwrap(); + tprintln!( + " wakeup {}/{}: type={:?}, flow={:?}", + i + 1, + count, + wakeup.twt_type, + wakeup.flow_id + ); + } +} + +#[esp_hal::main] +async fn main(_spawner: embassy_executor::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); + + esp_alloc::heap_allocator!(#[ram(reclaimed)] size: 64 * 1024); + esp_alloc::heap_allocator!(size: 36 * 1024); + + let timg0 = TimerGroup::new(peripherals.TIMG0); + let sw_int = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT); + esp_rtos::start(timg0.timer0, sw_int.software_interrupt0); + + let station_config = Config::Station( + StationConfig::default() + .with_ssid(SSID) + .with_password(PASSWORD.into()) + .with_protocols(Protocols::default().with_2_4(Protocol::AX.into())), + ); + + tprintln!("Starting wifi"); + let mut controller = esp_radio::wifi::WifiController::new( + peripherals.WIFI, + ControllerConfig::default().with_initial_config(station_config), + ) + .unwrap(); + + loop { + tprintln!("Connecting..."); + match controller.connect_async().await { + Ok(info) => { + tprintln!("Wifi connected: {:?}", info); + break; + } + Err(e) => { + tprintln!("Failed to connect: {e:?}, retrying in 5s"); + Timer::after(embassy_time::Duration::from_secs(5)).await; + } + } + } + + controller + .twt_config(&TwtConfig::default().with_post_wakeup_event(true)) + .unwrap(); + controller.set_power_saving(PowerSaveMode::Minimum).unwrap(); + + // --- Set up three flows with different intervals --- + + tprintln!("\n=== Setting up flow A (500ms interval, 65ms wake) ==="); + let flow_a = controller + .itwt_setup( + ITwtSetupConfig::default() + .with_trigger(false) + .with_wake_interval(Duration::from_millis(500)) + .with_min_wake_duration(Duration::from_millis(65)), + ) + .await; + match &flow_a { + Ok(info) => tprintln!("Flow A: {:?}", info.config), + Err(e) => tprintln!("Flow A FAILED: {e:?}"), + } + let flow_a_id = flow_a.as_ref().map(|i| i.config.flow_id).ok(); + print_flow_status(&controller); + + Timer::after(embassy_time::Duration::from_millis(200)).await; + tprintln!("\n=== Setting up flow B (1s interval, 10ms wake) ==="); + let flow_b = controller + .itwt_setup( + ITwtSetupConfig::default() + .with_wake_interval(Duration::from_secs(1)) + .with_min_wake_duration(Duration::from_millis(10)), + ) + .await; + match &flow_b { + Ok(info) => tprintln!("Flow B: {:?}", info.config), + Err(e) => tprintln!("Flow B FAILED: {e:?}"), + } + let flow_b_id = flow_b.as_ref().map(|i| i.config.flow_id).ok(); + print_flow_status(&controller); + + Timer::after(embassy_time::Duration::from_millis(200)).await; + tprintln!("\n=== Setting up flow C (2s interval, 20ms wake) ==="); + let flow_c = controller + .itwt_setup( + ITwtSetupConfig::default() + .with_wake_interval(Duration::from_secs(2)) + .with_min_wake_duration(Duration::from_millis(20)), + ) + .await; + match &flow_c { + Ok(info) => tprintln!("Flow C: {:?}", info.config), + Err(e) => tprintln!("Flow C FAILED: {e:?}"), + } + let flow_c_id = flow_c.as_ref().map(|i| i.config.flow_id).ok(); + print_flow_status(&controller); + + // --- Observe wakeups from all three flows --- + + tprintln!("\n=== Collecting wakeups from all three flows ==="); + collect_wakeups(&controller, 15).await; + + // --- Tear down flow B --- + + if let Some(id) = flow_b_id { + tprintln!("\n=== Tearing down flow B ({id:?}) ==="); + match controller.itwt_teardown(id).await { + Ok(status) => tprintln!("Teardown B: {status:?}"), + Err(e) => tprintln!("Teardown B failed: {e:?}"), + } + print_flow_status(&controller); + + tprintln!("\n=== Collecting wakeups (A + C only) ==="); + collect_wakeups(&controller, 10).await; + } + + // --- Suspend flow A for 10 seconds --- + + if let Some(id) = flow_a_id { + tprintln!("\n=== Suspending flow A ({id:?}) for 10s ==="); + match controller.itwt_suspend(id, Duration::from_secs(10)).await { + Ok(()) => tprintln!("Suspend A: OK"), + Err(e) => tprintln!("Suspend A failed: {e:?}"), + } + print_flow_status(&controller); + + tprintln!("\n=== Collecting wakeups during suspension (C only expected) ==="); + collect_wakeups(&controller, 8).await; + + tprintln!("\n=== Flow A should have resumed by now ==="); + print_flow_status(&controller); + + tprintln!("\n=== Collecting wakeups (A + C again) ==="); + collect_wakeups(&controller, 10).await; + } + + // --- Tear down flow C, leaving only flow A --- + + if let Some(id) = flow_c_id { + tprintln!("\n=== Tearing down flow C ({id:?}) ==="); + match controller.itwt_teardown(id).await { + Ok(status) => tprintln!("Teardown C: {status:?}"), + Err(e) => tprintln!("Teardown C failed: {e:?}"), + } + print_flow_status(&controller); + } + + // --- Single-flow loop with periodic probes --- + + tprintln!("\n=== Entering single-flow loop with periodic probes ==="); + print_flow_status(&controller); + + let mut wakeup_count: u32 = 0; + loop { + let wakeup = controller + .wait_for_next_twt_wakeup(EnumSet::::all()) + .await + .unwrap(); + wakeup_count += 1; + tprintln!( + "TWT wakeup #{}: type={:?}, flow={:?}", + wakeup_count, + wakeup.twt_type, + wakeup.flow_id + ); + + if wakeup_count % 20 == 0 { + match controller.itwt_send_probe(Duration::from_millis(50)).await { + Ok(status) => tprintln!("TSF probe: {status:?}"), + Err(e) => tprintln!("TSF probe failed: {e:?}"), + } + } + } +} diff --git a/examples/wifi/embassy_twt_udp/.cargo/config.toml b/examples/wifi/embassy_twt_udp/.cargo/config.toml new file mode 100644 index 00000000000..7a0eefe8322 --- /dev/null +++ b/examples/wifi/embassy_twt_udp/.cargo/config.toml @@ -0,0 +1,15 @@ +[target.'cfg(target_arch = "riscv32")'] +runner = "espflash flash --monitor" +rustflags = [ + "-C", "link-arg=-Tlinkall.x", + "-C", "force-frame-pointers", +] + +[env] +ESP_LOG = "info" +SSID = "SSID" +PASSWORD = "PASSWORD" +TARGET_IP = "255.255.255.255" + +[unstable] +build-std = ["alloc", "core"] diff --git a/examples/wifi/embassy_twt_udp/Cargo.toml b/examples/wifi/embassy_twt_udp/Cargo.toml new file mode 100644 index 00000000000..f287be25a67 --- /dev/null +++ b/examples/wifi/embassy_twt_udp/Cargo.toml @@ -0,0 +1,58 @@ +[package] +name = "embassy-twt-udp" +version = "0.0.0" +edition = "2024" +publish = false + +[dependencies] +embassy-executor = "0.10.0" +embassy-net = { version = "0.9.0", features = [ + "dhcpv4", + "medium-ethernet", + "udp", +] } +embassy-time = "0.5.0" +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 = { path = "../../../esp-hal", features = ["log-04", "unstable"] } +esp-println = { path = "../../../esp-println", features = ["log-04"] } +esp-rtos = { path = "../../../esp-rtos", features = ["esp-radio", "embassy", "log-04"] } +esp-radio = { path = "../../../esp-radio", features = [ + "log-04", + "wifi", + "unstable", +] } +static_cell = "2.1.0" + +[features] +esp32c5 = [ + "esp-backtrace/esp32c5", + "esp-bootloader-esp-idf/esp32c5", + "esp-hal/esp32c5", + "esp-rtos/esp32c5", + "esp-radio/esp32c5", +] +esp32c6 = [ + "esp-backtrace/esp32c6", + "esp-bootloader-esp-idf/esp32c6", + "esp-hal/esp32c6", + "esp-rtos/esp32c6", + "esp-radio/esp32c6", +] +esp32c61 = [ + "esp-backtrace/esp32c61", + "esp-bootloader-esp-idf/esp32c61", + "esp-hal/esp32c61", + "esp-rtos/esp32c61", + "esp-radio/esp32c61", +] + +[profile.release] +debug = true +debug-assertions = true +lto = "fat" +codegen-units = 1 diff --git a/examples/wifi/embassy_twt_udp/src/main.rs b/examples/wifi/embassy_twt_udp/src/main.rs new file mode 100644 index 00000000000..773ff2027ee --- /dev/null +++ b/examples/wifi/embassy_twt_udp/src/main.rs @@ -0,0 +1,184 @@ +//% CHIP_FILTER: wifi_has_wifi6 +//! Embassy iTWT + UDP Voice-over-WiFi Example +//! +//! Set SSID, PASSWORD, and TARGET_IP env variables before running this +//! example (defaults are in `.cargo/config.toml`). +//! +//! Simulates a voice-over-WiFi scenario: connects to a Wi-Fi 6 AP, +//! negotiates a 20ms iTWT wake interval, and sends a 160-byte UDP +//! packet (simulating a G.711 voice frame) on every TWT wakeup. +//! +//! The AP must support 802.11ax (Wi-Fi 6) for TWT to work. +//! +//! Listen for packets with: `socat UDP-LISTEN:4444,fork,reuseaddr -` + +#![no_std] +#![no_main] + +use embassy_executor::Spawner; +use embassy_net::{Runner, StackResources, udp::UdpSocket}; +use embassy_time::Timer; +use esp_alloc as _; +use esp_backtrace as _; +use esp_hal::{ + clock::CpuClock, + interrupt::software::SoftwareInterruptControl, + ram, + rng::Rng, + time::Duration, + timer::timg::TimerGroup, +}; +use esp_println::println; +use esp_radio::wifi::{ + Config, + ControllerConfig, + Interface, + PowerSaveMode, + Protocol, + Protocols, + sta::StationConfig, + twt::{ITwtSetupConfig, TwtConfig}, +}; + +esp_bootloader_esp_idf::esp_app_desc!(); + +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 TARGET_IP: &str = env!("TARGET_IP"); +const UDP_PORT: u16 = 4444; + +#[esp_hal::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); + + esp_alloc::heap_allocator!(#[ram(reclaimed)] size: 64 * 1024); + esp_alloc::heap_allocator!(size: 36 * 1024); + + let timg0 = TimerGroup::new(peripherals.TIMG0); + let sw_int = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT); + esp_rtos::start(timg0.timer0, sw_int.software_interrupt0); + + let station_config = Config::Station( + StationConfig::default() + .with_ssid(SSID) + .with_password(PASSWORD.into()) + .with_protocols(Protocols::default().with_2_4(Protocol::AX.into())), + ); + + println!("Starting wifi"); + let wifi_interface = esp_radio::wifi::Interface::station(); + let mut controller = esp_radio::wifi::WifiController::new( + peripherals.WIFI, + ControllerConfig::default().with_initial_config(station_config), + ) + .unwrap(); + + let rng = Rng::new(); + let seed = (rng.random() as u64) << 32 | rng.random() as u64; + + let (stack, runner) = embassy_net::new( + wifi_interface, + embassy_net::Config::dhcpv4(Default::default()), + mk_static!(StackResources<3>, StackResources::<3>::new()), + seed, + ); + + spawner.spawn(net_task(runner).unwrap()); + + loop { + println!("Connecting..."); + match controller.connect_async().await { + Ok(info) => { + println!("Wifi connected: {:?}", info); + break; + } + Err(e) => { + println!("Failed to connect: {e:?}, retrying in 5s"); + Timer::after(embassy_time::Duration::from_secs(5)).await; + } + } + } + + stack.wait_config_up().await; + if let Some(config) = stack.config_v4() { + println!("Got IP: {}", config.address); + } + + controller + .twt_config( + &TwtConfig::default() + .with_post_wakeup_event(true) + .with_enable_keep_alive(true), + ) + .unwrap(); + controller.set_power_saving(PowerSaveMode::Minimum).unwrap(); + + let setup_config = ITwtSetupConfig::default() + .with_wake_interval(Duration::from_micros(20_000)) + .with_min_wake_duration(Duration::from_micros(2048)); + + println!("Requesting iTWT setup..."); + let flow_id = match controller.itwt_setup(setup_config).await { + Ok(info) => { + println!("iTWT setup OK: {:?}", info.config); + info.config.flow_id + } + Err(e) => { + println!("iTWT setup FAILED: {e:?}"); + loop { + Timer::after(embassy_time::Duration::from_secs(60)).await; + } + } + }; + + let target: embassy_net::IpAddress = TARGET_IP.parse().unwrap(); + let target_endpoint = embassy_net::IpEndpoint::new(target, UDP_PORT); + + let mut rx_meta = [embassy_net::udp::PacketMetadata::EMPTY; 1]; + let mut rx_buf = [0u8; 1]; + let mut tx_meta = [embassy_net::udp::PacketMetadata::EMPTY; 2]; + let mut tx_buf = [0u8; 512]; + + let mut socket = UdpSocket::new(stack, &mut rx_meta, &mut rx_buf, &mut tx_meta, &mut tx_buf); + socket.bind(UDP_PORT).unwrap(); + + let mut voice_frame = [0u8; 160]; + let mut wakeup_count: u32 = 0; + + loop { + let wakeup = controller.wait_for_next_twt_wakeup(flow_id).await.unwrap(); + wakeup_count += 1; + + voice_frame[..4].copy_from_slice(&wakeup_count.to_le_bytes()); + + match socket.send_to(&voice_frame, target_endpoint).await { + Ok(()) => { + if wakeup_count % 50 == 1 { + println!( + "#{wakeup_count}: sent 160B voice frame (type={:?}, flow={:?})", + wakeup.twt_type, wakeup.flow_id + ); + } + } + Err(e) => { + println!("#{wakeup_count}: UDP send failed: {e:?}"); + } + } + } +} + +#[embassy_executor::task] +async fn net_task(mut runner: Runner<'static, Interface>) { + runner.run().await +} From 08f8a4f1b6af6cd2f7f31524540121286deac515 Mon Sep 17 00:00:00 2001 From: "main()" Date: Fri, 24 Jul 2026 09:31:40 +0200 Subject: [PATCH 3/9] Update esp-wifi-sys dependencies --- esp-phy/Cargo.toml | 18 +++++++++--------- esp-radio/Cargo.toml | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/esp-phy/Cargo.toml b/esp-phy/Cargo.toml index 404d0a4c6c9..d18bfb336a6 100644 --- a/esp-phy/Cargo.toml +++ b/esp-phy/Cargo.toml @@ -35,15 +35,15 @@ esp-metadata-generated = { version = "0.4.0", path = "../esp-metadata-generated" esp-sync = { version = "0.2.1", path = "../esp-sync" } # Make sure these are aligned with esp-radio's dependencies, too! -esp-wifi-sys-esp32 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "2ea8e3e" } -esp-wifi-sys-esp32c2 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "2ea8e3e" } -esp-wifi-sys-esp32c3 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "2ea8e3e" } -esp-wifi-sys-esp32c5 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "2ea8e3e" } -esp-wifi-sys-esp32c6 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "2ea8e3e" } -esp-wifi-sys-esp32c61 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "2ea8e3e" } -esp-wifi-sys-esp32h2 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "2ea8e3e" } -esp-wifi-sys-esp32s2 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "2ea8e3e" } -esp-wifi-sys-esp32s3 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "2ea8e3e" } +esp-wifi-sys-esp32 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "bc35171" } +esp-wifi-sys-esp32c2 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "bc35171" } +esp-wifi-sys-esp32c3 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "bc35171" } +esp-wifi-sys-esp32c5 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "bc35171" } +esp-wifi-sys-esp32c6 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "bc35171" } +esp-wifi-sys-esp32c61 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "bc35171" } +esp-wifi-sys-esp32h2 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "bc35171" } +esp-wifi-sys-esp32s2 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "bc35171" } +esp-wifi-sys-esp32s3 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "bc35171" } esp32 = { version = "0.40", features = ["critical-section"], optional = true, git = "https://github.com/esp-rs/esp-pacs", rev = "8fddffd" } esp32c2 = { version = "0.29", features = ["critical-section"], optional = true, git = "https://github.com/esp-rs/esp-pacs", rev = "8fddffd" } diff --git a/esp-radio/Cargo.toml b/esp-radio/Cargo.toml index c86303653a9..df489b0f172 100644 --- a/esp-radio/Cargo.toml +++ b/esp-radio/Cargo.toml @@ -80,15 +80,15 @@ ieee802154 = { version = "0.6.1", optional = true } heapless = "0.9" embassy-sync = "0.8" # Make sure these are aligned with esp-phy's dependencies, too! -esp-wifi-sys-esp32 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "2ea8e3e" } -esp-wifi-sys-esp32c2 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "2ea8e3e" } -esp-wifi-sys-esp32c3 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "2ea8e3e" } -esp-wifi-sys-esp32c5 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "2ea8e3e" } -esp-wifi-sys-esp32c6 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "2ea8e3e" } -esp-wifi-sys-esp32c61 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "2ea8e3e" } -esp-wifi-sys-esp32h2 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "2ea8e3e" } -esp-wifi-sys-esp32s2 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "2ea8e3e" } -esp-wifi-sys-esp32s3 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "2ea8e3e" } +esp-wifi-sys-esp32 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "bc35171" } +esp-wifi-sys-esp32c2 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "bc35171" } +esp-wifi-sys-esp32c3 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "bc35171" } +esp-wifi-sys-esp32c5 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "bc35171" } +esp-wifi-sys-esp32c6 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "bc35171" } +esp-wifi-sys-esp32c61 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "bc35171" } +esp-wifi-sys-esp32h2 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "bc35171" } +esp-wifi-sys-esp32s2 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "bc35171" } +esp-wifi-sys-esp32s3 = { version = "0.2.0", optional = true, git = "https://github.com/esp-rs/esp-wifi-sys.git", rev = "bc35171" } esp32 = { version = "0.40", features = ["critical-section"], optional = true , git = "https://github.com/esp-rs/esp-pacs", rev = "8fddffd" } esp32c2 = { version = "0.29", features = ["critical-section"], optional = true , git = "https://github.com/esp-rs/esp-pacs", rev = "8fddffd" } From f1dbfa1e61b15b3c20a4d93b6cb25a4739180c80 Mon Sep 17 00:00:00 2001 From: "main()" Date: Fri, 24 Jul 2026 10:51:31 +0200 Subject: [PATCH 4/9] Feature-gate docs to fix errors --- esp-radio/src/wifi/mod.rs | 28 ++++++++++++++-------------- esp-radio/src/wifi/twt.rs | 10 ++++++++-- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/esp-radio/src/wifi/mod.rs b/esp-radio/src/wifi/mod.rs index 63b3fee6dec..e9f08db4249 100644 --- a/esp-radio/src/wifi/mod.rs +++ b/esp-radio/src/wifi/mod.rs @@ -47,21 +47,21 @@ //! `WifiController::set_max_tx_power` (requires the `unstable` feature) using a value in the //! range [8, 84]. Note that values above roughly 65 (~16dBm) have been reported to cause //! authentication failures on some hardware, so setting it to the maximum is not always better. -//! -//! ## Power saving -//! -//! The following options are available to reduce Wi-Fi power consumption: -//! -//! **Power Save Mode (PSM)** -- Using [`WifiController::set_power_saving`] to activate a -//! [`PowerSaveMode`] allows the modem to turn off between beacon intervals. This is also known as -//! modem sleep. Applications that send packets frequently may not see any benefit from this -//! however. #![cfg_attr( - wifi_has_wifi6, - doc = r#" -**Target Wake Time (TWT)** -- On Wi-Fi 6 (802.11ax) networks using [`WifiController::itwt_setup`] to negotiate -an individual TWT can significantly lower power consumption, even at high transmit rates. -"# + feature = "unstable", + doc = "## Power saving + +The following options are available to reduce Wi-Fi power consumption: + +**Power Save Mode (PSM)** -- Using [`WifiController::set_power_saving`] to activate a +[`PowerSaveMode`] allows the modem to turn off between beacon intervals. This is also known as +modem sleep. Applications that send packets frequently may not see any benefit from this +however." +)] +#![cfg_attr( + all(feature = "unstable", wifi_has_wifi6), + doc = "**Target Wake Time (TWT)** -- On Wi-Fi 6 (802.11ax) networks using [`WifiController::itwt_setup`] to negotiate +an individual TWT can significantly lower power consumption, even at high transmit rates." )] use alloc::{borrow::ToOwned, collections::vec_deque::VecDeque, str, vec::Vec}; diff --git a/esp-radio/src/wifi/twt.rs b/esp-radio/src/wifi/twt.rs index 8a41cb78aef..0e080997d79 100644 --- a/esp-radio/src/wifi/twt.rs +++ b/esp-radio/src/wifi/twt.rs @@ -13,7 +13,10 @@ //! scheduled window. //! //! For best latency, send packets in response to -//! [`WifiController::wait_for_next_twt_wakeup`](crate::wifi::WifiController::wait_for_next_twt_wakeup) +#![cfg_attr( + wifi_has_wifi6, + doc = "[`WifiController::wait_for_next_twt_wakeup`](crate::wifi::WifiController::wait_for_next_twt_wakeup)" +)] //! events. An independent timer can desync from TWT wakeups, causing //! packets to miss their window. //! @@ -233,7 +236,10 @@ pub struct ITwtSetupConfig { /// Internal correlation ID used to match setup responses to requests. /// /// Automatically assigned by - /// [`WifiController::itwt_setup`](crate::wifi::WifiController::itwt_setup). + #[cfg_attr( + wifi_has_wifi6, + doc = "[`WifiController::itwt_setup`](crate::wifi::WifiController::itwt_setup)." + )] #[builder_lite(skip_setter)] pub twt_id: u16, /// The wake interval exponent (set via From 160d2246bbb8e175a2761c071faf9cdabbd477d8 Mon Sep 17 00:00:00 2001 From: "main()" Date: Fri, 24 Jul 2026 12:40:44 +0200 Subject: [PATCH 5/9] Extract error type to avoid bloating WifiError And move twt-specific stuff outof the general wifi module --- esp-radio/src/wifi/mod.rs | 36 ++++------------------------ esp-radio/src/wifi/twt.rs | 49 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 32 deletions(-) diff --git a/esp-radio/src/wifi/mod.rs b/esp-radio/src/wifi/mod.rs index e9f08db4249..39bb038cdb8 100644 --- a/esp-radio/src/wifi/mod.rs +++ b/esp-radio/src/wifi/mod.rs @@ -832,31 +832,6 @@ impl From<&[u8]> for Ssid { } } -/// Information about a successfully negotiated iTWT agreement. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[non_exhaustive] -#[instability::unstable] -pub struct ITwtSetupInfo { - /// The negotiated iTWT setup configuration (may differ from requested). - pub config: twt::ITwtSetupConfig, - /// TWT service period start time. - pub target_wake_time: u64, -} - -/// Information about a failed iTWT setup. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[non_exhaustive] -#[instability::unstable] -pub struct ITwtSetupFailedInfo { - /// The configuration returned in the failure event. - pub config: twt::ITwtSetupConfig, - /// Setup status code (non-1 value indicates failure). - pub status: i32, - /// Failure reason code. - pub reason: u8, -} static TX_QUEUE_SIZE: AtomicUsize = AtomicUsize::new(0); /// A receive packet queue. @@ -961,9 +936,6 @@ pub enum WifiError { /// TWT setup was rejected by the AP. TwtSetupRejected, - - /// iTWT setup failed (the AP responded with a non-success status). - TwtSetupFailed(ITwtSetupFailedInfo), } impl WifiError { @@ -3500,7 +3472,7 @@ impl WifiController<'_> { /// Negotiate an individual TWT (Target Wake Time) agreement with the AP. /// /// The AP may accept, modify, or reject the requested parameters. On - /// success, returns [`ITwtSetupInfo`] with the negotiated configuration. + /// success, returns [`twt::ITwtSetupInfo`] with the negotiated configuration. /// Up to 8 simultaneous agreements are supported. /// /// Concurrent calls are safe — each is assigned a unique `twt_id` for @@ -3530,7 +3502,7 @@ impl WifiController<'_> { pub async fn itwt_setup( &self, mut config: twt::ITwtSetupConfig, - ) -> Result { + ) -> Result { use portable_atomic::AtomicU16; /// Monotonic counter for assigning unique `twt_id` values to iTWT setup /// requests. @@ -3561,12 +3533,12 @@ impl WifiController<'_> { continue; } if status == 1 { - break Ok(ITwtSetupInfo { + break Ok(twt::ITwtSetupInfo { config: negotiated_config, target_wake_time, }); } else { - break Err(WifiError::TwtSetupFailed(ITwtSetupFailedInfo { + break Err(twt::ITwtSetupError::Failed(twt::ITwtSetupFailedInfo { config: negotiated_config, status, reason, diff --git a/esp-radio/src/wifi/twt.rs b/esp-radio/src/wifi/twt.rs index 0e080997d79..c2372b0cd05 100644 --- a/esp-radio/src/wifi/twt.rs +++ b/esp-radio/src/wifi/twt.rs @@ -27,10 +27,12 @@ use core::fmt; +use docsplay::Display; use enumset::EnumSetType; use esp_hal::time::Duration; use procmacros::BuilderLite; +use super::WifiError; #[cfg(wifi_has_wifi6)] use crate::sys::include::wifi_twt_config_t; use crate::sys::include::{ @@ -598,3 +600,50 @@ impl fmt::Display for TwtWaitError { } impl core::error::Error for TwtWaitError {} + +/// Information about a successfully negotiated iTWT agreement. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +#[instability::unstable] +pub struct ITwtSetupInfo { + /// The negotiated iTWT setup configuration (may differ from requested). + pub config: ITwtSetupConfig, + /// TWT service period start time. + pub target_wake_time: u64, +} + +/// Information about a failed iTWT setup. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +#[instability::unstable] +pub struct ITwtSetupFailedInfo { + /// The configuration returned in the failure event. + pub config: ITwtSetupConfig, + /// Setup status code (non-1 value indicates failure). + pub status: i32, + /// Failure reason code. + pub reason: u8, +} + +/// Errors that can occur during an iTWT setup. +#[derive(Display, Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +#[instability::unstable] +pub enum ITwtSetupError { + /// The AP responded with a non-success status. + Failed(ITwtSetupFailedInfo), + + /// A Wi-Fi error occurred. + WifiError(WifiError), +} + +impl From for ITwtSetupError { + fn from(error: WifiError) -> Self { + ITwtSetupError::WifiError(error) + } +} + +impl core::error::Error for ITwtSetupError {} From 105c7eb2a2cf56ef932690266c19bff0e26d1ece Mon Sep 17 00:00:00 2001 From: "main()" Date: Fri, 14 Aug 2026 22:16:47 +0200 Subject: [PATCH 6/9] Extract Into calls to avoid code bloat --- esp-radio/src/wifi/mod.rs | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/esp-radio/src/wifi/mod.rs b/esp-radio/src/wifi/mod.rs index 39bb038cdb8..9d546211d4a 100644 --- a/esp-radio/src/wifi/mod.rs +++ b/esp-radio/src/wifi/mod.rs @@ -3572,8 +3572,12 @@ impl WifiController<'_> { &self, target: impl Into, ) -> Result { - let target = target.into(); - + self.itwt_teardown_internal(target.into()).await + } + async fn itwt_teardown_internal( + &self, + target: twt::FlowTarget, + ) -> Result { event::enable_wifi_events(WifiEvent::IndividualTargetWakeTimeTeardown.into()); let mut subscriber = EVENT_CHANNEL @@ -3634,8 +3638,14 @@ impl WifiController<'_> { target: impl Into, suspend_time: Duration, ) -> Result<(), WifiError> { - let target = target.into(); - + self.itwt_suspend_internal(target.into(), suspend_time) + .await + } + async fn itwt_suspend_internal( + &self, + target: twt::FlowTarget, + suspend_time: Duration, + ) -> Result<(), WifiError> { event::enable_wifi_events(WifiEvent::IndividualTargetWakeTimeSuspend.into()); let mut subscriber = EVENT_CHANNEL @@ -3765,8 +3775,12 @@ impl WifiController<'_> { &self, flows: impl Into>, ) -> Result { - let flows = flows.into(); - + self.wait_for_next_twt_wakeup_internal(flows.into()).await + } + async fn wait_for_next_twt_wakeup_internal( + &self, + flows: EnumSet, + ) -> Result { event::enable_wifi_events( WifiEvent::TargetWakeTimeWakeup | WifiEvent::IndividualTargetWakeTimeTeardown From 2a4ee7c8938a6a96107ee3d4ecc5a1efc27ccc3c Mon Sep 17 00:00:00 2001 From: "main()" Date: Fri, 14 Aug 2026 22:28:02 +0200 Subject: [PATCH 7/9] Map ESP_ERR_NOT_SUPPORTED --- esp-radio/src/wifi/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esp-radio/src/wifi/mod.rs b/esp-radio/src/wifi/mod.rs index 9d546211d4a..d79e7daa649 100644 --- a/esp-radio/src/wifi/mod.rs +++ b/esp-radio/src/wifi/mod.rs @@ -910,6 +910,9 @@ pub enum WifiError { /// Passed arguments are invalid. InvalidArguments, + /// The operation is not supported + NotSupported, + /// Generic failure - not further specified. Failed, @@ -947,6 +950,7 @@ impl WifiError { match code as u32 { crate::sys::include::ESP_ERR_NO_MEM => WifiError::OutOfMemory, crate::sys::include::ESP_ERR_INVALID_ARG => WifiError::InvalidArguments, + crate::sys::include::ESP_ERR_NOT_SUPPORTED => WifiError::NotSupported, crate::sys::include::ESP_ERR_WIFI_SSID => WifiError::InvalidSsid, crate::sys::include::ESP_ERR_WIFI_PASSWORD => WifiError::InvalidPassword, crate::sys::include::ESP_ERR_WIFI_NOT_CONNECT => WifiError::NotConnected, From 4240e8ecc3e4aad5129eeb54f0985b492112347c Mon Sep 17 00:00:00 2001 From: "main()" Date: Sat, 12 Sep 2026 02:38:50 +0200 Subject: [PATCH 8/9] Fix problems after merging main branch --- esp-radio/src/wifi/mod.rs | 1 - examples/wifi/embassy_twt/Cargo.toml | 8 ++++++++ examples/wifi/embassy_twt/src/main.rs | 12 +++++++----- examples/wifi/embassy_twt_udp/Cargo.toml | 8 ++++++++ examples/wifi/embassy_twt_udp/src/main.rs | 19 +++++++------------ 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/esp-radio/src/wifi/mod.rs b/esp-radio/src/wifi/mod.rs index 35b7a56a880..414e0e8f9f1 100644 --- a/esp-radio/src/wifi/mod.rs +++ b/esp-radio/src/wifi/mod.rs @@ -1151,7 +1151,6 @@ impl WifiError { ESP_ERR_WIFI_TWT_SETUP_TXFAIL => WifiError::TwtSetupTxFail, ESP_ERR_WIFI_TWT_SETUP_REJECT => WifiError::TwtSetupRejected, - // Known driver state-machine and timeout codes. These occur in // perfectly normal operation. ESP_ERR_WIFI_NOT_INIT diff --git a/examples/wifi/embassy_twt/Cargo.toml b/examples/wifi/embassy_twt/Cargo.toml index 97bd7c57a8f..5cb7df50d10 100644 --- a/examples/wifi/embassy_twt/Cargo.toml +++ b/examples/wifi/embassy_twt/Cargo.toml @@ -45,6 +45,14 @@ esp32c61 = [ "esp-rtos/esp32c61", "esp-radio/esp32c61", ] +esp32s31 = [ + "esp-alloc/esp32s31", + "esp-backtrace/esp32s31", + "esp-bootloader-esp-idf/esp32s31", + "esp-hal/esp32s31", + "esp-rtos/esp32s31", + "esp-radio/esp32s31", +] [profile.release] debug = true diff --git a/examples/wifi/embassy_twt/src/main.rs b/examples/wifi/embassy_twt/src/main.rs index e7dff74d950..a3383eb1ff8 100644 --- a/examples/wifi/embassy_twt/src/main.rs +++ b/examples/wifi/embassy_twt/src/main.rs @@ -17,8 +17,8 @@ use esp_alloc as _; use esp_backtrace as _; use esp_hal::{ clock::CpuClock, - interrupt::software::SoftwareInterruptControl, ram, + rng::Rng, time::{Duration, Instant}, timer::timg::TimerGroup, }; @@ -33,6 +33,7 @@ macro_rules! tprintln { } use enumset::EnumSet; use esp_radio::wifi::{ + AuthenticationMethodConfig, Config, ControllerConfig, PowerSaveMode, @@ -81,13 +82,14 @@ async fn main(_spawner: embassy_executor::Spawner) -> ! { esp_alloc::heap_allocator!(size: 36 * 1024); let timg0 = TimerGroup::new(peripherals.TIMG0); - let sw_int = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT); - esp_rtos::start(timg0.timer0, sw_int.software_interrupt0); + esp_rtos::start(timg0.timer0, peripherals.FROM_CPU_INTR0); let station_config = Config::Station( StationConfig::default() - .with_ssid(SSID) - .with_password(PASSWORD.into()) + .with_ssid(SSID.try_into().unwrap()) + .with_authentication(AuthenticationMethodConfig::Wpa2Personal( + PASSWORD.try_into().unwrap(), + )) .with_protocols(Protocols::default().with_2_4(Protocol::AX.into())), ); diff --git a/examples/wifi/embassy_twt_udp/Cargo.toml b/examples/wifi/embassy_twt_udp/Cargo.toml index f287be25a67..2b9fb4f2545 100644 --- a/examples/wifi/embassy_twt_udp/Cargo.toml +++ b/examples/wifi/embassy_twt_udp/Cargo.toml @@ -50,6 +50,14 @@ esp32c61 = [ "esp-rtos/esp32c61", "esp-radio/esp32c61", ] +esp32s31 = [ + "esp-alloc/esp32s31", + "esp-backtrace/esp32s31", + "esp-bootloader-esp-idf/esp32s31", + "esp-hal/esp32s31", + "esp-rtos/esp32s31", + "esp-radio/esp32s31", +] [profile.release] debug = true diff --git a/examples/wifi/embassy_twt_udp/src/main.rs b/examples/wifi/embassy_twt_udp/src/main.rs index 773ff2027ee..1f3d8f0b678 100644 --- a/examples/wifi/embassy_twt_udp/src/main.rs +++ b/examples/wifi/embassy_twt_udp/src/main.rs @@ -20,16 +20,10 @@ use embassy_net::{Runner, StackResources, udp::UdpSocket}; use embassy_time::Timer; use esp_alloc as _; use esp_backtrace as _; -use esp_hal::{ - clock::CpuClock, - interrupt::software::SoftwareInterruptControl, - ram, - rng::Rng, - time::Duration, - timer::timg::TimerGroup, -}; +use esp_hal::{clock::CpuClock, ram, rng::Rng, time::Duration, timer::timg::TimerGroup}; use esp_println::println; use esp_radio::wifi::{ + AuthenticationMethodConfig, Config, ControllerConfig, Interface, @@ -66,13 +60,14 @@ async fn main(spawner: Spawner) -> ! { esp_alloc::heap_allocator!(size: 36 * 1024); let timg0 = TimerGroup::new(peripherals.TIMG0); - let sw_int = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT); - esp_rtos::start(timg0.timer0, sw_int.software_interrupt0); + esp_rtos::start(timg0.timer0, peripherals.FROM_CPU_INTR0); let station_config = Config::Station( StationConfig::default() - .with_ssid(SSID) - .with_password(PASSWORD.into()) + .with_ssid(SSID.try_into().unwrap()) + .with_authentication(AuthenticationMethodConfig::Wpa2Personal( + PASSWORD.try_into().unwrap(), + )) .with_protocols(Protocols::default().with_2_4(Protocol::AX.into())), ); From 49399facb921dbb708f8c9e85e5e610a940ab7b6 Mon Sep 17 00:00:00 2001 From: "main()" Date: Wed, 16 Sep 2026 20:39:58 +0200 Subject: [PATCH 9/9] update esp_rtos start --- examples/wifi/embassy_twt/src/main.rs | 2 +- examples/wifi/embassy_twt_udp/src/main.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/wifi/embassy_twt/src/main.rs b/examples/wifi/embassy_twt/src/main.rs index a3383eb1ff8..db55e0afa8b 100644 --- a/examples/wifi/embassy_twt/src/main.rs +++ b/examples/wifi/embassy_twt/src/main.rs @@ -82,7 +82,7 @@ async fn main(_spawner: embassy_executor::Spawner) -> ! { esp_alloc::heap_allocator!(size: 36 * 1024); let timg0 = TimerGroup::new(peripherals.TIMG0); - esp_rtos::start(timg0.timer0, peripherals.FROM_CPU_INTR0); + esp_rtos::start(timg0.timer0); let station_config = Config::Station( StationConfig::default() diff --git a/examples/wifi/embassy_twt_udp/src/main.rs b/examples/wifi/embassy_twt_udp/src/main.rs index 1f3d8f0b678..2833622fd9c 100644 --- a/examples/wifi/embassy_twt_udp/src/main.rs +++ b/examples/wifi/embassy_twt_udp/src/main.rs @@ -60,7 +60,7 @@ async fn main(spawner: Spawner) -> ! { esp_alloc::heap_allocator!(size: 36 * 1024); let timg0 = TimerGroup::new(peripherals.TIMG0); - esp_rtos::start(timg0.timer0, peripherals.FROM_CPU_INTR0); + esp_rtos::start(timg0.timer0); let station_config = Config::Station( StationConfig::default()