From 5755236d61bdabb5fc87764776843659764590b1 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Mon, 19 Jan 2026 23:28:08 -0600 Subject: [PATCH 01/11] feat(provisioning): Add new Provisioning component --- components/provisioning/CMakeLists.txt | 5 + components/provisioning/README.md | 55 ++ .../provisioning/example/CMakeLists.txt | 22 + components/provisioning/example/README.md | 105 +++ .../provisioning/example/main/CMakeLists.txt | 3 + .../example/main/Kconfig.projbuild | 82 ++ .../example/main/provisioning_example.cpp | 102 +++ .../provisioning/example/partitions.csv | 5 + .../provisioning/example/sdkconfig.defaults | 33 + components/provisioning/idf_component.yml | 17 + .../provisioning/include/provisioning.hpp | 138 ++++ components/provisioning/src/provisioning.cpp | 719 ++++++++++++++++++ 12 files changed, 1286 insertions(+) create mode 100644 components/provisioning/CMakeLists.txt create mode 100644 components/provisioning/README.md create mode 100644 components/provisioning/example/CMakeLists.txt create mode 100644 components/provisioning/example/README.md create mode 100644 components/provisioning/example/main/CMakeLists.txt create mode 100644 components/provisioning/example/main/Kconfig.projbuild create mode 100644 components/provisioning/example/main/provisioning_example.cpp create mode 100644 components/provisioning/example/partitions.csv create mode 100644 components/provisioning/example/sdkconfig.defaults create mode 100644 components/provisioning/idf_component.yml create mode 100644 components/provisioning/include/provisioning.hpp create mode 100644 components/provisioning/src/provisioning.cpp diff --git a/components/provisioning/CMakeLists.txt b/components/provisioning/CMakeLists.txt new file mode 100644 index 000000000..7ca8115a6 --- /dev/null +++ b/components/provisioning/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRCS "src/provisioning.cpp" + INCLUDE_DIRS "include" + REQUIRES esp_wifi esp_http_server nvs wifi logger base_component +) diff --git a/components/provisioning/README.md b/components/provisioning/README.md new file mode 100644 index 000000000..9daca00f8 --- /dev/null +++ b/components/provisioning/README.md @@ -0,0 +1,55 @@ +# Provisioning Component + +WiFi provisioning component with embedded web interface. + +## Features + +- Creates temporary WiFi AP for configuration +- Web-based UI for scanning and connecting to networks +- Tests credentials before saving +- Mobile-friendly responsive design +- Automatic AP shutdown after provisioning + +## Usage + +```cpp +//! [provisioning example] +#include "provisioning.hpp" +#include "nvs.hpp" + +// Initialize provisioning +espp::Provisioning::Config prov_config{ + .ap_ssid = "MyDevice-Setup", + .ap_password = "setup123", // Leave empty for open AP + .device_name = "My ESP32 Device", + .auto_shutdown_ap = true, + .on_provisioned = [](const std::string& ssid, const std::string& password) { + fmt::print("Provisioned! SSID: {}\n", ssid); + // Save credentials to NVS + espp::Nvs nvs; + nvs.set_string("wifi_ssid", ssid); + nvs.set_string("wifi_password", password); + } +}; + +espp::Provisioning prov(prov_config); +prov.start(); + +fmt::print("Connect to WiFi: {}\n", prov_config.ap_ssid); +fmt::print("Open browser to: http://{}\n", prov.get_ip_address()); + +while (prov.is_active()) { + std::this_thread::sleep_for(1s); +} +//! [provisioning example] +``` + +## Web Interface + +Connect to the AP and navigate to `http://192.168.4.1` to: +1. Scan for available networks +2. Select a network +3. Enter password +4. Test and save configuration + +The interface is mobile-friendly and provides visual feedback during scanning and connection. 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..b05eb1217 --- /dev/null +++ b/components/provisioning/example/README.md @@ -0,0 +1,105 @@ +# WiFi Provisioning Example + +Web-based WiFi provisioning system with temporary access point and mobile-friendly interface. + +## How to Use + +### Hardware Setup + +Any ESP32 module with WiFi capability. Optionally connect: +- Reset button (GPIO configurable in menuconfig) +- Status LED (GPIO configurable in menuconfig) + +### Configure the Project + +```bash +idf.py menuconfig +``` + +Navigate to `WiFi Provisioning Example Configuration`: +- Set AP SSID and password for provisioning +- Configure AP channel and max connections +- Set web server port (default: 80) +- Configure timeout (0 for no timeout) +- Enable auto-generate unique SSID (appends MAC address) +- Set reset button GPIO (or -1 to disable) +- Set status LED GPIO (or -1 to disable) +- Enable/disable credential validation +- Configure NVS namespace + +### Build and Flash + +```bash +idf.py build flash monitor +``` + +## Usage Flow + +### First Boot (No Credentials Stored) + +1. Device creates temporary WiFi access point +2. Connect to the AP with your phone/computer: + - SSID: `ESP32-Setup` (or your configured name) + - Password: `configure` (or your configured password) +3. Open browser to `http://192.168.4.1` (or configured IP:port) +4. Web interface shows: + - List of available WiFi networks (scanned) + - Manual SSID entry option + - Password field +5. Select network and enter password +6. Click "Connect" +7. Device validates credentials (if enabled) +8. Credentials saved to NVS +9. Device restarts and connects to WiFi + +### Subsequent Boots + +Device automatically connects using stored credentials. If connection fails, provisioning mode restarts. + +### Reset Credentials + +- Press reset button (if configured) to clear stored credentials +- Or manually erase NVS partition +- Device will enter provisioning mode on next boot + +## Example Output + +``` +I (380) Provisioning Example: Starting WiFi Provisioning Example +I (385) Provisioning Example: Starting WiFi provisioning... +I (561) Provisioning Example: Provisioning started! +I (562) Provisioning Example: Connect to WiFi: ESP32-Setup-AB12CD +I (563) Provisioning Example: Password: configure +I (564) Provisioning Example: Open browser to: http://192.168.4.1:80 +``` + +After provisioning: +``` +I (380) Provisioning Example: Found stored credentials for SSID: MyHomeWiFi +I (385) Provisioning Example: Attempting to connect to stored WiFi network... +I (2450) Provisioning Example: Successfully connected to WiFi! +I (2451) Provisioning Example: IP Address: 192.168.1.105 +``` + +## Features + +- **Mobile-friendly UI**: Responsive design works on phones and tablets +- **Network scanning**: Shows available WiFi networks with signal strength +- **Credential validation**: Tests connection before saving (configurable) +- **Unique SSID generation**: Appends MAC address to prevent conflicts +- **Status LED**: Visual feedback during provisioning (optional) +- **Reset button**: Clear credentials without reflashing (optional) +- **Timeout protection**: Automatically exits provisioning after timeout +- **NVS storage**: Persistent credential storage +- **Secure**: Credentials never displayed in logs + +## Customization + +All settings are configurable via menuconfig without modifying code. The provisioning component can be easily integrated into your own application. + +## Troubleshooting + +- **Can't see AP**: Check channel (some devices don't support channels 12-13) +- **Can't connect to AP**: Verify password is at least 8 characters (or leave empty for open network) +- **Stored credentials don't work**: Enable validation to test before saving +- **Need to reset**: Use reset button or manually erase NVS diff --git a/components/provisioning/example/main/CMakeLists.txt b/components/provisioning/example/main/CMakeLists.txt new file mode 100644 index 000000000..f12693968 --- /dev/null +++ b/components/provisioning/example/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRC_DIRS "." + INCLUDE_DIRS "." + PRIV_REQUIRES nvs_flash logger provisioning nvs) diff --git a/components/provisioning/example/main/Kconfig.projbuild b/components/provisioning/example/main/Kconfig.projbuild new file mode 100644 index 000000000..e3d964318 --- /dev/null +++ b/components/provisioning/example/main/Kconfig.projbuild @@ -0,0 +1,82 @@ +menu "WiFi Provisioning Example Configuration" + + config PROVISIONING_AP_SSID + string "Access Point SSID" + default "ESP32-Setup" + help + SSID (network name) for the temporary access point created for provisioning. + Leave empty to auto-generate based on MAC address. + + config PROVISIONING_AP_PASSWORD + string "Access Point Password" + default "configure" + help + Password for the temporary access point. + Must be at least 8 characters. Leave empty for open network. + + config PROVISIONING_AP_CHANNEL + int "Access Point Channel" + default 1 + range 1 13 + help + WiFi channel for the access point (1-13). + + config PROVISIONING_AP_MAX_CONNECTIONS + int "Max Simultaneous Connections" + default 4 + range 1 10 + help + Maximum number of stations that can connect to the AP simultaneously. + + config PROVISIONING_SERVER_PORT + int "Web Server Port" + default 80 + range 1 65535 + help + HTTP port for the provisioning web server. + + config PROVISIONING_TIMEOUT_SECONDS + int "Provisioning Timeout (seconds)" + default 300 + range 0 3600 + help + Time in seconds before provisioning times out. + Set to 0 for no timeout. + + config PROVISIONING_AUTO_GENERATE_SSID + bool "Auto-generate unique SSID" + default n + help + If enabled, appends the last 3 bytes of MAC address to the AP SSID + to make it unique (e.g., "ESP32-Setup-AB12CD"). + + config PROVISIONING_RESET_BUTTON_GPIO + int "Reset Button GPIO" + default -1 + range -1 48 + help + GPIO pin for reset button to clear stored credentials. + Set to -1 to disable reset button feature. + + config PROVISIONING_LED_GPIO + int "Status LED GPIO" + default -1 + range -1 48 + help + GPIO pin for status LED (blinks during provisioning). + Set to -1 to disable LED feature. + + config PROVISIONING_VALIDATE_CREDENTIALS + bool "Validate WiFi credentials before saving" + default y + help + Test WiFi connection before saving credentials to NVS. + Recommended to prevent saving invalid credentials. + + config PROVISIONING_NVS_NAMESPACE + string "NVS Namespace" + default "wifi_config" + help + NVS namespace to store WiFi credentials. + +endmenu diff --git a/components/provisioning/example/main/provisioning_example.cpp b/components/provisioning/example/main/provisioning_example.cpp new file mode 100644 index 000000000..ddd0154ac --- /dev/null +++ b/components/provisioning/example/main/provisioning_example.cpp @@ -0,0 +1,102 @@ +#include +#include + +#include "logger.hpp" +#include "provisioning.hpp" + +using namespace std::chrono_literals; + +extern "C" void app_main(void) { + 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; + } + + // Generate unique AP SSID + uint8_t mac[6]; + esp_efuse_mac_get_default(mac); + std::string ap_ssid = fmt::format("ESP-Prov-{:02X}{:02X}", mac[4], mac[5]); + logger.info("AP SSID: {}", ap_ssid); + + // Create provisioning configuration + espp::Provisioning::Config config{ + .ap_ssid = ap_ssid, + .ap_password = "", // Open network for simplicity + .device_name = "ESP32 Device", + .server_port = 80, + .auto_shutdown_ap = false, // Keep AP running for this example + .ap_timeout = 0s, // No timeout + .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: {}", ec.message()); + return; + } + + logger.info("Provisioning started"); + logger.info("Connect to WiFi network: {}", ap_ssid); + logger.info("Open browser to: http://192.168.4.1"); + + // Keep running + while (!provisioning.is_provisioned()) { + std::this_thread::sleep_for(1s); + } + + logger.info("Device has been provisioned, stopping provisioning service"); + provisioning.stop(); + } + + // destructor should clean up everything, this tests to ensure no issues + + logger.info("Provisioning example finished"); + 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..d63f9b633 --- /dev/null +++ b/components/provisioning/example/sdkconfig.defaults @@ -0,0 +1,33 @@ +# 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 + +# Default provisioning settings +CONFIG_PROVISIONING_AP_SSID="ESP32-Setup" +CONFIG_PROVISIONING_AP_PASSWORD="configure" +CONFIG_PROVISIONING_AP_CHANNEL=1 +CONFIG_PROVISIONING_AP_MAX_CONNECTIONS=4 +CONFIG_PROVISIONING_SERVER_PORT=80 +CONFIG_PROVISIONING_TIMEOUT_SECONDS=300 +CONFIG_PROVISIONING_AUTO_GENERATE_SSID=y +CONFIG_PROVISIONING_RESET_BUTTON_GPIO=-1 +CONFIG_PROVISIONING_LED_GPIO=-1 +CONFIG_PROVISIONING_VALIDATE_CREDENTIALS=y +CONFIG_PROVISIONING_NVS_NAMESPACE="wifi_config" diff --git a/components/provisioning/idf_component.yml b/components/provisioning/idf_component.yml new file mode 100644 index 000000000..30ce6be91 --- /dev/null +++ b/components/provisioning/idf_component.yml @@ -0,0 +1,17 @@ +## 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/provisioning.html" +tags: + - cpp + - WiFi + - Provisioning +dependencies: + idf: + version: '>=5.0' + espp/logger: '>=1.0' + espp/task: '>=1.0' diff --git a/components/provisioning/include/provisioning.hpp b/components/provisioning/include/provisioning.hpp new file mode 100644 index 000000000..9ec4e91ad --- /dev/null +++ b/components/provisioning/include/provisioning.hpp @@ -0,0 +1,138 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "base_component.hpp" +#include "nvs.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{"ESP32-Setup"}; ///< SSID for the provisioning AP + 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 + std::chrono::seconds ap_timeout{300}; ///< AP timeout (0 = no timeout) + 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 Get the IP address of the provisioning AP + * @return IP address string (e.g., "192.168.4.1") + */ + std::string get_ip_address() const; + +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::string provisioned_ssid_; + std::string provisioned_password_; + std::chrono::time_point ap_start_time_; +}; +} // namespace espp diff --git a/components/provisioning/src/provisioning.cpp b/components/provisioning/src/provisioning.cpp new file mode 100644 index 000000000..34eebbd54 --- /dev/null +++ b/components/provisioning/src/provisioning.cpp @@ -0,0 +1,719 @@ +#include "provisioning.hpp" + +#include +#include + +#include "esp_wifi.h" + +using namespace espp; +using namespace std::chrono_literals; + +static const char *HTML_TEMPLATE = R"HTML( + + + + + WiFi Setup + + + +
+

WiFi Setup

+
%DEVICE_NAME%
+ + + + + + + +
+ +
+
+ +
+ +
― 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; + 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; + ap_start_time_ = std::chrono::steady_clock::now(); + 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); + + // Set WiFi mode to APSTA to support both AP and scanning + esp_err_t ret = esp_wifi_set_mode(WIFI_MODE_APSTA); + if (ret != ESP_OK) { + logger_.error("Failed to set WiFi mode to APSTA: {}", esp_err_to_name(ret)); + return false; + } + logger_.info("WiFi mode set to APSTA"); + + 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 "192.168.4.1"; } + +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..."); + + uint16_t ap_count = 0; + wifi_scan_config_t scan_config = {.ssid = nullptr, + .bssid = nullptr, + .channel = 0, + .show_hidden = false, + .scan_type = WIFI_SCAN_TYPE_ACTIVE, + .scan_time = {.active = {.min = 100, .max = 300}}}; + + esp_err_t ret = esp_wifi_scan_start(&scan_config, true); + if (ret != ESP_OK) { + logger_.error("WiFi scan failed: {}", esp_err_to_name(ret)); + return R"({"networks":[],"error":"scan_failed"})"; + } + + ret = esp_wifi_scan_get_ap_num(&ap_count); + if (ret != ESP_OK) { + logger_.error("Failed to get AP count: {}", esp_err_to_name(ret)); + return R"({"networks":[],"error":"get_count_failed"})"; + } + + logger_.info("Found {} networks", ap_count); + + if (ap_count == 0) { + return R"({"networks":[]})"; + } + + std::vector ap_records(ap_count); + esp_wifi_scan_get_ap_records(&ap_count, ap_records.data()); + + // 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 < 20; i++) { + if (i > 0) + json += ","; + json += fmt::format(R"({{"ssid":"{}","rssi":{},"secure":{}}})", + reinterpret_cast(ap_records[i].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); + + std::atomic connected{false}; + std::atomic got_ip{false}; + + WifiSta::Config sta_config{.ssid = ssid, + .password = password, + .num_connect_retries = 5, + .on_connected = [&]() { connected = true; }, + .on_got_ip = [&](ip_event_got_ip_t *) { got_ip = true; }, + .log_level = config_.log_level}; + + test_sta_ = std::make_unique(sta_config); + + // Wait for connection (max 15 seconds) + auto start = std::chrono::steady_clock::now(); + while (!got_ip && (std::chrono::steady_clock::now() - start) < 15s) { + std::this_thread::sleep_for(100ms); + } + + bool success = got_ip; + + logger_.info("Connection test result: {}", success); + + // Clean up test STA immediately to avoid interfering with AP + test_sta_.reset(); + + // Give WiFi stack time to stabilize + std::this_thread::sleep_for(100ms); + + 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); + + char buf[512]; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid request"); + return ESP_FAIL; + } + buf[ret] = '\0'; + + // Parse JSON manually (simple approach) + std::string data(buf); + std::string ssid, password; + bool use_saved = false; + + size_t ssid_pos = data.find("\"ssid\":\""); + if (ssid_pos != std::string::npos) { + ssid_pos += 8; + size_t ssid_end = data.find("\"", ssid_pos); + ssid = data.substr(ssid_pos, ssid_end - ssid_pos); + } + + size_t pass_pos = data.find("\"password\":\""); + if (pass_pos != std::string::npos) { + pass_pos += 12; + size_t pass_end = data.find("\"", pass_pos); + password = data.substr(pass_pos, pass_end - pass_pos); + } + + size_t saved_pos = data.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); + + // Send response immediately + httpd_resp_send(req, "OK", 2); + + // Handle completion in background thread + std::thread([prov]() { + // Small delay to ensure response transmitted + std::this_thread::sleep_for(100ms); + + // Call callback 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) { + std::this_thread::sleep_for(2s); + prov->stop(); + } + }).detach(); + + 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); + + char buf[256]; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid request"); + return ESP_FAIL; + } + buf[ret] = '\0'; + + // Parse SSID from JSON + std::string data(buf); + std::string ssid; + + size_t ssid_pos = data.find("\"ssid\":\""); + if (ssid_pos != std::string::npos) { + ssid_pos += 8; + size_t ssid_end = data.find("\"", ssid_pos); + ssid = data.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 list of SSIDs from NVS + std::string ssid_list; + nvs.get("ssid_list", ssid_list, ec); + + if (!ec && !ssid_list.empty()) { + size_t pos = 0; + while ((pos = ssid_list.find(',')) != std::string::npos) { + ssids.push_back(ssid_list.substr(0, pos)); + ssid_list.erase(0, pos + 1); + } + if (!ssid_list.empty()) { + ssids.push_back(ssid_list); + } + } + + 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 ""; + } + + std::string key = "pwd_" + ssid; + std::string password; + nvs.get(key, password, ec); + + 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); + + // Save password + nvs.set("pwd_" + ssid, password, ec); + if (ec) { + logger_.error("Failed to save password"); + return; + } + + // Update SSID list + auto ssids = get_saved_ssids(); + if (std::find(ssids.begin(), ssids.end(), ssid) == ssids.end()) { + ssids.push_back(ssid); + + std::string list; + for (size_t i = 0; i < ssids.size(); i++) { + if (i > 0) + list += ","; + list += ssids[i]; + } + nvs.set("ssid_list", list, ec); + if (ec) { + logger_.error("Failed to save SSID list"); + 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); + + // Remove password + nvs.erase("pwd_" + ssid, ec); + + // Update SSID list + auto ssids = get_saved_ssids(); + ssids.erase(std::remove(ssids.begin(), ssids.end(), ssid), ssids.end()); + + std::string list; + for (size_t i = 0; i < ssids.size(); i++) { + if (i > 0) + list += ","; + list += ssids[i]; + } + nvs.set("ssid_list", list, ec); + if (ec) { + logger_.error("Failed to update SSID list"); + 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 += "\"" + ssids[i] + "\""; + } + json += "]}"; + + return json; +} From fa924869058782f140335342089a069ec27eaca3 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sat, 24 Jan 2026 16:54:23 -0600 Subject: [PATCH 02/11] add wifi sta logging --- components/wifi/include/wifi_sta.hpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/components/wifi/include/wifi_sta.hpp b/components/wifi/include/wifi_sta.hpp index 8639e2e74..4c5218165 100644 --- a/components/wifi/include/wifi_sta.hpp +++ b/components/wifi/include/wifi_sta.hpp @@ -563,17 +563,21 @@ 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 - initiating connection"); connected_ = false; esp_wifi_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_) { esp_wifi_connect(); attempts_++; - logger_.info("Retrying to connect to the AP"); + logger_.info("Retrying to connect to the AP (attempt {}/{})", attempts_.load(), + num_retries_.load()); // return early, don't call disconnect callback return; } @@ -582,6 +586,8 @@ class WifiSta : public WifiBase { if (disconnect_callback_) { disconnect_callback_(); } + } 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)); From fa721450ee3705ad05b9fa3a5cc4b7baa75e4732 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sat, 24 Jan 2026 23:54:05 -0600 Subject: [PATCH 03/11] work through improvements to wifi scanning, esp handling scanning while connected and scanning from start. update wifi example to better test these cases and to print test summary --- .../wifi/example/main/Kconfig.projbuild | 20 ++ components/wifi/example/main/wifi_example.cpp | 213 +++++++++++++--- components/wifi/include/wifi.hpp | 33 +++ components/wifi/include/wifi_base.hpp | 12 + components/wifi/include/wifi_sta.hpp | 227 +++++++++++++++--- components/wifi/include/wifi_sta_menu.hpp | 51 ++-- components/wifi/src/wifi_sta.cpp | 3 + 7 files changed, 487 insertions(+), 72 deletions(-) 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 4c5218165..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,9 +669,19 @@ 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 - initiating connection"); + 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; @@ -573,18 +689,28 @@ class WifiSta : public WifiBase { 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 (attempt {}/{})", attempts_.load(), - num_retries_.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"); @@ -593,11 +719,18 @@ class WifiSta : public WifiBase { 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); } } } @@ -646,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_; @@ -666,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(); From 87f61d5b33a9e44351059878bcd114e4a112acae Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 25 Jan 2026 00:03:06 -0600 Subject: [PATCH 04/11] improve provisioning example and code --- .github/workflows/build.yml | 2 + .github/workflows/upload_components.yml | 3 +- components/provisioning/CMakeLists.txt | 4 +- components/provisioning/README.md | 71 ++--- components/provisioning/example/README.md | 266 ++++++++++++------ .../example/main/provisioning_example.cpp | 67 ++--- components/provisioning/idf_component.yml | 7 +- .../provisioning/include/provisioning.hpp | 17 +- components/provisioning/src/provisioning.cpp | 94 ++++--- doc/Doxyfile | 2 + doc/en/network/index.rst | 1 + doc/en/network/provisioning.rst | 8 + doc/en/network/provisioning_example.md | 3 + 13 files changed, 331 insertions(+), 214 deletions(-) create mode 100644 doc/en/network/provisioning.rst create mode 100644 doc/en/network/provisioning_example.md 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 index 7ca8115a6..f6edaf0b0 100644 --- a/components/provisioning/CMakeLists.txt +++ b/components/provisioning/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register( - SRCS "src/provisioning.cpp" INCLUDE_DIRS "include" - REQUIRES esp_wifi esp_http_server nvs wifi logger base_component + SRC_DIRS "src" + REQUIRES esp_http_server nvs wifi base_component ) diff --git a/components/provisioning/README.md b/components/provisioning/README.md index 9daca00f8..fc1b932dd 100644 --- a/components/provisioning/README.md +++ b/components/provisioning/README.md @@ -1,55 +1,26 @@ # Provisioning Component -WiFi provisioning component with embedded web interface. +[![Badge](https://components.espressif.com/components/espp/provisioning/badge.svg)](https://components.espressif.com/components/espp/provisioning) -## Features - -- Creates temporary WiFi AP for configuration -- Web-based UI for scanning and connecting to networks -- Tests credentials before saving -- Mobile-friendly responsive design -- Automatic AP shutdown after provisioning - -## Usage - -```cpp -//! [provisioning example] -#include "provisioning.hpp" -#include "nvs.hpp" - -// Initialize provisioning -espp::Provisioning::Config prov_config{ - .ap_ssid = "MyDevice-Setup", - .ap_password = "setup123", // Leave empty for open AP - .device_name = "My ESP32 Device", - .auto_shutdown_ap = true, - .on_provisioned = [](const std::string& ssid, const std::string& password) { - fmt::print("Provisioned! SSID: {}\n", ssid); - // Save credentials to NVS - espp::Nvs nvs; - nvs.set_string("wifi_ssid", ssid); - nvs.set_string("wifi_password", password); - } -}; +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. -espp::Provisioning prov(prov_config); -prov.start(); - -fmt::print("Connect to WiFi: {}\n", prov_config.ap_ssid); -fmt::print("Open browser to: http://{}\n", prov.get_ip_address()); - -while (prov.is_active()) { - std::this_thread::sleep_for(1s); -} -//! [provisioning example] -``` - -## Web Interface - -Connect to the AP and navigate to `http://192.168.4.1` to: -1. Scan for available networks -2. Select a network -3. Enter password -4. Test and save configuration +## Features -The interface is mobile-friendly and provides visual feedback during scanning and connection. +- 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/README.md b/components/provisioning/example/README.md index b05eb1217..83b3d8bcb 100644 --- a/components/provisioning/example/README.md +++ b/components/provisioning/example/README.md @@ -1,105 +1,211 @@ -# WiFi Provisioning Example +# Provisioning Example -Web-based WiFi provisioning system with temporary access point and mobile-friendly interface. +This example demonstrates the `espp::Provisioning` component, which provides a +web-based WiFi configuration interface for ESP32 devices. -## How to Use +## How to use example -### Hardware Setup +### Hardware Required -Any ESP32 module with WiFi capability. Optionally connect: -- Reset button (GPIO configurable in menuconfig) -- Status LED (GPIO configurable in menuconfig) +This example can run on any ESP32 development board with WiFi capability. -### Configure the Project +### Build and Flash + +Build the project and flash it to the board, then run monitor tool to view serial output: -```bash -idf.py menuconfig +``` +idf.py -p PORT flash monitor ``` -Navigate to `WiFi Provisioning Example Configuration`: -- Set AP SSID and password for provisioning -- Configure AP channel and max connections -- Set web server port (default: 80) -- Configure timeout (0 for no timeout) -- Enable auto-generate unique SSID (appends MAC address) -- Set reset button GPIO (or -1 to disable) -- Set status LED GPIO (or -1 to disable) -- Enable/disable credential validation -- Configure NVS namespace +(Replace PORT with the name of the serial port to use.) -### Build and Flash +(To exit the serial monitor, type ``Ctrl-]``.) -```bash -idf.py build flash monitor -``` +See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. -## Usage Flow - -### First Boot (No Credentials Stored) - -1. Device creates temporary WiFi access point -2. Connect to the AP with your phone/computer: - - SSID: `ESP32-Setup` (or your configured name) - - Password: `configure` (or your configured password) -3. Open browser to `http://192.168.4.1` (or configured IP:port) -4. Web interface shows: - - List of available WiFi networks (scanned) - - Manual SSID entry option - - Password field -5. Select network and enter password -6. Click "Connect" -7. Device validates credentials (if enabled) -8. Credentials saved to NVS -9. Device restarts and connects to WiFi +## Example Usage -### Subsequent Boots +### First Time Setup -Device automatically connects using stored credentials. If connection fails, provisioning mode restarts. +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 -### Reset Credentials +### Subsequent Boots -- Press reset button (if configured) to clear stored credentials -- Or manually erase NVS partition -- Device will enter provisioning mode on next boot +Once provisioned, the device will automatically attempt to connect to the saved +WiFi network on boot. The provisioning interface will not start unless credentials +are missing or invalid. ## Example Output -``` -I (380) Provisioning Example: Starting WiFi Provisioning Example -I (385) Provisioning Example: Starting WiFi provisioning... -I (561) Provisioning Example: Provisioning started! -I (562) Provisioning Example: Connect to WiFi: ESP32-Setup-AB12CD -I (563) Provisioning Example: Password: configure -I (564) Provisioning Example: Open browser to: http://192.168.4.1:80 -``` - -After provisioning: -``` -I (380) Provisioning Example: Found stored credentials for SSID: MyHomeWiFi -I (385) Provisioning Example: Attempting to connect to stored WiFi network... -I (2450) Provisioning Example: Successfully connected to WiFi! -I (2451) Provisioning Example: IP Address: 192.168.1.105 +```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 ``` -## Features +## Configuration -- **Mobile-friendly UI**: Responsive design works on phones and tablets -- **Network scanning**: Shows available WiFi networks with signal strength -- **Credential validation**: Tests connection before saving (configurable) -- **Unique SSID generation**: Appends MAC address to prevent conflicts -- **Status LED**: Visual feedback during provisioning (optional) -- **Reset button**: Clear credentials without reflashing (optional) -- **Timeout protection**: Automatically exits provisioning after timeout -- **NVS storage**: Persistent credential storage -- **Secure**: Credentials never displayed in logs +The example can be configured through `idf.py menuconfig` under "Provisioning Example Configuration": -## Customization +- **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) -All settings are configurable via menuconfig without modifying code. The provisioning component can be easily integrated into your own application. +## Web Interface Features -## Troubleshooting +The embedded web interface provides: -- **Can't see AP**: Check channel (some devices don't support channels 12-13) -- **Can't connect to AP**: Verify password is at least 8 characters (or leave empty for open network) -- **Stored credentials don't work**: Enable validation to test before saving -- **Need to reset**: Use reset button or manually erase NVS +- **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/provisioning_example.cpp b/components/provisioning/example/main/provisioning_example.cpp index ddd0154ac..fa0b54694 100644 --- a/components/provisioning/example/main/provisioning_example.cpp +++ b/components/provisioning/example/main/provisioning_example.cpp @@ -21,17 +21,12 @@ extern "C" void app_main(void) { return; } - // Generate unique AP SSID - uint8_t mac[6]; - esp_efuse_mac_get_default(mac); - std::string ap_ssid = fmt::format("ESP-Prov-{:02X}{:02X}", mac[4], mac[5]); - logger.info("AP SSID: {}", ap_ssid); - // Create provisioning configuration espp::Provisioning::Config config{ - .ap_ssid = ap_ssid, - .ap_password = "", // Open network for simplicity - .device_name = "ESP32 Device", + .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 .ap_timeout = 0s, // No timeout @@ -56,44 +51,40 @@ extern "C" void app_main(void) { }, .log_level = espp::Logger::Verbosity::INFO}; - { - // Create provisioning - espp::Provisioning provisioning(config); + // Create provisioning + espp::Provisioning provisioning(config); - // print any existing network ssids that have been provisioned - { - espp::NvsHandle handle("wifi_config", ec); + // 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) { - 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"); - } + 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: {}", ec.message()); - return; - } - - logger.info("Provisioning started"); - logger.info("Connect to WiFi network: {}", ap_ssid); - logger.info("Open browser to: http://192.168.4.1"); + // Start provisioning + if (!provisioning.start()) { + logger.error("Failed to start provisioning"); + return; + } - // Keep running - while (!provisioning.is_provisioned()) { - std::this_thread::sleep_for(1s); - } + logger.info("Provisioning started"); + logger.info("Connect to WiFi network: {}", provisioning.get_ap_ssid()); + logger.info("Open browser to: http://192.168.4.1"); - logger.info("Device has been provisioned, stopping provisioning service"); - provisioning.stop(); + // Keep running until user completes provisioning + while (!provisioning.is_completed()) { + std::this_thread::sleep_for(1s); } - // destructor should clean up everything, this tests to ensure no issues + logger.info("Provisioning completed by user, stopping service"); + provisioning.stop(); logger.info("Provisioning example finished"); while (true) { diff --git a/components/provisioning/idf_component.yml b/components/provisioning/idf_component.yml index 30ce6be91..fbd293fb6 100644 --- a/components/provisioning/idf_component.yml +++ b/components/provisioning/idf_component.yml @@ -5,7 +5,7 @@ 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/provisioning.html" +documentation: "https://esp-cpp.github.io/espp/network/provisioning.html" tags: - cpp - WiFi @@ -13,5 +13,6 @@ tags: dependencies: idf: version: '>=5.0' - espp/logger: '>=1.0' - espp/task: '>=1.0' + espp/base_component: '>=1.0' + espp/nvs: '>=1.0' + espp/wifi: '>=1.0' diff --git a/components/provisioning/include/provisioning.hpp b/components/provisioning/include/provisioning.hpp index 9ec4e91ad..805518c21 100644 --- a/components/provisioning/include/provisioning.hpp +++ b/components/provisioning/include/provisioning.hpp @@ -9,6 +9,7 @@ #include "base_component.hpp" #include "nvs.hpp" +#include "wifi.hpp" #include "wifi_ap.hpp" #include "wifi_sta.hpp" @@ -47,7 +48,8 @@ class Provisioning : public BaseComponent { * @brief Configuration for the provisioning component */ struct Config { - std::string ap_ssid{"ESP32-Setup"}; ///< SSID for the provisioning AP + 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 @@ -91,12 +93,24 @@ class Provisioning : public BaseComponent { */ 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 + */ + std::string get_ap_ssid() const { return config_.ap_ssid; } + protected: void init(const Config &config); bool start_ap(); @@ -131,6 +145,7 @@ class Provisioning : public BaseComponent { 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_; std::chrono::time_point ap_start_time_; diff --git a/components/provisioning/src/provisioning.cpp b/components/provisioning/src/provisioning.cpp index 34eebbd54..d7d61190c 100644 --- a/components/provisioning/src/provisioning.cpp +++ b/components/provisioning/src/provisioning.cpp @@ -216,6 +216,14 @@ 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); } @@ -261,14 +269,7 @@ bool Provisioning::start_ap() { wifi_ap_ = std::make_unique(ap_config); - // Set WiFi mode to APSTA to support both AP and scanning - esp_err_t ret = esp_wifi_set_mode(WIFI_MODE_APSTA); - if (ret != ESP_OK) { - logger_.error("Failed to set WiFi mode to APSTA: {}", esp_err_to_name(ret)); - return false; - } - logger_.info("WiFi mode set to APSTA"); - + // WiFi mode is managed by WifiAp and WifiSta classes return wifi_ap_ != nullptr; } @@ -344,41 +345,26 @@ std::string Provisioning::generate_html() const { std::string Provisioning::scan_networks() { logger_.info("Starting WiFi scan..."); - uint16_t ap_count = 0; - wifi_scan_config_t scan_config = {.ssid = nullptr, - .bssid = nullptr, - .channel = 0, - .show_hidden = false, - .scan_type = WIFI_SCAN_TYPE_ACTIVE, - .scan_time = {.active = {.min = 100, .max = 300}}}; - - esp_err_t ret = esp_wifi_scan_start(&scan_config, true); - if (ret != ESP_OK) { - logger_.error("WiFi scan failed: {}", esp_err_to_name(ret)); - return R"({"networks":[],"error":"scan_failed"})"; - } - - ret = esp_wifi_scan_get_ap_num(&ap_count); - if (ret != ESP_OK) { - logger_.error("Failed to get AP count: {}", esp_err_to_name(ret)); - return R"({"networks":[],"error":"get_count_failed"})"; - } + // 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); - logger_.info("Found {} networks", ap_count); + auto ap_records = temp_sta.scan(20); - if (ap_count == 0) { + if (ap_records.empty()) { + logger_.info("No networks found"); return R"({"networks":[]})"; } - std::vector ap_records(ap_count); - esp_wifi_scan_get_ap_records(&ap_count, ap_records.data()); + 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 < 20; i++) { + for (size_t i = 0; i < ap_records.size(); i++) { if (i > 0) json += ","; json += fmt::format(R"({{"ssid":"{}","rssi":{},"secure":{}}})", @@ -392,33 +378,60 @@ std::string Provisioning::scan_networks() { 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, - .on_connected = [&]() { connected = true; }, - .on_got_ip = [&](ip_event_got_ip_t *) { got_ip = true; }, - .log_level = config_.log_level}; - + .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::DEBUG}; + + 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 && (std::chrono::steady_clock::now() - start) < 15s) { + 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: {}", success); + 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(100ms); + std::this_thread::sleep_for(200ms); return success; } @@ -515,6 +528,9 @@ esp_err_t Provisioning::connect_handler(httpd_req_t *req) { 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); 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..51851aa5c --- /dev/null +++ b/doc/en/network/provisioning.rst @@ -0,0 +1,8 @@ +Provisioning +============ + +API Reference +------------- + +.. doxygenfile:: provisioning.hpp + :project: espp diff --git a/doc/en/network/provisioning_example.md b/doc/en/network/provisioning_example.md new file mode 100644 index 000000000..95b0e817d --- /dev/null +++ b/doc/en/network/provisioning_example.md @@ -0,0 +1,3 @@ +# Provisioning Example + +@example provisioning_example.cpp From 66f3d9d4079c1e4d3a64587e654037b54a194c13 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 25 Jan 2026 00:21:17 -0600 Subject: [PATCH 05/11] update how nvs is stored and ensure that provisioned networks properly show up and work well in the html interface --- components/provisioning/example/README.md | 8 +- .../example/main/provisioning_example.cpp | 2 + components/provisioning/src/provisioning.cpp | 191 +++++++++++++----- 3 files changed, 148 insertions(+), 53 deletions(-) diff --git a/components/provisioning/example/README.md b/components/provisioning/example/README.md index 83b3d8bcb..27a3478fb 100644 --- a/components/provisioning/example/README.md +++ b/components/provisioning/example/README.md @@ -40,9 +40,11 @@ See the Getting Started Guide for full steps to configure and use ESP-IDF to bui ### Subsequent Boots -Once provisioned, the device will automatically attempt to connect to the saved -WiFi network on boot. The provisioning interface will not start unless credentials -are missing or invalid. +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 diff --git a/components/provisioning/example/main/provisioning_example.cpp b/components/provisioning/example/main/provisioning_example.cpp index fa0b54694..72f0957c3 100644 --- a/components/provisioning/example/main/provisioning_example.cpp +++ b/components/provisioning/example/main/provisioning_example.cpp @@ -7,6 +7,7 @@ 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"); @@ -87,6 +88,7 @@ extern "C" void app_main(void) { provisioning.stop(); logger.info("Provisioning example finished"); + //! [provisioning example] while (true) { std::this_thread::sleep_for(1s); } diff --git a/components/provisioning/src/provisioning.cpp b/components/provisioning/src/provisioning.cpp index d7d61190c..097fc7415 100644 --- a/components/provisioning/src/provisioning.cpp +++ b/components/provisioning/src/provisioning.cpp @@ -8,6 +8,47 @@ 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( @@ -25,11 +66,11 @@ static const char *HTML_TEMPLATE = R"HTML( .network { padding: 12px; margin: 5px 0; background: #f9f9f9; border-radius: 5px; cursor: pointer; border: 2px solid transparent; display: flex; justify-content: space-between; align-items: center; } .network:hover { border-color: #2196F3; } .network.selected { border-color: #2196F3; background: #e3f2fd; } - .network-name { flex-grow: 1; } + .network-name { flex-grow: 1; margin-right: 15px; } .signal { color: #666; margin-right: 10px; } - .delete-btn { background: #f44336; color: white; border: none; padding: 5px 10px; border-radius: 3px; cursor: pointer; font-size: 12px; } + .delete-btn { background: #f44336; color: white; border: none; padding: 8px 15px; border-radius: 3px; cursor: pointer; font-size: 13px; margin-left: 8px; } .delete-btn:hover { background: #d32f2f; } - .connect-saved-btn { background: #4CAF50; color: white; border: none; padding: 5px 15px; border-radius: 3px; cursor: pointer; font-size: 12px; margin-right: 5px; } + .connect-saved-btn { background: #4CAF50; color: white; border: none; padding: 8px 15px; border-radius: 3px; cursor: pointer; font-size: 13px; } .connect-saved-btn:hover { background: #45a049; } input { width: 100%; padding: 10px; margin: 5px 0; border: 1px solid #ddd; border-radius: 5px; box-sizing: border-box; } .status { padding: 10px; margin: 10px 0; border-radius: 5px; text-align: center; display: none; } @@ -367,9 +408,9 @@ std::string Provisioning::scan_networks() { for (size_t i = 0; i < ap_records.size(); i++) { if (i > 0) json += ","; - json += fmt::format(R"({{"ssid":"{}","rssi":{},"secure":{}}})", - reinterpret_cast(ap_records[i].ssid), ap_records[i].rssi, - ap_records[i].authmode != WIFI_AUTH_OPEN); + 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; @@ -618,18 +659,24 @@ std::vector Provisioning::get_saved_ssids() { return ssids; } - // Get list of SSIDs from NVS - std::string ssid_list; - nvs.get("ssid_list", ssid_list, ec); + // Get count of saved networks + uint32_t count = 0; + nvs.get("count", count, ec); + if (ec || count == 0) { + return ssids; + } - if (!ec && !ssid_list.empty()) { - size_t pos = 0; - while ((pos = ssid_list.find(',')) != std::string::npos) { - ssids.push_back(ssid_list.substr(0, pos)); - ssid_list.erase(0, pos + 1); - } - if (!ssid_list.empty()) { - ssids.push_back(ssid_list); + // 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); + } } } @@ -643,9 +690,22 @@ std::string Provisioning::get_saved_password(const std::string &ssid) { return ""; } - std::string key = "pwd_" + ssid; + // 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(key, password, ec); + 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; } @@ -660,29 +720,37 @@ void Provisioning::save_credentials(const std::string &ssid, const std::string & logger_.info("Saving credentials for: {}", ssid); - // Save password - nvs.set("pwd_" + ssid, password, ec); + // 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 password"); + logger_.error("Failed to save SSID"); return; } - // Update SSID list - auto ssids = get_saved_ssids(); - if (std::find(ssids.begin(), ssids.end(), ssid) == ssids.end()) { - ssids.push_back(ssid); + nvs.set(pwd_key, password, ec); + if (ec) { + logger_.error("Failed to save password"); + return; + } - std::string list; - for (size_t i = 0; i < ssids.size(); i++) { - if (i > 0) - list += ","; - list += ssids[i]; - } - nvs.set("ssid_list", list, ec); - if (ec) { - logger_.error("Failed to save SSID list"); - return; - } + // Save count + nvs.set("count", static_cast(ssids.size()), ec); + if (ec) { + logger_.error("Failed to save count"); + return; } nvs.commit(ec); @@ -698,22 +766,45 @@ void Provisioning::delete_credentials(const std::string &ssid) { logger_.info("Deleting credentials for: {}", ssid); - // Remove password - nvs.erase("pwd_" + ssid, ec); - - // Update SSID list + // Get current SSIDs auto ssids = get_saved_ssids(); - ssids.erase(std::remove(ssids.begin(), ssids.end(), ssid), ssids.end()); + auto it = std::find(ssids.begin(), ssids.end(), ssid); + if (it == ssids.end()) { + logger_.warn("SSID not found in saved credentials"); + return; + } - std::string list; - for (size_t i = 0; i < ssids.size(); i++) { - if (i > 0) - list += ","; - list += ssids[i]; + 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); } - nvs.set("ssid_list", list, 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 SSID list"); + logger_.error("Failed to update count"); return; } @@ -727,7 +818,7 @@ std::string Provisioning::get_saved_networks_json() { for (size_t i = 0; i < ssids.size(); i++) { if (i > 0) json += ","; - json += "\"" + ssids[i] + "\""; + json += "\"" + json_escape(ssids[i]) + "\""; } json += "]}"; From 2670c75fbc86191123bab5ff7568fcd60df6548d Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 25 Jan 2026 00:35:48 -0600 Subject: [PATCH 06/11] update readme --- components/provisioning/example/README.md | 42 +++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/components/provisioning/example/README.md b/components/provisioning/example/README.md index 27a3478fb..19d9c9f14 100644 --- a/components/provisioning/example/README.md +++ b/components/provisioning/example/README.md @@ -3,6 +3,33 @@ This example demonstrates the `espp::Provisioning` component, which provides a web-based WiFi configuration interface for ESP32 devices. +__Scanning for network and connecting__: + + + + + + + + + + + + +
scan imageenter password image
connecting imageconnected image
provisioning completed image
+ +__Managing saved networks__: + + + + + + + + + +
view imageconnect image
connected imagedelete image
+ ## How to use example ### Hardware Required @@ -48,6 +75,21 @@ retrieve saved credentials from NVS and use the `WifiSta` component to connect. ## Example Output + +Scanning for network and connecting: +image +image +image +image +image +image + +Managing saved networks: +image +image +image +image + ```console I (415) main_task: Calling app_main() [Provisioning Example/I][0.415]: Starting WiFi Provisioning Example From de1837c38e3d6e5346d1fdd9a5287173640de641 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 25 Jan 2026 00:36:40 -0600 Subject: [PATCH 07/11] fix sa --- components/provisioning/include/provisioning.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/provisioning/include/provisioning.hpp b/components/provisioning/include/provisioning.hpp index 805518c21..63a348744 100644 --- a/components/provisioning/include/provisioning.hpp +++ b/components/provisioning/include/provisioning.hpp @@ -109,7 +109,7 @@ class Provisioning : public BaseComponent { * @brief Get the actual AP SSID (after MAC appending) * @return The SSID of the provisioning AP */ - std::string get_ap_ssid() const { return config_.ap_ssid; } + const std::string &get_ap_ssid() const { return config_.ap_ssid; } protected: void init(const Config &config); From 991e7ad7f45a66e1731a0bd0b9c9b4a1b28793e0 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 25 Jan 2026 00:42:01 -0600 Subject: [PATCH 08/11] clean up code and example --- .../provisioning/example/main/CMakeLists.txt | 3 +- .../example/main/Kconfig.projbuild | 82 ------------------- .../provisioning/example/sdkconfig.defaults | 13 --- components/provisioning/src/provisioning.cpp | 2 +- 4 files changed, 2 insertions(+), 98 deletions(-) delete mode 100644 components/provisioning/example/main/Kconfig.projbuild diff --git a/components/provisioning/example/main/CMakeLists.txt b/components/provisioning/example/main/CMakeLists.txt index f12693968..a941e22ba 100644 --- a/components/provisioning/example/main/CMakeLists.txt +++ b/components/provisioning/example/main/CMakeLists.txt @@ -1,3 +1,2 @@ idf_component_register(SRC_DIRS "." - INCLUDE_DIRS "." - PRIV_REQUIRES nvs_flash logger provisioning nvs) + INCLUDE_DIRS ".") diff --git a/components/provisioning/example/main/Kconfig.projbuild b/components/provisioning/example/main/Kconfig.projbuild deleted file mode 100644 index e3d964318..000000000 --- a/components/provisioning/example/main/Kconfig.projbuild +++ /dev/null @@ -1,82 +0,0 @@ -menu "WiFi Provisioning Example Configuration" - - config PROVISIONING_AP_SSID - string "Access Point SSID" - default "ESP32-Setup" - help - SSID (network name) for the temporary access point created for provisioning. - Leave empty to auto-generate based on MAC address. - - config PROVISIONING_AP_PASSWORD - string "Access Point Password" - default "configure" - help - Password for the temporary access point. - Must be at least 8 characters. Leave empty for open network. - - config PROVISIONING_AP_CHANNEL - int "Access Point Channel" - default 1 - range 1 13 - help - WiFi channel for the access point (1-13). - - config PROVISIONING_AP_MAX_CONNECTIONS - int "Max Simultaneous Connections" - default 4 - range 1 10 - help - Maximum number of stations that can connect to the AP simultaneously. - - config PROVISIONING_SERVER_PORT - int "Web Server Port" - default 80 - range 1 65535 - help - HTTP port for the provisioning web server. - - config PROVISIONING_TIMEOUT_SECONDS - int "Provisioning Timeout (seconds)" - default 300 - range 0 3600 - help - Time in seconds before provisioning times out. - Set to 0 for no timeout. - - config PROVISIONING_AUTO_GENERATE_SSID - bool "Auto-generate unique SSID" - default n - help - If enabled, appends the last 3 bytes of MAC address to the AP SSID - to make it unique (e.g., "ESP32-Setup-AB12CD"). - - config PROVISIONING_RESET_BUTTON_GPIO - int "Reset Button GPIO" - default -1 - range -1 48 - help - GPIO pin for reset button to clear stored credentials. - Set to -1 to disable reset button feature. - - config PROVISIONING_LED_GPIO - int "Status LED GPIO" - default -1 - range -1 48 - help - GPIO pin for status LED (blinks during provisioning). - Set to -1 to disable LED feature. - - config PROVISIONING_VALIDATE_CREDENTIALS - bool "Validate WiFi credentials before saving" - default y - help - Test WiFi connection before saving credentials to NVS. - Recommended to prevent saving invalid credentials. - - config PROVISIONING_NVS_NAMESPACE - string "NVS Namespace" - default "wifi_config" - help - NVS namespace to store WiFi credentials. - -endmenu diff --git a/components/provisioning/example/sdkconfig.defaults b/components/provisioning/example/sdkconfig.defaults index d63f9b633..e20c8f577 100644 --- a/components/provisioning/example/sdkconfig.defaults +++ b/components/provisioning/example/sdkconfig.defaults @@ -18,16 +18,3 @@ CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 # HTTP Server CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024 CONFIG_HTTPD_MAX_URI_LEN=512 - -# Default provisioning settings -CONFIG_PROVISIONING_AP_SSID="ESP32-Setup" -CONFIG_PROVISIONING_AP_PASSWORD="configure" -CONFIG_PROVISIONING_AP_CHANNEL=1 -CONFIG_PROVISIONING_AP_MAX_CONNECTIONS=4 -CONFIG_PROVISIONING_SERVER_PORT=80 -CONFIG_PROVISIONING_TIMEOUT_SECONDS=300 -CONFIG_PROVISIONING_AUTO_GENERATE_SSID=y -CONFIG_PROVISIONING_RESET_BUTTON_GPIO=-1 -CONFIG_PROVISIONING_LED_GPIO=-1 -CONFIG_PROVISIONING_VALIDATE_CREDENTIALS=y -CONFIG_PROVISIONING_NVS_NAMESPACE="wifi_config" diff --git a/components/provisioning/src/provisioning.cpp b/components/provisioning/src/provisioning.cpp index 097fc7415..150bcffca 100644 --- a/components/provisioning/src/provisioning.cpp +++ b/components/provisioning/src/provisioning.cpp @@ -450,7 +450,7 @@ bool Provisioning::test_connection(const std::string &ssid, const std::string &p IP2STR(&event->ip_info.ip)); got_ip = true; }, - .log_level = Logger::Verbosity::DEBUG}; + .log_level = Logger::Verbosity::ERROR}; logger_.info("Creating test WiFi STA instance"); test_sta_ = std::make_unique(sta_config); From 53d033aa52b5f0997ea8f92e596563cc1fb061d2 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 25 Jan 2026 00:51:47 -0600 Subject: [PATCH 09/11] clean up some more --- .../provisioning/example/main/provisioning_example.cpp | 1 - components/provisioning/include/provisioning.hpp | 2 -- components/provisioning/src/provisioning.cpp | 6 ++++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/components/provisioning/example/main/provisioning_example.cpp b/components/provisioning/example/main/provisioning_example.cpp index 72f0957c3..14726b870 100644 --- a/components/provisioning/example/main/provisioning_example.cpp +++ b/components/provisioning/example/main/provisioning_example.cpp @@ -30,7 +30,6 @@ extern "C" void app_main(void) { .device_name = "Provisioning Example", .server_port = 80, .auto_shutdown_ap = false, // Keep AP running for this example - .ap_timeout = 0s, // No timeout .on_provisioned = [&logger](const std::string &ssid, const std::string &password) { logger.info("Provisioned successfully!"); diff --git a/components/provisioning/include/provisioning.hpp b/components/provisioning/include/provisioning.hpp index 63a348744..cb7dc444b 100644 --- a/components/provisioning/include/provisioning.hpp +++ b/components/provisioning/include/provisioning.hpp @@ -54,7 +54,6 @@ class Provisioning : public BaseComponent { 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 - std::chrono::seconds ap_timeout{300}; ///< AP timeout (0 = no timeout) provisioned_callback on_provisioned{nullptr}; ///< Called on successful provisioning Logger::Verbosity log_level{Logger::Verbosity::WARN}; ///< Log verbosity }; @@ -148,6 +147,5 @@ class Provisioning : public BaseComponent { std::atomic is_completed_{false}; std::string provisioned_ssid_; std::string provisioned_password_; - std::chrono::time_point ap_start_time_; }; } // namespace espp diff --git a/components/provisioning/src/provisioning.cpp b/components/provisioning/src/provisioning.cpp index 150bcffca..eb87aa60f 100644 --- a/components/provisioning/src/provisioning.cpp +++ b/components/provisioning/src/provisioning.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "esp_wifi.h" @@ -286,7 +287,6 @@ bool Provisioning::start() { } is_active_ = true; - ap_start_time_ = std::chrono::steady_clock::now(); logger_.info("Provisioning started at http://{}", get_ip_address()); return true; } @@ -372,7 +372,9 @@ void Provisioning::stop_server() { void Provisioning::stop_ap() { wifi_ap_.reset(); } -std::string Provisioning::get_ip_address() const { return "192.168.4.1"; } +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; From 1202da21162b77fee11560f4290b19b73a4ab331 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 25 Jan 2026 01:02:30 -0600 Subject: [PATCH 10/11] address comments --- components/provisioning/src/provisioning.cpp | 102 ++++++++++++------- 1 file changed, 63 insertions(+), 39 deletions(-) diff --git a/components/provisioning/src/provisioning.cpp b/components/provisioning/src/provisioning.cpp index eb87aa60f..2e38f3572 100644 --- a/components/provisioning/src/provisioning.cpp +++ b/components/provisioning/src/provisioning.cpp @@ -4,8 +4,6 @@ #include #include -#include "esp_wifi.h" - using namespace espp; using namespace std::chrono_literals; @@ -499,34 +497,50 @@ esp_err_t Provisioning::scan_handler(httpd_req_t *req) { esp_err_t Provisioning::connect_handler(httpd_req_t *req) { auto *prov = static_cast(req->user_ctx); - char buf[512]; - int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); - if (ret <= 0) { - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid request"); + // 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; } - buf[ret] = '\0'; + + // 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 data(buf); std::string ssid, password; bool use_saved = false; - size_t ssid_pos = data.find("\"ssid\":\""); + size_t ssid_pos = body.find("\"ssid\":\""); if (ssid_pos != std::string::npos) { ssid_pos += 8; - size_t ssid_end = data.find("\"", ssid_pos); - ssid = data.substr(ssid_pos, ssid_end - ssid_pos); + size_t ssid_end = body.find("\"", ssid_pos); + ssid = body.substr(ssid_pos, ssid_end - ssid_pos); } - size_t pass_pos = data.find("\"password\":\""); + size_t pass_pos = body.find("\"password\":\""); if (pass_pos != std::string::npos) { pass_pos += 12; - size_t pass_end = data.find("\"", pass_pos); - password = data.substr(pass_pos, pass_end - pass_pos); + size_t pass_end = body.find("\"", pass_pos); + password = body.substr(pass_pos, pass_end - pass_pos); } - size_t saved_pos = data.find("\"use_saved\":true"); + size_t saved_pos = body.find("\"use_saved\":true"); if (saved_pos != std::string::npos) { use_saved = true; } @@ -577,22 +591,16 @@ esp_err_t Provisioning::complete_handler(httpd_req_t *req) { // Send response immediately httpd_resp_send(req, "OK", 2); - // Handle completion in background thread - std::thread([prov]() { - // Small delay to ensure response transmitted - std::this_thread::sleep_for(100ms); - - // Call callback with stored credentials - if (prov->config_.on_provisioned && prov->is_provisioned_) { - prov->config_.on_provisioned(prov->provisioned_ssid_, prov->provisioned_password_); - } + // 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) { - std::this_thread::sleep_for(2s); - prov->stop(); - } - }).detach(); + // 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; } @@ -617,23 +625,39 @@ esp_err_t Provisioning::saved_handler(httpd_req_t *req) { esp_err_t Provisioning::delete_handler(httpd_req_t *req) { auto *prov = static_cast(req->user_ctx); - char buf[256]; - int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); - if (ret <= 0) { - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid request"); + // 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; } - buf[ret] = '\0'; + + // 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 data(buf); std::string ssid; - size_t ssid_pos = data.find("\"ssid\":\""); + size_t ssid_pos = body.find("\"ssid\":\""); if (ssid_pos != std::string::npos) { ssid_pos += 8; - size_t ssid_end = data.find("\"", ssid_pos); - ssid = data.substr(ssid_pos, ssid_end - ssid_pos); + size_t ssid_end = body.find("\"", ssid_pos); + ssid = body.substr(ssid_pos, ssid_end - ssid_pos); } if (ssid.empty()) { From 7787b91ab6abd3e94c065916e884ffe6e1c54c52 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 25 Jan 2026 11:02:25 -0600 Subject: [PATCH 11/11] fix doc --- components/provisioning/idf_component.yml | 2 ++ doc/en/network/provisioning.rst | 18 +++++++++++++++--- doc/en/network/provisioning_example.md | 5 ++--- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/components/provisioning/idf_component.yml b/components/provisioning/idf_component.yml index fbd293fb6..aa87ef7ce 100644 --- a/components/provisioning/idf_component.yml +++ b/components/provisioning/idf_component.yml @@ -16,3 +16,5 @@ dependencies: espp/base_component: '>=1.0' espp/nvs: '>=1.0' espp/wifi: '>=1.0' +examples: + - path: example diff --git a/doc/en/network/provisioning.rst b/doc/en/network/provisioning.rst index 51851aa5c..f2ab5297c 100644 --- a/doc/en/network/provisioning.rst +++ b/doc/en/network/provisioning.rst @@ -1,8 +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 ------------- -.. doxygenfile:: provisioning.hpp - :project: espp +.. include-build-file:: inc/provisioning.inc diff --git a/doc/en/network/provisioning_example.md b/doc/en/network/provisioning_example.md index 95b0e817d..d2fbb19f2 100644 --- a/doc/en/network/provisioning_example.md +++ b/doc/en/network/provisioning_example.md @@ -1,3 +1,2 @@ -# Provisioning Example - -@example provisioning_example.cpp +```{include} ../../../components/provisioning/example/README.md +```