Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WiFi Easy Connect (DPP) wrappers and associated example #228

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"] }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, will remove :)

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>,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this should probably be &mut so that it guarantees the bootstrapper binding cannot be used while the bootstrapped binding is in scope.

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..."),
}
}
}
}