diff --git a/esp-radio/src/wifi/event.rs b/esp-radio/src/wifi/event.rs index c8cd8db1974..5ccd7a1ad86 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!( @@ -1017,19 +1099,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, @@ -1247,19 +1360,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 8734a373483..414e0e8f9f1 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. +#![cfg_attr( + 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}; 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}; @@ -102,6 +123,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; @@ -1070,6 +1093,12 @@ pub enum WifiError { /// Passed arguments are invalid. InvalidArguments, + /// The operation is not supported + NotSupported, + + /// Generic failure - not further specified. + Failed, + /// An unspecified error occurred in the radio driver. Other, @@ -1084,6 +1113,18 @@ 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, } impl WifiError { @@ -1101,9 +1142,14 @@ impl WifiError { // are returned without any logging. ESP_ERR_NO_MEM => WifiError::OutOfMemory, ESP_ERR_INVALID_ARG => WifiError::InvalidArguments, + ESP_ERR_NOT_SUPPORTED => WifiError::NotSupported, ESP_ERR_WIFI_SSID => WifiError::InvalidSsid, ESP_ERR_WIFI_PASSWORD => WifiError::InvalidPassword, ESP_ERR_WIFI_NOT_CONNECT => WifiError::NotConnected, + ESP_ERR_WIFI_TWT_FULL => WifiError::TwtFull, + ESP_ERR_WIFI_TWT_SETUP_TIMEOUT => WifiError::TwtSetupTimeout, + 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. @@ -3785,3 +3831,412 @@ 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 [`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 + /// 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(twt::ITwtSetupInfo { + config: negotiated_config, + target_wake_time, + }); + } else { + break Err(twt::ITwtSetupError::Failed(twt::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 { + 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 + .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> { + 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 + .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 { + 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 + | 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/os_adapter/mod.rs b/esp-radio/src/wifi/os_adapter/mod.rs index d07f670c1d6..4891fb0802f 100644 --- a/esp-radio/src/wifi/os_adapter/mod.rs +++ b/esp-radio/src/wifi/os_adapter/mod.rs @@ -903,8 +903,12 @@ pub unsafe extern "C" fn wifi_clock_enable() { /// /// ************************************************************************* pub unsafe extern "C" fn wifi_clock_disable() { - trace!("wifi_clock_disable"); - crate::radio_clocks::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::enable_wifi(false); + } } /// ************************************************************************** diff --git a/esp-radio/src/wifi/state.rs b/esp-radio/src/wifi/state.rs index 055b7c5ff61..efb8162367e 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: LazyLock = LazyLock::new(|| SemaphoreHandle::new(SemaphoreKind::Mutex)); diff --git a/esp-radio/src/wifi/twt.rs b/esp-radio/src/wifi/twt.rs new file mode 100644 index 00000000000..c2372b0cd05 --- /dev/null +++ b/esp-radio/src/wifi/twt.rs @@ -0,0 +1,649 @@ +//! 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 +#![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. +//! +//! # 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 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::{ + 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 + #[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 + /// [`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 {} + +/// 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 {} 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..5cb7df50d10 --- /dev/null +++ b/examples/wifi/embassy_twt/Cargo.toml @@ -0,0 +1,61 @@ +[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", +] +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 +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..db55e0afa8b --- /dev/null +++ b/examples/wifi/embassy_twt/src/main.rs @@ -0,0 +1,248 @@ +//% 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, + ram, + rng::Rng, + 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::{ + AuthenticationMethodConfig, + 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); + esp_rtos::start(timg0.timer0); + + let station_config = Config::Station( + StationConfig::default() + .with_ssid(SSID.try_into().unwrap()) + .with_authentication(AuthenticationMethodConfig::Wpa2Personal( + PASSWORD.try_into().unwrap(), + )) + .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..2b9fb4f2545 --- /dev/null +++ b/examples/wifi/embassy_twt_udp/Cargo.toml @@ -0,0 +1,66 @@ +[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", +] +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 +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..2833622fd9c --- /dev/null +++ b/examples/wifi/embassy_twt_udp/src/main.rs @@ -0,0 +1,179 @@ +//% 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, ram, rng::Rng, time::Duration, timer::timg::TimerGroup}; +use esp_println::println; +use esp_radio::wifi::{ + AuthenticationMethodConfig, + 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); + esp_rtos::start(timg0.timer0); + + let station_config = Config::Station( + StationConfig::default() + .with_ssid(SSID.try_into().unwrap()) + .with_authentication(AuthenticationMethodConfig::Wpa2Personal( + PASSWORD.try_into().unwrap(), + )) + .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 +}