diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5db1612a7..4b03432f0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -153,6 +153,8 @@ jobs: target: esp32 - path: 'components/ping/example' target: esp32s3 + - path: 'components/provisioning/example' + target: esp32s3 - path: 'components/qmi8658/example' target: esp32s3 - path: 'components/qtpy/example' diff --git a/.github/workflows/upload_components.yml b/.github/workflows/upload_components.yml index 7b3307f85..a84c47a80 100644 --- a/.github/workflows/upload_components.yml +++ b/.github/workflows/upload_components.yml @@ -94,10 +94,11 @@ jobs: components/neopixel components/nvs components/odrive_ascii + components/pcf85063 components/pi4ioe5v components/pid components/ping - components/pcf85063 + components/provisioning components/qmi8658 components/qtpy components/qwiicnes diff --git a/components/provisioning/CMakeLists.txt b/components/provisioning/CMakeLists.txt new file mode 100644 index 000000000..f6edaf0b0 --- /dev/null +++ b/components/provisioning/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + INCLUDE_DIRS "include" + SRC_DIRS "src" + REQUIRES esp_http_server nvs wifi base_component +) diff --git a/components/provisioning/README.md b/components/provisioning/README.md new file mode 100644 index 000000000..fc1b932dd --- /dev/null +++ b/components/provisioning/README.md @@ -0,0 +1,26 @@ +# Provisioning Component + +[![Badge](https://components.espressif.com/components/espp/provisioning/badge.svg)](https://components.espressif.com/components/espp/provisioning) + +The `Provisioning` component provides a web-based WiFi configuration system +for ESP32 devices. It creates a temporary WiFi access point with an embedded +web server, allowing users to scan for available networks, test credentials, +and save configuration through a mobile-friendly interface. + +## Features + +- Creates temporary WiFi AP with configurable SSID and password +- Embedded web server with responsive HTML interface +- Network scanning with signal strength indication +- Manual SSID entry option for hidden networks +- Credential validation before saving (tests actual connection) +- Manages stored credentials (view, delete, reconnect) +- Automatic AP shutdown after successful provisioning +- NVS-based persistent storage +- Callbacks for provisioning events + +## Example + +The [example](./example) demonstrates how to use the `espp::Provisioning` +class to configure WiFi credentials via a web interface, with support for +scanning networks, testing connections, and managing stored credentials. diff --git a/components/provisioning/example/CMakeLists.txt b/components/provisioning/example/CMakeLists.txt new file mode 100644 index 000000000..d8042dba3 --- /dev/null +++ b/components/provisioning/example/CMakeLists.txt @@ -0,0 +1,22 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.20) + +set(ENV{IDF_COMPONENT_MANAGER} "0") +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +# add the component directories that we want to use +set(EXTRA_COMPONENT_DIRS + "../../../components/" +) + +set( + COMPONENTS + "main esptool_py provisioning wifi task" + CACHE STRING + "List of components to include" + ) + +project(provisioning_example) + +set(CMAKE_CXX_STANDARD 20) diff --git a/components/provisioning/example/README.md b/components/provisioning/example/README.md new file mode 100644 index 000000000..19d9c9f14 --- /dev/null +++ b/components/provisioning/example/README.md @@ -0,0 +1,255 @@ +# Provisioning Example + +This example demonstrates the `espp::Provisioning` component, which provides a +web-based WiFi configuration interface for ESP32 devices. + +__Scanning for network and connecting__: + + + + + + + + + + + + +
scan imageenter password image
connecting imageconnected image
provisioning completed image
+ +__Managing saved networks__: + + + + + + + + + +
view imageconnect image
connected imagedelete image
+ +## How to use example + +### Hardware Required + +This example can run on any ESP32 development board with WiFi capability. + +### Build and Flash + +Build the project and flash it to the board, then run monitor tool to view serial output: + +``` +idf.py -p PORT flash monitor +``` + +(Replace PORT with the name of the serial port to use.) + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. + +## Example Usage + +### First Time Setup + +1. Flash the example to your ESP32 device +2. The device will create a WiFi access point named `ESP-Prov-XXXX` (where XXXX is part of the MAC address) +3. Connect to this AP using your phone or computer (password: "configure") +4. Open a web browser and navigate to `http://192.168.4.1` +5. Use the web interface to: + - Click "Scan Networks" to see available WiFi networks + - Select a network from the list (or manually enter SSID) + - Enter the network password + - Click "Connect" to test the connection + - If successful, click "Complete Provisioning" to save and exit + +### Subsequent Boots + +The device stores WiFi credentials in NVS for future use. The provisioning web +interface allows you to view saved networks and reconnect to them. Note: This +example does not automatically connect to saved networks - it only demonstrates +the provisioning UI. To implement auto-connect on boot, your application should +retrieve saved credentials from NVS and use the `WifiSta` component to connect. + +## Example Output + + +Scanning for network and connecting: +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 +[Provisioning Example/I][0.415]: Initializing NVS... +[Provisioning/I][0.465]: Initialized with AP SSID: ESP-Prov-8590 +[Provisioning Example/I][0.465]: Existing provisioned network SSID: HouseOfBoo +[WifiAp/I][0.465]: Creating network interface and ensuring WiFi stack is initialized +I (475) pp: pp rom version: e7ae62f +I (475) net80211: net80211 rom version: e7ae62f +I (495) wifi:wifi driver task: 3fcedc7c, prio:23, stack:6656, core=0 +I (505) wifi:wifi firmware version: ee91c8c +I (505) wifi:wifi certification version: v7.0 +I (505) wifi:config NVS flash: enabled +I (505) wifi:config nano formatting: disabled +I (505) wifi:Init data frame dynamic rx buffer num: 32 +I (515) wifi:Init static rx mgmt buffer num: 5 +I (515) wifi:Init management short buffer num: 32 +I (525) wifi:Init dynamic tx buffer num: 32 +I (525) wifi:Init static tx FG buffer num: 2 +I (535) wifi:Init static rx buffer size: 1600 +I (535) wifi:Init static rx buffer num: 10 +I (535) wifi:Init dynamic rx buffer num: 32 +I (545) wifi_init: rx ba win: 6 +I (545) wifi_init: accept mbox: 6 +I (545) wifi_init: tcpip mbox: 32 +I (555) wifi_init: udp mbox: 6 +I (555) wifi_init: tcp mbox: 6 +I (555) wifi_init: tcp tx win: 5760 +I (565) wifi_init: tcp rx win: 5760 +I (565) wifi_init: tcp mss: 1440 +I (565) wifi_init: WiFi IRAM OP enabled +I (575) wifi_init: WiFi RX IRAM OP enabled +[WifiAp/I][0.575]: Reconfiguring WiFi AP +[WifiAp/I][0.575]: WiFi AP stopped +I (585) phy_init: phy_version 711,97bcf0a2,Aug 25 2025,19:04:10 +I (625) wifi:mode : softAP (f4:12:fa:5a:85:91) +I (625) wifi:Total power save buffer number: 16 +I (625) wifi:Init max length of beacon: 752/752 +I (625) wifi:Init max length of beacon: 752/752 +I (635) esp_netif_lwip: DHCP server started on interface WIFI_AP_DEF with IP: 192.168.4.1 +[WifiAp/I][0.635]: WiFi AP started +[WifiAp/I][0.645]: WiFi AP started +[WifiAp/I][0.645]: WiFi AP started, SSID: 'ESP-Prov-8590' +[Provisioning/I][0.655]: Provisioning started at http://192.168.4.1 +[Provisioning Example/I][0.655]: Provisioning started +[Provisioning Example/I][0.665]: Connect to WiFi network: ESP-Prov-8590 +[Provisioning Example/I][0.675]: Open browser to: http://192.168.4.1 +I (7265) wifi:new:<1,0>, old:<1,1>, ap:<1,1>, sta:<255,255>, prof:1, snd_ch_cfg:0x0 +I (7265) wifi:station: a2:51:f2:06:a8:34 join, AID=1, bgn, 20 +[WifiAp/I][7.265]: Station join, AID=1 +I (7475) wifi:idx:2 (ifx:1, a2:51:f2:06:a8:34), tid:0, ssn:643, winSize:64 +I (7695) esp_netif_lwip: DHCP server assigned IP to a client, IP is: 192.168.4.2 +I (7935) wifi:idx:3 (ifx:1, a2:51:f2:06:a8:34), tid:6, ssn:2922, winSize:64 +[WifiAp/I][15.025]: Station leave, AID=1 +I (15035) wifi:idx:2, tid:0 +I (15035) wifi:idx:3, tid:6 +I (15035) wifi:new:<1,0>, old:<1,0>, ap:<1,1>, sta:<255,255>, prof:1, snd_ch_cfg:0x0 +I (15035) wifi:station: a2:51:f2:06:a8:34 join, AID=1, bgn, 20 +[WifiAp/I][15.045]: Station join, AID=1 +I (15205) wifi:idx:2 (ifx:1, a2:51:f2:06:a8:34), tid:0, ssn:2714, winSize:64 +I (15345) esp_netif_lwip: DHCP server assigned IP to a client, IP is: 192.168.4.2 +I (15655) wifi:idx:3 (ifx:1, a2:51:f2:06:a8:34), tid:6, ssn:3177, winSize:64 +[Provisioning/I][17.785]: Starting WiFi scan... +I (17795) wifi:mode : sta (f4:12:fa:5a:85:90) + softAP (f4:12:fa:5a:85:91) +I (17795) wifi:enable tsf +I (20705) wifi:mode : softAP (f4:12:fa:5a:85:91) +[Provisioning/I][20.715]: Found 3 networks +[Provisioning/I][34.945]: Testing connection to: HouseOfBoo +[Provisioning/I][34.955]: Creating test WiFi STA instance +[WifiSta/I][34.955]: Creating network interface and ensuring WiFi stack is initialized +[WifiSta/D][34.965]: Adding event handler for WIFI_EVENT(s) +[WifiSta/D][34.965]: Adding IP event handler for IP_EVENT_STA_GOT_IP +[WifiSta/I][34.975]: Reconfiguring WiFi STA +[WifiSta/D][34.975]: Setting WiFi SSID to 'HouseOfBoo' +[WifiSta/D][34.985]: AP mode already active, setting mode to APSTA +[WifiSta/D][34.995]: Setting WiFi mode to WIFI_MODE_APSTA +I (35005) wifi:mode : sta (f4:12:fa:5a:85:90) + softAP (f4:12:fa:5a:85:91) +I (35005) wifi:enable tsf +[WifiSta/D][35.025]: Setting WiFi config +W (35025) wifi:Password length matches WPA2 standards, authmode threshold changes from OPEN to WPA2 +[WifiSta/D][35.025]: WIFI_EVENT_STA_START - initiating connection +[WifiSta/D][35.035]: Setting WiFi PHY rate to MCS0_LGI (6.5-13.5 Mbps) +I (35045) wifi:primary chan differ, old=1, new=9, start CSA timer +[WifiSta/I][35.045]: WiFi STA reconfigured successfully +[WifiSta/D][35.055]: Starting WiFi +[WifiSta/I][35.055]: WiFi started +[WifiSta/I][35.065]: WiFi STA started, SSID: 'HouseOfBoo' +[Provisioning/I][35.065]: Waiting for connection (max 15 seconds)... +I (35445) wifi:switch to channel 9 +I (35445) wifi:ap channel adjust o:1,1 n:9,2 +I (35445) wifi:new:<9,0>, old:<1,0>, ap:<9,2>, sta:<0,0>, prof:1, snd_ch_cfg:0x0 +I (35455) wifi:new:<9,2>, old:<9,0>, ap:<9,2>, sta:<9,0>, prof:1, snd_ch_cfg:0x0 +I (35455) wifi:state: init -> auth (0xb0) +I (35475) wifi:state: auth -> assoc (0x0) +I (35495) wifi:state: assoc -> run (0x10) +I (35525) wifi:connected with HouseOfBoo, aid = 7, channel 9, BW20, bssid = 08:02:8e:8a:54:73 +I (35525) wifi:security: WPA2-PSK, phy: bgn, rssi: -55 +I (35525) wifi:pm start, type: 1 + +I (35525) wifi:dp: 1, bi: 102400, li: 3, scale listen interval from 307200 us to 307200 us +I (35535) wifi:set rx beacon pti, rx_bcn_pti: 0, bcn_timeout: 25000, mt_pti: 0, mt_time: 10000 +I (35545) wifi:dp: 2, bi: 102400, li: 4, scale listen interval from 307200 us to 409600 us +I (35555) wifi:AP's beacon interval = 102400 us, DTIM period = 2 +[WifiSta/D][35.565]: WIFI_EVENT_STA_CONNECTED - waiting for IP +I (36905) wifi:idx:0 (ifx:0, 08:02:8e:8a:54:73), tid:6, ssn:4, winSize:64 +I (37905) esp_netif_handlers: sta ip: 192.168.1.25, mask: 255.255.255.0, gw: 192.168.1.1 +[WifiSta/I][37.905]: got ip: 192.168.1.25 +[Provisioning/I][37.905]: Connection callback - connected to AP +[Provisioning/I][37.905]: Got IP callback - 192.168.1.25 +[Provisioning/I][38.015]: Connection test result: true (got_ip=true, failed=false) +[Provisioning/I][38.015]: Cleaning up test STA +[WifiSta/D][38.015]: Destroying WiFiSta +[WifiSta/D][38.015]: Unregistering event handlers +[WifiSta/D][38.025]: Unregistering any wifi event handler +[WifiSta/D][38.035]: Unregistering got ip event handler +[WifiSta/D][38.035]: In APSTA mode, disconnecting STA interface +I (38045) wifi:state: run -> init (0x0) +I (38065) wifi:pm stop, total sleep time: 1777943 us / 2533745 us + +I (38065) wifi:idx:0, tid:6 +I (38065) wifi:new:<9,0>, old:<9,2>, ap:<9,2>, sta:<9,0>, prof:1, snd_ch_cfg:0x0 +I (38065) wifi:mode : softAP (f4:12:fa:5a:85:91) +[WifiSta/I][38.075]: WiFi STA disconnected, AP still running +[Provisioning/I][38.285]: Saving credentials for: HouseOfBoo +[Provisioning Example/I][40.965]: Provisioned successfully! +[Provisioning Example/I][40.965]: SSID: HouseOfBoo +[Provisioning Example/I][40.965]: Password: ******** +[Provisioning Example/I][40.965]: Credentials saved to NVS +[Provisioning Example/I][41.085]: Provisioning completed by user, stopping service +I (41185) wifi:station: a2:51:f2:06:a8:34 leave, AID = 1, reason = 2, bss_flags is 33721443, bss:0x3fcb5a9c +I (41185) wifi:new:<9,0>, old:<9,0>, ap:<9,2>, sta:<255,255>, prof:1, snd_ch_cfg:0x0 +I (41185) wifi:idx:2, tid:0 +I (41195) wifi:idx:3, tid:6 +I (41195) wifi:new:<9,2>, old:<9,0>, ap:<9,2>, sta:<255,255>, prof:1, snd_ch_cfg:0x0 +I (41255) wifi:flush txq +I (41255) wifi:stop sw txq +I (41255) wifi:lmac stop hw txq +[WifiAp/I][41.255]: WiFi AP stopped +[Provisioning/I][41.255]: Provisioning stopped +[Provisioning Example/I][41.255]: Provisioning example finished +``` + +## Configuration + +The example can be configured through `idf.py menuconfig` under "Provisioning Example Configuration": + +- **AP SSID**: Name of the provisioning access point (default: "ESP-Prov") +- **AP Password**: Password for the provisioning AP (default: "configure") +- **Device Name**: Friendly name shown in web interface +- **Auto-generate unique SSID**: Append MAC address to SSID to avoid conflicts +- **Auto-shutdown AP**: Automatically stop AP after successful provisioning +- **Web Server Port**: HTTP server port (default: 80) + +## Web Interface Features + +The embedded web interface provides: + +- **Network Scanner**: Shows available WiFi networks with signal strength indicators +- **Manual Entry**: Option to manually enter SSID for hidden networks +- **Connection Testing**: Validates credentials before saving +- **Credential Management**: View, delete, and reconnect to stored networks +- **Responsive Design**: Works on mobile phones, tablets, and desktop browsers +- **Real-time Feedback**: Shows connection status and error messages diff --git a/components/provisioning/example/main/CMakeLists.txt b/components/provisioning/example/main/CMakeLists.txt new file mode 100644 index 000000000..a941e22ba --- /dev/null +++ b/components/provisioning/example/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRC_DIRS "." + INCLUDE_DIRS ".") diff --git a/components/provisioning/example/main/provisioning_example.cpp b/components/provisioning/example/main/provisioning_example.cpp new file mode 100644 index 000000000..14726b870 --- /dev/null +++ b/components/provisioning/example/main/provisioning_example.cpp @@ -0,0 +1,94 @@ +#include +#include + +#include "logger.hpp" +#include "provisioning.hpp" + +using namespace std::chrono_literals; + +extern "C" void app_main(void) { + //! [provisioning example] + espp::Logger logger({.tag = "Provisioning Example", .level = espp::Logger::Verbosity::INFO}); + + logger.info("Starting WiFi Provisioning Example"); + + // Initialize NVS (required for WiFi) + logger.info("Initializing NVS..."); + std::error_code ec; + espp::Nvs nvs; + nvs.init(ec); + if (ec) { + logger.error("Failed to initialize NVS: {}", ec.message()); + return; + } + + // Create provisioning configuration + espp::Provisioning::Config config{ + .ap_ssid = "ESP-Prov", + .append_mac_to_ssid = true, // Will append MAC to make unique + .ap_password = "", // Open network for simplicity + .device_name = "Provisioning Example", + .server_port = 80, + .auto_shutdown_ap = false, // Keep AP running for this example + .on_provisioned = + [&logger](const std::string &ssid, const std::string &password) { + logger.info("Provisioned successfully!"); + logger.info(" SSID: {}", ssid); + logger.info(" Password: {}", password.empty() ? "(none)" : "********"); + + // Save credentials to NVS + std::error_code ec; + espp::NvsHandle handle("wifi_config", ec); + if (!ec) { + handle.set("ssid", ssid, ec); + handle.set("password", password, ec); + if (!ec) { + logger.info("Credentials saved to NVS"); + } else { + logger.error("Failed to save credentials: {}", ec.message()); + } + } + }, + .log_level = espp::Logger::Verbosity::INFO}; + + // Create provisioning + espp::Provisioning provisioning(config); + + // print any existing network ssids that have been provisioned + { + espp::NvsHandle handle("wifi_config", ec); + if (!ec) { + std::string saved_ssid; + handle.get("ssid", saved_ssid, ec); + if (!ec) { + logger.info("Existing provisioned network SSID: {}", saved_ssid); + } else { + logger.info("No existing provisioned network found in NVS"); + } + } + } + + // Start provisioning + if (!provisioning.start()) { + logger.error("Failed to start provisioning"); + return; + } + + logger.info("Provisioning started"); + logger.info("Connect to WiFi network: {}", provisioning.get_ap_ssid()); + logger.info("Open browser to: http://192.168.4.1"); + + // Keep running until user completes provisioning + while (!provisioning.is_completed()) { + std::this_thread::sleep_for(1s); + } + + logger.info("Provisioning completed by user, stopping service"); + provisioning.stop(); + + logger.info("Provisioning example finished"); + //! [provisioning example] + while (true) { + std::this_thread::sleep_for(1s); + } +} diff --git a/components/provisioning/example/partitions.csv b/components/provisioning/example/partitions.csv new file mode 100644 index 000000000..c4217ab9e --- /dev/null +++ b/components/provisioning/example/partitions.csv @@ -0,0 +1,5 @@ +# ESP-IDF Partition Table +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 1500K, diff --git a/components/provisioning/example/sdkconfig.defaults b/components/provisioning/example/sdkconfig.defaults new file mode 100644 index 000000000..e20c8f577 --- /dev/null +++ b/components/provisioning/example/sdkconfig.defaults @@ -0,0 +1,20 @@ +# Flash size +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_ESPTOOLPY_FLASHSIZE="4MB" + +# CONFIG_ESP32_WIFI_NVS_ENABLED=n + +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_OFFSET=0x8000 +CONFIG_PARTITION_TABLE_MD5=y + +# Common ESP-related +# +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 + +# HTTP Server +CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024 +CONFIG_HTTPD_MAX_URI_LEN=512 diff --git a/components/provisioning/idf_component.yml b/components/provisioning/idf_component.yml new file mode 100644 index 000000000..aa87ef7ce --- /dev/null +++ b/components/provisioning/idf_component.yml @@ -0,0 +1,20 @@ +## IDF Component Manager Manifest File +license: "MIT" +description: "WiFi provisioning component with web interface" +url: "https://github.com/esp-cpp/espp/tree/main/components/provisioning" +repository: "git://github.com/esp-cpp/espp.git" +maintainers: + - William Emfinger +documentation: "https://esp-cpp.github.io/espp/network/provisioning.html" +tags: + - cpp + - WiFi + - Provisioning +dependencies: + idf: + version: '>=5.0' + espp/base_component: '>=1.0' + espp/nvs: '>=1.0' + espp/wifi: '>=1.0' +examples: + - path: example diff --git a/components/provisioning/include/provisioning.hpp b/components/provisioning/include/provisioning.hpp new file mode 100644 index 000000000..cb7dc444b --- /dev/null +++ b/components/provisioning/include/provisioning.hpp @@ -0,0 +1,151 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "base_component.hpp" +#include "nvs.hpp" +#include "wifi.hpp" +#include "wifi_ap.hpp" +#include "wifi_sta.hpp" + +#include "esp_http_server.h" + +namespace espp { + +/** + * @brief WiFi Provisioning Component + * + * Provides a web-based WiFi provisioning interface. Creates a WiFi access point + * with an embedded web server that allows users to scan for and connect to WiFi networks. + * + * Features: + * - Automatic AP creation with customizable SSID/password + * - HTML web interface for WiFi scanning and configuration + * - Network scanning and signal strength display + * - Credential validation before saving + * - Callback notification on successful provisioning + * - Automatic AP shutdown after provisioning (optional) + * + * \section provisioning_ex1 Provisioning Example + * \snippet provisioning_example.cpp provisioning example + */ +class Provisioning : public BaseComponent { +public: + /** + * @brief Callback when provisioning completes successfully + * @param ssid The SSID that was provisioned + * @param password The password that was configured + */ + typedef std::function + provisioned_callback; + + /** + * @brief Configuration for the provisioning component + */ + struct Config { + std::string ap_ssid{"ESP-Prov"}; ///< Base SSID for the provisioning AP + bool append_mac_to_ssid{true}; ///< Append MAC address to AP SSID + std::string ap_password{""}; ///< Password for AP (empty = open) + std::string device_name{"ESP32 Device"}; ///< Device name shown in UI + uint16_t server_port{80}; ///< HTTP server port + bool auto_shutdown_ap{true}; ///< Shutdown AP after provisioning + provisioned_callback on_provisioned{nullptr}; ///< Called on successful provisioning + Logger::Verbosity log_level{Logger::Verbosity::WARN}; ///< Log verbosity + }; + + /** + * @brief Construct provisioning component + * @param config Configuration structure + */ + explicit Provisioning(const Config &config); + + /** + * @brief Destructor - stops server and AP + */ + ~Provisioning(); + + /** + * @brief Start the provisioning process + * @return true if started successfully + */ + bool start(); + + /** + * @brief Stop the provisioning process + */ + void stop(); + + /** + * @brief Check if provisioning is active + * @return true if the AP and server are running + */ + bool is_active() const { return is_active_; } + + /** + * @brief Check if device has been provisioned + * @return true if provisioning callback has been called + */ + bool is_provisioned() const { return is_provisioned_; } + + /** + * @brief Check if user has completed provisioning + * @return true if user clicked "Complete Setup" button + */ + bool is_completed() const { return is_completed_; } + + /** + * @brief Get the IP address of the provisioning AP + * @return IP address string (e.g., "192.168.4.1") + */ + std::string get_ip_address() const; + + /** + * @brief Get the actual AP SSID (after MAC appending) + * @return The SSID of the provisioning AP + */ + const std::string &get_ap_ssid() const { return config_.ap_ssid; } + +protected: + void init(const Config &config); + bool start_ap(); + bool start_server(); + void stop_server(); + void stop_ap(); + + // HTTP handlers + static esp_err_t root_handler(httpd_req_t *req); + static esp_err_t scan_handler(httpd_req_t *req); + static esp_err_t connect_handler(httpd_req_t *req); + static esp_err_t complete_handler(httpd_req_t *req); + static esp_err_t status_handler(httpd_req_t *req); + static esp_err_t saved_handler(httpd_req_t *req); + static esp_err_t delete_handler(httpd_req_t *req); + + // Helper methods + std::string generate_html() const; + std::string scan_networks(); + bool test_connection(const std::string &ssid, const std::string &password); + + // Credential storage methods + std::vector get_saved_ssids(); + std::string get_saved_password(const std::string &ssid); + void save_credentials(const std::string &ssid, const std::string &password); + void delete_credentials(const std::string &ssid); + std::string get_saved_networks_json(); + + Config config_; + std::unique_ptr wifi_ap_; + std::unique_ptr test_sta_; + httpd_handle_t server_{nullptr}; + std::atomic is_active_{false}; + std::atomic is_provisioned_{false}; + std::atomic is_completed_{false}; + std::string provisioned_ssid_; + std::string provisioned_password_; +}; +} // namespace espp diff --git a/components/provisioning/src/provisioning.cpp b/components/provisioning/src/provisioning.cpp new file mode 100644 index 000000000..2e38f3572 --- /dev/null +++ b/components/provisioning/src/provisioning.cpp @@ -0,0 +1,852 @@ +#include "provisioning.hpp" + +#include +#include +#include + +using namespace espp; +using namespace std::chrono_literals; + +// Helper function to escape JSON strings +static std::string json_escape(const std::string &str) { + std::string escaped; + escaped.reserve(str.size()); + for (char c : str) { + switch (c) { + case '"': + escaped += "\\\""; + break; + case '\\': + escaped += "\\\\"; + break; + case '\b': + escaped += "\\b"; + break; + case '\f': + escaped += "\\f"; + break; + case '\n': + escaped += "\\n"; + break; + case '\r': + escaped += "\\r"; + break; + case '\t': + escaped += "\\t"; + break; + default: + if (c < 0x20) { + // Control characters - escape as \uXXXX + char buf[7]; + snprintf(buf, sizeof(buf), "\\u%04x", static_cast(c)); + escaped += buf; + } else { + escaped += c; + } + } + } + return escaped; +} + +static const char *HTML_TEMPLATE = R"HTML( + + + + + WiFi Setup + + + +
+

WiFi Setup

+
%DEVICE_NAME%
+ + + + + + + +
+ +
+
+ +
+ +
― OR ―
+ + +
+

Manual Network Entry

+ + + +
+ + + +
+ + + +)HTML"; + +Provisioning::Provisioning(const Config &config) + : BaseComponent("Provisioning", config.log_level) { + init(config); +} + +Provisioning::~Provisioning() { stop(); } + +void Provisioning::init(const Config &config) { + config_ = config; + + // Append MAC address to SSID if requested + if (config_.append_mac_to_ssid) { + uint8_t mac[6]; + esp_efuse_mac_get_default(mac); + config_.ap_ssid = fmt::format("{}-{:02X}{:02X}", config_.ap_ssid, mac[4], mac[5]); + } + + logger_.info("Initialized with AP SSID: {}", config_.ap_ssid); +} + +bool Provisioning::start() { + if (is_active_) { + logger_.warn("Provisioning already active"); + return false; + } + + if (!start_ap()) { + logger_.error("Failed to start AP"); + return false; + } + + if (!start_server()) { + logger_.error("Failed to start HTTP server"); + stop_ap(); + return false; + } + + is_active_ = true; + logger_.info("Provisioning started at http://{}", get_ip_address()); + return true; +} + +void Provisioning::stop() { + if (!is_active_) { + return; + } + + stop_server(); + stop_ap(); + is_active_ = false; + logger_.info("Provisioning stopped"); +} + +bool Provisioning::start_ap() { + WifiAp::Config ap_config{.ssid = config_.ap_ssid, + .password = config_.ap_password, + .max_number_of_stations = 4, + .log_level = config_.log_level}; + + wifi_ap_ = std::make_unique(ap_config); + + // WiFi mode is managed by WifiAp and WifiSta classes + return wifi_ap_ != nullptr; +} + +bool Provisioning::start_server() { + httpd_config_t server_config = HTTPD_DEFAULT_CONFIG(); + server_config.server_port = config_.server_port; + server_config.max_uri_handlers = 8; + server_config.lru_purge_enable = true; + server_config.stack_size = 8192; + + if (httpd_start(&server_, &server_config) != ESP_OK) { + logger_.error("Failed to start HTTP server"); + return false; + } + + // Root handler + httpd_uri_t root = {.uri = "/", .method = HTTP_GET, .handler = root_handler, .user_ctx = this}; + httpd_register_uri_handler(server_, &root); + + // Scan handler + httpd_uri_t scan = { + .uri = "/scan", .method = HTTP_GET, .handler = scan_handler, .user_ctx = this}; + httpd_register_uri_handler(server_, &scan); + + // Connect handler + httpd_uri_t connect = { + .uri = "/connect", .method = HTTP_POST, .handler = connect_handler, .user_ctx = this}; + httpd_register_uri_handler(server_, &connect); + + // Complete handler + httpd_uri_t complete = { + .uri = "/complete", .method = HTTP_POST, .handler = complete_handler, .user_ctx = this}; + httpd_register_uri_handler(server_, &complete); + + // Status handler + httpd_uri_t status = { + .uri = "/status", .method = HTTP_GET, .handler = status_handler, .user_ctx = this}; + httpd_register_uri_handler(server_, &status); + + // Saved networks handler + httpd_uri_t saved = { + .uri = "/saved", .method = HTTP_GET, .handler = saved_handler, .user_ctx = this}; + httpd_register_uri_handler(server_, &saved); + + // Delete handler + httpd_uri_t delete_net = { + .uri = "/delete", .method = HTTP_POST, .handler = delete_handler, .user_ctx = this}; + httpd_register_uri_handler(server_, &delete_net); + + return true; +} + +void Provisioning::stop_server() { + if (server_) { + httpd_stop(server_); + server_ = nullptr; + } +} + +void Provisioning::stop_ap() { wifi_ap_.reset(); } + +std::string Provisioning::get_ip_address() const { + return wifi_ap_ ? wifi_ap_->get_ip_address() : "N/A"; +} + +std::string Provisioning::generate_html() const { + std::string html = HTML_TEMPLATE; + size_t pos = html.find("%DEVICE_NAME%"); + if (pos != std::string::npos) { + html.replace(pos, 13, config_.device_name); + } + return html; +} + +std::string Provisioning::scan_networks() { + logger_.info("Starting WiFi scan..."); + + // Create a temporary WifiSta for scanning (auto_connect=false so it doesn't try to connect) + WifiSta::Config scan_config{ + .ssid = "", .password = "", .auto_connect = false, .log_level = Logger::Verbosity::WARN}; + WifiSta temp_sta(scan_config); + + auto ap_records = temp_sta.scan(20); + + if (ap_records.empty()) { + logger_.info("No networks found"); + return R"({"networks":[]})"; + } + + logger_.info("Found {} networks", ap_records.size()); + + // Sort by signal strength + std::sort(ap_records.begin(), ap_records.end(), + [](const wifi_ap_record_t &a, const wifi_ap_record_t &b) { return a.rssi > b.rssi; }); + + std::string json = R"({"networks":[)"; + for (size_t i = 0; i < ap_records.size(); i++) { + if (i > 0) + json += ","; + std::string ssid = reinterpret_cast(ap_records[i].ssid); + json += fmt::format(R"({{"ssid":"{}","rssi":{},"secure":{}}})", json_escape(ssid), + ap_records[i].rssi, ap_records[i].authmode != WIFI_AUTH_OPEN); + } + json += "]}"; + return json; +} + +bool Provisioning::test_connection(const std::string &ssid, const std::string &password) { + logger_.info("Testing connection to: {}", ssid); + + // Clean up any existing test STA first + if (test_sta_) { + logger_.info("Cleaning up previous test STA"); + test_sta_.reset(); + std::this_thread::sleep_for(500ms); // Give WiFi stack time to clean up + } + + std::atomic connected{false}; + std::atomic got_ip{false}; + std::atomic failed{false}; + + WifiSta::Config sta_config{.ssid = ssid, + .password = password, + .num_connect_retries = 5, + .auto_connect = true, // Auto-connect for testing + .on_connected = + [&]() { + logger_.info("Connection callback - connected to AP"); + connected = true; + }, + .on_disconnected = + [&]() { + logger_.info("Disconnection callback - failed to connect"); + failed = true; + }, + .on_got_ip = + [&](ip_event_got_ip_t *event) { + logger_.info("Got IP callback - {}.{}.{}.{}", + IP2STR(&event->ip_info.ip)); + got_ip = true; + }, + .log_level = Logger::Verbosity::ERROR}; + + logger_.info("Creating test WiFi STA instance"); + test_sta_ = std::make_unique(sta_config); + + logger_.info("Waiting for connection (max 15 seconds)..."); + // Wait for connection (max 15 seconds) + auto start = std::chrono::steady_clock::now(); + while (!got_ip && !failed && (std::chrono::steady_clock::now() - start) < 15s) { + std::this_thread::sleep_for(100ms); + } + + bool success = got_ip; + + logger_.info("Connection test result: {} (got_ip={}, failed={})", success, got_ip.load(), + failed.load()); + + // Clean up test STA immediately to avoid interfering with AP + logger_.info("Cleaning up test STA"); + test_sta_.reset(); + + // Give WiFi stack time to stabilize + std::this_thread::sleep_for(200ms); + + return success; +} + +// HTTP Handlers +esp_err_t Provisioning::root_handler(httpd_req_t *req) { + auto *prov = static_cast(req->user_ctx); + std::string html = prov->generate_html(); + httpd_resp_set_type(req, "text/html"); + httpd_resp_send(req, html.c_str(), html.length()); + return ESP_OK; +} + +esp_err_t Provisioning::scan_handler(httpd_req_t *req) { + auto *prov = static_cast(req->user_ctx); + std::string json = prov->scan_networks(); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, json.c_str(), json.length()); + return ESP_OK; +} + +esp_err_t Provisioning::connect_handler(httpd_req_t *req) { + auto *prov = static_cast(req->user_ctx); + + // Check content length + size_t content_len = req->content_len; + if (content_len == 0 || content_len > 2048) { + httpd_resp_send_err(req, + content_len > 2048 ? HTTPD_413_CONTENT_TOO_LARGE : HTTPD_400_BAD_REQUEST, + "Invalid request size"); + return ESP_FAIL; + } + + // Read full request body + std::string body; + body.resize(content_len); + size_t total_read = 0; + while (total_read < content_len) { + int ret = httpd_req_recv(req, &body[total_read], content_len - total_read); + if (ret <= 0) { + if (ret == HTTPD_SOCK_ERR_TIMEOUT) { + continue; + } + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Failed to read request"); + return ESP_FAIL; + } + total_read += ret; + } + + // Parse JSON manually (simple approach) + std::string ssid, password; + bool use_saved = false; + + size_t ssid_pos = body.find("\"ssid\":\""); + if (ssid_pos != std::string::npos) { + ssid_pos += 8; + size_t ssid_end = body.find("\"", ssid_pos); + ssid = body.substr(ssid_pos, ssid_end - ssid_pos); + } + + size_t pass_pos = body.find("\"password\":\""); + if (pass_pos != std::string::npos) { + pass_pos += 12; + size_t pass_end = body.find("\"", pass_pos); + password = body.substr(pass_pos, pass_end - pass_pos); + } + + size_t saved_pos = body.find("\"use_saved\":true"); + if (saved_pos != std::string::npos) { + use_saved = true; + } + + if (ssid.empty()) { + std::string response = R"({"success":false,"message":"Invalid SSID"})"; + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, response.c_str(), response.length()); + return ESP_OK; + } + + // If using saved credentials, retrieve password + if (use_saved) { + password = prov->get_saved_password(ssid); + } + + // Test connection + bool success = prov->test_connection(ssid, password); + + std::string response; + if (success) { + response = R"({"success":true})"; + prov->is_provisioned_ = true; + prov->provisioned_ssid_ = ssid; + prov->provisioned_password_ = password; + + // Save credentials if not already saved + if (!use_saved) { + prov->save_credentials(ssid, password); + } + } else { + response = R"({"success":false,"message":"Failed to connect"})"; + } + + // Send response - don't stop AP or call callback yet + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, response.c_str(), response.length()); + + return ESP_OK; +} + +esp_err_t Provisioning::complete_handler(httpd_req_t *req) { + auto *prov = static_cast(req->user_ctx); + + // Mark as completed + prov->is_completed_ = true; + + // Send response immediately + httpd_resp_send(req, "OK", 2); + + // Call callback directly with stored credentials + if (prov->config_.on_provisioned && prov->is_provisioned_) { + prov->config_.on_provisioned(prov->provisioned_ssid_, prov->provisioned_password_); + } + + // Auto-shutdown if configured + if (prov->config_.auto_shutdown_ap) { + // Note: Stopping AP while in HTTP handler is safe - response is already sent + prov->stop(); + } + + return ESP_OK; +} + +esp_err_t Provisioning::status_handler(httpd_req_t *req) { + auto *prov = static_cast(req->user_ctx); + std::string response = + fmt::format(R"({{"active":{},"provisioned":{}}})", prov->is_active_, prov->is_provisioned_); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, response.c_str(), response.length()); + return ESP_OK; +} + +esp_err_t Provisioning::saved_handler(httpd_req_t *req) { + auto *prov = static_cast(req->user_ctx); + std::string json = prov->get_saved_networks_json(); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, json.c_str(), json.length()); + return ESP_OK; +} + +esp_err_t Provisioning::delete_handler(httpd_req_t *req) { + auto *prov = static_cast(req->user_ctx); + + // Check content length + size_t content_len = req->content_len; + if (content_len == 0 || content_len > 1024) { + httpd_resp_send_err(req, + content_len > 1024 ? HTTPD_413_CONTENT_TOO_LARGE : HTTPD_400_BAD_REQUEST, + "Invalid request size"); + return ESP_FAIL; + } + + // Read full request body + std::string body; + body.resize(content_len); + size_t total_read = 0; + while (total_read < content_len) { + int ret = httpd_req_recv(req, &body[total_read], content_len - total_read); + if (ret <= 0) { + if (ret == HTTPD_SOCK_ERR_TIMEOUT) { + continue; + } + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Failed to read request"); + return ESP_FAIL; + } + total_read += ret; + } + + // Parse SSID from JSON + std::string ssid; + + size_t ssid_pos = body.find("\"ssid\":\""); + if (ssid_pos != std::string::npos) { + ssid_pos += 8; + size_t ssid_end = body.find("\"", ssid_pos); + ssid = body.substr(ssid_pos, ssid_end - ssid_pos); + } + + if (ssid.empty()) { + std::string response = R"({"success":false})"; + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, response.c_str(), response.length()); + return ESP_OK; + } + + prov->delete_credentials(ssid); + + std::string response = R"({"success":true})"; + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, response.c_str(), response.length()); + return ESP_OK; +} + +// Credential storage methods +std::vector Provisioning::get_saved_ssids() { + std::vector ssids; + + std::error_code ec; + espp::NvsHandle nvs("wifi_creds", ec); + if (ec) { + return ssids; + } + + // Get count of saved networks + uint32_t count = 0; + nvs.get("count", count, ec); + if (ec || count == 0) { + return ssids; + } + + // Load each SSID by index + for (uint32_t i = 0; i < count; i++) { + std::string ssid_key = fmt::format("ssid_{}", i); + std::string ssid; + nvs.get(ssid_key, ssid, ec); + if (!ec && !ssid.empty()) { + // Remove any trailing null bytes + ssid.erase(std::find(ssid.begin(), ssid.end(), '\0'), ssid.end()); + if (!ssid.empty()) { + ssids.push_back(ssid); + } + } + } + + return ssids; +} + +std::string Provisioning::get_saved_password(const std::string &ssid) { + std::error_code ec; + espp::NvsHandle nvs("wifi_creds", ec); + if (ec) { + return ""; + } + + // Find the index for this SSID + auto ssids = get_saved_ssids(); + auto it = std::find(ssids.begin(), ssids.end(), ssid); + if (it == ssids.end()) { + return ""; + } + + size_t index = std::distance(ssids.begin(), it); + std::string pwd_key = fmt::format("pwd_{}", index); + std::string password; + nvs.get(pwd_key, password, ec); + + if (!ec) { + // Remove any trailing null bytes + password.erase(std::find(password.begin(), password.end(), '\0'), password.end()); + } + + return ec ? "" : password; +} + +void Provisioning::save_credentials(const std::string &ssid, const std::string &password) { + std::error_code ec; + espp::NvsHandle nvs("wifi_creds", ec); + if (ec) { + logger_.error("Failed to open NVS"); + return; + } + + logger_.info("Saving credentials for: {}", ssid); + + // Get or create index for this SSID + auto ssids = get_saved_ssids(); + size_t index = ssids.size(); + auto it = std::find(ssids.begin(), ssids.end(), ssid); + if (it != ssids.end()) { + index = std::distance(ssids.begin(), it); + } else { + ssids.push_back(ssid); + } + + // Save SSID and password using index-based keys (keeps keys under 15 chars) + std::string ssid_key = fmt::format("ssid_{}", index); + std::string pwd_key = fmt::format("pwd_{}", index); + + nvs.set(ssid_key, ssid, ec); + if (ec) { + logger_.error("Failed to save SSID"); + return; + } + + nvs.set(pwd_key, password, ec); + if (ec) { + logger_.error("Failed to save password"); + return; + } + + // Save count + nvs.set("count", static_cast(ssids.size()), ec); + if (ec) { + logger_.error("Failed to save count"); + return; + } + + nvs.commit(ec); +} + +void Provisioning::delete_credentials(const std::string &ssid) { + std::error_code ec; + espp::NvsHandle nvs("wifi_creds", ec); + if (ec) { + logger_.error("Failed to open NVS"); + return; + } + + logger_.info("Deleting credentials for: {}", ssid); + + // Get current SSIDs + auto ssids = get_saved_ssids(); + auto it = std::find(ssids.begin(), ssids.end(), ssid); + if (it == ssids.end()) { + logger_.warn("SSID not found in saved credentials"); + return; + } + + size_t index_to_delete = std::distance(ssids.begin(), it); + ssids.erase(it); + + // Rewrite all credentials with compacted indices + for (size_t i = index_to_delete; i < ssids.size(); i++) { + // Move credentials from index i+1 to index i + std::string old_ssid_key = fmt::format("ssid_{}", i + 1); + std::string old_pwd_key = fmt::format("pwd_{}", i + 1); + std::string new_ssid_key = fmt::format("ssid_{}", i); + std::string new_pwd_key = fmt::format("pwd_{}", i); + + std::string ssid_val, pwd_val; + nvs.get(old_ssid_key, ssid_val, ec); + nvs.get(old_pwd_key, pwd_val, ec); + + nvs.set(new_ssid_key, ssid_val, ec); + nvs.set(new_pwd_key, pwd_val, ec); + } + + // Erase the last entry (now duplicated) + if (!ssids.empty()) { + std::string last_ssid_key = fmt::format("ssid_{}", ssids.size()); + std::string last_pwd_key = fmt::format("pwd_{}", ssids.size()); + nvs.erase(last_ssid_key, ec); + nvs.erase(last_pwd_key, ec); + } + + // Update count + nvs.set("count", static_cast(ssids.size()), ec); + if (ec) { + logger_.error("Failed to update count"); + return; + } + + nvs.commit(ec); +} + +std::string Provisioning::get_saved_networks_json() { + auto ssids = get_saved_ssids(); + + std::string json = R"({"networks":[)"; + for (size_t i = 0; i < ssids.size(); i++) { + if (i > 0) + json += ","; + json += "\"" + json_escape(ssids[i]) + "\""; + } + json += "]}"; + + return json; +} diff --git a/components/wifi/example/main/Kconfig.projbuild b/components/wifi/example/main/Kconfig.projbuild index d68cd524b..92457705c 100644 --- a/components/wifi/example/main/Kconfig.projbuild +++ b/components/wifi/example/main/Kconfig.projbuild @@ -18,4 +18,24 @@ menu "WiFi Example Configuration" help Set the Maximum retry to avoid station reconnecting to the AP unlimited when the AP is really inexistent. + config TEST_WIFI_SCAN + bool "Test WiFi scanning" + default y + help + Enable WiFi scanning tests in the example. + + config TEST_SCAN_BEFORE_STA + bool "Test scan before STA connection" + default y + depends on TEST_WIFI_SCAN + help + Test scanning before attempting STA connection. + + config TEST_SCAN_WITH_NO_CONFIG + bool "Test scan with no WiFi config" + default y + depends on TEST_WIFI_SCAN + help + Test scanning when WiFi is not configured (no STA or AP active). + endmenu diff --git a/components/wifi/example/main/wifi_example.cpp b/components/wifi/example/main/wifi_example.cpp index aef5d8009..eb536eb05 100644 --- a/components/wifi/example/main/wifi_example.cpp +++ b/components/wifi/example/main/wifi_example.cpp @@ -1,4 +1,5 @@ #include +#include #include #include "sdkconfig.h" @@ -19,6 +20,15 @@ using namespace std::chrono_literals; +// Test result tracking +struct TestResult { + std::string name; + bool passed; + std::string details; +}; + +std::vector test_results; + extern "C" void app_main(void) { espp::Logger logger({.tag = "wifi_example", .level = espp::Logger::Verbosity::INFO}); logger.info("Starting WiFi example..."); @@ -115,6 +125,34 @@ extern "C" void app_main(void) { logger.info("WiFi AP example complete!"); } + { +#if CONFIG_TEST_WIFI_SCAN && CONFIG_TEST_SCAN_WITH_NO_CONFIG + logger.info("Starting WiFi scan test (no config)..."); + //! [wifi scan no config example] + auto &wifi = espp::Wifi::get(); + wifi.set_log_level(espp::Logger::Verbosity::DEBUG); + + // Initialize the WiFi stack + if (!wifi.init()) { + logger.error("Failed to initialize WiFi stack"); + return; + } + + // Test scanning with no active configuration + logger.info("Scanning for networks (no STA/AP active)..."); + auto scan_results = wifi.scan(10); + logger.info("Found {} networks:", scan_results.size()); + for (const auto &ap : scan_results) { + logger.info(" SSID: {}, RSSI: {}, Channel: {}", (char *)ap.ssid, ap.rssi, ap.primary); + } + + wifi.deinit(); + logger.info("WiFi scan test (no config) complete!"); + std::this_thread::sleep_for(2s); + //! [wifi scan no config example] +#endif + } + { logger.info("Starting WiFi singleton example..."); //! [wifi example] @@ -128,20 +166,72 @@ extern "C" void app_main(void) { } // Register multiple station configurations - wifi.register_sta("home", {.ssid = "", // use whatever was saved to NVS (if any) - .password = "", // use whatever was saved to NVS (if any) - .num_connect_retries = CONFIG_ESP_MAXIMUM_RETRY, - .on_got_ip = - [&](ip_event_got_ip_t *eventdata) { - logger.info("Home network - got IP: {}.{}.{}.{}", - IP2STR(&eventdata->ip_info.ip)); - }, - .log_level = espp::Logger::Verbosity::INFO}); + wifi.register_sta( + "home", + {.ssid = "", // use whatever was saved to NVS (if any) + .password = "", // use whatever was saved to NVS (if any) + .num_connect_retries = CONFIG_ESP_MAXIMUM_RETRY, + .auto_connect = + !CONFIG_TEST_SCAN_BEFORE_STA, // Don't auto-connect if we're going to scan first + .on_got_ip = + [&](ip_event_got_ip_t *eventdata) { + logger.info("Home network - got IP: {}.{}.{}.{}", IP2STR(&eventdata->ip_info.ip)); + }, + .log_level = espp::Logger::Verbosity::INFO}); // set the 'home' network to be active and ensure that the backup // registration (when added) doesn't override it wifi.switch_to_sta("home"); +#if CONFIG_TEST_WIFI_SCAN && CONFIG_TEST_SCAN_BEFORE_STA + logger.info("\n=== Testing scan before STA connection ==="); + //! [wifi scan before sta example] + + bool test_passed = false; + std::string test_details; + + // STA was created with auto_connect=false, so it won't connect automatically + // Scan for networks first + logger.info("Scanning for networks before connecting..."); + auto scan_results = wifi.scan(10); + logger.info("Found {} networks:", scan_results.size()); + for (const auto &ap : scan_results) { + logger.info(" SSID: {}, RSSI: {}, Channel: {}", (char *)ap.ssid, ap.rssi, ap.primary); + } + + // Now manually initiate the connection + logger.info("Attempting to connect to STA after scan..."); + auto *sta = wifi.get_sta(); + if (sta) { + sta->connect(); // Manually trigger connection + + int wait_count = 0; + while (!sta->is_connected() && wait_count < 150) { // 15 second timeout + std::this_thread::sleep_for(100ms); + wait_count++; + } + if (sta->is_connected()) { + logger.info("STA connected successfully after scan!"); + std::string ip; + if (wifi.get_ip_address(ip)) { + logger.info("IP address: {}", ip); + test_passed = true; + test_details = fmt::format("Connected, IP: {}", ip); + } + } else { + logger.warn("STA failed to connect after scan within timeout"); + test_details = "Connection timeout"; + } + } else { + test_details = "No STA instance"; + } + + test_results.push_back( + {.name = "Scan before STA connect", .passed = test_passed, .details = test_details}); + std::this_thread::sleep_for(2s); + //! [wifi scan before sta example] +#endif + wifi.register_sta("backup", {.ssid = "BackupNetwork", .password = "backuppassword", .num_connect_retries = CONFIG_ESP_MAXIMUM_RETRY, @@ -188,30 +278,37 @@ extern "C" void app_main(void) { // Check what's currently active std::string active_name = wifi.get_active_name(); - if (active_name != "device_ap") { - logger.error("Active interface is not 'device_ap' after switch_to_ap"); - return; - } - logger.info("Active interface: '{}' (is_ap={}, is_sta={})", active_name, wifi.is_active_ap(), - wifi.is_active_sta()); + bool ap_test_passed = (active_name == "device_ap"); + std::string ap_test_details; - // Wait for the active interface to be ready/connected - auto *active = wifi.get_active(); - if (active) { - logger.info("Waiting for '{}' to be connected...", active_name); - while (!active->is_connected()) { - std::this_thread::sleep_for(100ms); - } - logger.info("'{}' is now connected!", active_name); + if (!ap_test_passed) { + logger.error("Active interface is not 'device_ap' after switch_to_ap"); + ap_test_details = fmt::format("Wrong active: {}", active_name); + } else { + logger.info("Active interface: '{}' (is_ap={}, is_sta={})", active_name, wifi.is_active_ap(), + wifi.is_active_sta()); + + // Wait for the active interface to be ready/connected + auto *active = wifi.get_active(); + if (active) { + logger.info("Waiting for '{}' to be connected...", active_name); + while (!active->is_connected()) { + std::this_thread::sleep_for(100ms); + } + logger.info("'{}' is now connected!", active_name); - logger.info("Checking IP address..."); - // Get and display IP - std::string ip; - if (wifi.get_ip_address(ip)) { - logger.info("IP address: {}", ip); + logger.info("Checking IP address..."); + // Get and display IP + std::string ip; + if (wifi.get_ip_address(ip)) { + logger.info("IP address: {}", ip); + ap_test_details = fmt::format("AP ready, IP: {}", ip); + } } } + test_results.push_back( + {.name = "Switch to AP", .passed = ap_test_passed, .details = ap_test_details}); std::this_thread::sleep_for(num_seconds_to_run * 1s); // Access STA instance (single instance, manages all configs) @@ -230,12 +327,24 @@ extern "C" void app_main(void) { // Demonstrate switching to STA logger.info("\n=== Testing switch to STA ==="); wifi.switch_to_sta("home"); + + // If auto_connect was disabled (e.g., from scan test), manually connect + sta = wifi.get_sta(); + if (sta && !sta->is_connected()) { + logger.info("STA not auto-connected, manually connecting..."); + sta->connect(); + } + active_name = wifi.get_active_name(); logger.info("Active interface: '{}' (is_ap={}, is_sta={})", active_name, wifi.is_active_ap(), wifi.is_active_sta()); + bool sta_test_passed = false; + std::string sta_test_details; + // Wait for STA to connect - active = wifi.get_active(); + auto active = wifi.get_active(); + active_name = wifi.get_active_name(); if (active) { logger.info("Waiting for '{}' to be connected...", active_name); int wait_count = 0; @@ -248,12 +357,19 @@ extern "C" void app_main(void) { std::string ip; if (wifi.get_ip_address(ip)) { logger.info("IP address: {}", ip); + sta_test_passed = true; + sta_test_details = fmt::format("Connected, IP: {}", ip); } } else { logger.warn("'{}' failed to connect within timeout", active_name); + sta_test_details = "Connection timeout"; } + } else { + sta_test_details = "No active interface"; } + test_results.push_back( + {.name = "Switch to STA", .passed = sta_test_passed, .details = sta_test_details}); std::this_thread::sleep_for(2s); // Demonstrate switching back to AP @@ -263,6 +379,9 @@ extern "C" void app_main(void) { logger.info("Active interface: '{}' (is_ap={}, is_sta={})", active_name, wifi.is_active_ap(), wifi.is_active_sta()); + bool ap_switch_back_passed = (active_name == "device_ap"); + std::string ap_switch_back_details; + // Wait for AP to be ready active = wifi.get_active(); if (active) { @@ -277,9 +396,16 @@ extern "C" void app_main(void) { std::string ip; if (wifi.get_ip_address(ip)) { logger.info("IP address: {}", ip); + ap_switch_back_details = fmt::format("AP ready, IP: {}", ip); } + } else { + ap_switch_back_passed = false; + ap_switch_back_details = "No active interface"; } + test_results.push_back({.name = "Switch back to AP", + .passed = ap_switch_back_passed, + .details = ap_switch_back_details}); std::this_thread::sleep_for(2s); // Demonstrate stop @@ -288,6 +414,12 @@ extern "C" void app_main(void) { active_name = wifi.get_active_name(); logger.info("Active interface after stop: '{}' (empty=stopped)", active_name); + bool stop_test_passed = active_name.empty(); + std::string stop_test_details = + stop_test_passed ? "WiFi stopped successfully" : "WiFi still active"; + test_results.push_back( + {.name = "Stop WiFi", .passed = stop_test_passed, .details = stop_test_details}); + //! [wifi example] logger.info("WiFi singleton example complete!"); @@ -301,6 +433,29 @@ extern "C" void app_main(void) { } logger.info("WiFi stack deinitialized"); + // Print test summary + logger.info("\n=========================================================="); + logger.info(" TEST SUMMARY "); + logger.info("=========================================================="); + + int passed = 0; + int failed = 0; + + for (const auto &result : test_results) { + std::string status = result.passed ? "✓ PASS" : "✗ FAIL"; + logger.info("{}: {} - {}", status, result.name, result.details); + + if (result.passed) { + passed++; + } else { + failed++; + } + } + + logger.info("=========================================================="); + logger.info("Total: {} | Passed: {} | Failed: {}", test_results.size(), passed, failed); + logger.info("==========================================================\n"); + logger.info("WiFi example complete!"); while (true) { diff --git a/components/wifi/include/wifi.hpp b/components/wifi/include/wifi.hpp index c12f67779..69fb4b785 100644 --- a/components/wifi/include/wifi.hpp +++ b/components/wifi/include/wifi.hpp @@ -293,6 +293,39 @@ class Wifi : public espp::BaseComponent { return set_max_tx_power_raw(power_quarter_dbm); } + /// @brief Scan for available WiFi networks. + /// @param max_aps Maximum number of APs to return (default 20). + /// @param show_hidden Whether to show hidden SSIDs (default false). + /// @return Vector of wifi_ap_record_t structures, empty on error. + /// @note This creates a temporary WifiSta instance to perform the scan. + /// @note The scan will temporarily put WiFi in APSTA mode if in AP mode. + std::vector scan(uint16_t max_aps = 20, bool show_hidden = false) { + std::vector results; + + // Save current mode + wifi_mode_t current_mode = WIFI_MODE_NULL; + esp_err_t err = esp_wifi_get_mode(¤t_mode); + if (err != ESP_OK) { + logger_.error("Failed to get current WiFi mode: {}", esp_err_to_name(err)); + return results; + } + + // Create a temporary WifiSta instance for scanning + { + WifiSta::Config scan_config{.ssid = "", .password = "", .auto_connect = false}; + WifiSta temp_sta(scan_config); + results = temp_sta.scan(max_aps, show_hidden); + } + + // Restore previous mode + err = esp_wifi_set_mode(current_mode); + if (err != ESP_OK) { + logger_.warn("Failed to restore WiFi mode after scan: {}", esp_err_to_name(err)); + } + + return results; + } + /// @brief Register a new WiFi Access Point configuration. /// @param name Unique identifier for this AP configuration. /// @param config WifiAp::Config structure with AP configuration. diff --git a/components/wifi/include/wifi_base.hpp b/components/wifi/include/wifi_base.hpp index e94153a7c..2b56032d2 100644 --- a/components/wifi/include/wifi_base.hpp +++ b/components/wifi/include/wifi_base.hpp @@ -104,6 +104,18 @@ class WifiBase : public BaseComponent { */ esp_netif_t *get_netif() const { return netif_; } + /** + * @brief Check if WiFi subsystem has been initialized + * @return True if WiFi is initialized, false otherwise + * @note This is a static method that checks global WiFi state + */ + static bool is_initialized() { + wifi_mode_t mode; + bool success = esp_wifi_get_mode(&mode) == ESP_OK; + bool valid = mode != WIFI_MODE_NULL; + return success && valid; + } + protected: /** * @brief Constructor for WifiBase diff --git a/components/wifi/include/wifi_sta.hpp b/components/wifi/include/wifi_sta.hpp index 8639e2e74..6a8605e6b 100644 --- a/components/wifi/include/wifi_sta.hpp +++ b/components/wifi/include/wifi_sta.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include "esp_event.h" @@ -45,6 +46,14 @@ class WifiSta : public WifiBase { */ typedef std::function ip_callback; + /** + * @brief Scan configuration for WiFi networks + */ + struct ScanConfig { + uint16_t max_results{20}; ///< Maximum number of scan results to return + bool show_hidden{false}; ///< Whether to show hidden SSIDs + }; + /** * @brief Configuration structure for the WiFi Station. */ @@ -58,6 +67,8 @@ class WifiSta : public WifiBase { size_t num_connect_retries{ 0}; /**< Number of times to retry connecting to the AP before stopping. After this many retries, on_disconnected will be called. */ + bool auto_connect{true}; /**< Whether to automatically connect when started. If false, call + connect() manually. */ connect_callback on_connected{ nullptr}; /**< Called when the station connects, or fails to connect. */ disconnect_callback on_disconnected{nullptr}; /**< Called when the station disconnects. */ @@ -97,9 +108,9 @@ class WifiSta : public WifiBase { connected_ = false; disconnecting_ = true; attempts_ = 0; - connect_callback_ = nullptr; - disconnect_callback_ = nullptr; - ip_callback_ = nullptr; + config_.on_connected = nullptr; + config_.on_disconnected = nullptr; + config_.on_got_ip = nullptr; // unregister our event handlers logger_.debug("Unregistering event handlers"); @@ -239,6 +250,82 @@ class WifiSta : public WifiBase { strlen((const char *)ap_info.ssid)); } + /** + * @brief Scan for available WiFi networks + * @param max_results Maximum number of scan results to return + * @param show_hidden Whether to show hidden SSIDs + * @return Vector of AP records found during scan (empty on error) + * @note This method requires WiFi to be in STA or APSTA mode + * @note Scanning will disconnect any active STA connection + * @note WiFi will be restarted after scan if auto_connect is enabled + */ + std::vector scan(uint16_t max_results = 20, bool show_hidden = false) { + ScanConfig config; + config.max_results = max_results; + config.show_hidden = show_hidden; + + std::vector results; + + logger_.info("Scanning for WiFi networks (max: {}, show_hidden: {})", config.max_results, + config.show_hidden); + + // Remember if we were connected + bool was_connected = is_connected(); + bool saved_auto_connect; + { + std::lock_guard lock(config_mutex_); + saved_auto_connect = config_.auto_connect; + } + + // Disconnect if currently connected (scan requires disconnection) + esp_wifi_disconnect(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Configure scan parameters + wifi_scan_config_t scan_config = {}; + scan_config.show_hidden = config.show_hidden; + + // Start the scan + esp_err_t err = esp_wifi_scan_start(&scan_config, true); // true = blocking + if (err != ESP_OK) { + logger_.error("WiFi scan failed: {}", esp_err_to_name(err)); + if (was_connected && saved_auto_connect) { + start(); + } + return results; + } + + // Get scan results + uint16_t ap_count = config.max_results; + results.resize(ap_count); + err = esp_wifi_scan_get_ap_records(&ap_count, results.data()); + if (err != ESP_OK) { + logger_.error("Failed to get scan results: {}", esp_err_to_name(err)); + results.clear(); + if (was_connected && saved_auto_connect) { + start(); + } + return results; + } + + results.resize(ap_count); + logger_.info("Found {} networks", ap_count); + + // Reconnect/restart if needed + if (was_connected) { + // We were connected before the scan, reconnect + if (saved_auto_connect) { + start(); + } + } else if (!WifiBase::is_initialized()) { + // We were not connected, but WiFi was stopped during scan restoration + // Restart WiFi so the STA interface is in a proper state for future connection attempts + start(); + } + + return results; + } + /** * @brief Get the RSSI (Received Signal Strength Indicator) of the access point. * @return RSSI of the access point. @@ -289,7 +376,8 @@ class WifiSta : public WifiBase { */ void set_num_retries(size_t num_retries) { if (num_retries > 0) { - num_retries_ = num_retries; + std::lock_guard lock(config_mutex_); + config_.num_connect_retries = num_retries; } else { logger_.warn("Number of retries must be greater than 0, not setting it"); } @@ -299,19 +387,28 @@ class WifiSta : public WifiBase { * @brief Set the callback to be called when the station connects to the access point. * @param callback Callback to be called when the station connects to the access point. */ - void set_connect_callback(connect_callback callback) { connect_callback_ = callback; } + void set_connect_callback(connect_callback callback) { + std::lock_guard lock(config_mutex_); + config_.on_connected = callback; + } /** * @brief Set the callback to be called when the station disconnects from the access point. * @param callback Callback to be called when the station disconnects from the access point. */ - void set_disconnect_callback(disconnect_callback callback) { disconnect_callback_ = callback; } + void set_disconnect_callback(disconnect_callback callback) { + std::lock_guard lock(config_mutex_); + config_.on_disconnected = callback; + } /** * @brief Set the callback to be called when the station gets an IP address. * @param callback Callback to be called when the station gets an IP address. */ - void set_ip_callback(ip_callback callback) { ip_callback_ = callback; } + void set_ip_callback(ip_callback callback) { + std::lock_guard lock(config_mutex_); + config_.on_got_ip = callback; + } /** * @brief Get the saved WiFi configuration. @@ -348,11 +445,11 @@ class WifiSta : public WifiBase { } } - // Update callbacks - num_retries_ = config.num_connect_retries; - connect_callback_ = config.on_connected; - disconnect_callback_ = config.on_disconnected; - ip_callback_ = config.on_got_ip; + // Update stored configuration using assignment operator + { + std::lock_guard lock(config_mutex_); + config_ = config; + } // Update WiFi configuration wifi_config_t wifi_config; @@ -457,6 +554,13 @@ class WifiSta : public WifiBase { */ bool connect() { esp_err_t err; + + // Ensure WiFi is properly configured before connecting + if (!ensure_wifi_ready()) { + logger_.error("Could not ensure WiFi is ready for connection"); + return false; + } + // ensure retries are reset attempts_ = 0; connected_ = false; @@ -526,6 +630,7 @@ class WifiSta : public WifiBase { logger_.error("Invalid parameters for scan"); return -1; } + uint16_t number = max_records; uint16_t ap_count = 0; static constexpr bool blocking = true; // blocking scan @@ -549,6 +654,7 @@ class WifiSta : public WifiBase { logger_.warn("Found {} access points, but only {} can be stored", ap_count, max_records); ap_count = max_records; } + return number; } @@ -563,35 +669,68 @@ class WifiSta : public WifiBase { void event_handler(esp_event_base_t event_base, int32_t event_id, void *event_data) { if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { + logger_.debug("WIFI_EVENT_STA_START"); connected_ = false; - esp_wifi_connect(); + bool should_auto_connect; + { + std::lock_guard lock(config_mutex_); + should_auto_connect = config_.auto_connect; + } + if (should_auto_connect) { + logger_.debug("auto_connect enabled - initiating connection"); + esp_wifi_connect(); + } else { + logger_.debug("auto_connect disabled - waiting for manual connect()"); + } } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { + logger_.debug("WIFI_EVENT_STA_DISCONNECTED"); connected_ = false; if (disconnecting_) { + logger_.debug("Intentional disconnect, not retrying"); disconnecting_ = false; } else { - if (attempts_ < num_retries_) { + size_t max_retries; + { + std::lock_guard lock(config_mutex_); + max_retries = config_.num_connect_retries; + } + if (attempts_ < max_retries) { esp_wifi_connect(); attempts_++; - logger_.info("Retrying to connect to the AP"); + logger_.info("Retrying to connect to the AP (attempt {}/{})", attempts_.load(), + max_retries); // return early, don't call disconnect callback return; } } logger_.info("Failed to connect to the AP after {} attempts", attempts_.load()); - if (disconnect_callback_) { - disconnect_callback_(); + disconnect_callback cb; + { + std::lock_guard lock(config_mutex_); + cb = config_.on_disconnected; } + if (cb) { + cb(); + } + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_CONNECTED) { + logger_.debug("WIFI_EVENT_STA_CONNECTED - waiting for IP"); } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; logger_.info("got ip: {}.{}.{}.{}", IP2STR(&event->ip_info.ip)); attempts_ = 0; connected_ = true; - if (connect_callback_) { - connect_callback_(); + connect_callback on_conn; + ip_callback on_ip; + { + std::lock_guard lock(config_mutex_); + on_conn = config_.on_connected; + on_ip = config_.on_got_ip; + } + if (on_conn) { + on_conn(); } - if (ip_callback_) { - ip_callback_(event); + if (on_ip) { + on_ip(event); } } } @@ -640,11 +779,47 @@ class WifiSta : public WifiBase { void init(const Config &config); + /** + * @brief Ensure WiFi is initialized, configured, and started for this STA instance. + * @return true if WiFi is ready, false otherwise. + * @note This method will reconfigure and restart WiFi if needed. + */ + bool ensure_wifi_ready() { + // Check if WiFi is initialized + if (!is_initialized()) { + logger_.error("WiFi not initialized - cannot auto-recover. Please reinitialize WiFi."); + return false; + } + + // Check if WiFi is started + wifi_mode_t current_mode; + esp_err_t err = esp_wifi_get_mode(¤t_mode); + if (err != ESP_OK || current_mode == WIFI_MODE_NULL) { + logger_.warn("WiFi not started - reconfiguring and starting"); + // Reconfigure using stored config (without changing callbacks) + if (!reconfigure(config_)) { + logger_.error("Could not reconfigure WiFi STA"); + return false; + } + if (!start()) { + logger_.error("Could not start WiFi"); + return false; + } + } else { + // WiFi is started, just make sure it's started (handles ESP_ERR_WIFI_STATE gracefully) + err = esp_wifi_start(); + if (err != ESP_OK && err != ESP_ERR_WIFI_STATE) { + logger_.error("Could not start WiFi: {}", esp_err_to_name(err)); + return false; + } + } + + return true; + } + + Config config_; /**< Stored configuration for state recovery */ + mutable std::recursive_mutex config_mutex_; /**< Mutex for protecting config_ access */ std::atomic attempts_{0}; - std::atomic num_retries_{0}; - connect_callback connect_callback_{nullptr}; - disconnect_callback disconnect_callback_{nullptr}; - ip_callback ip_callback_{nullptr}; std::atomic connected_{false}; std::atomic disconnecting_{false}; esp_event_handler_instance_t event_handler_instance_any_id_; @@ -660,11 +835,11 @@ template <> struct formatter : fmt::formatterInsert( - "scan", - [this](std::ostream &out, int count) -> void { - wifi_ap_record_t ap_records[count]; - int num_records = wifi_sta_.get().scan(ap_records, count); - if (num_records > 0) { - out << fmt::format("Found {} WiFi networks:\n", num_records); - for (int i = 0; i < num_records; ++i) { - out << fmt::format("SSID: '{}', RSSI: {}, BSSID: {}\n", - std::string_view(reinterpret_cast(ap_records[i].ssid)), - ap_records[i].rssi, ap_records[i].bssid); - } - } else { - out << "No WiFi networks found.\n"; + "scan", [this](std::ostream &out) -> void { scan_networks(out, 20); }, + "Scan for available WiFi networks (shows up to 20 networks)."); + menu->Insert( + "scan", {"max_results"}, + [this](std::ostream &out, const std::string &max_str) -> void { + char *end; + long max_long = std::strtol(max_str.c_str(), &end, 10); + if (end == max_str.c_str() || *end != '\0' || max_long < 0 || max_long > UINT16_MAX) { + out << "Invalid max_results value. Please provide a number between 0 and " << UINT16_MAX + << ".\n"; + return; } + uint16_t max_results = static_cast(max_long); + scan_networks(out, max_results); }, - "Scan for available WiFi networks."); + "Scan for available WiFi networks with specified maximum results."); return menu; } protected: + /// @brief Scan for WiFi networks and display the results. + /// @param out The output stream to write to. + /// @param max_results Maximum number of networks to return. + void scan_networks(std::ostream &out, uint16_t max_results) { + out << fmt::format("Scanning for WiFi networks (max {})...\n", max_results); + auto scan_results = wifi_sta_.get().scan(max_results); + if (scan_results.empty()) { + out << "No WiFi networks found.\n"; + } else { + out << fmt::format("Found {} networks:\n", scan_results.size()); + for (const auto &ap : scan_results) { + out << fmt::format(" SSID: '{}', RSSI: {} dBm, Channel: {}, Auth: {}\n", (char *)ap.ssid, + ap.rssi, ap.primary, + ap.authmode == WIFI_AUTH_OPEN ? "Open" + : ap.authmode == WIFI_AUTH_WEP ? "WEP" + : ap.authmode == WIFI_AUTH_WPA_PSK ? "WPA" + : ap.authmode == WIFI_AUTH_WPA2_PSK ? "WPA2" + : ap.authmode == WIFI_AUTH_WPA_WPA2_PSK ? "WPA/WPA2" + : "Unknown"); + } + } + } + /// @brief Set the log level for the WiFi station. /// @param out The output stream to write to. /// @param verbosity The verbosity level to set. diff --git a/components/wifi/src/wifi_sta.cpp b/components/wifi/src/wifi_sta.cpp index ddbd6e63c..1ca8e10af 100644 --- a/components/wifi/src/wifi_sta.cpp +++ b/components/wifi/src/wifi_sta.cpp @@ -4,6 +4,9 @@ namespace espp { void WifiSta::init(const Config &config) { + // Store the configuration for later recovery if needed + config_ = config; + if (netif_ == nullptr) { logger_.info("Creating network interface and ensuring WiFi stack is initialized"); auto &wifi = Wifi::get(); diff --git a/doc/Doxyfile b/doc/Doxyfile index 80d233967..78d398bc3 100644 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -130,6 +130,7 @@ EXAMPLE_PATH = \ $(PROJECT_PATH)/components/odrive_ascii/example/main/odrive_ascii_example.cpp \ $(PROJECT_PATH)/components/pcf85063/example/main/pcf85063_example.cpp \ $(PROJECT_PATH)/components/pid/example/main/pid_example.cpp \ + $(PROJECT_PATH)/components/provisioning/example/main/provisioning_example.cpp \ $(PROJECT_PATH)/components/qtpy/example/main/qtpy_example.cpp \ $(PROJECT_PATH)/components/qmi8658/example/main/qmi8658_example.cpp \ $(PROJECT_PATH)/components/qwiicnes/example/main/qwiicnes_example.cpp \ @@ -293,6 +294,7 @@ INPUT = \ $(PROJECT_PATH)/components/pcf85063/include/pcf85063.hpp \ $(PROJECT_PATH)/components/pid/include/pid.hpp \ $(PROJECT_PATH)/components/ping/include/ping.hpp \ + $(PROJECT_PATH)/components/provisioning/include/provisioning.hpp \ $(PROJECT_PATH)/components/qmi8658/include/qmi8658.hpp \ $(PROJECT_PATH)/components/qmi8658/include/qmi8658_detail.hpp \ $(PROJECT_PATH)/components/qtpy/include/qtpy.hpp \ diff --git a/doc/en/network/index.rst b/doc/en/network/index.rst index 6bf506465..c2f7d258d 100644 --- a/doc/en/network/index.rst +++ b/doc/en/network/index.rst @@ -5,6 +5,7 @@ Network APIs :maxdepth: 1 ping + provisioning socket_example socket tcp_socket diff --git a/doc/en/network/provisioning.rst b/doc/en/network/provisioning.rst new file mode 100644 index 000000000..f2ab5297c --- /dev/null +++ b/doc/en/network/provisioning.rst @@ -0,0 +1,20 @@ +Provisioning +************ + +The `Provisioning` component provides WiFi provisioning via a web interface hosted +on a temporary access point. Users can scan for networks, select or manually enter +credentials, test the connection, and save multiple network configurations for easy +switching. + +.. ------------------------------- Example ------------------------------------- + +.. toctree:: + + provisioning_example + +.. ---------------------------- API Reference ---------------------------------- + +API Reference +------------- + +.. include-build-file:: inc/provisioning.inc diff --git a/doc/en/network/provisioning_example.md b/doc/en/network/provisioning_example.md new file mode 100644 index 000000000..d2fbb19f2 --- /dev/null +++ b/doc/en/network/provisioning_example.md @@ -0,0 +1,2 @@ +```{include} ../../../components/provisioning/example/README.md +```