diff --git a/Cargo.toml b/Cargo.toml index 8cb40586b19..1a0f472828d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,8 +30,8 @@ log = { version = "0.4", default-features = false } uncased = "0.9.7" anyhow = { version = "1", default-features = false, optional = true } # Only used by the deprecated httpd module embedded-svc = { version = "0.24", default-features = false } -esp-idf-sys = { version = "0.32.1", default-features = false, features = ["native"] } -esp-idf-hal = { version = "0.40", default-features = false, features = ["esp-idf-sys"] } +esp-idf-sys = { path = "/home/jasta/software/esp-idf-sys", default-features = false, features = ["native"] } +esp-idf-hal = { path = "/home/jasta/software/esp-idf-hal", default-features = false, features = ["esp-idf-sys"] } embassy-sync = { version = "0.1", optional = true } embassy-time = { version = "0.1", optional = true, features = ["tick-hz-1_000_000"] } diff --git a/examples/wifi_dpp_setup.rs b/examples/wifi_dpp_setup.rs new file mode 100644 index 00000000000..57c59cf6c94 --- /dev/null +++ b/examples/wifi_dpp_setup.rs @@ -0,0 +1,76 @@ +//! Example using Wi-Fi Easy Connect (DPP) to get a device onto a Wi-Fi network +//! without hardcoding credentials. + +extern crate core; + +use esp_idf_hal as _; + +use std::time::Duration; +use embedded_svc::wifi::{ClientConfiguration, Configuration, Wifi}; +use esp_idf_hal::peripherals::Peripherals; +use esp_idf_svc::eventloop::EspSystemEventLoop; +use esp_idf_svc::log::EspLogger; +use esp_idf_svc::nvs::EspDefaultNvsPartition; +use esp_idf_svc::wifi::{EspWifi, WifiWait}; +use esp_idf_sys::EspError; +use log::{error, info, LevelFilter, warn}; +use esp_idf_svc::wifi_dpp::EspDppBootstrapper; + +fn main() { + esp_idf_sys::link_patches(); + + EspLogger::initialize_default(); + + let peripherals = Peripherals::take().unwrap(); + let sysloop = EspSystemEventLoop::take().unwrap(); + let nvs = EspDefaultNvsPartition::take().unwrap(); + + let mut wifi = EspWifi::new(peripherals.modem, sysloop.clone(), Some(nvs)).unwrap(); + + if !wifi_has_sta_config(&wifi).unwrap() { + info!("No existing STA config, let's try DPP..."); + let config = dpp_listen_forever(&mut wifi).unwrap(); + info!("Got config: {config:?}"); + wifi.set_configuration(&Configuration::Client(config)).unwrap(); + } + + wifi.start().unwrap(); + + let timeout = Duration::from_secs(60); + loop { + let ssid = match wifi.get_configuration().unwrap() { + Configuration::None => None, + Configuration::Client(ap) => Some(ap.ssid), + Configuration::AccessPoint(_) => None, + Configuration::Mixed(_, _) => None, + }.unwrap(); + info!("Connecting to {ssid}..."); + wifi.connect().unwrap(); + let waiter = WifiWait::new(&sysloop).unwrap(); + let is_connected = waiter.wait_with_timeout(timeout, || wifi.is_connected().unwrap()); + if is_connected { + info!("Connected!"); + waiter.wait(|| !wifi.is_connected().unwrap()); + warn!("Got disconnected, connecting again..."); + } else { + error!("Failed to connect after {}s, trying again...", timeout.as_secs()); + } + } +} + +fn wifi_has_sta_config(wifi: &EspWifi) -> Result { + match wifi.get_configuration()? { + Configuration::Client(c) => Ok(!c.ssid.is_empty()), + _ => Ok(false), + } +} + +fn dpp_listen_forever(wifi: &mut EspWifi) -> Result { + let mut dpp = EspDppBootstrapper::new(wifi)?; + let channels: Vec<_> = (1..=11).collect(); + let bootstrapped = dpp.gen_qrcode(&channels, None, None)?; + println!("Got: {}", bootstrapped.data.0); + println!("(use a QR code generator and scan the code in the Wi-Fi setup flow on your phone)"); + + bootstrapped.listen_forever() +} diff --git a/sdkconfig.defaults b/sdkconfig.defaults new file mode 100644 index 00000000000..dfaeb82ac35 --- /dev/null +++ b/sdkconfig.defaults @@ -0,0 +1,8 @@ +CONFIG_LOG_DEFAULT_LEVEL_INFO=y +CONFIG_LOG_MAXIMUM_LEVEL_INFO=y + +CONFIG_WPA_DPP_SUPPORT=y + +CONFIG_ESP_MAIN_TASK_STACK_SIZE=15000 +CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=12000 +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=8000 diff --git a/src/lib.rs b/src/lib.rs index c4b9e8d0b38..7a65e9b1cb1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,14 @@ #[macro_use] extern crate alloc; +#[cfg(all( + feature = "alloc", + esp_idf_comp_wpa_supplicant_enabled, + any( + esp_idf_esp_wifi_dpp_support, + esp_idf_wpa_dpp_support + )))] +pub mod wifi_dpp; pub mod errors; #[cfg(all( feature = "alloc", diff --git a/src/wifi_dpp.rs b/src/wifi_dpp.rs new file mode 100644 index 00000000000..32a1faa8755 --- /dev/null +++ b/src/wifi_dpp.rs @@ -0,0 +1,223 @@ +//! Wi-Fi Easy Connect (DPP) support +//! +//! To use this feature, you must add CONFIG_WPA_DPP_SUPPORT=y to your sdkconfig. + +use ::log::*; + +use std::ffi::{c_char, CStr, CString}; +use std::fmt::Write; +use std::ops::Deref; +use std::ptr; +use std::sync::mpsc::{Receiver, sync_channel, SyncSender}; +use embedded_svc::wifi::{ClientConfiguration, Configuration, Wifi}; +use esp_idf_sys::*; +use esp_idf_sys::EspError; +use crate::private::common::Newtype; +use crate::private::mutex; +use crate::wifi::EspWifi; + +static EVENTS_TX: mutex::Mutex>> = + mutex::Mutex::wrap(mutex::RawMutex::new(), None); + +pub struct EspDppBootstrapper<'d, 'w> { + wifi: &'d mut EspWifi<'w>, + events_rx: Receiver, +} + +impl<'d, 'w> EspDppBootstrapper<'d, 'w> { + pub fn new(wifi: &'d mut EspWifi<'w>) -> Result { + if wifi.is_started()? { + wifi.disconnect()?; + wifi.stop()?; + } + + Self::init(wifi) + } + + fn init(wifi: &'d mut EspWifi<'w>) -> Result { + let (events_tx, events_rx) = sync_channel(1); + let mut dpp_event_relay = EVENTS_TX.lock(); + *dpp_event_relay = Some(events_tx); + drop(dpp_event_relay); + esp!(unsafe { esp_supp_dpp_init(Some(Self::dpp_event_cb_unsafe)) })?; + Ok(Self { + wifi, + events_rx, + }) + } + + /// Generate a QR code that can be scanned by a mobile phone or other configurator + /// to securely provide us with the Wi-Fi credentials. Must invoke a listen API on the returned + /// bootstrapped instance (e.g. [EspDppBootstrapped::listen_once]) or scanning the + /// QR code will not be able to deliver the credentials to us. + /// + /// Important implementation notes: + /// + /// 1. You must provide _all_ viable channels that the AP could be using + /// in order to successfully acquire credentials! For example, in the US, you can use + /// `(1..=11).collect()`. + /// + /// 2. The WiFi driver will be forced started and with a default STA config unless the + /// state is set-up ahead of time. It's unclear if the AuthMethod that you select + /// for this STA config affects the results. + pub fn gen_qrcode<'b>( + &'b mut self, + channels: &[u8], + key: Option<&[u8; 32]>, + associated_data: Option<&[u8]> + ) -> Result, EspError> { + let mut channels_str = channels.into_iter() + .fold(String::new(), |mut a, c| { + write!(a, "{c},").unwrap(); + a + }); + channels_str.pop(); + let channels_cstr = CString::new(channels_str).unwrap(); + let key_ascii_cstr = key.map(|k| { + let result = k.iter() + .fold(String::new(), |mut a, b| { + write!(a, "{b:02X}").unwrap(); + a + }); + CString::new(result).unwrap() + }); + let associated_data_cstr = match associated_data { + Some(associated_data) => { + Some(CString::new(associated_data) + .map_err(|_| { + warn!("associated data contains an embedded NUL character!"); + EspError::from_infallible::() + })?) + } + None => None, + }; + debug!("dpp_bootstrap_gen..."); + esp!(unsafe { + esp_supp_dpp_bootstrap_gen( + channels_cstr.as_ptr(), + dpp_bootstrap_type_DPP_BOOTSTRAP_QR_CODE, + key_ascii_cstr.map_or_else(ptr::null, |x| x.as_ptr()), + associated_data_cstr.map_or_else(ptr::null, |x| x.as_ptr())) + })?; + let event = self.events_rx.recv() + .map_err(|_| { + warn!("Internal error receiving event!"); + EspError::from_infallible::() + })?; + debug!("dpp_bootstrap_gen got: {event:?}"); + match event { + DppEvent::UriReady(qrcode) => { + // Bit of a hack to put the wifi driver in the correct mode. + self.ensure_config_and_start()?; + Ok(EspDppBootstrapped:: { + events_rx: &self.events_rx, + data: QrCode(qrcode), + }) + } + _ => { + warn!("Got unexpected event: {event:?}"); + Err(EspError::from_infallible::()) + }, + } + } + + fn ensure_config_and_start(&mut self) -> Result { + let operating_config = match self.wifi.get_configuration()? { + Configuration::Client(c) => c, + _ => { + let fallback_config = ClientConfiguration::default(); + self.wifi.set_configuration(&Configuration::Client(fallback_config.clone()))?; + fallback_config + }, + }; + if !self.wifi.is_started()? { + self.wifi.start()?; + } + Ok(operating_config) + } + + unsafe extern "C" fn dpp_event_cb_unsafe( + evt: esp_supp_dpp_event_t, + data: *mut ::core::ffi::c_void + ) { + debug!("dpp_event_cb_unsafe: evt={evt}"); + let event = match evt { + esp_supp_dpp_event_t_ESP_SUPP_DPP_URI_READY => { + DppEvent::UriReady(CStr::from_ptr(data as *mut c_char).to_str().unwrap().into()) + }, + esp_supp_dpp_event_t_ESP_SUPP_DPP_CFG_RECVD => { + let config = data as *mut wifi_config_t; + // TODO: We're losing pmf_cfg.required=true setting due to missing + // information in ClientConfiguration. + DppEvent::ConfigurationReceived(Newtype((*config).sta).into()) + }, + esp_supp_dpp_event_t_ESP_SUPP_DPP_FAIL => { + DppEvent::Fail(EspError::from(data as esp_err_t).unwrap()) + } + _ => panic!(), + }; + dpp_event_cb(event) + } +} + +fn dpp_event_cb(event: DppEvent) { + match EVENTS_TX.lock().deref() { + Some(tx) => { + debug!("Sending: {event:?}"); + if let Err(e) = tx.try_send(event) { + error!("Cannot relay event: {e}"); + } + } + None => warn!("Got spurious {event:?} ???"), + } +} + + +#[derive(Debug)] +enum DppEvent { + UriReady(String), + ConfigurationReceived(ClientConfiguration), + Fail(EspError), +} + +impl<'d, 'w> Drop for EspDppBootstrapper<'d, 'w> { + fn drop(&mut self) { + unsafe { esp_supp_dpp_deinit() }; + } +} + +pub struct EspDppBootstrapped<'d, T> { + events_rx: &'d Receiver, + pub data: T, +} + +#[derive(Debug, Clone)] +pub struct QrCode(pub String); + +impl<'d, T> EspDppBootstrapped<'d, T> { + pub fn listen_once(&self) -> Result { + esp!(unsafe { esp_supp_dpp_start_listen() })?; + let event = self.events_rx.recv() + .map_err(|e| { + warn!("Internal receive error: {e}"); + EspError::from_infallible::() + })?; + match event { + DppEvent::ConfigurationReceived(config) => Ok(config), + DppEvent::Fail(e) => Err(e), + _ => { + warn!("Ignoring unexpected event {event:?}"); + Err(EspError::from_infallible::()) + } + } + } + + pub fn listen_forever(&self) -> Result { + loop { + match self.listen_once() { + Ok(config) => return Ok(config), + Err(e) => warn!("DPP error: {e}, trying again..."), + } + } + } +}