Skip to content

Commit c156ba9

Browse files
committed
Add WiFi Easy Connect (DPP) wrappers and associated example
1 parent bbc129a commit c156ba9

File tree

4 files changed

+309
-2
lines changed

4 files changed

+309
-2
lines changed

Cargo.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ log = { version = "0.4", default-features = false }
3030
uncased = "0.9.7"
3131
anyhow = { version = "1", default-features = false, optional = true } # Only used by the deprecated httpd module
3232
embedded-svc = { version = "0.24", default-features = false }
33-
esp-idf-sys = { version = "0.32.1", default-features = false, features = ["native"] }
34-
esp-idf-hal = { version = "0.40", default-features = false, features = ["esp-idf-sys"] }
33+
esp-idf-sys = { path = "/home/jasta/software/esp-idf-sys", default-features = false, features = ["native"] }
34+
esp-idf-hal = { path = "/home/jasta/software/esp-idf-hal", default-features = false, features = ["esp-idf-sys"] }
3535
embassy-sync = { version = "0.1", optional = true }
3636
embassy-time = { version = "0.1", optional = true, features = ["tick-hz-1_000_000"] }
3737

examples/wifi_dpp_setup.rs

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//! Example using Wi-Fi Easy Connect (DPP) to get a device onto a Wi-Fi network
2+
//! without hardcoding credentials.
3+
4+
extern crate core;
5+
6+
use esp_idf_hal as _;
7+
8+
use std::time::Duration;
9+
use embedded_svc::wifi::{ClientConfiguration, Configuration, Wifi};
10+
use esp_idf_hal::peripherals::Peripherals;
11+
use esp_idf_svc::eventloop::EspSystemEventLoop;
12+
use esp_idf_svc::log::EspLogger;
13+
use esp_idf_svc::nvs::EspDefaultNvsPartition;
14+
use esp_idf_svc::wifi::{EspWifi, WifiWait};
15+
use esp_idf_sys::EspError;
16+
use log::{error, info, LevelFilter, warn};
17+
use esp_idf_svc::wifi_dpp::EspDppBootstrapper;
18+
19+
fn main() -> anyhow::Result<()> {
20+
esp_idf_sys::link_patches();
21+
22+
EspLogger::initialize_default();
23+
24+
let peripherals = Peripherals::take().unwrap();
25+
let sysloop = EspSystemEventLoop::take()?;
26+
let nvs = EspDefaultNvsPartition::take()?;
27+
28+
let mut wifi = EspWifi::new(peripherals.modem, sysloop.clone(), Some(nvs))?;
29+
30+
if !wifi_has_sta_config(&wifi)? {
31+
info!("No existing STA config, let's try DPP...");
32+
let config = dpp_listen_forever(&mut wifi)?;
33+
info!("Got config: {config:?}");
34+
wifi.set_configuration(&Configuration::Client(config))?;
35+
}
36+
37+
wifi.start()?;
38+
39+
let timeout = Duration::from_secs(60);
40+
loop {
41+
let ssid = match wifi.get_configuration()? {
42+
Configuration::None => None,
43+
Configuration::Client(ap) => Some(ap.ssid),
44+
Configuration::AccessPoint(_) => None,
45+
Configuration::Mixed(_, _) => None,
46+
}.unwrap();
47+
info!("Connecting to {ssid}...");
48+
wifi.connect()?;
49+
let waiter = WifiWait::new(&sysloop)?;
50+
let is_connected = waiter.wait_with_timeout(timeout, || wifi.is_connected().unwrap());
51+
if is_connected {
52+
info!("Connected!");
53+
waiter.wait(|| !wifi.is_connected().unwrap());
54+
warn!("Got disconnected, connecting again...");
55+
} else {
56+
error!("Failed to connect after {}s, trying again...", timeout.as_secs());
57+
}
58+
}
59+
}
60+
61+
fn wifi_has_sta_config(wifi: &EspWifi) -> Result<bool, EspError> {
62+
match wifi.get_configuration()? {
63+
Configuration::Client(c) => Ok(!c.ssid.is_empty()),
64+
_ => Ok(false),
65+
}
66+
}
67+
68+
fn dpp_listen_forever(wifi: &mut EspWifi) -> Result<ClientConfiguration, EspError> {
69+
let mut dpp = EspDppBootstrapper::new(wifi)?;
70+
let channels: Vec<_> = (1..=11).collect();
71+
let bootstrapped = dpp.gen_qrcode(&channels, None, None)?;
72+
println!("Got: {}", bootstrapped.data.0);
73+
println!("(use a QR code generator and scan the code in the Wi-Fi setup flow on your phone)");
74+
75+
bootstrapped.listen_forever()
76+
}

src/lib.rs

+8
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@
2525
#[macro_use]
2626
extern crate alloc;
2727

28+
#[cfg(all(
29+
feature = "alloc",
30+
esp_idf_comp_wpa_supplicant_enabled,
31+
any(
32+
esp_idf_esp_wifi_dpp_support,
33+
esp_idf_wpa_dpp_support
34+
)))]
35+
pub mod wifi_dpp;
2836
pub mod errors;
2937
#[cfg(all(
3038
feature = "alloc",

src/wifi_dpp.rs

+223
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
//! Wi-Fi Easy Connect (DPP) support
2+
//!
3+
//! To use this feature, you must add CONFIG_WPA_DPP_SUPPORT=y to your sdkconfig.
4+
5+
use ::log::*;
6+
7+
use std::ffi::{c_char, CStr, CString};
8+
use std::fmt::Write;
9+
use std::ops::Deref;
10+
use std::ptr;
11+
use std::sync::mpsc::{Receiver, sync_channel, SyncSender};
12+
use embedded_svc::wifi::{ClientConfiguration, Configuration, Wifi};
13+
use esp_idf_sys::*;
14+
use esp_idf_sys::EspError;
15+
use crate::private::common::Newtype;
16+
use crate::private::mutex;
17+
use crate::wifi::EspWifi;
18+
19+
static EVENTS_TX: mutex::Mutex<Option<SyncSender<DppEvent>>> =
20+
mutex::Mutex::wrap(mutex::RawMutex::new(), None);
21+
22+
pub struct EspDppBootstrapper<'d, 'w> {
23+
wifi: &'d mut EspWifi<'w>,
24+
events_rx: Receiver<DppEvent>,
25+
}
26+
27+
impl<'d, 'w> EspDppBootstrapper<'d, 'w> {
28+
pub fn new(wifi: &'d mut EspWifi<'w>) -> Result<Self, EspError> {
29+
if wifi.is_started()? {
30+
wifi.disconnect()?;
31+
wifi.stop()?;
32+
}
33+
34+
Self::init(wifi)
35+
}
36+
37+
fn init(wifi: &'d mut EspWifi<'w>) -> Result<Self, EspError> {
38+
let (events_tx, events_rx) = sync_channel(1);
39+
let mut dpp_event_relay = EVENTS_TX.lock();
40+
*dpp_event_relay = Some(events_tx);
41+
drop(dpp_event_relay);
42+
esp!(unsafe { esp_supp_dpp_init(Some(Self::dpp_event_cb_unsafe)) })?;
43+
Ok(Self {
44+
wifi,
45+
events_rx,
46+
})
47+
}
48+
49+
/// Generate a QR code that can be scanned by a mobile phone or other configurator
50+
/// to securely provide us with the Wi-Fi credentials. Must invoke a listen API on the returned
51+
/// bootstrapped instance (e.g. [EspDppBootstrapped::listen_once]) or scanning the
52+
/// QR code will not be able to deliver the credentials to us.
53+
///
54+
/// Important implementation notes:
55+
///
56+
/// 1. You must provide _all_ viable channels that the AP could be using
57+
/// in order to successfully acquire credentials! For example, in the US, you can use
58+
/// `(1..=11).collect()`.
59+
///
60+
/// 2. The WiFi driver will be forced started and with a default STA config unless the
61+
/// state is set-up ahead of time. It's unclear if the AuthMethod that you select
62+
/// for this STA config affects the results.
63+
pub fn gen_qrcode<'b>(
64+
&'b mut self,
65+
channels: &[u8],
66+
key: Option<&[u8; 32]>,
67+
associated_data: Option<&[u8]>
68+
) -> Result<EspDppBootstrapped<'b, QrCode>, EspError> {
69+
let mut channels_str = channels.into_iter()
70+
.fold(String::new(), |mut a, c| {
71+
write!(a, "{c},").unwrap();
72+
a
73+
});
74+
channels_str.pop();
75+
let channels_cstr = CString::new(channels_str).unwrap();
76+
let key_ascii_cstr = key.map(|k| {
77+
let result = k.iter()
78+
.fold(String::new(), |mut a, b| {
79+
write!(a, "{b:02X}").unwrap();
80+
a
81+
});
82+
CString::new(result).unwrap()
83+
});
84+
let associated_data_cstr = match associated_data {
85+
Some(associated_data) => {
86+
Some(CString::new(associated_data)
87+
.map_err(|_| {
88+
warn!("associated data contains an embedded NUL character!");
89+
EspError::from_infallible::<ESP_ERR_INVALID_ARG>()
90+
})?)
91+
}
92+
None => None,
93+
};
94+
debug!("dpp_bootstrap_gen...");
95+
esp!(unsafe {
96+
esp_supp_dpp_bootstrap_gen(
97+
channels_cstr.as_ptr(),
98+
dpp_bootstrap_type_DPP_BOOTSTRAP_QR_CODE,
99+
key_ascii_cstr.map_or_else(ptr::null, |x| x.as_ptr()),
100+
associated_data_cstr.map_or_else(ptr::null, |x| x.as_ptr()))
101+
})?;
102+
let event = self.events_rx.recv()
103+
.map_err(|_| {
104+
warn!("Internal error receiving event!");
105+
EspError::from_infallible::<ESP_ERR_INVALID_STATE>()
106+
})?;
107+
debug!("dpp_bootstrap_gen got: {event:?}");
108+
match event {
109+
DppEvent::UriReady(qrcode) => {
110+
// Bit of a hack to put the wifi driver in the correct mode.
111+
self.ensure_config_and_start()?;
112+
Ok(EspDppBootstrapped::<QrCode> {
113+
events_rx: &self.events_rx,
114+
data: QrCode(qrcode),
115+
})
116+
}
117+
_ => {
118+
warn!("Got unexpected event: {event:?}");
119+
Err(EspError::from_infallible::<ESP_ERR_INVALID_STATE>())
120+
},
121+
}
122+
}
123+
124+
fn ensure_config_and_start(&mut self) -> Result<ClientConfiguration, EspError> {
125+
let operating_config = match self.wifi.get_configuration()? {
126+
Configuration::Client(c) => c,
127+
_ => {
128+
let fallback_config = ClientConfiguration::default();
129+
self.wifi.set_configuration(&Configuration::Client(fallback_config.clone()))?;
130+
fallback_config
131+
},
132+
};
133+
if !self.wifi.is_started()? {
134+
self.wifi.start()?;
135+
}
136+
Ok(operating_config)
137+
}
138+
139+
unsafe extern "C" fn dpp_event_cb_unsafe(
140+
evt: esp_supp_dpp_event_t,
141+
data: *mut ::core::ffi::c_void
142+
) {
143+
debug!("dpp_event_cb_unsafe: evt={evt}");
144+
let event = match evt {
145+
esp_supp_dpp_event_t_ESP_SUPP_DPP_URI_READY => {
146+
DppEvent::UriReady(CStr::from_ptr(data as *mut c_char).to_str().unwrap().into())
147+
},
148+
esp_supp_dpp_event_t_ESP_SUPP_DPP_CFG_RECVD => {
149+
let config = data as *mut wifi_config_t;
150+
// TODO: We're losing pmf_cfg.required=true setting due to missing
151+
// information in ClientConfiguration.
152+
DppEvent::ConfigurationReceived(Newtype((*config).sta).into())
153+
},
154+
esp_supp_dpp_event_t_ESP_SUPP_DPP_FAIL => {
155+
DppEvent::Fail(EspError::from(data as esp_err_t).unwrap())
156+
}
157+
_ => panic!(),
158+
};
159+
dpp_event_cb(event)
160+
}
161+
}
162+
163+
fn dpp_event_cb(event: DppEvent) {
164+
match EVENTS_TX.lock().deref() {
165+
Some(tx) => {
166+
debug!("Sending: {event:?}");
167+
if let Err(e) = tx.try_send(event) {
168+
error!("Cannot relay event: {e}");
169+
}
170+
}
171+
None => warn!("Got spurious {event:?} ???"),
172+
}
173+
}
174+
175+
176+
#[derive(Debug)]
177+
enum DppEvent {
178+
UriReady(String),
179+
ConfigurationReceived(ClientConfiguration),
180+
Fail(EspError),
181+
}
182+
183+
impl<'d, 'w> Drop for EspDppBootstrapper<'d, 'w> {
184+
fn drop(&mut self) {
185+
unsafe { esp_supp_dpp_deinit() };
186+
}
187+
}
188+
189+
pub struct EspDppBootstrapped<'d, T> {
190+
events_rx: &'d Receiver<DppEvent>,
191+
pub data: T,
192+
}
193+
194+
#[derive(Debug, Clone)]
195+
pub struct QrCode(pub String);
196+
197+
impl<'d, T> EspDppBootstrapped<'d, T> {
198+
pub fn listen_once(&self) -> Result<ClientConfiguration, EspError> {
199+
esp!(unsafe { esp_supp_dpp_start_listen() })?;
200+
let event = self.events_rx.recv()
201+
.map_err(|e| {
202+
warn!("Internal receive error: {e}");
203+
EspError::from_infallible::<ESP_ERR_INVALID_STATE>()
204+
})?;
205+
match event {
206+
DppEvent::ConfigurationReceived(config) => Ok(config),
207+
DppEvent::Fail(e) => Err(e),
208+
_ => {
209+
warn!("Ignoring unexpected event {event:?}");
210+
Err(EspError::from_infallible::<ESP_ERR_INVALID_STATE>())
211+
}
212+
}
213+
}
214+
215+
pub fn listen_forever(&self) -> Result<ClientConfiguration, EspError> {
216+
loop {
217+
match self.listen_once() {
218+
Ok(config) => return Ok(config),
219+
Err(e) => warn!("DPP error: {e}, trying again..."),
220+
}
221+
}
222+
}
223+
}

0 commit comments

Comments
 (0)