diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 5db1612a7..4b03432f0 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -153,6 +153,8 @@ jobs:
target: esp32
- path: 'components/ping/example'
target: esp32s3
+ - path: 'components/provisioning/example'
+ target: esp32s3
- path: 'components/qmi8658/example'
target: esp32s3
- path: 'components/qtpy/example'
diff --git a/.github/workflows/upload_components.yml b/.github/workflows/upload_components.yml
index 7b3307f85..a84c47a80 100644
--- a/.github/workflows/upload_components.yml
+++ b/.github/workflows/upload_components.yml
@@ -94,10 +94,11 @@ jobs:
components/neopixel
components/nvs
components/odrive_ascii
+ components/pcf85063
components/pi4ioe5v
components/pid
components/ping
- components/pcf85063
+ components/provisioning
components/qmi8658
components/qtpy
components/qwiicnes
diff --git a/components/provisioning/CMakeLists.txt b/components/provisioning/CMakeLists.txt
new file mode 100644
index 000000000..f6edaf0b0
--- /dev/null
+++ b/components/provisioning/CMakeLists.txt
@@ -0,0 +1,5 @@
+idf_component_register(
+ INCLUDE_DIRS "include"
+ SRC_DIRS "src"
+ REQUIRES esp_http_server nvs wifi base_component
+)
diff --git a/components/provisioning/README.md b/components/provisioning/README.md
new file mode 100644
index 000000000..fc1b932dd
--- /dev/null
+++ b/components/provisioning/README.md
@@ -0,0 +1,26 @@
+# Provisioning Component
+
+[](https://components.espressif.com/components/espp/provisioning)
+
+The `Provisioning` component provides a web-based WiFi configuration system
+for ESP32 devices. It creates a temporary WiFi access point with an embedded
+web server, allowing users to scan for available networks, test credentials,
+and save configuration through a mobile-friendly interface.
+
+## Features
+
+- Creates temporary WiFi AP with configurable SSID and password
+- Embedded web server with responsive HTML interface
+- Network scanning with signal strength indication
+- Manual SSID entry option for hidden networks
+- Credential validation before saving (tests actual connection)
+- Manages stored credentials (view, delete, reconnect)
+- Automatic AP shutdown after successful provisioning
+- NVS-based persistent storage
+- Callbacks for provisioning events
+
+## Example
+
+The [example](./example) demonstrates how to use the `espp::Provisioning`
+class to configure WiFi credentials via a web interface, with support for
+scanning networks, testing connections, and managing stored credentials.
diff --git a/components/provisioning/example/CMakeLists.txt b/components/provisioning/example/CMakeLists.txt
new file mode 100644
index 000000000..d8042dba3
--- /dev/null
+++ b/components/provisioning/example/CMakeLists.txt
@@ -0,0 +1,22 @@
+# The following lines of boilerplate have to be in your project's CMakeLists
+# in this exact order for cmake to work correctly
+cmake_minimum_required(VERSION 3.20)
+
+set(ENV{IDF_COMPONENT_MANAGER} "0")
+include($ENV{IDF_PATH}/tools/cmake/project.cmake)
+
+# add the component directories that we want to use
+set(EXTRA_COMPONENT_DIRS
+ "../../../components/"
+)
+
+set(
+ COMPONENTS
+ "main esptool_py provisioning wifi task"
+ CACHE STRING
+ "List of components to include"
+ )
+
+project(provisioning_example)
+
+set(CMAKE_CXX_STANDARD 20)
diff --git a/components/provisioning/example/README.md b/components/provisioning/example/README.md
new file mode 100644
index 000000000..19d9c9f14
--- /dev/null
+++ b/components/provisioning/example/README.md
@@ -0,0 +1,255 @@
+# Provisioning Example
+
+This example demonstrates the `espp::Provisioning` component, which provides a
+web-based WiFi configuration interface for ESP32 devices.
+
+__Scanning for network and connecting__:
+
+
+
scan
+
enter password
+
+
+
connecting
+
connected
+
+
+
provisioning completed
+
+
+
+__Managing saved networks__:
+
+
+
view
+
connect
+
+
+
connected
+
delete
+
+
+
+## How to use example
+
+### Hardware Required
+
+This example can run on any ESP32 development board with WiFi capability.
+
+### Build and Flash
+
+Build the project and flash it to the board, then run monitor tool to view serial output:
+
+```
+idf.py -p PORT flash monitor
+```
+
+(Replace PORT with the name of the serial port to use.)
+
+(To exit the serial monitor, type ``Ctrl-]``.)
+
+See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects.
+
+## Example Usage
+
+### First Time Setup
+
+1. Flash the example to your ESP32 device
+2. The device will create a WiFi access point named `ESP-Prov-XXXX` (where XXXX is part of the MAC address)
+3. Connect to this AP using your phone or computer (password: "configure")
+4. Open a web browser and navigate to `http://192.168.4.1`
+5. Use the web interface to:
+ - Click "Scan Networks" to see available WiFi networks
+ - Select a network from the list (or manually enter SSID)
+ - Enter the network password
+ - Click "Connect" to test the connection
+ - If successful, click "Complete Provisioning" to save and exit
+
+### Subsequent Boots
+
+The device stores WiFi credentials in NVS for future use. The provisioning web
+interface allows you to view saved networks and reconnect to them. Note: This
+example does not automatically connect to saved networks - it only demonstrates
+the provisioning UI. To implement auto-connect on boot, your application should
+retrieve saved credentials from NVS and use the `WifiSta` component to connect.
+
+## Example Output
+
+
+Scanning for network and connecting:
+
+
+
+
+
+
+
+Managing saved networks:
+
+
+
+
+
+```console
+I (415) main_task: Calling app_main()
+[Provisioning Example/I][0.415]: Starting WiFi Provisioning Example
+[Provisioning Example/I][0.415]: Initializing NVS...
+[Provisioning/I][0.465]: Initialized with AP SSID: ESP-Prov-8590
+[Provisioning Example/I][0.465]: Existing provisioned network SSID: HouseOfBoo
+[WifiAp/I][0.465]: Creating network interface and ensuring WiFi stack is initialized
+I (475) pp: pp rom version: e7ae62f
+I (475) net80211: net80211 rom version: e7ae62f
+I (495) wifi:wifi driver task: 3fcedc7c, prio:23, stack:6656, core=0
+I (505) wifi:wifi firmware version: ee91c8c
+I (505) wifi:wifi certification version: v7.0
+I (505) wifi:config NVS flash: enabled
+I (505) wifi:config nano formatting: disabled
+I (505) wifi:Init data frame dynamic rx buffer num: 32
+I (515) wifi:Init static rx mgmt buffer num: 5
+I (515) wifi:Init management short buffer num: 32
+I (525) wifi:Init dynamic tx buffer num: 32
+I (525) wifi:Init static tx FG buffer num: 2
+I (535) wifi:Init static rx buffer size: 1600
+I (535) wifi:Init static rx buffer num: 10
+I (535) wifi:Init dynamic rx buffer num: 32
+I (545) wifi_init: rx ba win: 6
+I (545) wifi_init: accept mbox: 6
+I (545) wifi_init: tcpip mbox: 32
+I (555) wifi_init: udp mbox: 6
+I (555) wifi_init: tcp mbox: 6
+I (555) wifi_init: tcp tx win: 5760
+I (565) wifi_init: tcp rx win: 5760
+I (565) wifi_init: tcp mss: 1440
+I (565) wifi_init: WiFi IRAM OP enabled
+I (575) wifi_init: WiFi RX IRAM OP enabled
+[WifiAp/I][0.575]: Reconfiguring WiFi AP
+[WifiAp/I][0.575]: WiFi AP stopped
+I (585) phy_init: phy_version 711,97bcf0a2,Aug 25 2025,19:04:10
+I (625) wifi:mode : softAP (f4:12:fa:5a:85:91)
+I (625) wifi:Total power save buffer number: 16
+I (625) wifi:Init max length of beacon: 752/752
+I (625) wifi:Init max length of beacon: 752/752
+I (635) esp_netif_lwip: DHCP server started on interface WIFI_AP_DEF with IP: 192.168.4.1
+[WifiAp/I][0.635]: WiFi AP started
+[WifiAp/I][0.645]: WiFi AP started
+[WifiAp/I][0.645]: WiFi AP started, SSID: 'ESP-Prov-8590'
+[Provisioning/I][0.655]: Provisioning started at http://192.168.4.1
+[Provisioning Example/I][0.655]: Provisioning started
+[Provisioning Example/I][0.665]: Connect to WiFi network: ESP-Prov-8590
+[Provisioning Example/I][0.675]: Open browser to: http://192.168.4.1
+I (7265) wifi:new:<1,0>, old:<1,1>, ap:<1,1>, sta:<255,255>, prof:1, snd_ch_cfg:0x0
+I (7265) wifi:station: a2:51:f2:06:a8:34 join, AID=1, bgn, 20
+[WifiAp/I][7.265]: Station join, AID=1
+I (7475) wifi:idx:2 (ifx:1, a2:51:f2:06:a8:34), tid:0, ssn:643, winSize:64
+I (7695) esp_netif_lwip: DHCP server assigned IP to a client, IP is: 192.168.4.2
+I (7935) wifi:idx:3 (ifx:1, a2:51:f2:06:a8:34), tid:6, ssn:2922, winSize:64
+[WifiAp/I][15.025]: Station leave, AID=1
+I (15035) wifi:idx:2, tid:0
+I (15035) wifi:idx:3, tid:6
+I (15035) wifi:new:<1,0>, old:<1,0>, ap:<1,1>, sta:<255,255>, prof:1, snd_ch_cfg:0x0
+I (15035) wifi:station: a2:51:f2:06:a8:34 join, AID=1, bgn, 20
+[WifiAp/I][15.045]: Station join, AID=1
+I (15205) wifi:idx:2 (ifx:1, a2:51:f2:06:a8:34), tid:0, ssn:2714, winSize:64
+I (15345) esp_netif_lwip: DHCP server assigned IP to a client, IP is: 192.168.4.2
+I (15655) wifi:idx:3 (ifx:1, a2:51:f2:06:a8:34), tid:6, ssn:3177, winSize:64
+[Provisioning/I][17.785]: Starting WiFi scan...
+I (17795) wifi:mode : sta (f4:12:fa:5a:85:90) + softAP (f4:12:fa:5a:85:91)
+I (17795) wifi:enable tsf
+I (20705) wifi:mode : softAP (f4:12:fa:5a:85:91)
+[Provisioning/I][20.715]: Found 3 networks
+[Provisioning/I][34.945]: Testing connection to: HouseOfBoo
+[Provisioning/I][34.955]: Creating test WiFi STA instance
+[WifiSta/I][34.955]: Creating network interface and ensuring WiFi stack is initialized
+[WifiSta/D][34.965]: Adding event handler for WIFI_EVENT(s)
+[WifiSta/D][34.965]: Adding IP event handler for IP_EVENT_STA_GOT_IP
+[WifiSta/I][34.975]: Reconfiguring WiFi STA
+[WifiSta/D][34.975]: Setting WiFi SSID to 'HouseOfBoo'
+[WifiSta/D][34.985]: AP mode already active, setting mode to APSTA
+[WifiSta/D][34.995]: Setting WiFi mode to WIFI_MODE_APSTA
+I (35005) wifi:mode : sta (f4:12:fa:5a:85:90) + softAP (f4:12:fa:5a:85:91)
+I (35005) wifi:enable tsf
+[WifiSta/D][35.025]: Setting WiFi config
+W (35025) wifi:Password length matches WPA2 standards, authmode threshold changes from OPEN to WPA2
+[WifiSta/D][35.025]: WIFI_EVENT_STA_START - initiating connection
+[WifiSta/D][35.035]: Setting WiFi PHY rate to MCS0_LGI (6.5-13.5 Mbps)
+I (35045) wifi:primary chan differ, old=1, new=9, start CSA timer
+[WifiSta/I][35.045]: WiFi STA reconfigured successfully
+[WifiSta/D][35.055]: Starting WiFi
+[WifiSta/I][35.055]: WiFi started
+[WifiSta/I][35.065]: WiFi STA started, SSID: 'HouseOfBoo'
+[Provisioning/I][35.065]: Waiting for connection (max 15 seconds)...
+I (35445) wifi:switch to channel 9
+I (35445) wifi:ap channel adjust o:1,1 n:9,2
+I (35445) wifi:new:<9,0>, old:<1,0>, ap:<9,2>, sta:<0,0>, prof:1, snd_ch_cfg:0x0
+I (35455) wifi:new:<9,2>, old:<9,0>, ap:<9,2>, sta:<9,0>, prof:1, snd_ch_cfg:0x0
+I (35455) wifi:state: init -> auth (0xb0)
+I (35475) wifi:state: auth -> assoc (0x0)
+I (35495) wifi:state: assoc -> run (0x10)
+I (35525) wifi:connected with HouseOfBoo, aid = 7, channel 9, BW20, bssid = 08:02:8e:8a:54:73
+I (35525) wifi:security: WPA2-PSK, phy: bgn, rssi: -55
+I (35525) wifi:pm start, type: 1
+
+I (35525) wifi:dp: 1, bi: 102400, li: 3, scale listen interval from 307200 us to 307200 us
+I (35535) wifi:set rx beacon pti, rx_bcn_pti: 0, bcn_timeout: 25000, mt_pti: 0, mt_time: 10000
+I (35545) wifi:dp: 2, bi: 102400, li: 4, scale listen interval from 307200 us to 409600 us
+I (35555) wifi:AP's beacon interval = 102400 us, DTIM period = 2
+[WifiSta/D][35.565]: WIFI_EVENT_STA_CONNECTED - waiting for IP
+I (36905) wifi:idx:0 (ifx:0, 08:02:8e:8a:54:73), tid:6, ssn:4, winSize:64
+I (37905) esp_netif_handlers: sta ip: 192.168.1.25, mask: 255.255.255.0, gw: 192.168.1.1
+[WifiSta/I][37.905]: got ip: 192.168.1.25
+[Provisioning/I][37.905]: Connection callback - connected to AP
+[Provisioning/I][37.905]: Got IP callback - 192.168.1.25
+[Provisioning/I][38.015]: Connection test result: true (got_ip=true, failed=false)
+[Provisioning/I][38.015]: Cleaning up test STA
+[WifiSta/D][38.015]: Destroying WiFiSta
+[WifiSta/D][38.015]: Unregistering event handlers
+[WifiSta/D][38.025]: Unregistering any wifi event handler
+[WifiSta/D][38.035]: Unregistering got ip event handler
+[WifiSta/D][38.035]: In APSTA mode, disconnecting STA interface
+I (38045) wifi:state: run -> init (0x0)
+I (38065) wifi:pm stop, total sleep time: 1777943 us / 2533745 us
+
+I (38065) wifi:idx:0, tid:6
+I (38065) wifi:new:<9,0>, old:<9,2>, ap:<9,2>, sta:<9,0>, prof:1, snd_ch_cfg:0x0
+I (38065) wifi:mode : softAP (f4:12:fa:5a:85:91)
+[WifiSta/I][38.075]: WiFi STA disconnected, AP still running
+[Provisioning/I][38.285]: Saving credentials for: HouseOfBoo
+[Provisioning Example/I][40.965]: Provisioned successfully!
+[Provisioning Example/I][40.965]: SSID: HouseOfBoo
+[Provisioning Example/I][40.965]: Password: ********
+[Provisioning Example/I][40.965]: Credentials saved to NVS
+[Provisioning Example/I][41.085]: Provisioning completed by user, stopping service
+I (41185) wifi:station: a2:51:f2:06:a8:34 leave, AID = 1, reason = 2, bss_flags is 33721443, bss:0x3fcb5a9c
+I (41185) wifi:new:<9,0>, old:<9,0>, ap:<9,2>, sta:<255,255>, prof:1, snd_ch_cfg:0x0
+I (41185) wifi:idx:2, tid:0
+I (41195) wifi:idx:3, tid:6
+I (41195) wifi:new:<9,2>, old:<9,0>, ap:<9,2>, sta:<255,255>, prof:1, snd_ch_cfg:0x0
+I (41255) wifi:flush txq
+I (41255) wifi:stop sw txq
+I (41255) wifi:lmac stop hw txq
+[WifiAp/I][41.255]: WiFi AP stopped
+[Provisioning/I][41.255]: Provisioning stopped
+[Provisioning Example/I][41.255]: Provisioning example finished
+```
+
+## Configuration
+
+The example can be configured through `idf.py menuconfig` under "Provisioning Example Configuration":
+
+- **AP SSID**: Name of the provisioning access point (default: "ESP-Prov")
+- **AP Password**: Password for the provisioning AP (default: "configure")
+- **Device Name**: Friendly name shown in web interface
+- **Auto-generate unique SSID**: Append MAC address to SSID to avoid conflicts
+- **Auto-shutdown AP**: Automatically stop AP after successful provisioning
+- **Web Server Port**: HTTP server port (default: 80)
+
+## Web Interface Features
+
+The embedded web interface provides:
+
+- **Network Scanner**: Shows available WiFi networks with signal strength indicators
+- **Manual Entry**: Option to manually enter SSID for hidden networks
+- **Connection Testing**: Validates credentials before saving
+- **Credential Management**: View, delete, and reconnect to stored networks
+- **Responsive Design**: Works on mobile phones, tablets, and desktop browsers
+- **Real-time Feedback**: Shows connection status and error messages
diff --git a/components/provisioning/example/main/CMakeLists.txt b/components/provisioning/example/main/CMakeLists.txt
new file mode 100644
index 000000000..a941e22ba
--- /dev/null
+++ b/components/provisioning/example/main/CMakeLists.txt
@@ -0,0 +1,2 @@
+idf_component_register(SRC_DIRS "."
+ INCLUDE_DIRS ".")
diff --git a/components/provisioning/example/main/provisioning_example.cpp b/components/provisioning/example/main/provisioning_example.cpp
new file mode 100644
index 000000000..14726b870
--- /dev/null
+++ b/components/provisioning/example/main/provisioning_example.cpp
@@ -0,0 +1,94 @@
+#include
+#include
+
+#include "logger.hpp"
+#include "provisioning.hpp"
+
+using namespace std::chrono_literals;
+
+extern "C" void app_main(void) {
+ //! [provisioning example]
+ espp::Logger logger({.tag = "Provisioning Example", .level = espp::Logger::Verbosity::INFO});
+
+ logger.info("Starting WiFi Provisioning Example");
+
+ // Initialize NVS (required for WiFi)
+ logger.info("Initializing NVS...");
+ std::error_code ec;
+ espp::Nvs nvs;
+ nvs.init(ec);
+ if (ec) {
+ logger.error("Failed to initialize NVS: {}", ec.message());
+ return;
+ }
+
+ // Create provisioning configuration
+ espp::Provisioning::Config config{
+ .ap_ssid = "ESP-Prov",
+ .append_mac_to_ssid = true, // Will append MAC to make unique
+ .ap_password = "", // Open network for simplicity
+ .device_name = "Provisioning Example",
+ .server_port = 80,
+ .auto_shutdown_ap = false, // Keep AP running for this example
+ .on_provisioned =
+ [&logger](const std::string &ssid, const std::string &password) {
+ logger.info("Provisioned successfully!");
+ logger.info(" SSID: {}", ssid);
+ logger.info(" Password: {}", password.empty() ? "(none)" : "********");
+
+ // Save credentials to NVS
+ std::error_code ec;
+ espp::NvsHandle handle("wifi_config", ec);
+ if (!ec) {
+ handle.set("ssid", ssid, ec);
+ handle.set("password", password, ec);
+ if (!ec) {
+ logger.info("Credentials saved to NVS");
+ } else {
+ logger.error("Failed to save credentials: {}", ec.message());
+ }
+ }
+ },
+ .log_level = espp::Logger::Verbosity::INFO};
+
+ // Create provisioning
+ espp::Provisioning provisioning(config);
+
+ // print any existing network ssids that have been provisioned
+ {
+ espp::NvsHandle handle("wifi_config", ec);
+ if (!ec) {
+ std::string saved_ssid;
+ handle.get("ssid", saved_ssid, ec);
+ if (!ec) {
+ logger.info("Existing provisioned network SSID: {}", saved_ssid);
+ } else {
+ logger.info("No existing provisioned network found in NVS");
+ }
+ }
+ }
+
+ // Start provisioning
+ if (!provisioning.start()) {
+ logger.error("Failed to start provisioning");
+ return;
+ }
+
+ logger.info("Provisioning started");
+ logger.info("Connect to WiFi network: {}", provisioning.get_ap_ssid());
+ logger.info("Open browser to: http://192.168.4.1");
+
+ // Keep running until user completes provisioning
+ while (!provisioning.is_completed()) {
+ std::this_thread::sleep_for(1s);
+ }
+
+ logger.info("Provisioning completed by user, stopping service");
+ provisioning.stop();
+
+ logger.info("Provisioning example finished");
+ //! [provisioning example]
+ while (true) {
+ std::this_thread::sleep_for(1s);
+ }
+}
diff --git a/components/provisioning/example/partitions.csv b/components/provisioning/example/partitions.csv
new file mode 100644
index 000000000..c4217ab9e
--- /dev/null
+++ b/components/provisioning/example/partitions.csv
@@ -0,0 +1,5 @@
+# ESP-IDF Partition Table
+# Name, Type, SubType, Offset, Size, Flags
+nvs, data, nvs, 0x9000, 0x6000,
+phy_init, data, phy, 0xf000, 0x1000,
+factory, app, factory, 0x10000, 1500K,
diff --git a/components/provisioning/example/sdkconfig.defaults b/components/provisioning/example/sdkconfig.defaults
new file mode 100644
index 000000000..e20c8f577
--- /dev/null
+++ b/components/provisioning/example/sdkconfig.defaults
@@ -0,0 +1,20 @@
+# Flash size
+CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
+CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
+
+# CONFIG_ESP32_WIFI_NVS_ENABLED=n
+
+CONFIG_PARTITION_TABLE_CUSTOM=y
+CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
+CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
+CONFIG_PARTITION_TABLE_OFFSET=0x8000
+CONFIG_PARTITION_TABLE_MD5=y
+
+# Common ESP-related
+#
+CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096
+CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
+
+# HTTP Server
+CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024
+CONFIG_HTTPD_MAX_URI_LEN=512
diff --git a/components/provisioning/idf_component.yml b/components/provisioning/idf_component.yml
new file mode 100644
index 000000000..aa87ef7ce
--- /dev/null
+++ b/components/provisioning/idf_component.yml
@@ -0,0 +1,20 @@
+## IDF Component Manager Manifest File
+license: "MIT"
+description: "WiFi provisioning component with web interface"
+url: "https://github.com/esp-cpp/espp/tree/main/components/provisioning"
+repository: "git://github.com/esp-cpp/espp.git"
+maintainers:
+ - William Emfinger
+documentation: "https://esp-cpp.github.io/espp/network/provisioning.html"
+tags:
+ - cpp
+ - WiFi
+ - Provisioning
+dependencies:
+ idf:
+ version: '>=5.0'
+ espp/base_component: '>=1.0'
+ espp/nvs: '>=1.0'
+ espp/wifi: '>=1.0'
+examples:
+ - path: example
diff --git a/components/provisioning/include/provisioning.hpp b/components/provisioning/include/provisioning.hpp
new file mode 100644
index 000000000..cb7dc444b
--- /dev/null
+++ b/components/provisioning/include/provisioning.hpp
@@ -0,0 +1,151 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "base_component.hpp"
+#include "nvs.hpp"
+#include "wifi.hpp"
+#include "wifi_ap.hpp"
+#include "wifi_sta.hpp"
+
+#include "esp_http_server.h"
+
+namespace espp {
+
+/**
+ * @brief WiFi Provisioning Component
+ *
+ * Provides a web-based WiFi provisioning interface. Creates a WiFi access point
+ * with an embedded web server that allows users to scan for and connect to WiFi networks.
+ *
+ * Features:
+ * - Automatic AP creation with customizable SSID/password
+ * - HTML web interface for WiFi scanning and configuration
+ * - Network scanning and signal strength display
+ * - Credential validation before saving
+ * - Callback notification on successful provisioning
+ * - Automatic AP shutdown after provisioning (optional)
+ *
+ * \section provisioning_ex1 Provisioning Example
+ * \snippet provisioning_example.cpp provisioning example
+ */
+class Provisioning : public BaseComponent {
+public:
+ /**
+ * @brief Callback when provisioning completes successfully
+ * @param ssid The SSID that was provisioned
+ * @param password The password that was configured
+ */
+ typedef std::function
+ provisioned_callback;
+
+ /**
+ * @brief Configuration for the provisioning component
+ */
+ struct Config {
+ std::string ap_ssid{"ESP-Prov"}; ///< Base SSID for the provisioning AP
+ bool append_mac_to_ssid{true}; ///< Append MAC address to AP SSID
+ std::string ap_password{""}; ///< Password for AP (empty = open)
+ std::string device_name{"ESP32 Device"}; ///< Device name shown in UI
+ uint16_t server_port{80}; ///< HTTP server port
+ bool auto_shutdown_ap{true}; ///< Shutdown AP after provisioning
+ provisioned_callback on_provisioned{nullptr}; ///< Called on successful provisioning
+ Logger::Verbosity log_level{Logger::Verbosity::WARN}; ///< Log verbosity
+ };
+
+ /**
+ * @brief Construct provisioning component
+ * @param config Configuration structure
+ */
+ explicit Provisioning(const Config &config);
+
+ /**
+ * @brief Destructor - stops server and AP
+ */
+ ~Provisioning();
+
+ /**
+ * @brief Start the provisioning process
+ * @return true if started successfully
+ */
+ bool start();
+
+ /**
+ * @brief Stop the provisioning process
+ */
+ void stop();
+
+ /**
+ * @brief Check if provisioning is active
+ * @return true if the AP and server are running
+ */
+ bool is_active() const { return is_active_; }
+
+ /**
+ * @brief Check if device has been provisioned
+ * @return true if provisioning callback has been called
+ */
+ bool is_provisioned() const { return is_provisioned_; }
+
+ /**
+ * @brief Check if user has completed provisioning
+ * @return true if user clicked "Complete Setup" button
+ */
+ bool is_completed() const { return is_completed_; }
+
+ /**
+ * @brief Get the IP address of the provisioning AP
+ * @return IP address string (e.g., "192.168.4.1")
+ */
+ std::string get_ip_address() const;
+
+ /**
+ * @brief Get the actual AP SSID (after MAC appending)
+ * @return The SSID of the provisioning AP
+ */
+ const std::string &get_ap_ssid() const { return config_.ap_ssid; }
+
+protected:
+ void init(const Config &config);
+ bool start_ap();
+ bool start_server();
+ void stop_server();
+ void stop_ap();
+
+ // HTTP handlers
+ static esp_err_t root_handler(httpd_req_t *req);
+ static esp_err_t scan_handler(httpd_req_t *req);
+ static esp_err_t connect_handler(httpd_req_t *req);
+ static esp_err_t complete_handler(httpd_req_t *req);
+ static esp_err_t status_handler(httpd_req_t *req);
+ static esp_err_t saved_handler(httpd_req_t *req);
+ static esp_err_t delete_handler(httpd_req_t *req);
+
+ // Helper methods
+ std::string generate_html() const;
+ std::string scan_networks();
+ bool test_connection(const std::string &ssid, const std::string &password);
+
+ // Credential storage methods
+ std::vector get_saved_ssids();
+ std::string get_saved_password(const std::string &ssid);
+ void save_credentials(const std::string &ssid, const std::string &password);
+ void delete_credentials(const std::string &ssid);
+ std::string get_saved_networks_json();
+
+ Config config_;
+ std::unique_ptr wifi_ap_;
+ std::unique_ptr test_sta_;
+ httpd_handle_t server_{nullptr};
+ std::atomic is_active_{false};
+ std::atomic is_provisioned_{false};
+ std::atomic is_completed_{false};
+ std::string provisioned_ssid_;
+ std::string provisioned_password_;
+};
+} // namespace espp
diff --git a/components/provisioning/src/provisioning.cpp b/components/provisioning/src/provisioning.cpp
new file mode 100644
index 000000000..2e38f3572
--- /dev/null
+++ b/components/provisioning/src/provisioning.cpp
@@ -0,0 +1,852 @@
+#include "provisioning.hpp"
+
+#include
+#include
+#include
+
+using namespace espp;
+using namespace std::chrono_literals;
+
+// Helper function to escape JSON strings
+static std::string json_escape(const std::string &str) {
+ std::string escaped;
+ escaped.reserve(str.size());
+ for (char c : str) {
+ switch (c) {
+ case '"':
+ escaped += "\\\"";
+ break;
+ case '\\':
+ escaped += "\\\\";
+ break;
+ case '\b':
+ escaped += "\\b";
+ break;
+ case '\f':
+ escaped += "\\f";
+ break;
+ case '\n':
+ escaped += "\\n";
+ break;
+ case '\r':
+ escaped += "\\r";
+ break;
+ case '\t':
+ escaped += "\\t";
+ break;
+ default:
+ if (c < 0x20) {
+ // Control characters - escape as \uXXXX
+ char buf[7];
+ snprintf(buf, sizeof(buf), "\\u%04x", static_cast(c));
+ escaped += buf;
+ } else {
+ escaped += c;
+ }
+ }
+ }
+ return escaped;
+}
+
+static const char *HTML_TEMPLATE = R"HTML(
+
+
+
+
+ WiFi Setup
+
+
+
+
+
WiFi Setup
+
%DEVICE_NAME%
+
+
+
+
Saved Networks
+
+
+
+
― OR ―
+
+
+
+
+
+
+
+
Connect to
+
+
+
+
+
+
― OR ―
+
+
+
+
Manual Network Entry
+
+
+
+
+
+
+
+
+
+
+
+)HTML";
+
+Provisioning::Provisioning(const Config &config)
+ : BaseComponent("Provisioning", config.log_level) {
+ init(config);
+}
+
+Provisioning::~Provisioning() { stop(); }
+
+void Provisioning::init(const Config &config) {
+ config_ = config;
+
+ // Append MAC address to SSID if requested
+ if (config_.append_mac_to_ssid) {
+ uint8_t mac[6];
+ esp_efuse_mac_get_default(mac);
+ config_.ap_ssid = fmt::format("{}-{:02X}{:02X}", config_.ap_ssid, mac[4], mac[5]);
+ }
+
+ logger_.info("Initialized with AP SSID: {}", config_.ap_ssid);
+}
+
+bool Provisioning::start() {
+ if (is_active_) {
+ logger_.warn("Provisioning already active");
+ return false;
+ }
+
+ if (!start_ap()) {
+ logger_.error("Failed to start AP");
+ return false;
+ }
+
+ if (!start_server()) {
+ logger_.error("Failed to start HTTP server");
+ stop_ap();
+ return false;
+ }
+
+ is_active_ = true;
+ logger_.info("Provisioning started at http://{}", get_ip_address());
+ return true;
+}
+
+void Provisioning::stop() {
+ if (!is_active_) {
+ return;
+ }
+
+ stop_server();
+ stop_ap();
+ is_active_ = false;
+ logger_.info("Provisioning stopped");
+}
+
+bool Provisioning::start_ap() {
+ WifiAp::Config ap_config{.ssid = config_.ap_ssid,
+ .password = config_.ap_password,
+ .max_number_of_stations = 4,
+ .log_level = config_.log_level};
+
+ wifi_ap_ = std::make_unique(ap_config);
+
+ // WiFi mode is managed by WifiAp and WifiSta classes
+ return wifi_ap_ != nullptr;
+}
+
+bool Provisioning::start_server() {
+ httpd_config_t server_config = HTTPD_DEFAULT_CONFIG();
+ server_config.server_port = config_.server_port;
+ server_config.max_uri_handlers = 8;
+ server_config.lru_purge_enable = true;
+ server_config.stack_size = 8192;
+
+ if (httpd_start(&server_, &server_config) != ESP_OK) {
+ logger_.error("Failed to start HTTP server");
+ return false;
+ }
+
+ // Root handler
+ httpd_uri_t root = {.uri = "/", .method = HTTP_GET, .handler = root_handler, .user_ctx = this};
+ httpd_register_uri_handler(server_, &root);
+
+ // Scan handler
+ httpd_uri_t scan = {
+ .uri = "/scan", .method = HTTP_GET, .handler = scan_handler, .user_ctx = this};
+ httpd_register_uri_handler(server_, &scan);
+
+ // Connect handler
+ httpd_uri_t connect = {
+ .uri = "/connect", .method = HTTP_POST, .handler = connect_handler, .user_ctx = this};
+ httpd_register_uri_handler(server_, &connect);
+
+ // Complete handler
+ httpd_uri_t complete = {
+ .uri = "/complete", .method = HTTP_POST, .handler = complete_handler, .user_ctx = this};
+ httpd_register_uri_handler(server_, &complete);
+
+ // Status handler
+ httpd_uri_t status = {
+ .uri = "/status", .method = HTTP_GET, .handler = status_handler, .user_ctx = this};
+ httpd_register_uri_handler(server_, &status);
+
+ // Saved networks handler
+ httpd_uri_t saved = {
+ .uri = "/saved", .method = HTTP_GET, .handler = saved_handler, .user_ctx = this};
+ httpd_register_uri_handler(server_, &saved);
+
+ // Delete handler
+ httpd_uri_t delete_net = {
+ .uri = "/delete", .method = HTTP_POST, .handler = delete_handler, .user_ctx = this};
+ httpd_register_uri_handler(server_, &delete_net);
+
+ return true;
+}
+
+void Provisioning::stop_server() {
+ if (server_) {
+ httpd_stop(server_);
+ server_ = nullptr;
+ }
+}
+
+void Provisioning::stop_ap() { wifi_ap_.reset(); }
+
+std::string Provisioning::get_ip_address() const {
+ return wifi_ap_ ? wifi_ap_->get_ip_address() : "N/A";
+}
+
+std::string Provisioning::generate_html() const {
+ std::string html = HTML_TEMPLATE;
+ size_t pos = html.find("%DEVICE_NAME%");
+ if (pos != std::string::npos) {
+ html.replace(pos, 13, config_.device_name);
+ }
+ return html;
+}
+
+std::string Provisioning::scan_networks() {
+ logger_.info("Starting WiFi scan...");
+
+ // Create a temporary WifiSta for scanning (auto_connect=false so it doesn't try to connect)
+ WifiSta::Config scan_config{
+ .ssid = "", .password = "", .auto_connect = false, .log_level = Logger::Verbosity::WARN};
+ WifiSta temp_sta(scan_config);
+
+ auto ap_records = temp_sta.scan(20);
+
+ if (ap_records.empty()) {
+ logger_.info("No networks found");
+ return R"({"networks":[]})";
+ }
+
+ logger_.info("Found {} networks", ap_records.size());
+
+ // Sort by signal strength
+ std::sort(ap_records.begin(), ap_records.end(),
+ [](const wifi_ap_record_t &a, const wifi_ap_record_t &b) { return a.rssi > b.rssi; });
+
+ std::string json = R"({"networks":[)";
+ for (size_t i = 0; i < ap_records.size(); i++) {
+ if (i > 0)
+ json += ",";
+ std::string ssid = reinterpret_cast(ap_records[i].ssid);
+ json += fmt::format(R"({{"ssid":"{}","rssi":{},"secure":{}}})", json_escape(ssid),
+ ap_records[i].rssi, ap_records[i].authmode != WIFI_AUTH_OPEN);
+ }
+ json += "]}";
+ return json;
+}
+
+bool Provisioning::test_connection(const std::string &ssid, const std::string &password) {
+ logger_.info("Testing connection to: {}", ssid);
+
+ // Clean up any existing test STA first
+ if (test_sta_) {
+ logger_.info("Cleaning up previous test STA");
+ test_sta_.reset();
+ std::this_thread::sleep_for(500ms); // Give WiFi stack time to clean up
+ }
+
+ std::atomic connected{false};
+ std::atomic got_ip{false};
+ std::atomic failed{false};
+
+ WifiSta::Config sta_config{.ssid = ssid,
+ .password = password,
+ .num_connect_retries = 5,
+ .auto_connect = true, // Auto-connect for testing
+ .on_connected =
+ [&]() {
+ logger_.info("Connection callback - connected to AP");
+ connected = true;
+ },
+ .on_disconnected =
+ [&]() {
+ logger_.info("Disconnection callback - failed to connect");
+ failed = true;
+ },
+ .on_got_ip =
+ [&](ip_event_got_ip_t *event) {
+ logger_.info("Got IP callback - {}.{}.{}.{}",
+ IP2STR(&event->ip_info.ip));
+ got_ip = true;
+ },
+ .log_level = Logger::Verbosity::ERROR};
+
+ logger_.info("Creating test WiFi STA instance");
+ test_sta_ = std::make_unique(sta_config);
+
+ logger_.info("Waiting for connection (max 15 seconds)...");
+ // Wait for connection (max 15 seconds)
+ auto start = std::chrono::steady_clock::now();
+ while (!got_ip && !failed && (std::chrono::steady_clock::now() - start) < 15s) {
+ std::this_thread::sleep_for(100ms);
+ }
+
+ bool success = got_ip;
+
+ logger_.info("Connection test result: {} (got_ip={}, failed={})", success, got_ip.load(),
+ failed.load());
+
+ // Clean up test STA immediately to avoid interfering with AP
+ logger_.info("Cleaning up test STA");
+ test_sta_.reset();
+
+ // Give WiFi stack time to stabilize
+ std::this_thread::sleep_for(200ms);
+
+ return success;
+}
+
+// HTTP Handlers
+esp_err_t Provisioning::root_handler(httpd_req_t *req) {
+ auto *prov = static_cast(req->user_ctx);
+ std::string html = prov->generate_html();
+ httpd_resp_set_type(req, "text/html");
+ httpd_resp_send(req, html.c_str(), html.length());
+ return ESP_OK;
+}
+
+esp_err_t Provisioning::scan_handler(httpd_req_t *req) {
+ auto *prov = static_cast(req->user_ctx);
+ std::string json = prov->scan_networks();
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, json.c_str(), json.length());
+ return ESP_OK;
+}
+
+esp_err_t Provisioning::connect_handler(httpd_req_t *req) {
+ auto *prov = static_cast(req->user_ctx);
+
+ // Check content length
+ size_t content_len = req->content_len;
+ if (content_len == 0 || content_len > 2048) {
+ httpd_resp_send_err(req,
+ content_len > 2048 ? HTTPD_413_CONTENT_TOO_LARGE : HTTPD_400_BAD_REQUEST,
+ "Invalid request size");
+ return ESP_FAIL;
+ }
+
+ // Read full request body
+ std::string body;
+ body.resize(content_len);
+ size_t total_read = 0;
+ while (total_read < content_len) {
+ int ret = httpd_req_recv(req, &body[total_read], content_len - total_read);
+ if (ret <= 0) {
+ if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
+ continue;
+ }
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Failed to read request");
+ return ESP_FAIL;
+ }
+ total_read += ret;
+ }
+
+ // Parse JSON manually (simple approach)
+ std::string ssid, password;
+ bool use_saved = false;
+
+ size_t ssid_pos = body.find("\"ssid\":\"");
+ if (ssid_pos != std::string::npos) {
+ ssid_pos += 8;
+ size_t ssid_end = body.find("\"", ssid_pos);
+ ssid = body.substr(ssid_pos, ssid_end - ssid_pos);
+ }
+
+ size_t pass_pos = body.find("\"password\":\"");
+ if (pass_pos != std::string::npos) {
+ pass_pos += 12;
+ size_t pass_end = body.find("\"", pass_pos);
+ password = body.substr(pass_pos, pass_end - pass_pos);
+ }
+
+ size_t saved_pos = body.find("\"use_saved\":true");
+ if (saved_pos != std::string::npos) {
+ use_saved = true;
+ }
+
+ if (ssid.empty()) {
+ std::string response = R"({"success":false,"message":"Invalid SSID"})";
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, response.c_str(), response.length());
+ return ESP_OK;
+ }
+
+ // If using saved credentials, retrieve password
+ if (use_saved) {
+ password = prov->get_saved_password(ssid);
+ }
+
+ // Test connection
+ bool success = prov->test_connection(ssid, password);
+
+ std::string response;
+ if (success) {
+ response = R"({"success":true})";
+ prov->is_provisioned_ = true;
+ prov->provisioned_ssid_ = ssid;
+ prov->provisioned_password_ = password;
+
+ // Save credentials if not already saved
+ if (!use_saved) {
+ prov->save_credentials(ssid, password);
+ }
+ } else {
+ response = R"({"success":false,"message":"Failed to connect"})";
+ }
+
+ // Send response - don't stop AP or call callback yet
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, response.c_str(), response.length());
+
+ return ESP_OK;
+}
+
+esp_err_t Provisioning::complete_handler(httpd_req_t *req) {
+ auto *prov = static_cast(req->user_ctx);
+
+ // Mark as completed
+ prov->is_completed_ = true;
+
+ // Send response immediately
+ httpd_resp_send(req, "OK", 2);
+
+ // Call callback directly with stored credentials
+ if (prov->config_.on_provisioned && prov->is_provisioned_) {
+ prov->config_.on_provisioned(prov->provisioned_ssid_, prov->provisioned_password_);
+ }
+
+ // Auto-shutdown if configured
+ if (prov->config_.auto_shutdown_ap) {
+ // Note: Stopping AP while in HTTP handler is safe - response is already sent
+ prov->stop();
+ }
+
+ return ESP_OK;
+}
+
+esp_err_t Provisioning::status_handler(httpd_req_t *req) {
+ auto *prov = static_cast(req->user_ctx);
+ std::string response =
+ fmt::format(R"({{"active":{},"provisioned":{}}})", prov->is_active_, prov->is_provisioned_);
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, response.c_str(), response.length());
+ return ESP_OK;
+}
+
+esp_err_t Provisioning::saved_handler(httpd_req_t *req) {
+ auto *prov = static_cast(req->user_ctx);
+ std::string json = prov->get_saved_networks_json();
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, json.c_str(), json.length());
+ return ESP_OK;
+}
+
+esp_err_t Provisioning::delete_handler(httpd_req_t *req) {
+ auto *prov = static_cast(req->user_ctx);
+
+ // Check content length
+ size_t content_len = req->content_len;
+ if (content_len == 0 || content_len > 1024) {
+ httpd_resp_send_err(req,
+ content_len > 1024 ? HTTPD_413_CONTENT_TOO_LARGE : HTTPD_400_BAD_REQUEST,
+ "Invalid request size");
+ return ESP_FAIL;
+ }
+
+ // Read full request body
+ std::string body;
+ body.resize(content_len);
+ size_t total_read = 0;
+ while (total_read < content_len) {
+ int ret = httpd_req_recv(req, &body[total_read], content_len - total_read);
+ if (ret <= 0) {
+ if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
+ continue;
+ }
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Failed to read request");
+ return ESP_FAIL;
+ }
+ total_read += ret;
+ }
+
+ // Parse SSID from JSON
+ std::string ssid;
+
+ size_t ssid_pos = body.find("\"ssid\":\"");
+ if (ssid_pos != std::string::npos) {
+ ssid_pos += 8;
+ size_t ssid_end = body.find("\"", ssid_pos);
+ ssid = body.substr(ssid_pos, ssid_end - ssid_pos);
+ }
+
+ if (ssid.empty()) {
+ std::string response = R"({"success":false})";
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, response.c_str(), response.length());
+ return ESP_OK;
+ }
+
+ prov->delete_credentials(ssid);
+
+ std::string response = R"({"success":true})";
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, response.c_str(), response.length());
+ return ESP_OK;
+}
+
+// Credential storage methods
+std::vector Provisioning::get_saved_ssids() {
+ std::vector ssids;
+
+ std::error_code ec;
+ espp::NvsHandle nvs("wifi_creds", ec);
+ if (ec) {
+ return ssids;
+ }
+
+ // Get count of saved networks
+ uint32_t count = 0;
+ nvs.get("count", count, ec);
+ if (ec || count == 0) {
+ return ssids;
+ }
+
+ // Load each SSID by index
+ for (uint32_t i = 0; i < count; i++) {
+ std::string ssid_key = fmt::format("ssid_{}", i);
+ std::string ssid;
+ nvs.get(ssid_key, ssid, ec);
+ if (!ec && !ssid.empty()) {
+ // Remove any trailing null bytes
+ ssid.erase(std::find(ssid.begin(), ssid.end(), '\0'), ssid.end());
+ if (!ssid.empty()) {
+ ssids.push_back(ssid);
+ }
+ }
+ }
+
+ return ssids;
+}
+
+std::string Provisioning::get_saved_password(const std::string &ssid) {
+ std::error_code ec;
+ espp::NvsHandle nvs("wifi_creds", ec);
+ if (ec) {
+ return "";
+ }
+
+ // Find the index for this SSID
+ auto ssids = get_saved_ssids();
+ auto it = std::find(ssids.begin(), ssids.end(), ssid);
+ if (it == ssids.end()) {
+ return "";
+ }
+
+ size_t index = std::distance(ssids.begin(), it);
+ std::string pwd_key = fmt::format("pwd_{}", index);
+ std::string password;
+ nvs.get(pwd_key, password, ec);
+
+ if (!ec) {
+ // Remove any trailing null bytes
+ password.erase(std::find(password.begin(), password.end(), '\0'), password.end());
+ }
+
+ return ec ? "" : password;
+}
+
+void Provisioning::save_credentials(const std::string &ssid, const std::string &password) {
+ std::error_code ec;
+ espp::NvsHandle nvs("wifi_creds", ec);
+ if (ec) {
+ logger_.error("Failed to open NVS");
+ return;
+ }
+
+ logger_.info("Saving credentials for: {}", ssid);
+
+ // Get or create index for this SSID
+ auto ssids = get_saved_ssids();
+ size_t index = ssids.size();
+ auto it = std::find(ssids.begin(), ssids.end(), ssid);
+ if (it != ssids.end()) {
+ index = std::distance(ssids.begin(), it);
+ } else {
+ ssids.push_back(ssid);
+ }
+
+ // Save SSID and password using index-based keys (keeps keys under 15 chars)
+ std::string ssid_key = fmt::format("ssid_{}", index);
+ std::string pwd_key = fmt::format("pwd_{}", index);
+
+ nvs.set(ssid_key, ssid, ec);
+ if (ec) {
+ logger_.error("Failed to save SSID");
+ return;
+ }
+
+ nvs.set(pwd_key, password, ec);
+ if (ec) {
+ logger_.error("Failed to save password");
+ return;
+ }
+
+ // Save count
+ nvs.set("count", static_cast(ssids.size()), ec);
+ if (ec) {
+ logger_.error("Failed to save count");
+ return;
+ }
+
+ nvs.commit(ec);
+}
+
+void Provisioning::delete_credentials(const std::string &ssid) {
+ std::error_code ec;
+ espp::NvsHandle nvs("wifi_creds", ec);
+ if (ec) {
+ logger_.error("Failed to open NVS");
+ return;
+ }
+
+ logger_.info("Deleting credentials for: {}", ssid);
+
+ // Get current SSIDs
+ auto ssids = get_saved_ssids();
+ auto it = std::find(ssids.begin(), ssids.end(), ssid);
+ if (it == ssids.end()) {
+ logger_.warn("SSID not found in saved credentials");
+ return;
+ }
+
+ size_t index_to_delete = std::distance(ssids.begin(), it);
+ ssids.erase(it);
+
+ // Rewrite all credentials with compacted indices
+ for (size_t i = index_to_delete; i < ssids.size(); i++) {
+ // Move credentials from index i+1 to index i
+ std::string old_ssid_key = fmt::format("ssid_{}", i + 1);
+ std::string old_pwd_key = fmt::format("pwd_{}", i + 1);
+ std::string new_ssid_key = fmt::format("ssid_{}", i);
+ std::string new_pwd_key = fmt::format("pwd_{}", i);
+
+ std::string ssid_val, pwd_val;
+ nvs.get(old_ssid_key, ssid_val, ec);
+ nvs.get(old_pwd_key, pwd_val, ec);
+
+ nvs.set(new_ssid_key, ssid_val, ec);
+ nvs.set(new_pwd_key, pwd_val, ec);
+ }
+
+ // Erase the last entry (now duplicated)
+ if (!ssids.empty()) {
+ std::string last_ssid_key = fmt::format("ssid_{}", ssids.size());
+ std::string last_pwd_key = fmt::format("pwd_{}", ssids.size());
+ nvs.erase(last_ssid_key, ec);
+ nvs.erase(last_pwd_key, ec);
+ }
+
+ // Update count
+ nvs.set("count", static_cast(ssids.size()), ec);
+ if (ec) {
+ logger_.error("Failed to update count");
+ return;
+ }
+
+ nvs.commit(ec);
+}
+
+std::string Provisioning::get_saved_networks_json() {
+ auto ssids = get_saved_ssids();
+
+ std::string json = R"({"networks":[)";
+ for (size_t i = 0; i < ssids.size(); i++) {
+ if (i > 0)
+ json += ",";
+ json += "\"" + json_escape(ssids[i]) + "\"";
+ }
+ json += "]}";
+
+ return json;
+}
diff --git a/components/wifi/example/main/Kconfig.projbuild b/components/wifi/example/main/Kconfig.projbuild
index d68cd524b..92457705c 100644
--- a/components/wifi/example/main/Kconfig.projbuild
+++ b/components/wifi/example/main/Kconfig.projbuild
@@ -18,4 +18,24 @@ menu "WiFi Example Configuration"
help
Set the Maximum retry to avoid station reconnecting to the AP unlimited when the AP is really inexistent.
+ config TEST_WIFI_SCAN
+ bool "Test WiFi scanning"
+ default y
+ help
+ Enable WiFi scanning tests in the example.
+
+ config TEST_SCAN_BEFORE_STA
+ bool "Test scan before STA connection"
+ default y
+ depends on TEST_WIFI_SCAN
+ help
+ Test scanning before attempting STA connection.
+
+ config TEST_SCAN_WITH_NO_CONFIG
+ bool "Test scan with no WiFi config"
+ default y
+ depends on TEST_WIFI_SCAN
+ help
+ Test scanning when WiFi is not configured (no STA or AP active).
+
endmenu
diff --git a/components/wifi/example/main/wifi_example.cpp b/components/wifi/example/main/wifi_example.cpp
index aef5d8009..eb536eb05 100644
--- a/components/wifi/example/main/wifi_example.cpp
+++ b/components/wifi/example/main/wifi_example.cpp
@@ -1,4 +1,5 @@
#include
+#include
#include
#include "sdkconfig.h"
@@ -19,6 +20,15 @@
using namespace std::chrono_literals;
+// Test result tracking
+struct TestResult {
+ std::string name;
+ bool passed;
+ std::string details;
+};
+
+std::vector test_results;
+
extern "C" void app_main(void) {
espp::Logger logger({.tag = "wifi_example", .level = espp::Logger::Verbosity::INFO});
logger.info("Starting WiFi example...");
@@ -115,6 +125,34 @@ extern "C" void app_main(void) {
logger.info("WiFi AP example complete!");
}
+ {
+#if CONFIG_TEST_WIFI_SCAN && CONFIG_TEST_SCAN_WITH_NO_CONFIG
+ logger.info("Starting WiFi scan test (no config)...");
+ //! [wifi scan no config example]
+ auto &wifi = espp::Wifi::get();
+ wifi.set_log_level(espp::Logger::Verbosity::DEBUG);
+
+ // Initialize the WiFi stack
+ if (!wifi.init()) {
+ logger.error("Failed to initialize WiFi stack");
+ return;
+ }
+
+ // Test scanning with no active configuration
+ logger.info("Scanning for networks (no STA/AP active)...");
+ auto scan_results = wifi.scan(10);
+ logger.info("Found {} networks:", scan_results.size());
+ for (const auto &ap : scan_results) {
+ logger.info(" SSID: {}, RSSI: {}, Channel: {}", (char *)ap.ssid, ap.rssi, ap.primary);
+ }
+
+ wifi.deinit();
+ logger.info("WiFi scan test (no config) complete!");
+ std::this_thread::sleep_for(2s);
+ //! [wifi scan no config example]
+#endif
+ }
+
{
logger.info("Starting WiFi singleton example...");
//! [wifi example]
@@ -128,20 +166,72 @@ extern "C" void app_main(void) {
}
// Register multiple station configurations
- wifi.register_sta("home", {.ssid = "", // use whatever was saved to NVS (if any)
- .password = "", // use whatever was saved to NVS (if any)
- .num_connect_retries = CONFIG_ESP_MAXIMUM_RETRY,
- .on_got_ip =
- [&](ip_event_got_ip_t *eventdata) {
- logger.info("Home network - got IP: {}.{}.{}.{}",
- IP2STR(&eventdata->ip_info.ip));
- },
- .log_level = espp::Logger::Verbosity::INFO});
+ wifi.register_sta(
+ "home",
+ {.ssid = "", // use whatever was saved to NVS (if any)
+ .password = "", // use whatever was saved to NVS (if any)
+ .num_connect_retries = CONFIG_ESP_MAXIMUM_RETRY,
+ .auto_connect =
+ !CONFIG_TEST_SCAN_BEFORE_STA, // Don't auto-connect if we're going to scan first
+ .on_got_ip =
+ [&](ip_event_got_ip_t *eventdata) {
+ logger.info("Home network - got IP: {}.{}.{}.{}", IP2STR(&eventdata->ip_info.ip));
+ },
+ .log_level = espp::Logger::Verbosity::INFO});
// set the 'home' network to be active and ensure that the backup
// registration (when added) doesn't override it
wifi.switch_to_sta("home");
+#if CONFIG_TEST_WIFI_SCAN && CONFIG_TEST_SCAN_BEFORE_STA
+ logger.info("\n=== Testing scan before STA connection ===");
+ //! [wifi scan before sta example]
+
+ bool test_passed = false;
+ std::string test_details;
+
+ // STA was created with auto_connect=false, so it won't connect automatically
+ // Scan for networks first
+ logger.info("Scanning for networks before connecting...");
+ auto scan_results = wifi.scan(10);
+ logger.info("Found {} networks:", scan_results.size());
+ for (const auto &ap : scan_results) {
+ logger.info(" SSID: {}, RSSI: {}, Channel: {}", (char *)ap.ssid, ap.rssi, ap.primary);
+ }
+
+ // Now manually initiate the connection
+ logger.info("Attempting to connect to STA after scan...");
+ auto *sta = wifi.get_sta();
+ if (sta) {
+ sta->connect(); // Manually trigger connection
+
+ int wait_count = 0;
+ while (!sta->is_connected() && wait_count < 150) { // 15 second timeout
+ std::this_thread::sleep_for(100ms);
+ wait_count++;
+ }
+ if (sta->is_connected()) {
+ logger.info("STA connected successfully after scan!");
+ std::string ip;
+ if (wifi.get_ip_address(ip)) {
+ logger.info("IP address: {}", ip);
+ test_passed = true;
+ test_details = fmt::format("Connected, IP: {}", ip);
+ }
+ } else {
+ logger.warn("STA failed to connect after scan within timeout");
+ test_details = "Connection timeout";
+ }
+ } else {
+ test_details = "No STA instance";
+ }
+
+ test_results.push_back(
+ {.name = "Scan before STA connect", .passed = test_passed, .details = test_details});
+ std::this_thread::sleep_for(2s);
+ //! [wifi scan before sta example]
+#endif
+
wifi.register_sta("backup", {.ssid = "BackupNetwork",
.password = "backuppassword",
.num_connect_retries = CONFIG_ESP_MAXIMUM_RETRY,
@@ -188,30 +278,37 @@ extern "C" void app_main(void) {
// Check what's currently active
std::string active_name = wifi.get_active_name();
- if (active_name != "device_ap") {
- logger.error("Active interface is not 'device_ap' after switch_to_ap");
- return;
- }
- logger.info("Active interface: '{}' (is_ap={}, is_sta={})", active_name, wifi.is_active_ap(),
- wifi.is_active_sta());
+ bool ap_test_passed = (active_name == "device_ap");
+ std::string ap_test_details;
- // Wait for the active interface to be ready/connected
- auto *active = wifi.get_active();
- if (active) {
- logger.info("Waiting for '{}' to be connected...", active_name);
- while (!active->is_connected()) {
- std::this_thread::sleep_for(100ms);
- }
- logger.info("'{}' is now connected!", active_name);
+ if (!ap_test_passed) {
+ logger.error("Active interface is not 'device_ap' after switch_to_ap");
+ ap_test_details = fmt::format("Wrong active: {}", active_name);
+ } else {
+ logger.info("Active interface: '{}' (is_ap={}, is_sta={})", active_name, wifi.is_active_ap(),
+ wifi.is_active_sta());
+
+ // Wait for the active interface to be ready/connected
+ auto *active = wifi.get_active();
+ if (active) {
+ logger.info("Waiting for '{}' to be connected...", active_name);
+ while (!active->is_connected()) {
+ std::this_thread::sleep_for(100ms);
+ }
+ logger.info("'{}' is now connected!", active_name);
- logger.info("Checking IP address...");
- // Get and display IP
- std::string ip;
- if (wifi.get_ip_address(ip)) {
- logger.info("IP address: {}", ip);
+ logger.info("Checking IP address...");
+ // Get and display IP
+ std::string ip;
+ if (wifi.get_ip_address(ip)) {
+ logger.info("IP address: {}", ip);
+ ap_test_details = fmt::format("AP ready, IP: {}", ip);
+ }
}
}
+ test_results.push_back(
+ {.name = "Switch to AP", .passed = ap_test_passed, .details = ap_test_details});
std::this_thread::sleep_for(num_seconds_to_run * 1s);
// Access STA instance (single instance, manages all configs)
@@ -230,12 +327,24 @@ extern "C" void app_main(void) {
// Demonstrate switching to STA
logger.info("\n=== Testing switch to STA ===");
wifi.switch_to_sta("home");
+
+ // If auto_connect was disabled (e.g., from scan test), manually connect
+ sta = wifi.get_sta();
+ if (sta && !sta->is_connected()) {
+ logger.info("STA not auto-connected, manually connecting...");
+ sta->connect();
+ }
+
active_name = wifi.get_active_name();
logger.info("Active interface: '{}' (is_ap={}, is_sta={})", active_name, wifi.is_active_ap(),
wifi.is_active_sta());
+ bool sta_test_passed = false;
+ std::string sta_test_details;
+
// Wait for STA to connect
- active = wifi.get_active();
+ auto active = wifi.get_active();
+ active_name = wifi.get_active_name();
if (active) {
logger.info("Waiting for '{}' to be connected...", active_name);
int wait_count = 0;
@@ -248,12 +357,19 @@ extern "C" void app_main(void) {
std::string ip;
if (wifi.get_ip_address(ip)) {
logger.info("IP address: {}", ip);
+ sta_test_passed = true;
+ sta_test_details = fmt::format("Connected, IP: {}", ip);
}
} else {
logger.warn("'{}' failed to connect within timeout", active_name);
+ sta_test_details = "Connection timeout";
}
+ } else {
+ sta_test_details = "No active interface";
}
+ test_results.push_back(
+ {.name = "Switch to STA", .passed = sta_test_passed, .details = sta_test_details});
std::this_thread::sleep_for(2s);
// Demonstrate switching back to AP
@@ -263,6 +379,9 @@ extern "C" void app_main(void) {
logger.info("Active interface: '{}' (is_ap={}, is_sta={})", active_name, wifi.is_active_ap(),
wifi.is_active_sta());
+ bool ap_switch_back_passed = (active_name == "device_ap");
+ std::string ap_switch_back_details;
+
// Wait for AP to be ready
active = wifi.get_active();
if (active) {
@@ -277,9 +396,16 @@ extern "C" void app_main(void) {
std::string ip;
if (wifi.get_ip_address(ip)) {
logger.info("IP address: {}", ip);
+ ap_switch_back_details = fmt::format("AP ready, IP: {}", ip);
}
+ } else {
+ ap_switch_back_passed = false;
+ ap_switch_back_details = "No active interface";
}
+ test_results.push_back({.name = "Switch back to AP",
+ .passed = ap_switch_back_passed,
+ .details = ap_switch_back_details});
std::this_thread::sleep_for(2s);
// Demonstrate stop
@@ -288,6 +414,12 @@ extern "C" void app_main(void) {
active_name = wifi.get_active_name();
logger.info("Active interface after stop: '{}' (empty=stopped)", active_name);
+ bool stop_test_passed = active_name.empty();
+ std::string stop_test_details =
+ stop_test_passed ? "WiFi stopped successfully" : "WiFi still active";
+ test_results.push_back(
+ {.name = "Stop WiFi", .passed = stop_test_passed, .details = stop_test_details});
+
//! [wifi example]
logger.info("WiFi singleton example complete!");
@@ -301,6 +433,29 @@ extern "C" void app_main(void) {
}
logger.info("WiFi stack deinitialized");
+ // Print test summary
+ logger.info("\n==========================================================");
+ logger.info(" TEST SUMMARY ");
+ logger.info("==========================================================");
+
+ int passed = 0;
+ int failed = 0;
+
+ for (const auto &result : test_results) {
+ std::string status = result.passed ? "✓ PASS" : "✗ FAIL";
+ logger.info("{}: {} - {}", status, result.name, result.details);
+
+ if (result.passed) {
+ passed++;
+ } else {
+ failed++;
+ }
+ }
+
+ logger.info("==========================================================");
+ logger.info("Total: {} | Passed: {} | Failed: {}", test_results.size(), passed, failed);
+ logger.info("==========================================================\n");
+
logger.info("WiFi example complete!");
while (true) {
diff --git a/components/wifi/include/wifi.hpp b/components/wifi/include/wifi.hpp
index c12f67779..69fb4b785 100644
--- a/components/wifi/include/wifi.hpp
+++ b/components/wifi/include/wifi.hpp
@@ -293,6 +293,39 @@ class Wifi : public espp::BaseComponent {
return set_max_tx_power_raw(power_quarter_dbm);
}
+ /// @brief Scan for available WiFi networks.
+ /// @param max_aps Maximum number of APs to return (default 20).
+ /// @param show_hidden Whether to show hidden SSIDs (default false).
+ /// @return Vector of wifi_ap_record_t structures, empty on error.
+ /// @note This creates a temporary WifiSta instance to perform the scan.
+ /// @note The scan will temporarily put WiFi in APSTA mode if in AP mode.
+ std::vector scan(uint16_t max_aps = 20, bool show_hidden = false) {
+ std::vector results;
+
+ // Save current mode
+ wifi_mode_t current_mode = WIFI_MODE_NULL;
+ esp_err_t err = esp_wifi_get_mode(¤t_mode);
+ if (err != ESP_OK) {
+ logger_.error("Failed to get current WiFi mode: {}", esp_err_to_name(err));
+ return results;
+ }
+
+ // Create a temporary WifiSta instance for scanning
+ {
+ WifiSta::Config scan_config{.ssid = "", .password = "", .auto_connect = false};
+ WifiSta temp_sta(scan_config);
+ results = temp_sta.scan(max_aps, show_hidden);
+ }
+
+ // Restore previous mode
+ err = esp_wifi_set_mode(current_mode);
+ if (err != ESP_OK) {
+ logger_.warn("Failed to restore WiFi mode after scan: {}", esp_err_to_name(err));
+ }
+
+ return results;
+ }
+
/// @brief Register a new WiFi Access Point configuration.
/// @param name Unique identifier for this AP configuration.
/// @param config WifiAp::Config structure with AP configuration.
diff --git a/components/wifi/include/wifi_base.hpp b/components/wifi/include/wifi_base.hpp
index e94153a7c..2b56032d2 100644
--- a/components/wifi/include/wifi_base.hpp
+++ b/components/wifi/include/wifi_base.hpp
@@ -104,6 +104,18 @@ class WifiBase : public BaseComponent {
*/
esp_netif_t *get_netif() const { return netif_; }
+ /**
+ * @brief Check if WiFi subsystem has been initialized
+ * @return True if WiFi is initialized, false otherwise
+ * @note This is a static method that checks global WiFi state
+ */
+ static bool is_initialized() {
+ wifi_mode_t mode;
+ bool success = esp_wifi_get_mode(&mode) == ESP_OK;
+ bool valid = mode != WIFI_MODE_NULL;
+ return success && valid;
+ }
+
protected:
/**
* @brief Constructor for WifiBase
diff --git a/components/wifi/include/wifi_sta.hpp b/components/wifi/include/wifi_sta.hpp
index 8639e2e74..6a8605e6b 100644
--- a/components/wifi/include/wifi_sta.hpp
+++ b/components/wifi/include/wifi_sta.hpp
@@ -2,6 +2,7 @@
#include
#include
+#include
#include
#include "esp_event.h"
@@ -45,6 +46,14 @@ class WifiSta : public WifiBase {
*/
typedef std::function ip_callback;
+ /**
+ * @brief Scan configuration for WiFi networks
+ */
+ struct ScanConfig {
+ uint16_t max_results{20}; ///< Maximum number of scan results to return
+ bool show_hidden{false}; ///< Whether to show hidden SSIDs
+ };
+
/**
* @brief Configuration structure for the WiFi Station.
*/
@@ -58,6 +67,8 @@ class WifiSta : public WifiBase {
size_t num_connect_retries{
0}; /**< Number of times to retry connecting to the AP before stopping. After this many
retries, on_disconnected will be called. */
+ bool auto_connect{true}; /**< Whether to automatically connect when started. If false, call
+ connect() manually. */
connect_callback on_connected{
nullptr}; /**< Called when the station connects, or fails to connect. */
disconnect_callback on_disconnected{nullptr}; /**< Called when the station disconnects. */
@@ -97,9 +108,9 @@ class WifiSta : public WifiBase {
connected_ = false;
disconnecting_ = true;
attempts_ = 0;
- connect_callback_ = nullptr;
- disconnect_callback_ = nullptr;
- ip_callback_ = nullptr;
+ config_.on_connected = nullptr;
+ config_.on_disconnected = nullptr;
+ config_.on_got_ip = nullptr;
// unregister our event handlers
logger_.debug("Unregistering event handlers");
@@ -239,6 +250,82 @@ class WifiSta : public WifiBase {
strlen((const char *)ap_info.ssid));
}
+ /**
+ * @brief Scan for available WiFi networks
+ * @param max_results Maximum number of scan results to return
+ * @param show_hidden Whether to show hidden SSIDs
+ * @return Vector of AP records found during scan (empty on error)
+ * @note This method requires WiFi to be in STA or APSTA mode
+ * @note Scanning will disconnect any active STA connection
+ * @note WiFi will be restarted after scan if auto_connect is enabled
+ */
+ std::vector scan(uint16_t max_results = 20, bool show_hidden = false) {
+ ScanConfig config;
+ config.max_results = max_results;
+ config.show_hidden = show_hidden;
+
+ std::vector results;
+
+ logger_.info("Scanning for WiFi networks (max: {}, show_hidden: {})", config.max_results,
+ config.show_hidden);
+
+ // Remember if we were connected
+ bool was_connected = is_connected();
+ bool saved_auto_connect;
+ {
+ std::lock_guard lock(config_mutex_);
+ saved_auto_connect = config_.auto_connect;
+ }
+
+ // Disconnect if currently connected (scan requires disconnection)
+ esp_wifi_disconnect();
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
+
+ // Configure scan parameters
+ wifi_scan_config_t scan_config = {};
+ scan_config.show_hidden = config.show_hidden;
+
+ // Start the scan
+ esp_err_t err = esp_wifi_scan_start(&scan_config, true); // true = blocking
+ if (err != ESP_OK) {
+ logger_.error("WiFi scan failed: {}", esp_err_to_name(err));
+ if (was_connected && saved_auto_connect) {
+ start();
+ }
+ return results;
+ }
+
+ // Get scan results
+ uint16_t ap_count = config.max_results;
+ results.resize(ap_count);
+ err = esp_wifi_scan_get_ap_records(&ap_count, results.data());
+ if (err != ESP_OK) {
+ logger_.error("Failed to get scan results: {}", esp_err_to_name(err));
+ results.clear();
+ if (was_connected && saved_auto_connect) {
+ start();
+ }
+ return results;
+ }
+
+ results.resize(ap_count);
+ logger_.info("Found {} networks", ap_count);
+
+ // Reconnect/restart if needed
+ if (was_connected) {
+ // We were connected before the scan, reconnect
+ if (saved_auto_connect) {
+ start();
+ }
+ } else if (!WifiBase::is_initialized()) {
+ // We were not connected, but WiFi was stopped during scan restoration
+ // Restart WiFi so the STA interface is in a proper state for future connection attempts
+ start();
+ }
+
+ return results;
+ }
+
/**
* @brief Get the RSSI (Received Signal Strength Indicator) of the access point.
* @return RSSI of the access point.
@@ -289,7 +376,8 @@ class WifiSta : public WifiBase {
*/
void set_num_retries(size_t num_retries) {
if (num_retries > 0) {
- num_retries_ = num_retries;
+ std::lock_guard lock(config_mutex_);
+ config_.num_connect_retries = num_retries;
} else {
logger_.warn("Number of retries must be greater than 0, not setting it");
}
@@ -299,19 +387,28 @@ class WifiSta : public WifiBase {
* @brief Set the callback to be called when the station connects to the access point.
* @param callback Callback to be called when the station connects to the access point.
*/
- void set_connect_callback(connect_callback callback) { connect_callback_ = callback; }
+ void set_connect_callback(connect_callback callback) {
+ std::lock_guard lock(config_mutex_);
+ config_.on_connected = callback;
+ }
/**
* @brief Set the callback to be called when the station disconnects from the access point.
* @param callback Callback to be called when the station disconnects from the access point.
*/
- void set_disconnect_callback(disconnect_callback callback) { disconnect_callback_ = callback; }
+ void set_disconnect_callback(disconnect_callback callback) {
+ std::lock_guard lock(config_mutex_);
+ config_.on_disconnected = callback;
+ }
/**
* @brief Set the callback to be called when the station gets an IP address.
* @param callback Callback to be called when the station gets an IP address.
*/
- void set_ip_callback(ip_callback callback) { ip_callback_ = callback; }
+ void set_ip_callback(ip_callback callback) {
+ std::lock_guard lock(config_mutex_);
+ config_.on_got_ip = callback;
+ }
/**
* @brief Get the saved WiFi configuration.
@@ -348,11 +445,11 @@ class WifiSta : public WifiBase {
}
}
- // Update callbacks
- num_retries_ = config.num_connect_retries;
- connect_callback_ = config.on_connected;
- disconnect_callback_ = config.on_disconnected;
- ip_callback_ = config.on_got_ip;
+ // Update stored configuration using assignment operator
+ {
+ std::lock_guard lock(config_mutex_);
+ config_ = config;
+ }
// Update WiFi configuration
wifi_config_t wifi_config;
@@ -457,6 +554,13 @@ class WifiSta : public WifiBase {
*/
bool connect() {
esp_err_t err;
+
+ // Ensure WiFi is properly configured before connecting
+ if (!ensure_wifi_ready()) {
+ logger_.error("Could not ensure WiFi is ready for connection");
+ return false;
+ }
+
// ensure retries are reset
attempts_ = 0;
connected_ = false;
@@ -526,6 +630,7 @@ class WifiSta : public WifiBase {
logger_.error("Invalid parameters for scan");
return -1;
}
+
uint16_t number = max_records;
uint16_t ap_count = 0;
static constexpr bool blocking = true; // blocking scan
@@ -549,6 +654,7 @@ class WifiSta : public WifiBase {
logger_.warn("Found {} access points, but only {} can be stored", ap_count, max_records);
ap_count = max_records;
}
+
return number;
}
@@ -563,35 +669,68 @@ class WifiSta : public WifiBase {
void event_handler(esp_event_base_t event_base, int32_t event_id, void *event_data) {
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
+ logger_.debug("WIFI_EVENT_STA_START");
connected_ = false;
- esp_wifi_connect();
+ bool should_auto_connect;
+ {
+ std::lock_guard lock(config_mutex_);
+ should_auto_connect = config_.auto_connect;
+ }
+ if (should_auto_connect) {
+ logger_.debug("auto_connect enabled - initiating connection");
+ esp_wifi_connect();
+ } else {
+ logger_.debug("auto_connect disabled - waiting for manual connect()");
+ }
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
+ logger_.debug("WIFI_EVENT_STA_DISCONNECTED");
connected_ = false;
if (disconnecting_) {
+ logger_.debug("Intentional disconnect, not retrying");
disconnecting_ = false;
} else {
- if (attempts_ < num_retries_) {
+ size_t max_retries;
+ {
+ std::lock_guard lock(config_mutex_);
+ max_retries = config_.num_connect_retries;
+ }
+ if (attempts_ < max_retries) {
esp_wifi_connect();
attempts_++;
- logger_.info("Retrying to connect to the AP");
+ logger_.info("Retrying to connect to the AP (attempt {}/{})", attempts_.load(),
+ max_retries);
// return early, don't call disconnect callback
return;
}
}
logger_.info("Failed to connect to the AP after {} attempts", attempts_.load());
- if (disconnect_callback_) {
- disconnect_callback_();
+ disconnect_callback cb;
+ {
+ std::lock_guard lock(config_mutex_);
+ cb = config_.on_disconnected;
}
+ if (cb) {
+ cb();
+ }
+ } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_CONNECTED) {
+ logger_.debug("WIFI_EVENT_STA_CONNECTED - waiting for IP");
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
logger_.info("got ip: {}.{}.{}.{}", IP2STR(&event->ip_info.ip));
attempts_ = 0;
connected_ = true;
- if (connect_callback_) {
- connect_callback_();
+ connect_callback on_conn;
+ ip_callback on_ip;
+ {
+ std::lock_guard lock(config_mutex_);
+ on_conn = config_.on_connected;
+ on_ip = config_.on_got_ip;
+ }
+ if (on_conn) {
+ on_conn();
}
- if (ip_callback_) {
- ip_callback_(event);
+ if (on_ip) {
+ on_ip(event);
}
}
}
@@ -640,11 +779,47 @@ class WifiSta : public WifiBase {
void init(const Config &config);
+ /**
+ * @brief Ensure WiFi is initialized, configured, and started for this STA instance.
+ * @return true if WiFi is ready, false otherwise.
+ * @note This method will reconfigure and restart WiFi if needed.
+ */
+ bool ensure_wifi_ready() {
+ // Check if WiFi is initialized
+ if (!is_initialized()) {
+ logger_.error("WiFi not initialized - cannot auto-recover. Please reinitialize WiFi.");
+ return false;
+ }
+
+ // Check if WiFi is started
+ wifi_mode_t current_mode;
+ esp_err_t err = esp_wifi_get_mode(¤t_mode);
+ if (err != ESP_OK || current_mode == WIFI_MODE_NULL) {
+ logger_.warn("WiFi not started - reconfiguring and starting");
+ // Reconfigure using stored config (without changing callbacks)
+ if (!reconfigure(config_)) {
+ logger_.error("Could not reconfigure WiFi STA");
+ return false;
+ }
+ if (!start()) {
+ logger_.error("Could not start WiFi");
+ return false;
+ }
+ } else {
+ // WiFi is started, just make sure it's started (handles ESP_ERR_WIFI_STATE gracefully)
+ err = esp_wifi_start();
+ if (err != ESP_OK && err != ESP_ERR_WIFI_STATE) {
+ logger_.error("Could not start WiFi: {}", esp_err_to_name(err));
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ Config config_; /**< Stored configuration for state recovery */
+ mutable std::recursive_mutex config_mutex_; /**< Mutex for protecting config_ access */
std::atomic attempts_{0};
- std::atomic num_retries_{0};
- connect_callback connect_callback_{nullptr};
- disconnect_callback disconnect_callback_{nullptr};
- ip_callback ip_callback_{nullptr};
std::atomic connected_{false};
std::atomic disconnecting_{false};
esp_event_handler_instance_t event_handler_instance_any_id_;
@@ -660,11 +835,11 @@ template <> struct formatter : fmt::formatterInsert(
- "scan",
- [this](std::ostream &out, int count) -> void {
- wifi_ap_record_t ap_records[count];
- int num_records = wifi_sta_.get().scan(ap_records, count);
- if (num_records > 0) {
- out << fmt::format("Found {} WiFi networks:\n", num_records);
- for (int i = 0; i < num_records; ++i) {
- out << fmt::format("SSID: '{}', RSSI: {}, BSSID: {}\n",
- std::string_view(reinterpret_cast(ap_records[i].ssid)),
- ap_records[i].rssi, ap_records[i].bssid);
- }
- } else {
- out << "No WiFi networks found.\n";
+ "scan", [this](std::ostream &out) -> void { scan_networks(out, 20); },
+ "Scan for available WiFi networks (shows up to 20 networks).");
+ menu->Insert(
+ "scan", {"max_results"},
+ [this](std::ostream &out, const std::string &max_str) -> void {
+ char *end;
+ long max_long = std::strtol(max_str.c_str(), &end, 10);
+ if (end == max_str.c_str() || *end != '\0' || max_long < 0 || max_long > UINT16_MAX) {
+ out << "Invalid max_results value. Please provide a number between 0 and " << UINT16_MAX
+ << ".\n";
+ return;
}
+ uint16_t max_results = static_cast(max_long);
+ scan_networks(out, max_results);
},
- "Scan for available WiFi networks.");
+ "Scan for available WiFi networks with specified maximum results.");
return menu;
}
protected:
+ /// @brief Scan for WiFi networks and display the results.
+ /// @param out The output stream to write to.
+ /// @param max_results Maximum number of networks to return.
+ void scan_networks(std::ostream &out, uint16_t max_results) {
+ out << fmt::format("Scanning for WiFi networks (max {})...\n", max_results);
+ auto scan_results = wifi_sta_.get().scan(max_results);
+ if (scan_results.empty()) {
+ out << "No WiFi networks found.\n";
+ } else {
+ out << fmt::format("Found {} networks:\n", scan_results.size());
+ for (const auto &ap : scan_results) {
+ out << fmt::format(" SSID: '{}', RSSI: {} dBm, Channel: {}, Auth: {}\n", (char *)ap.ssid,
+ ap.rssi, ap.primary,
+ ap.authmode == WIFI_AUTH_OPEN ? "Open"
+ : ap.authmode == WIFI_AUTH_WEP ? "WEP"
+ : ap.authmode == WIFI_AUTH_WPA_PSK ? "WPA"
+ : ap.authmode == WIFI_AUTH_WPA2_PSK ? "WPA2"
+ : ap.authmode == WIFI_AUTH_WPA_WPA2_PSK ? "WPA/WPA2"
+ : "Unknown");
+ }
+ }
+ }
+
/// @brief Set the log level for the WiFi station.
/// @param out The output stream to write to.
/// @param verbosity The verbosity level to set.
diff --git a/components/wifi/src/wifi_sta.cpp b/components/wifi/src/wifi_sta.cpp
index ddbd6e63c..1ca8e10af 100644
--- a/components/wifi/src/wifi_sta.cpp
+++ b/components/wifi/src/wifi_sta.cpp
@@ -4,6 +4,9 @@
namespace espp {
void WifiSta::init(const Config &config) {
+ // Store the configuration for later recovery if needed
+ config_ = config;
+
if (netif_ == nullptr) {
logger_.info("Creating network interface and ensuring WiFi stack is initialized");
auto &wifi = Wifi::get();
diff --git a/doc/Doxyfile b/doc/Doxyfile
index 80d233967..78d398bc3 100644
--- a/doc/Doxyfile
+++ b/doc/Doxyfile
@@ -130,6 +130,7 @@ EXAMPLE_PATH = \
$(PROJECT_PATH)/components/odrive_ascii/example/main/odrive_ascii_example.cpp \
$(PROJECT_PATH)/components/pcf85063/example/main/pcf85063_example.cpp \
$(PROJECT_PATH)/components/pid/example/main/pid_example.cpp \
+ $(PROJECT_PATH)/components/provisioning/example/main/provisioning_example.cpp \
$(PROJECT_PATH)/components/qtpy/example/main/qtpy_example.cpp \
$(PROJECT_PATH)/components/qmi8658/example/main/qmi8658_example.cpp \
$(PROJECT_PATH)/components/qwiicnes/example/main/qwiicnes_example.cpp \
@@ -293,6 +294,7 @@ INPUT = \
$(PROJECT_PATH)/components/pcf85063/include/pcf85063.hpp \
$(PROJECT_PATH)/components/pid/include/pid.hpp \
$(PROJECT_PATH)/components/ping/include/ping.hpp \
+ $(PROJECT_PATH)/components/provisioning/include/provisioning.hpp \
$(PROJECT_PATH)/components/qmi8658/include/qmi8658.hpp \
$(PROJECT_PATH)/components/qmi8658/include/qmi8658_detail.hpp \
$(PROJECT_PATH)/components/qtpy/include/qtpy.hpp \
diff --git a/doc/en/network/index.rst b/doc/en/network/index.rst
index 6bf506465..c2f7d258d 100644
--- a/doc/en/network/index.rst
+++ b/doc/en/network/index.rst
@@ -5,6 +5,7 @@ Network APIs
:maxdepth: 1
ping
+ provisioning
socket_example
socket
tcp_socket
diff --git a/doc/en/network/provisioning.rst b/doc/en/network/provisioning.rst
new file mode 100644
index 000000000..f2ab5297c
--- /dev/null
+++ b/doc/en/network/provisioning.rst
@@ -0,0 +1,20 @@
+Provisioning
+************
+
+The `Provisioning` component provides WiFi provisioning via a web interface hosted
+on a temporary access point. Users can scan for networks, select or manually enter
+credentials, test the connection, and save multiple network configurations for easy
+switching.
+
+.. ------------------------------- Example -------------------------------------
+
+.. toctree::
+
+ provisioning_example
+
+.. ---------------------------- API Reference ----------------------------------
+
+API Reference
+-------------
+
+.. include-build-file:: inc/provisioning.inc
diff --git a/doc/en/network/provisioning_example.md b/doc/en/network/provisioning_example.md
new file mode 100644
index 000000000..d2fbb19f2
--- /dev/null
+++ b/doc/en/network/provisioning_example.md
@@ -0,0 +1,2 @@
+```{include} ../../../components/provisioning/example/README.md
+```