Skip to content

Commit

Permalink
Add WiFi Easy Connect (DPP) wrappers and associated example
Browse files Browse the repository at this point in the history
  • Loading branch information
jasta committed Feb 15, 2023
1 parent bbc129a commit 89ad844
Show file tree
Hide file tree
Showing 5 changed files with 317 additions and 2 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }

Expand Down
76 changes: 76 additions & 0 deletions examples/wifi_dpp_setup.rs
Original file line number Diff line number Diff line change
@@ -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<bool, EspError> {
match wifi.get_configuration()? {
Configuration::Client(c) => Ok(!c.ssid.is_empty()),
_ => Ok(false),
}
}

fn dpp_listen_forever(wifi: &mut EspWifi) -> Result<ClientConfiguration, EspError> {
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()
}
8 changes: 8 additions & 0 deletions sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
223 changes: 223 additions & 0 deletions src/wifi_dpp.rs
Original file line number Diff line number Diff line change
@@ -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<Option<SyncSender<DppEvent>>> =
mutex::Mutex::wrap(mutex::RawMutex::new(), None);

pub struct EspDppBootstrapper<'d, 'w> {
wifi: &'d mut EspWifi<'w>,
events_rx: Receiver<DppEvent>,
}

impl<'d, 'w> EspDppBootstrapper<'d, 'w> {
pub fn new(wifi: &'d mut EspWifi<'w>) -> Result<Self, EspError> {
if wifi.is_started()? {
wifi.disconnect()?;
wifi.stop()?;
}

Self::init(wifi)
}

fn init(wifi: &'d mut EspWifi<'w>) -> Result<Self, EspError> {
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<EspDppBootstrapped<'b, QrCode>, 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::<ESP_ERR_INVALID_ARG>()
})?)
}
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::<ESP_ERR_INVALID_STATE>()
})?;
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::<QrCode> {
events_rx: &self.events_rx,
data: QrCode(qrcode),
})
}
_ => {
warn!("Got unexpected event: {event:?}");
Err(EspError::from_infallible::<ESP_ERR_INVALID_STATE>())
},
}
}

fn ensure_config_and_start(&mut self) -> Result<ClientConfiguration, EspError> {
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<DppEvent>,
pub data: T,
}

#[derive(Debug, Clone)]
pub struct QrCode(pub String);

impl<'d, T> EspDppBootstrapped<'d, T> {
pub fn listen_once(&self) -> Result<ClientConfiguration, EspError> {
esp!(unsafe { esp_supp_dpp_start_listen() })?;
let event = self.events_rx.recv()
.map_err(|e| {
warn!("Internal receive error: {e}");
EspError::from_infallible::<ESP_ERR_INVALID_STATE>()
})?;
match event {
DppEvent::ConfigurationReceived(config) => Ok(config),
DppEvent::Fail(e) => Err(e),
_ => {
warn!("Ignoring unexpected event {event:?}");
Err(EspError::from_infallible::<ESP_ERR_INVALID_STATE>())
}
}
}

pub fn listen_forever(&self) -> Result<ClientConfiguration, EspError> {
loop {
match self.listen_once() {
Ok(config) => return Ok(config),
Err(e) => warn!("DPP error: {e}, trying again..."),
}
}
}
}

0 comments on commit 89ad844

Please sign in to comment.