📟 pmme-device: BT_NIMBLE support for wifi provisioning

This commit is contained in:
Joseph Ferano 2025-06-12 14:40:59 +07:00
parent 9d5b9366d5
commit 4e07dbb461
7 changed files with 243 additions and 97 deletions

View File

@ -19,8 +19,7 @@ opt-level = "z"
[features] [features]
default = [] default = []
experimental = ["esp-idf-svc/experimental"]
# experimental = ["esp-idf-svc/experimental"]
[dependencies] [dependencies]
log = "0.4" log = "0.4"
@ -28,7 +27,6 @@ esp-idf-svc = { version = "0.51", features = ["critical-section", "embassy-time-
anyhow = "1.0.98" anyhow = "1.0.98"
embedded-graphics = "0.8.1" embedded-graphics = "0.8.1"
ssd1306 = "0.10.0" ssd1306 = "0.10.0"
# esp-idf-hal = { version = "0.45.2", features = ["std"] }
[build-dependencies] [build-dependencies]
embuild = "0.33" embuild = "0.33"

View File

@ -1,5 +1,13 @@
# Rust often needs a bit of an extra main task stack size compared to C (the default is 3K) # Rust often needs a bit of an extra main task stack size compared to C (the default is 3K)
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8000 CONFIG_ESP_MAIN_TASK_STACK_SIZE=8000
# CONFIG_PARTITION_TABLE_CUSTOM=y
# CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="./partitions.csv"
CONFIG_BT_ENABLED=y
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_WIFI_PROV_SCHEME_BLE=y
# CONFIG_WIFI_PROV_AUTOSTOP_TIMEOUT=30
# CONFIG_WIFI_PROV_STA_ALL_CHANNEL_SCAN=y
# Use this to set FreeRTOS kernel tick frequency to 1000 Hz (100 Hz by default). # Use this to set FreeRTOS kernel tick frequency to 1000 Hz (100 Hz by default).
# This allows to use 1 ms granularity for thread sleeps (10 ms by default). # This allows to use 1 ms granularity for thread sleeps (10 ms by default).

View File

@ -1,13 +1,23 @@
use crate::Pms7003Data; use crate::Pms7003Data;
use std::sync::{Arc, Mutex};
use log::info;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use log::info;
use std::sync::{Arc, Mutex};
use embedded_graphics::{Drawable, image::{Image, ImageRaw}, mono_font::{ascii::FONT_10X20, MonoTextStyle}, pixelcolor::BinaryColor, prelude::Point, text::Text};
use embedded_graphics::draw_target::DrawTarget; use embedded_graphics::draw_target::DrawTarget;
use embedded_graphics::{
image::{Image, ImageRaw},
mono_font::{ascii::FONT_10X20, MonoTextStyle},
pixelcolor::BinaryColor,
prelude::Point,
text::Text,
Drawable,
};
use esp_idf_svc::hal::{delay::FreeRtos, i2c::I2cDriver}; use esp_idf_svc::hal::{delay::FreeRtos, i2c::I2cDriver};
use ssd1306::{mode::DisplayConfig, prelude::DisplayRotation, size::DisplaySize128x64, I2CDisplayInterface, Ssd1306}; use ssd1306::{
mode::DisplayConfig, prelude::DisplayRotation, size::DisplaySize128x64, I2CDisplayInterface,
Ssd1306,
};
pub fn oled_task(pm_data: Arc<Mutex<Pms7003Data>>, i2c: I2cDriver) -> Result<()> { pub fn oled_task(pm_data: Arc<Mutex<Pms7003Data>>, i2c: I2cDriver) -> Result<()> {
info!("Staring OLED Task"); info!("Staring OLED Task");
@ -41,9 +51,7 @@ pub fn oled_task(pm_data: Arc<Mutex<Pms7003Data>>, i2c: I2cDriver) -> Result<()>
.map_err(|e| anyhow!("Could not draw text! {:?}", e))?; .map_err(|e| anyhow!("Could not draw text! {:?}", e))?;
display display
.flush() .flush()
.map_err(|e| { .map_err(|e| anyhow!("Could not flush display! {:?}", e))?;
anyhow!("Could not flush display! {:?}", e)
})?;
FreeRtos::delay_ms(500); FreeRtos::delay_ms(500);
} }
} }

View File

@ -1,6 +1,6 @@
pub mod wifi;
pub mod sensors;
pub mod display; pub mod display;
pub mod sensors;
pub mod wifi;
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct Pms7003Data { pub struct Pms7003Data {

View File

@ -1,15 +1,23 @@
use std::{sync::{Arc, Mutex}, thread};
use anyhow::Result; use anyhow::Result;
use std::{
use esp_idf_svc::hal::{ sync::{Arc, Mutex},
gpio, thread,
i2c::{I2cConfig, I2cDriver},
peripherals::Peripherals,
prelude::FromValueType,
uart::{config, UartDriver},
units::Hertz,
}; };
use pmme_device::{display::oled_task, sensors::pm_sensor_task, Pms7003Data};
use esp_idf_svc::{
eventloop::EspSystemEventLoop,
hal::{
gpio,
i2c::{I2cConfig, I2cDriver},
peripherals::Peripherals,
prelude::FromValueType,
uart::{self, UartDriver},
units::Hertz,
},
log::EspLogger,
nvs::EspDefaultNvsPartition,
};
use pmme_device::{display::oled_task, sensors::sensors_task, wifi::wifi_task, Pms7003Data};
fn main() -> Result<()> { fn main() -> Result<()> {
// It is necessary to call this function once. Otherwise some patches to the runtime // It is necessary to call this function once. Otherwise some patches to the runtime
@ -17,9 +25,8 @@ fn main() -> Result<()> {
esp_idf_svc::sys::link_patches(); esp_idf_svc::sys::link_patches();
// Initialize NVS // Initialize NVS
// esp_idf_hal::nvs::EspDefaultNvsPartition::take() EspLogger::initialize_default();
// .context("Could not take NVS partition")?; let nvs = EspDefaultNvsPartition::take()?;
esp_idf_svc::log::EspLogger::initialize_default();
let peripherals = Peripherals::take()?; let peripherals = Peripherals::take()?;
let i2c = peripherals.i2c0; let i2c = peripherals.i2c0;
@ -32,7 +39,7 @@ fn main() -> Result<()> {
let tx = peripherals.pins.gpio17; let tx = peripherals.pins.gpio17;
let rx = peripherals.pins.gpio16; let rx = peripherals.pins.gpio16;
let config = config::Config::new().baudrate(Hertz(9600)); let config = uart::config::Config::new().baudrate(Hertz(9600));
let uart = UartDriver::new( let uart = UartDriver::new(
peripherals.uart2, peripherals.uart2,
tx, tx,
@ -42,19 +49,28 @@ fn main() -> Result<()> {
&config, &config,
)?; )?;
let pm_data = Arc::new(Mutex::new(Pms7003Data::default())); let sysloop = EspSystemEventLoop::take()?;
let modem = peripherals.modem;
let pm_data1 = Arc::clone(&pm_data); let pm_data = Arc::new(Mutex::new(Pms7003Data::default()));
let pm_data2 = Arc::clone(&pm_data); let oled_arc = Arc::clone(&pm_data);
let sensors_arc = Arc::clone(&pm_data);
// let wifi_arc = Arc::clone(&pm_data);
let handles = vec![ let handles = vec![
thread::spawn(move || { thread::spawn(move || {
if let Err(e) = oled_task(pm_data1, i2c) { if let Err(e) = oled_task(oled_arc, i2c) {
log::error!("OLED Task Error: {:?}", e); log::error!("OLED Task Error: {:?}", e);
} }
}), }),
thread::spawn(move || { thread::spawn(move || {
if let Err(e) = pm_sensor_task(pm_data2, &uart) { if let Err(e) = sensors_task(sensors_arc, &uart) {
log::error!("PM Sensor Task Error: {:?}", e);
}
}),
thread::spawn(move || {
// if let Err(e) = wifi_task(wifi_arc, &uart) {
if let Err(e) = wifi_task(modem, sysloop, nvs) {
log::error!("PM Sensor Task Error: {:?}", e); log::error!("PM Sensor Task Error: {:?}", e);
} }
}), }),

View File

@ -1,10 +1,13 @@
use crate::Pms7003Data; use crate::Pms7003Data;
use std::sync::{Arc, Mutex};
use log::{info, warn};
use anyhow::Result; use anyhow::Result;
use log::{info, warn};
use std::sync::{Arc, Mutex};
use esp_idf_svc::hal::{delay::{FreeRtos, BLOCK}, uart::UartDriver}; use esp_idf_svc::hal::{
delay::{FreeRtos, BLOCK},
uart::UartDriver,
};
// u16::from_be_bytes is too verbose // u16::from_be_bytes is too verbose
fn get_u16(buf: &[u8]) -> u16 { fn get_u16(buf: &[u8]) -> u16 {
@ -45,7 +48,7 @@ fn send_pms7003_command(uart: &UartDriver, cmd: u8, datah: u8, datal: u8) -> Res
Ok(()) Ok(())
} }
pub fn pm_sensor_task(pm_data: Arc<Mutex<Pms7003Data>>, uart: &UartDriver) -> Result<()> { pub fn sensors_task(pm_data: Arc<Mutex<Pms7003Data>>, uart: &UartDriver) -> Result<()> {
info!("Staring UART PM Sensor Task"); info!("Staring UART PM Sensor Task");
send_pms7003_command(uart, 0xE1, 0x00, 0x00)?; send_pms7003_command(uart, 0xE1, 0x00, 0x00)?;

View File

@ -1,79 +1,192 @@
use anyhow::Result;
use log::info; use log::info;
use anyhow::bail;
use esp_idf_svc::{ use esp_idf_svc::{
eventloop::EspSystemEventLoop, eventloop::EspSystemEventLoop,
hal::peripheral, hal::peripheral,
wifi::{AuthMethod, BlockingWifi, ClientConfiguration, Configuration, EspWifi}, nvs::EspDefaultNvsPartition,
sys::{
esp, esp_err_t, wifi_prov_event_handler_t, wifi_prov_mgr_config_t, wifi_prov_mgr_deinit, wifi_prov_mgr_init, wifi_prov_mgr_is_provisioned, wifi_prov_mgr_start_provisioning, wifi_prov_mgr_stop_provisioning, wifi_prov_mgr_wait, wifi_prov_scheme_ble, wifi_prov_security_WIFI_PROV_SECURITY_1, wifi_prov_security_t, EspError
},
wifi::{BlockingWifi, ClientConfiguration, Configuration, EspWifi},
}; };
use std::ffi::c_void;
use std::ffi::CString;
use std::ptr;
pub fn wifi( pub struct WifiProvisioning;
ssid: &str,
pass: &str, impl WifiProvisioning {
pub fn new() -> Result<Self, EspError> {
unsafe {
// Updated struct initialization
let config = wifi_prov_mgr_config_t {
scheme: wifi_prov_scheme_ble, // ble provisioning
scheme_event_handler: wifi_prov_event_handler_t {
event_cb: None, // No custom callback
user_data: ptr::null_mut(),
},
app_event_handler: wifi_prov_event_handler_t {
event_cb: None, // No custom callback
user_data: ptr::null_mut(),
},
};
esp!(wifi_prov_mgr_init(config))?;
}
Ok(WifiProvisioning)
}
pub fn start_provisioning(
&self,
security: wifi_prov_security_t,
pop: &str,
service_name: &str,
service_key: Option<&str>,
) -> Result<(), EspError> {
let pop = CString::new(pop).unwrap();
let service_name = CString::new(service_name).unwrap();
let service_key = service_key.map(|key| CString::new(key).unwrap());
let pop_ptr: *const c_void = pop.as_ptr() as *const c_void;
unsafe {
esp!(wifi_prov_mgr_start_provisioning(
security,
pop_ptr,
service_name.as_ptr(),
service_key.map_or(ptr::null(), |k| k.as_ptr()),
))?;
}
Ok(())
}
pub fn wait(&self) {
unsafe {
wifi_prov_mgr_wait();
}
}
pub fn is_provisioned(&self) -> Result<bool, EspError> {
let mut provisioned: bool = false;
let result: esp_err_t = unsafe { wifi_prov_mgr_is_provisioned(&mut provisioned) };
if result == 0 {
Ok(provisioned)
} else {
Err(EspError::from(result).unwrap())
}
}
pub fn stop(&self) {
unsafe {
wifi_prov_mgr_stop_provisioning();
}
}
}
impl Drop for WifiProvisioning {
fn drop(&mut self) {
unsafe {
wifi_prov_mgr_deinit();
}
}
}
pub fn wifi_task(
modem: impl peripheral::Peripheral<P = esp_idf_svc::hal::modem::Modem> + 'static, modem: impl peripheral::Peripheral<P = esp_idf_svc::hal::modem::Modem> + 'static,
sysloop: EspSystemEventLoop, sysloop: EspSystemEventLoop,
) -> anyhow::Result<Box<EspWifi<'static>>> { nvs: EspDefaultNvsPartition,
let mut auth_method = AuthMethod::WPA2Personal; ) -> Result<()> {
if ssid.is_empty() { info!("Provisioning device!");
bail!("Missing WiFi name") return Ok(());
} let wifi = EspWifi::new(modem, sysloop.clone(), Some(nvs))?;
if pass.is_empty() { let mut wifi = BlockingWifi::wrap(wifi, sysloop)?;
auth_method = AuthMethod::None; let prov = WifiProvisioning::new()?;
info!("Wifi password is empty"); if !prov.is_provisioned()? {
} let wifi_configuration: Configuration = Configuration::Client(ClientConfiguration {
let mut esp_wifi = EspWifi::new(modem, sysloop.clone(), None)?; ..Default::default()
});
wifi.set_configuration(&wifi_configuration)?;
wifi.start()?;
prov.start_provisioning(
wifi_prov_security_WIFI_PROV_SECURITY_1,
"abcd1234", // Proof of Possession (POP)
"PROV_ESP32", // Service Name
None, // No Service Key
)?;
let mut wifi = BlockingWifi::wrap(&mut esp_wifi, sysloop)?; println!("Waiting for Wi-Fi provisioning...");
prov.wait();
wifi.set_configuration(&Configuration::Client(ClientConfiguration::default()))?; println!("Provisioning completed. Stopping...");
prov.stop();
info!("Starting wifi...");
wifi.start()?;
info!("Scanning...");
let ap_infos = wifi.scan()?;
let ours = ap_infos.into_iter().find(|a| a.ssid == ssid);
let channel = if let Some(ours) = ours {
info!(
"Found configured access point {} on channel {}",
ssid, ours.channel
);
Some(ours.channel)
} else { } else {
info!( wifi.start()?;
"Configured access point {} not found during scanning, will go with unknown channel", wifi.connect()?;
ssid }
);
None
};
wifi.set_configuration(&Configuration::Client(ClientConfiguration {
ssid: ssid
.try_into()
.expect("Could not parse the given SSID into WiFi config"),
password: pass
.try_into()
.expect("Could not parse the given password into WiFi config"),
channel,
auth_method,
..Default::default()
}))?;
info!("Connecting wifi...");
wifi.connect()?;
info!("Waiting for DHCP lease...");
wifi.wait_netif_up()?; wifi.wait_netif_up()?;
let ip_info = wifi.wifi().sta_netif().get_ip_info()?; let ip_info = wifi.wifi().sta_netif().get_ip_info()?;
println!("Wifi DHCP info: {:?}", ip_info);
info!("Wifi DHCP info: {:?}", ip_info); // let mut auth_method = AuthMethod::WPA2Personal;
// if ssid.is_empty() {
// bail!("Missing WiFi name")
// }
// if pass.is_empty() {
// auth_method = AuthMethod::None;
// info!("Wifi password is empty");
// }
// let mut esp_wifi = EspWifi::new(modem, sysloop.clone(), None)?;
Ok(Box::new(esp_wifi)) // let mut wifi = BlockingWifi::wrap(&mut esp_wifi, sysloop)?;
// wifi.set_configuration(&Configuration::Client(ClientConfiguration::default()))?;
// info!("Starting wifi...");
// wifi.start()?;
// info!("Scanning...");
// let ap_infos = wifi.scan()?;
// let ours = ap_infos.into_iter().find(|a| a.ssid == ssid);
// let channel = if let Some(ours) = ours {
// info!(
// "Found configured access point {} on channel {}",
// ssid, ours.channel
// );
// Some(ours.channel)
// } else {
// info!(
// "Configured access point {} not found during scanning, will go with unknown channel",
// ssid
// );
// None
// };
// wifi.set_configuration(&Configuration::Client(ClientConfiguration {
// ssid: ssid
// .try_into()
// .expect("Could not parse the given SSID into WiFi config"),
// password: pass
// .try_into()
// .expect("Could not parse the given password into WiFi config"),
// channel,
// auth_method,
// ..Default::default()
// }))?;
// info!("Connecting wifi...");
// wifi.connect()?;
// info!("Waiting for DHCP lease...");
// wifi.wait_netif_up()?;
// let ip_info = wifi.wifi().sta_netif().get_ip_info()?;
// info!("Wifi DHCP info: {:?}", ip_info);
// Ok(Box::new(esp_wifi))
Ok(())
} }