# ESP WiFi Config — Complete Documentation > This file contains the complete documentation for ESP WiFi Config, an ESP-IDF > component for WiFi configuration with multi-network support, auto-reconnect, > and multiple provisioning interfaces. > > Website: https://configwifi.com > GitHub: https://github.com/thorrak/esp_wifi_config > Component Registry: https://components.espressif.com/components/thorrak/esp_wifi_config ================================================================================ Overview Source: https://configwifi.com/docs ================================================================================ # ESP WiFi Config [![Component Registry](https://components.espressif.com/components/thorrak/esp_wifi_config/badge.svg)](https://components.espressif.com/components/thorrak/esp_wifi_config) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ESP WiFi Config is a WiFi configuration component for ESP-IDF that handles multi-network management, automatic reconnection, and device provisioning through multiple interfaces. ## Features - **Multi-network support** — Save multiple WiFi networks with priority-based auto-connect - **Auto-reconnect** — Automatic retry with exponential backoff and failover between saved networks - **SoftAP mode** — Captive portal for initial configuration (triggers OS popup) - **Web UI** — Embedded responsive web interface (Preact-based, ~10KB gzipped) - **CLI interface** — Serial console commands for configuration - **Network Provisioning over BLE** — ESP-IDF's official `wifi_prov_mgr` (BLE scheme), driven by Espressif's "ESP BLE Provisioning" mobile apps or `esp_prov` - **Improv WiFi** — Open standard provisioning via [Web Bluetooth](https://www.improv-wifi.com/) (mutually exclusive with Network Provisioning BLE) or Web Serial (Chrome/Edge) - **REST API** — HTTP endpoints for remote configuration with CORS support - **Basic Auth** — Optional authentication for HTTP endpoints - **Custom variables** — Key-value storage for application settings - **NVS persistence** — Networks, variables, and AP config stored in flash - **Event-driven** — Connection, provisioning and config-change events on the default event loop ## Supported Targets ESP32, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6, ESP32-H2 ## Architecture ``` ┌──────────────────────────────────────────────────────────────────────┐ │ ESP_WIFI_CONFIG │ │ ┌────────────────────────────────────────────────────────────────┐ │ │ │ WiFi Core │ NVS Storage │ │ │ │ ───────── │ ─────────── │ │ │ │ • Multi-network │ • Saved networks │ │ │ │ • Auto retry + backoff │ • AP configuration │ │ │ │ • Reconnect logic │ • Custom variables │ │ │ │ • Captive portal + DNS │ │ │ │ └────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────── Configuration Interfaces ───────────────────────┐ │ │ │ │ │ │ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ │ │ Web UI │ │ HTTP │ │ CLI │ │ BLE │ │ Improv │ │ │ │ │ │(Preact)│ │ API │ │(Console│ │ Prov │ │ WiFi │ │ │ │ │ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘ │ │ │ │ │ │ │ │ │ │ │ │ │ └──────────┴──────────┴──────────┴──────────┘ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ ┌─────────────────────────┐ │ │ │ │ │ WiFi Config Core API │ │ │ │ │ └─────────────────────────┘ │ │ │ └────────────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────────┘ │ │ │ events │ requests ▼ ▼ ┌──────────────────────────────────────────────────────────────────────┐ │ ESP_BUS │ └──────────────────────────────────────────────────────────────────────┘ ``` All configuration interfaces feed into the same core API. Your application handles events (connected, disconnected, got IP, etc.) on ESP-IDF's default event loop under the `WIFI_CFG_EVENT` base — the same way it already handles `WIFI_EVENT` and `IP_EVENT` — rather than polling. ## Dependencies - **ESP-IDF** >= 5.4 (was 5.0 in 0.0.x; the Network Provisioning integration in 0.1.0 needs 5.4+) - **cJSON** — JSON parsing (included in ESP-IDF) - **mbedTLS** — Base64 for Basic Auth (included in ESP-IDF) ## Next Steps - [Getting Started](./getting-started.md) — Install the component and build your first project - [AI Integration Guide](./ai-integration-guide.md) — For AI coding assistants: how to integrate this library - [Provisioning Modes](./provisioning/modes.md) — Understand when and how provisioning interfaces activate - [API Reference](./api/c-api.md) — Full C API documentation ================================================================================ Getting Started Source: https://configwifi.com/docs/getting-started ================================================================================ # Getting Started ## Prerequisites - ESP-IDF >= 5.4 - An ESP32-series target (ESP32, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6, or ESP32-H2) ## Installation ### Using ESP-IDF Component Manager (Recommended) Add to your project's `idf_component.yml` (in the `main/` directory): ```yaml dependencies: thorrak/esp_wifi_config: "*" ``` The component manager will download `esp_wifi_config` on the next build. It has no third-party dependencies. ### Manual Installation Clone into your project's `components/` directory: ```bash cd components git clone https://github.com/thorrak/esp_wifi_config.git ``` ## Quick Start This minimal example connects to WiFi with automatic provisioning when no networks are saved or all saved networks fail: ```c #include "esp_wifi_config.h" #include "nvs_flash.h" #include "esp_log.h" static const char *TAG = "my_app"; static void on_connected(void *arg, esp_event_base_t base, int32_t id, void *data) { wifi_connected_t *info = (wifi_connected_t *)data; ESP_LOGI(TAG, "Connected to %s, RSSI: %d", info->ssid, info->rssi); } static void on_got_ip(void *arg, esp_event_base_t base, int32_t id, void *data) { const esp_netif_ip_info_t *ip = (const esp_netif_ip_info_t *)data; ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&ip->ip)); } void app_main(void) { // Initialize NVS (required) nvs_flash_init(); // Events are published on the default event loop. Create it before // registering so you catch what is emitted during startup — // wifi_cfg_init() creates it too, and a second create is harmless. ESP_ERROR_CHECK(esp_event_loop_create_default()); esp_event_handler_register(WIFI_CFG_EVENT, WIFI_CFG_EVENT_CONNECTED, on_connected, NULL); esp_event_handler_register(WIFI_CFG_EVENT, WIFI_CFG_EVENT_GOT_IP, on_got_ip, NULL); // Initialize WiFi Config. Always start from WIFI_CFG_DEFAULTS — it // carries every documented default, and wifi_cfg_init() does not patch // fields you leave at zero. wifi_cfg_init(&(wifi_cfg_config_t){ WIFI_CFG_DEFAULTS, .default_networks = (wifi_network_t[]){ {"HomeWifi", "password123", 10}, // priority 10 (highest) {"OfficeWifi", "office456", 5}, // priority 5 (fallback) }, .default_network_count = 2, // provisioning_mode defaults to WIFI_PROV_ON_FAILURE: the AP starts // when no networks are saved or every saved network fails. .stop_provisioning_on_connect = true, .provisioning_teardown_delay_ms = 5000, .enable_ap = true, }); // Wait for connection (30 second timeout) if (wifi_cfg_wait_connected(30000) == ESP_OK) { ESP_LOGI(TAG, "WiFi connected!"); } } ``` ### What This Does 1. Initializes NVS (a required prerequisite) and registers event handlers 2. Starts from `WIFI_CFG_DEFAULTS` and overrides only what differs — retry policy, `auto_reconnect = true` and `provisioning_mode = WIFI_PROV_ON_FAILURE` all come from the macro. `wifi_cfg_init(NULL)` gives you the defaults unmodified; a config built *without* the macro is rejected with `ESP_ERR_INVALID_ARG` (zero retry interval). 3. Subscribes to connected and got-IP events 4. Tries saved networks from NVS first (sorted by priority, highest first) 5. If no networks are saved, uses the default networks provided in the config 6. If all networks fail, starts a SoftAP captive portal so the user can configure WiFi via a web browser 7. After connecting, waits 5 seconds then tears down the provisioning interfaces ### Required sdkconfig No special sdkconfig is needed for basic WiFi — the defaults work. To enable optional features, see [Kconfig Options](./api/kconfig.md). ## Building and Flashing ```bash idf.py set-target esp32s3 # or esp32, esp32c3, etc. idf.py build idf.py -p /dev/ttyUSB0 flash monitor ``` ## Next Steps - [Provisioning Modes](./provisioning/modes.md) — Control when AP/BLE/Improv start - [Kconfig Options](./api/kconfig.md) — Enable Web UI, CLI, Network Provisioning BLE, Improv - [Examples](./examples.md) — Complete example projects - [AI Integration Guide](./ai-integration-guide.md) — Scenario-based configuration recipes ================================================================================ AI Integration Guide Source: https://configwifi.com/docs/ai-integration-guide ================================================================================ # AI Integration Guide This page is written for AI coding assistants (Claude Code, Codex, Cursor, etc.) to help them integrate ESP WiFi Config into a user's ESP-IDF project. It is structured as a decision tree: walk through the questions with the user, collect their answers, then generate the appropriate code using the recipes at the bottom. If you are a human reading this, you can follow the same flow — answer the questions, then use the matching recipe. A standalone version of this questionnaire is available at [/llms-onboarding.txt](/llms-onboarding.txt) for loading into an AI coding agent's context. --- ## Prerequisites (always required) Every project using ESP WiFi Config needs: 1. **ESP-IDF >= 5.4** installed and configured (5.0.0 was supported by 0.0.x; the new Network Provisioning integration in 0.1.0 needs 5.4 or newer) 2. **NVS flash** initialized before calling `wifi_cfg_init()` --- ## Question Flow Questions are numbered. Some are conditional — only ask them if the indicated condition is met. Early answers prune later questions. ### Q1: Target chip > What ESP32 chip are you targeting? | Target | WiFi | Bluetooth | Notes | |---|---|---|---| | ESP32 | Yes | Classic + BLE | Most common, 2.4GHz only | | ESP32-S2 | Yes | **No** | No BLE — skip all BLE/Improv BLE options | | ESP32-S3 | Yes | BLE | USB-OTG, 2.4GHz only | | ESP32-C3 | Yes | BLE | RISC-V, low cost | | ESP32-C6 | Yes | BLE | WiFi 6, Thread/Zigbee | | ESP32-H2 | **No** | BLE | **No WiFi — this library cannot be used** | **Gate:** If ESP32-H2, stop. If ESP32-S2, disable all BLE and Improv BLE options in subsequent questions. ### Q2: Installation method > How do you want to install the library? | Method | Action | |---|---| | **ESP-IDF Component Manager** (recommended) | Add `thorrak/esp_wifi_config: "*"` to `main/idf_component.yml` | | **Manual** | Clone into `components/` (no third-party dependencies) | | **PlatformIO** | Add to `lib_deps` in `platformio.ini` | This affects project setup but not runtime code. ### Q3: Provisioning interfaces > Which provisioning interfaces do you want to enable? (select all that apply) | Interface | Description | Requires | Kconfig | |---|---|---|---| | **SoftAP + Captive Portal** | Device creates a WiFi AP; user connects and configures via browser popup | Nothing extra | (none — runtime `enable_ap = true`) | | **Web UI** | Embedded Preact frontend served on the captive portal (richer than plain API) | SoftAP enabled | `CONFIG_WIFI_CFG_ENABLE_WEBUI=y` | | **Network Provisioning (BLE)** | Provision via Espressif's official "ESP BLE Provisioning" app or `esp_prov` Python tool | Bluetooth-capable chip | `CONFIG_WIFI_CFG_ENABLE_NETWORK_PROVISIONING=y` + BT stack | | **Improv WiFi (BLE)** | Open standard provisioning via Chrome/Edge Web Bluetooth or ESPHome app | Bluetooth-capable chip | `CONFIG_WIFI_CFG_ENABLE_IMPROV_BLE=y` + BT stack — **mutually exclusive** with Network Provisioning BLE | | **Improv WiFi (Serial)** | Open standard provisioning via Chrome/Edge Web Serial | UART access | `CONFIG_WIFI_CFG_ENABLE_IMPROV_SERIAL=y` | | **CLI** | Serial console commands (`wifi status`, `wifi scan`, etc.) | ESP Console REPL init in app code | `CONFIG_WIFI_CFG_ENABLE_CLI=y` | | **None** | No provisioning — device only connects to hardcoded/NVS networks | — | — | **If "None" is selected:** Skip Q4–Q8, jump to Q9. **Guidance for the user:** Most consumer IoT devices want **SoftAP + Captive Portal** at minimum. Adding **Network Provisioning BLE** (Espressif's standard apps) or **Improv BLE** (Web Bluetooth / ESPHome ecosystem) gives a smoother mobile experience — pick one, they cannot both ship in the same firmware. **Improv Serial** is useful for development/flashing workflows. **CLI** is primarily for development/debugging. ### Q4: When should provisioning activate? > When should the provisioning interfaces start? | Mode | `provisioning_mode` value | Best for | |---|---|---| | **When connection fails** | `WIFI_PROV_ON_FAILURE` (0, **the default**) | Most IoT devices — try saved networks first, fall back to provisioning | | **First boot only** | `WIFI_PROV_WHEN_UNPROVISIONED` (1) | Configure once, never show provisioning again | | **Manual trigger only** | `WIFI_PROV_MANUAL` (2) | App controls when provisioning starts (e.g., button press, GPIO) | **Default recommendation:** `WIFI_PROV_ON_FAILURE`, which `WIFI_CFG_DEFAULTS` already supplies — omit the field entirely unless the user wants another mode. :::caution `WIFI_PROV_ALWAYS` is disabled The value still exists in the enum but is bypassed at runtime (treated as `WIFI_PROV_MANUAL` with a warning log). `wifi_prov_mgr_start_provisioning()` calls `nimble_port_init()`, which fails if the app has already brought up the BLE stack. There is no current path to "always-on BLE provisioning". For an always-accessible config UI, expose the SoftAP captive portal or REST API after provisioning completes instead. ::: ### Q5: Per-interface configuration Only ask about interfaces selected in Q3. #### Q5a: SoftAP settings (if SoftAP selected) > Do you want to customize the AP name, password, or IP? | Setting | Default | Notes | |---|---|---| | AP SSID | `WIFI_CFG_DEFAULT_AP_SSID` = `"ESP32-Config"` | Supports `{id}` placeholder for last 3 MAC bytes (e.g., `"MyDevice-{id}"` → `"MyDevice-AABBCC"`) | | AP Password | `WIFI_CFG_DEFAULT_AP_PASSWORD` = `""` (open network) | Set a password for a secured AP; must be 8+ characters if non-empty | | AP IP | `WIFI_CFG_DEFAULT_AP_IP` = `"192.168.4.1"` | Only change if it conflicts with the user's network | These three are public macros in `esp_wifi_config.h`. Naming `.default_ap` in a designated initialiser replaces the whole sub-struct; `wifi_cfg_init()` backfills the fields you leave blank. #### Q5b: BLE settings (if Network Provisioning BLE or Improv BLE selected) > Do you want to customize the BLE device name? | Setting | Used by | Default | Notes | |---|---|---|---| | `.prov_ble.device_name` | Network Provisioning BLE | `"PROV_{id}"` | GAP name template; `{id}` replaced with last 3 MAC bytes (e.g. `"PROV_AB12CD"`) | | `.prov_ble.security` | Network Provisioning BLE | `WIFI_CFG_PROV_SECURITY_1` | `_DEFAULT` resolves to Security 1. Set `_SECURITY_2` for SRP6a (also requires `security2_salt`/`_verifier`). | | `.prov_ble.pop` | Network Provisioning BLE Security 1 | (none — NULL means no-PoP) | Set a per-device secret for production | | `.prov_ble.reset_on_failure` / `.max_failed_attempts` | Network Provisioning BLE | `false` / `3` | Set `reset_on_failure = true` to accept fresh credentials after a wrong password without rebooting; the threshold already defaults to 3 | | `.prov_ble.memory_policy` | Network Provisioning BLE | `WIFI_CFG_PROV_MEM_FREE_BTDM` | Bluetooth memory cleanup policy on prov deinit. Use `_FREE_BLE` if the app needs Classic BT after prov, `_FREE_BT` if it needs BLE, `_KEEP_ALL` if the app owns the BT stack. See [C API → Bluetooth memory policy](api/c-api). | | `.improv.ble_device_name` | Improv BLE GAP advertising | `"ESP32-WiFi-{id}"` | `{id}` replaced with last 3 MAC bytes | > Which Bluetooth stack do you prefer? | Stack | Kconfig | Flash / RAM | Notes | |---|---|---|---| | **NimBLE** | `CONFIG_BT_NIMBLE_ENABLED=y` | ~50 KB / ~20 KB | Recommended; supported by both Network Provisioning and Improv. | | **Bluedroid** | `CONFIG_BT_BLUEDROID_ENABLED=y` | ~100 KB / ~40 KB | Also supported by both. Heavier but the long-standing ESP-IDF default. | Improv BLE works with NimBLE and Bluedroid. Network Provisioning BLE works with both as well (the manager itself selects the host based on the active Kconfig). #### Q5c: Improv settings (if Improv selected) > What firmware name, version, and device name should Improv report? | Setting | Required | Notes | |---|---|---| | `firmware_name` | Yes | Shown in Improv UI (e.g., project name) | | `firmware_version` | Yes | Shown in Improv UI (e.g., `"1.0.0"`) | | `device_name` | Yes | Human-readable device name | | `on_identify` callback | No | Optional function to flash LED / beep when Improv sends Identify | #### Q5d: Improv Serial settings (if Improv Serial selected) > Which UART port and baud rate? | Setting | Default | Notes | |---|---|---| | UART port | `0` | Usually UART0 for USB | | Baud rate | `115200` | Match the monitor baud rate | #### Q5e: CLI settings (if CLI selected) No configuration needed from the user. The library auto-registers commands when `CONFIG_WIFI_CFG_ENABLE_CLI=y`. The user must initialize the ESP Console REPL in their `app_main()`. ### Q6: Shared HTTP server > Does your application already run an HTTP server, or will it need one alongside the WiFi config endpoints? | Scenario | Action | |---|---| | **No existing server, no custom endpoints needed** | Do nothing — the library creates and manages its own HTTPD | | **No existing server, but I want to add custom endpoints** | After `wifi_cfg_init()`, call `wifi_cfg_get_httpd()` to get the server handle and register custom routes | | **I already have an HTTP server** | Pass the existing `httpd_handle_t` via `.http.httpd` — the library registers its routes on your server | **Why this matters:** If the user passes their own HTTPD, the library never creates or destroys the server — it only registers/unregisters its own URI handlers. This is important for apps that run a web server for purposes beyond WiFi config. ### Q7: Post-provisioning teardown > After the device successfully connects to WiFi, what should happen to the provisioning interfaces? #### Q7a: Tear down provisioning? | Choice | Config | |---|---| | **Yes, stop AP/BLE after connecting** (most common) | `.stop_provisioning_on_connect = true` | | **No, keep provisioning running** | `.stop_provisioning_on_connect = false` | #### Q7b: Teardown delay (if tearing down) > Should there be a delay before teardown so the captive portal UI can show the connection result? | Choice | Config | |---|---| | **Yes, 5 seconds** (recommended) | `.provisioning_teardown_delay_ms = 5000` | | **No delay** | `.provisioning_teardown_delay_ms = 0` | | **Custom** | `.provisioning_teardown_delay_ms = ` | #### Q7c: HTTP behavior after provisioning stops > After provisioning stops, what should happen to the HTTP server? | Choice | `http_post_prov_mode` | Use case | |---|---|---| | **Keep everything** (Web UI + API) | `WIFI_HTTP_FULL` | Device serves a local dashboard over STA | | **Keep API only** | `WIFI_HTTP_API_ONLY` | Other devices on the network can query/manage WiFi config | | **Shut down HTTP entirely** | `WIFI_HTTP_DISABLED` | Minimal resource usage after provisioning | **Default recommendation:** `WIFI_HTTP_DISABLED` for resource-constrained headless devices, `WIFI_HTTP_API_ONLY` for devices that benefit from remote management, `WIFI_HTTP_FULL` if a web UI should remain accessible. ### Q8: Reconnection behavior > If the device loses WiFi after a successful connection, what should happen? #### Q8a: Auto-reconnect | Choice | Config | |---|---| | **Yes, auto-reconnect** (recommended) | omit the field — `WIFI_CFG_DEFAULTS` sets it `true` | | **No, just emit a disconnect event** | `.auto_reconnect = false` | :::warning `.auto_reconnect = false` only means false if the config **started from `WIFI_CFG_DEFAULTS`**. `wifi_cfg_init()` does not patch unset fields, so a struct built without the macro gets `false` whether the user asked for it or not — and a zero `retry_interval_ms` on top of that makes init return `ESP_ERR_INVALID_ARG`. Always emit `WIFI_CFG_DEFAULTS` as the first initialiser. ::: #### Q8b: Reconnect exhaustion (if auto-reconnect enabled) > If reconnection fails repeatedly, what should happen? | Choice | Config | |---|---| | **Retry forever** (default) | omit both fields — `max_reconnect_attempts` defaults to 0 | | **After N failures, reboot** | `.max_reconnect_attempts = N` — `on_reconnect_exhausted` already defaults to `WIFI_ON_RECONNECT_EXHAUSTED_RESTART` | :::caution `WIFI_ON_RECONNECT_EXHAUSTED_PROVISION` is disabled This option still exists in the enum but is bypassed at runtime (treated as `max_reconnect_attempts = 0` — keep retrying indefinitely — with a warning log). The re-enter-provisioning path called `wifi_prov_mgr_start_provisioning()` → `nimble_port_init()`, which fails when the app already owns the BLE stack. Use `_RESTART` or indefinite retry. ::: ### Q9: Custom variables > Does your application need key-value settings that are configurable through the provisioning interfaces? (e.g., server URL, device name, update interval) If yes: - Collect the variable names and default values - These are set via `.default_vars` and `.default_var_count` - Max variables controlled by `CONFIG_WIFI_CFG_MAX_VARS` (default 10) ### Q10: HTTP authentication > Should the WiFi config REST API endpoints require authentication? | Choice | Config | |---|---| | **No auth** (default) | Do nothing | | **Basic Auth** | `.http.enable_auth = true`, `.http.auth_username = "..."`, `.http.auth_password = "..."` | ### Q11: Default networks > Should the firmware include any hardcoded WiFi networks as fallbacks? (These are only written to NVS on first boot) If yes: - Collect SSIDs, passwords, and priorities (0–255, higher = tried first) - Set via `.default_networks` and `.default_network_count` --- ## Code Generation Once all questions are answered, generate three files: ### 1. sdkconfig.defaults Build the sdkconfig from Q3 and Q5 answers: ```kconfig # === Always required === # (none — defaults work for basic WiFi) # === BLE host (if Q3 includes any BLE provisioning) === CONFIG_BT_ENABLED=y CONFIG_BT_NIMBLE_ENABLED=y # or CONFIG_BT_BLUEDROID_ENABLED=y CONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=6144 # only if NimBLE CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y # BLE adds ~100KB flash # === Network Provisioning BLE (if selected; mutually exclusive with Improv BLE) === # Security version, PoP, and device-name template are set on # wifi_cfg_prov_config_t in your wifi_cfg_init() call — not in sdkconfig. CONFIG_WIFI_CFG_ENABLE_NETWORK_PROVISIONING=y CONFIG_WIFI_CFG_NETWORK_PROVISIONING_BLE=y # === Improv BLE (if selected; mutually exclusive with Network Provisioning) === # CONFIG_WIFI_CFG_ENABLE_IMPROV_BLE=y # === Improv Serial (independent of BLE — safe to combine with Network Provisioning) === # CONFIG_WIFI_CFG_ENABLE_IMPROV_SERIAL=y # === Web UI (if Q3 includes Web UI) === CONFIG_WIFI_CFG_ENABLE_WEBUI=y # === CLI (if Q3 includes CLI) === CONFIG_WIFI_CFG_ENABLE_CLI=y ``` ### 2. main/idf_component.yml ```yaml dependencies: thorrak/esp_wifi_config: "*" ``` ### 3. main/main.c Assemble the `wifi_cfg_config_t` struct from the collected answers. **`WIFI_CFG_DEFAULTS` must be the first initialiser** — it carries every documented default, and `wifi_cfg_init()` does not patch fields left at zero. The template below shows all fields — **only include fields where the user's answer differs from the default or where the field is required for their chosen interfaces**. ```c #include "esp_wifi_config.h" #include "nvs_flash.h" #include "esp_log.h" static const char *TAG = "app"; static void on_got_ip(void *arg, esp_event_base_t base, int32_t id, void *data) { const esp_netif_ip_info_t *ip = (const esp_netif_ip_info_t *)data; ESP_LOGI(TAG, "Connected! IP: " IPSTR, IP2STR(&ip->ip)); // >>> User's post-connection application logic goes here <<< } void app_main(void) { // --- Required initialization --- nvs_flash_init(); // --- Event subscriptions (before wifi_cfg_init) --- esp_event_handler_register(WIFI_CFG_EVENT, WIFI_CFG_EVENT_GOT_IP, on_got_ip, NULL); // --- WiFi Config initialization --- wifi_cfg_init(&(wifi_cfg_config_t){ // REQUIRED first initialiser: every documented default. WIFI_CFG_DEFAULTS, // -- Default networks (Q11) -- // .default_networks = (wifi_network_t[]){ // {"SSID", "password", PRIORITY}, // }, // .default_network_count = N, // -- Provisioning mode (Q4) -- // Already WIFI_PROV_ON_FAILURE from the macro. Name it only for // another mode: // .provisioning_mode = WIFI_PROV_WHEN_UNPROVISIONED, // Q4 answer // -- Provisioning teardown (Q7) -- .stop_provisioning_on_connect = true, // Q7a .provisioning_teardown_delay_ms = 5000, // Q7b // -- HTTP post-provisioning (Q7c; macro sets WIFI_HTTP_FULL) -- // .http_post_prov_mode = WIFI_HTTP_DISABLED, // Q7c answer // -- SoftAP (Q3 + Q5a) -- .enable_ap = true, // set to true if SoftAP selected in Q3 // .default_ap = { // .ssid = "MyDevice-{id}", // Q5a; the rest is backfilled // }, // -- Reconnection (Q8) -- // auto_reconnect is true and max_reconnect_attempts is 0 from the // macro; on_reconnect_exhausted is already _RESTART (_PROVISION is // disabled). Name these only to change them: // .auto_reconnect = false, // Q8a // .max_reconnect_attempts = 10, // Q8b (0 = infinite) // -- HTTP config (Q6, Q10). api_base_path and auth_username come // from the macro. -- // .http = { // .httpd = my_server, // Q6: pass existing server // .enable_auth = true, // Q10 // .auth_password = "changeme", // Q10 // }, // -- Network Provisioning BLE (Q3 + Q5b) // Enabled via CONFIG_WIFI_CFG_ENABLE_NETWORK_PROVISIONING=y // NOTE: by default the device REBOOTS after a successful BLE // provisioning flow — there is no clean in-place teardown for // wifi_prov_mgr's BLE stack. Set // .disable_reboot_on_provisioning_success = true only if the // app owns the BLE/Wi-Fi handoff itself. // Defaults, so normally omitted: device_name ("PROV_{id}"), // security (Security 1), memory_policy (FREE_BTDM), // max_failed_attempts (3), reboot_max_wait_ms (15000 ms backstop), // disable_reboot_on_provisioning_success (false = reboot on). // .prov_ble = { // .pop = "1234abcd", // Security 1 PoP (NULL → no PoP) // .wifi_conn_attempts = 5, // 0 = infinite // .reset_on_failure = true, // accept retries without reboot // .firmware_version = "1.0.0", // }, // -- Improv (Q3 + Q5b + Q5c + Q5d) -- // Transports selected at compile time via Kconfig (CONFIG_WIFI_CFG_ENABLE_IMPROV_BLE / _SERIAL). // .ble_device_name controls the BLE GAP advertised name (what BLE // scanners display). .device_name is what the Improv companion app // shows once it has connected. // .improv = { // .ble_device_name = "ESP32-WiFi-{id}", // Q5b // .firmware_name = "my_project", // Q5c // .firmware_version = "1.0.0", // Q5c // .device_name = "My Device", // Q5c // }, // -- Custom variables (Q9) -- // .default_vars = (wifi_var_t[]){ // {"server_url", "https://api.example.com"}, // {"device_name", "my-device"}, // }, // .default_var_count = 2, }); // --- CLI setup (if Q3 includes CLI) --- // esp_console_repl_t *repl = NULL; // esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT(); // repl_config.prompt = "esp> "; // esp_console_register_help_command(); // esp_console_dev_usb_serial_jtag_config_t hw_config = // ESP_CONSOLE_DEV_USB_SERIAL_JTAG_CONFIG_DEFAULT(); // esp_console_new_repl_usb_serial_jtag(&hw_config, &repl_config, &repl); // esp_console_start_repl(repl); // --- Shared HTTP server (if Q6 = "add custom endpoints") --- // httpd_handle_t server = wifi_cfg_get_httpd(); // if (server) { // httpd_uri_t my_endpoint = { // .uri = "/api/my-data", // .method = HTTP_GET, // .handler = my_handler, // }; // httpd_register_uri_handler(server, &my_endpoint); // } wifi_cfg_wait_connected(30000); } ``` **Important code generation rules:** 1. **Only uncomment sections relevant to the user's answers.** Do not include commented-out blocks in the final output — only include active code. 2. **Subscribe to events before `wifi_cfg_init()`** to catch events fired during initialization. 3. **Use compound literals** for the config struct (the `&(wifi_cfg_config_t){...}` pattern) — this is idiomatic ESP-IDF C. 4. **Always emit `WIFI_CFG_DEFAULTS` as the first initialiser** in the struct. `wifi_cfg_init()` does not patch fields left at zero: without the macro, `retry_interval_ms` is 0 and init returns `ESP_ERR_INVALID_ARG`, and `auto_reconnect` silently comes out `false`. If the user needs no overrides at all, `wifi_cfg_init(NULL)` is equivalent and simpler. 5. **Do not set fields to their default values.** There are no longer any exceptions — `provisioning_mode` and `on_reconnect_exhausted` now default to `WIFI_PROV_ON_FAILURE` and `WIFI_ON_RECONNECT_EXHAUSTED_RESTART` via the macro, so omit them unless the user chose something else. (`WIFI_PROV_ALWAYS` and `WIFI_ON_RECONNECT_EXHAUSTED_PROVISION` are `[DISABLED]`; `wifi_cfg_init()` logs a warning if either is selected.) 6. **Include `#include "esp_console.h"`** only if CLI is enabled. --- ## Gotchas 1. **Must init NVS first**: Call `nvs_flash_init()` before `wifi_cfg_init()`. For robustness, handle `ESP_ERR_NVS_NO_FREE_PAGES` by erasing and re-initializing. 2. **Events go to the default event loop** under the `WIFI_CFG_EVENT` base. Handlers run on the event loop task, not on the emitting task. 3. **BLE requires Bluetooth enabled**: `CONFIG_BT_ENABLED=y` and either `CONFIG_BT_BLUEDROID_ENABLED=y` or `CONFIG_BT_NIMBLE_ENABLED=y`. 4. **BLE needs larger partition table**: Use `CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y` when enabling BLE. 5. **NimBLE needs stack size**: Set `CONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=6144` when using NimBLE. 6. **Improv BLE works with both NimBLE and Bluedroid**: previous releases were Bluedroid-only — that limitation is gone. 7. **Network Provisioning BLE and Improv BLE are mutually exclusive**: `CONFIG_WIFI_CFG_ENABLE_NETWORK_PROVISIONING` and `CONFIG_WIFI_CFG_ENABLE_IMPROV_BLE` cannot be enabled together — they each want to own the BLE GAP advertising and the host stack. Improv Serial is independent of BLE and remains safe alongside Network Provisioning. 8. **Default networks are seeds**: They're only written to NVS on first boot. After that, NVS is the source of truth. 9. **ESP32 is 2.4GHz only**: The device cannot connect to 5GHz WiFi networks. 10. **Register handlers before init**: call `esp_event_loop_create_default()` then `esp_event_handler_register()` before `wifi_cfg_init()` to catch events fired during initialization. `wifi_cfg_init()` creates the loop too, so creating it first is harmless. 11. **ESP32-S2 has no Bluetooth**: BLE and Improv BLE cannot be used. 12. **ESP32-H2 has no WiFi**: This library cannot be used on ESP32-H2. --- ## Quick Reference: Config Field Defaults All of these come from `WIFI_CFG_DEFAULTS`. Omit them unless the user's answer differs: | Field | Default | Notes | |---|---|---| | `auto_reconnect` | `true` | Plain bool. `false` means false — provided you started from the macro | | `max_retry_per_network` | `3` | 0 is accepted but no connection is ever attempted (init warns) | | `retry_interval_ms` | `5000` | **0 → `ESP_ERR_INVALID_ARG`** | | `retry_max_interval_ms` | `60000` | Exponential backoff cap. **0 → `ESP_ERR_INVALID_ARG`** | | `max_reconnect_attempts` | `0` | 0 = infinite | | `on_reconnect_exhausted` | `WIFI_ON_RECONNECT_EXHAUSTED_RESTART` | `_PROVISION` is `[DISABLED]` | | `provisioning_mode` | `WIFI_PROV_ON_FAILURE` | `WIFI_PROV_ALWAYS` is `[DISABLED]` | | `provisioning_teardown_delay_ms` | `0` | Recommend 5000 for captive portal | | `http_post_prov_mode` | `WIFI_HTTP_FULL` | | | `default_ap.ssid` | `"ESP32-Config"` | `WIFI_CFG_DEFAULT_AP_SSID` | | `default_ap.password` | `""` (open) | `WIFI_CFG_DEFAULT_AP_PASSWORD` | | `default_ap.ip` / `.gateway` | `"192.168.4.1"` | `WIFI_CFG_DEFAULT_AP_IP` | | `default_ap.netmask` | `"255.255.255.0"` | | | `default_ap.max_connections` | `4` | | | `default_ap.dhcp_start` / `.dhcp_end` | `"192.168.4.2"` / `"192.168.4.20"` | | | `http.api_base_path` | `"/api/wifi"` | | | `http.auth_username` | `"admin"` | Only relevant if `enable_auth = true` | | `http.auth_password` | `"admin"` | Only relevant if `enable_auth = true` | | `improv.serial_uart_num` | `0` | From `CONFIG_WIFI_MGR_IMPROV_SERIAL_UART_NUM` | | `improv.serial_baud_rate` | `115200` | From `CONFIG_WIFI_MGR_IMPROV_SERIAL_BAUD` | | `prov_ble.cleanup_delay_ms` | `1000` | | | `prov_ble.reboot_max_wait_ms` | `15000` | | | `prov_ble.max_failed_attempts` | `3` | | --- ## Scenario Quick-Picks If the user describes their use case in general terms, map to these common configurations: `ON_FAILURE` is the default, so those rows need no `provisioning_mode` field at all. `WIFI_PROV_ALWAYS` is `[DISABLED]` — never recommend it; for an always-reachable config surface, keep `http_post_prov_mode = WIFI_HTTP_FULL` instead. | Use case | Q4 mode | Interfaces | Post-prov HTTP | Reconnect | |---|---|---|---|---| | **Consumer IoT device** | `ON_FAILURE` (default) | SoftAP + Web UI | `DISABLED` | Retry forever | | **Development / prototyping** | `ON_FAILURE` (default) | SoftAP + Web UI + CLI | `FULL` | Retry forever | | **Mobile-app-provisioned device** | `ON_FAILURE` (default) | BLE (+ optionally SoftAP) | `DISABLED` | Reboot after N | | **ESPHome-style device** | `ON_FAILURE` (default) | Improv BLE + SoftAP | `DISABLED` | Reboot after N | | **Local dashboard / gateway** | `ON_FAILURE` (default) | SoftAP + Web UI | `FULL` | Retry forever | | **Factory-configured device** | `MANUAL` | SoftAP + BLE (on button press) | `API_ONLY` | Reboot after N | | **Headless sensor** | `ON_FAILURE` (default) | SoftAP only | `DISABLED` | Reboot after N | ================================================================================ Multi-Network & Auto-Reconnect Source: https://configwifi.com/docs/guides/multi-network ================================================================================ # Multi-Network & Auto-Reconnect ESP WiFi Config supports saving multiple WiFi networks and automatically connecting to the best available one. ## How It Works Networks are tried in **priority order** (highest first). For each network, the library retries up to `max_retry_per_network` times with exponential backoff before moving to the next one. ``` boot → load saved networks from NVS (sorted by priority DESC) → For each network: 1. Attempt connection (up to max_retry_per_network times) 2. Exponential backoff between retries 3. Success → done 4. Fail → try next network → All networks failed → trigger provisioning (if configured) ``` ## Configuration ```c wifi_cfg_init(&(wifi_cfg_config_t){ WIFI_CFG_DEFAULTS, // Default networks used when NVS is empty .default_networks = (wifi_network_t[]){ {"PrimaryWifi", "password1", 10}, // priority 10 — tried first {"BackupWifi", "password2", 5}, // priority 5 — fallback {"EmergencyWifi", "password3", 1}, // priority 1 — last resort }, .default_network_count = 3, // What to do after reconnect retries are exhausted. The default is // .max_reconnect_attempts = 0 (retry forever); a bounded budget makes // on_reconnect_exhausted reachable. .max_reconnect_attempts = 10, }); ``` ## Retry Tuning `WIFI_CFG_DEFAULTS` supplies the retry policy, so most projects change nothing here: | Field | Default | Meaning | |---|---|---| | `max_retry_per_network` | 3 | Attempts per network before moving to the next | | `retry_interval_ms` | 5000 | Base interval; the backoff is `retry_interval_ms << retry` | | `retry_max_interval_ms` | 60000 | Cap on the exponential backoff | | `auto_reconnect` | `true` | Reconnect after a post-connect disconnect | | `max_reconnect_attempts` | 0 | 0 = retry forever | Override only what you need — but you must start from the macro, because `wifi_cfg_init()` no longer patches fields left at zero: ```c wifi_cfg_init(&(wifi_cfg_config_t){ WIFI_CFG_DEFAULTS, .max_retry_per_network = 5, // more patient per network .retry_interval_ms = 2000, // faster first retry .retry_max_interval_ms = 30000, // lower backoff ceiling .auto_reconnect = false, // means false — you started from the macro }); ``` :::warning `retry_interval_ms = 0` or `retry_max_interval_ms = 0` makes `wifi_cfg_init()` return `ESP_ERR_INVALID_ARG` — a zero base retries with no delay at all. A config built without `WIFI_CFG_DEFAULTS` hits this. ::: ## Default Networks vs. Saved Networks - **Default networks** (in the config struct) are used as initial seed data when NVS has no saved networks - **Saved networks** (in NVS) are the runtime set — these are what actually get tried - Once a network is added via any interface (Web UI, REST API, BLE, CLI), it's saved to NVS - Default networks are only written to NVS on first boot (when NVS is empty) ## Reconnect Exhaustion After a successful connection is lost (post-connect disconnect), the library retries up to `max_reconnect_attempts` times. When attempts are exhausted: | `on_reconnect_exhausted` | Behavior | |---|---| | `WIFI_ON_RECONNECT_EXHAUSTED_RESTART` | Reboot the device via `esp_restart()`. **The default**, and the zero value. | | `WIFI_ON_RECONNECT_EXHAUSTED_PROVISION` | **Disabled** — kept in the API for compatibility; currently treated as "continue retrying indefinitely" (equivalent to `max_reconnect_attempts = 0`). | Set `max_reconnect_attempts = 0` for infinite retries (never exhausted). :::caution `WIFI_ON_RECONNECT_EXHAUSTED_PROVISION` is disabled The re-enter-provisioning path called `wifi_prov_mgr_start_provisioning()` → `nimble_port_init()`, which fails if the app already owns the BLE stack. The library now logs a warning, resets the counter, and falls through to normal exponential-backoff retry. Use `WIFI_ON_RECONNECT_EXHAUSTED_RESTART` or leave `max_reconnect_attempts = 0` to retry indefinitely. See [MIGRATION.md](https://github.com/thorrak/esp_wifi_config/blob/main/MIGRATION.md). ::: ### Counter Semantics `max_reconnect_attempts` counts consecutive failed reconnects since the last successful connection. The counter resets to zero on a successful STA connection (the `GOT_IP` event). `WIFI_ON_RECONNECT_EXHAUSTED_RESTART` works with any `provisioning_mode` — use it for devices that must maintain connectivity and prefer a clean reboot over degraded operation. ## Managing Networks at Runtime ```c // Add a new network wifi_cfg_add_network(&(wifi_network_t){"NewWifi", "password", 8}); // Update an existing network (matched by SSID) wifi_cfg_update_network(&(wifi_network_t){"NewWifi", "newpass", 12}); // Remove a network wifi_cfg_remove_network("NewWifi"); // List all saved networks wifi_network_t networks[5]; size_t count; wifi_cfg_list_networks(networks, 5, &count); ``` Networks can also be managed via the [REST API](../api/rest-api.md), [BLE GATT](../provisioning/ble-gatt.md), or [CLI](../api/cli-commands.md). ## Connection Flow Diagram ``` boot → evaluate provisioning_mode → ├── WIFI_PROV_ON_FAILURE (default) → │ ├── no saved networks → start provisioning │ └── has saved networks → try connect → │ ├── success → emit CONNECTED → GOT_IP │ └── all fail → start provisioning ├── WIFI_PROV_WHEN_UNPROVISIONED → │ ├── no saved networks → start provisioning │ └── has saved networks → try connect ├── WIFI_PROV_MANUAL → try connect only (user starts provisioning explicitly) └── WIFI_PROV_ALWAYS → [DISABLED — treated as MANUAL] try connect only Post-connect disconnect: 1. Auto-reconnect up to max_reconnect_attempts (0 = infinite) 2. If exhausted → on_reconnect_exhausted action: a. WIFI_ON_RECONNECT_EXHAUSTED_PROVISION → [DISABLED — keeps retrying] b. WIFI_ON_RECONNECT_EXHAUSTED_RESTART → esp_restart() Successful BLE provisioning (when CONFIG_WIFI_CFG_ENABLE_NETWORK_PROVISIONING=y): CRED_RECV → CRED_SUCCESS → esp_restart() (default; opt out via prov_ble.disable_reboot_on_provisioning_success) ``` ================================================================================ Custom Variables Source: https://configwifi.com/docs/guides/custom-variables ================================================================================ # Custom Variables ESP WiFi Config includes a key-value store for application settings that persists in NVS alongside WiFi configuration. Variables can be managed via any interface — C API, REST API, BLE, CLI, or Web UI. ## Configuration Provide default variables in the init config. These are written to NVS only on first boot (when no variables exist): ```c wifi_cfg_init(&(wifi_cfg_config_t){ WIFI_CFG_DEFAULTS, .default_vars = (wifi_var_t[]){ {"server_url", "https://api.example.com"}, {"device_name", "my-device"}, }, .default_var_count = 2, // ... other config }); ``` The maximum number of variables is controlled by `CONFIG_WIFI_CFG_MAX_VARS` (default: 10). ## C API Usage ```c // Set a variable (creates or updates) wifi_cfg_set_var("server_url", "https://api.example.com"); wifi_cfg_set_var("device_id", "device-001"); // Get a variable char value[128]; if (wifi_cfg_get_var("server_url", value, sizeof(value)) == ESP_OK) { ESP_LOGI(TAG, "Server URL: %s", value); } // Delete a variable wifi_cfg_del_var("device_id"); ``` ## Listening for Changes Subscribe to the `WIFI_CFG_EVENT_VAR_CHANGED` event to react when any interface modifies a variable: ```c static void on_var_changed(void *arg, esp_event_base_t base, int32_t id, void *data) { const wifi_var_t *info = (const wifi_var_t *)data; ESP_LOGI(TAG, "Variable changed: %s = %s", info->key, info->value); } esp_event_handler_register(WIFI_CFG_EVENT, WIFI_CFG_EVENT_VAR_CHANGED, on_var_changed, NULL); ``` ## Managing Variables via Other Interfaces Variables are accessible from all configuration interfaces: - **REST API**: `PUT /api/wifi/vars/:key`, `GET /api/wifi/vars`, `DELETE /api/wifi/vars/:key` - **BLE**: `set_var`, `get_var`, `list_vars`, `del_var` commands - **CLI**: `wifi var set `, `wifi var get ` - **Web UI**: Variables section in the embedded web interface ================================================================================ Events Source: https://configwifi.com/docs/guides/events ================================================================================ # Events ESP WiFi Config publishes its events on ESP-IDF's default event loop, under the event base `WIFI_CFG_EVENT`. If you already handle `WIFI_EVENT` or `IP_EVENT`, there is nothing new to learn — it is the same API. ```c #include "esp_wifi_config.h" esp_event_handler_register(WIFI_CFG_EVENT, WIFI_CFG_EVENT_GOT_IP, on_got_ip, NULL); ``` ## Available Events | Event ID | Payload Type | Description | |---|---|---| | `WIFI_CFG_EVENT_CONNECTED` | `wifi_connected_t` | STA associated with an AP | | `WIFI_CFG_EVENT_DISCONNECTED` | `wifi_disconnected_t` | STA lost its association | | `WIFI_CFG_EVENT_CONNECTING` | `char[]` (SSID) | Association attempt started | | `WIFI_CFG_EVENT_SCAN_DONE` | `uint16_t` (AP count) | WiFi scan completed | | `WIFI_CFG_EVENT_GOT_IP` | `esp_netif_ip_info_t` | STA obtained an IP address | | `WIFI_CFG_EVENT_LOST_IP` | none | IP lease lost | | `WIFI_CFG_EVENT_AP_START` | none | SoftAP started | | `WIFI_CFG_EVENT_AP_STOP` | none | SoftAP stopped | | `WIFI_CFG_EVENT_AP_STA_CONNECTED` | `uint8_t[6]` (MAC) | A station joined the SoftAP | | `WIFI_CFG_EVENT_NETWORK_ADDED` | `wifi_network_t` | Network added to the store | | `WIFI_CFG_EVENT_NETWORK_UPDATED` | `wifi_network_t` | Stored network updated | | `WIFI_CFG_EVENT_NETWORK_REMOVED` | `char[]` (SSID) | Network removed from the store | | `WIFI_CFG_EVENT_VAR_CHANGED` | `wifi_var_t` | A custom variable was set or deleted | | `WIFI_CFG_EVENT_PROVISIONING_STARTED` | none | Provisioning interfaces started | | `WIFI_CFG_EVENT_PROVISIONING_STOPPED` | none | Provisioning interfaces stopped | | `WIFI_CFG_EVENT_PROV_CRED_RECV` | `wifi_cfg_prov_creds_t` | Provisioning client sent credentials | | `WIFI_CFG_EVENT_PROV_CRED_FAIL` | `int` (reason) | Connect with provisioned credentials failed | | `WIFI_CFG_EVENT_PROV_CRED_SUCCESS` | none | Connect with provisioned credentials succeeded | Pass `ESP_EVENT_ANY_ID` instead of a specific id to receive all of them through one handler. ## Handler Signature The standard esp_event handler: ```c void handler(void *arg, esp_event_base_t base, int32_t event_id, void *event_data) ``` - `arg` — the context pointer you passed when registering - `base` — always `WIFI_CFG_EVENT` for these - `event_id` — one of the ids above. `wifi_cfg_event_name(event_id)` gives you a string for logging. - `event_data` — pointer to the payload, or `NULL` for events that carry none. The loop hands you a private copy, valid for the duration of the handler. :::note Handlers run on the event loop task Not on the task that emitted the event — so a slow handler will not stall the WiFi state machine, block an HTTP request, or consume the httpd task's stack. It does share the system event loop with `WIFI_EVENT` and `IP_EVENT` handlers, so a handler that blocks delays those. Keep handlers short and push heavy work onto your own task. ::: ## Registering The default event loop must exist before you register. `wifi_cfg_init()` creates it, so you have two options: **Register before init** (catches events emitted during startup) — create the loop yourself first. Creating it twice is harmless; the library tolerates a loop that already exists. ```c void app_main(void) { nvs_flash_init(); ESP_ERROR_CHECK(esp_event_loop_create_default()); esp_event_handler_register(WIFI_CFG_EVENT, WIFI_CFG_EVENT_CONNECTED, on_connected, NULL); esp_event_handler_register(WIFI_CFG_EVENT, WIFI_CFG_EVENT_GOT_IP, on_got_ip, NULL); wifi_cfg_init(&(wifi_cfg_config_t){ WIFI_CFG_DEFAULTS, .enable_ap = true }); } ``` **Register after init** — simpler, but you miss anything emitted while `wifi_cfg_init()` was running. ## Example Handlers ```c static void on_connected(void *arg, esp_event_base_t base, int32_t id, void *data) { const wifi_connected_t *info = (const wifi_connected_t *)data; ESP_LOGI(TAG, "Connected to %s, RSSI: %d", info->ssid, info->rssi); } static void on_got_ip(void *arg, esp_event_base_t base, int32_t id, void *data) { const esp_netif_ip_info_t *ip = (const esp_netif_ip_info_t *)data; ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&ip->ip)); } static void on_var_changed(void *arg, esp_event_base_t base, int32_t id, void *data) { const wifi_var_t *var = (const wifi_var_t *)data; ESP_LOGI(TAG, "Variable changed: %s = %s", var->key, var->value); } ``` ### One handler for everything ```c static void on_any(void *arg, esp_event_base_t base, int32_t id, void *data) { ESP_LOGI(TAG, "wifi event: %s", wifi_cfg_event_name(id)); switch (id) { case WIFI_CFG_EVENT_GOT_IP: /* ... */ break; case WIFI_CFG_EVENT_DISCONNECTED: /* ... */ break; default: break; } } esp_event_handler_register(WIFI_CFG_EVENT, ESP_EVENT_ANY_ID, on_any, NULL); ``` ## Unregistering Use the instance API when you need to unregister later: ```c esp_event_handler_instance_t inst; esp_event_handler_instance_register(WIFI_CFG_EVENT, WIFI_CFG_EVENT_GOT_IP, on_got_ip, NULL, &inst); // ... esp_event_handler_instance_unregister(WIFI_CFG_EVENT, WIFI_CFG_EVENT_GOT_IP, inst); ``` Registrations are owned by the event loop, not by the library, so they survive `wifi_cfg_deinit()`. ## Dropped Events `esp_event_post()` is called with a zero timeout so that a full loop queue never blocks the WiFi state machine — trading a stalled reconnect for a lost notification would be the worse failure. If a post does fail, the library logs it at warning level: ``` W (12345) wifi_cfg_event: event 'got_ip' not posted: ESP_ERR_TIMEOUT ``` If you see that, raise `CONFIG_ESP_SYSTEM_EVENT_QUEUE_SIZE` or find the handler that is blocking the loop. ## Event Timing - `CONNECTED` fires when the WiFi STA association completes (before IP assignment) - `GOT_IP` fires when DHCP assigns an IP address — this is typically the event you want to trigger application logic - `PROVISIONING_STOPPED` fires after the teardown delay (`provisioning_teardown_delay_ms`) when `stop_provisioning_on_connect` is true Because delivery is asynchronous, a handler runs shortly *after* the state change that caused it. `wifi_cfg_add_network()` returns before its `NETWORK_ADDED` handler runs. ================================================================================ HTTP Server Sharing Source: https://configwifi.com/docs/guides/http-server-sharing ================================================================================ # HTTP Server Sharing ESP WiFi Config can share its HTTP server with your application, allowing you to add custom endpoints alongside the WiFi configuration API. ## Getting the Server Handle If the library started the HTTP server (during provisioning or based on `http_post_prov_mode`), you can get the handle: ```c httpd_handle_t server = wifi_cfg_get_httpd(); if (server) { // Register your own endpoints httpd_uri_t my_endpoint = { .uri = "/api/my-data", .method = HTTP_GET, .handler = my_data_handler, }; httpd_register_uri_handler(server, &my_endpoint); } ``` ## Passing an Existing Server If your application already runs an HTTP server, pass it in the config so the library registers its handlers on your server instead of creating its own: ```c // Bring up the TCP/IP stack yourself here. Everywhere else the library does // it for you inside wifi_cfg_init() — but starting a server opens a socket, // and that happens before wifi_cfg_init() is reached. ESP_ERROR_CHECK(esp_netif_init()); httpd_handle_t my_server = start_my_webserver(); wifi_cfg_init(&(wifi_cfg_config_t){ WIFI_CFG_DEFAULTS, .http = { .httpd = my_server, // Use existing server // api_base_path and auth_username/password come from the macro; // naming .http here replaces the whole sub-struct, and the library // falls back to "/api/wifi" and admin/admin for the blanks. }, // ... }); ``` :::warning Call `esp_netif_init()` first This is the one documented flow where your application runs network code *before* `wifi_cfg_init()`, so it is the one place the library cannot initialise lwIP in time. `wifi_cfg_init()` calls `esp_netif_init()` itself and tolerates having been beaten to it, so calling it early is safe and calling it twice is harmless. Skip it and the device aborts inside lwIP the moment your server opens its socket: ``` assert failed: tcpip_send_msg_wait_sem ... (Invalid mbox) ``` Nothing in that names netif, or this library, or the line that caused it — which is why it is called out here rather than left to the reader. ::: Two more things become yours in this flow, both silent when wrong: - **`max_uri_handlers` must leave room for the library's routes.** `HTTPD_DEFAULT_CONFIG()` allows 8; the library registers about 22 between the REST API, the Web UI and the captive-portal probes. As of 0.2.1 an undersized server is reported — `"N of the API's routes could not be registered"` — rather than silently giving you an API that answers some paths and 404s the rest. - **`uri_match_fn` must be `httpd_uri_match_wildcard`** for the Web UI's catch-all route to match. The default matcher compares URIs exactly, so the captive portal answers only its handful of literal paths and the phone's "sign in to network" sheet opens blank. The library sets both on the server it creates itself; on yours, it cannot. [`examples/with_shared_httpd`](https://github.com/thorrak/esp_wifi_config/tree/main/examples/with_shared_httpd) is this whole flow as a buildable project. When you pass an existing server: - The library registers its API routes on your server - On `wifi_cfg_deinit()`, the library unregisters its routes but does **not** stop the server - Your server and its other routes remain active ## Post-Provisioning HTTP Behavior The `http_post_prov_mode` field controls what happens to the HTTP server after provisioning stops: | Mode | Behavior | |---|---| | `WIFI_HTTP_FULL` | Keep the full HTTP server running (Web UI + API) | | `WIFI_HTTP_API_ONLY` | Keep only REST API endpoints, remove Web UI and captive portal routes | | `WIFI_HTTP_DISABLED` | Stop the HTTP server entirely | ```c wifi_cfg_init(&(wifi_cfg_config_t){ WIFI_CFG_DEFAULTS, .stop_provisioning_on_connect = true, .enable_ap = true, // After provisioning, keep REST API but drop the Web UI // (WIFI_CFG_DEFAULTS sets WIFI_HTTP_FULL) .http_post_prov_mode = WIFI_HTTP_API_ONLY, }); ``` ## Stopping the HTTP Server You can manually stop the library-owned HTTP server: ```c esp_err_t err = wifi_cfg_stop_http(); ``` This returns `ESP_ERR_INVALID_STATE` and refuses to act if any of the following apply: - You passed an existing `httpd_handle_t` (the library never tears down a server it doesn't own — it only deregisters its own URI handlers). - Provisioning is currently active. - The reconnect constraint applies: `enable_ap = true` AND `on_reconnect_exhausted = WIFI_ON_RECONNECT_EXHAUSTED_PROVISION` AND `max_reconnect_attempts > 0`. The SoftAP might need to restart later after a post-connect disconnect, and it requires the HTTP server alive. _This guard is currently dormant because_ `WIFI_ON_RECONNECT_EXHAUSTED_PROVISION` _is itself disabled — it will reactivate cleanly if that path is re-enabled._ ## Automatic HTTPD Teardown When `http_post_prov_mode = WIFI_HTTP_DISABLED`, the library also tries to fully stop the HTTPD server (not just deregister handlers) — but only when it's safe. The decision depends on three factors: who owns the server, whether provisioning might restart, and whether the SoftAP might need to come back up after a post-connect disconnect. | Provisioning mode | Library owns HTTPD | Shared HTTPD (you passed `.http.httpd`) | |---|---|---| | `WIFI_PROV_WHEN_UNPROVISIONED` | Auto-teardown after transition* | Deregister handlers only — your server stays running | | `WIFI_PROV_MANUAL` | Keep server alive; you call `wifi_cfg_stop_http()` explicitly* | Deregister handlers only | | `WIFI_PROV_ON_FAILURE` | Keep server alive (provisioning may restart) | Deregister handlers only | | `WIFI_PROV_ALWAYS` | Currently disabled — behaves like `WIFI_PROV_MANUAL` (see [Provisioning Modes](../provisioning/modes.md#modes)) | Deregister handlers only | \* **Reconnect constraint**: If `enable_ap = true` AND `on_reconnect_exhausted = WIFI_ON_RECONNECT_EXHAUSTED_PROVISION` AND `max_reconnect_attempts > 0`, auto-teardown would be suppressed even in `WHEN_UNPROVISIONED`/`MANUAL` mode. This guard is **currently dormant** because `WIFI_ON_RECONNECT_EXHAUSTED_PROVISION` is itself disabled — auto-teardown runs normally regardless of the exhaustion-action config. The rules are conservative — the library never tears down a server it can't bring back up cleanly later. ## Authentication If you want to protect the WiFi configuration endpoints: ```c wifi_cfg_init(&(wifi_cfg_config_t){ WIFI_CFG_DEFAULTS, .http = { .enable_auth = true, .auth_username = "admin", // the default; shown for clarity .auth_password = "secret", }, }); ``` When enabled, all `/api/wifi/*` endpoints require HTTP Basic Auth. Your own custom endpoints are not affected. ================================================================================ Custom Web UI Source: https://configwifi.com/docs/guides/custom-webui ================================================================================ # Custom Web UI The library can serve a captive-portal frontend from a filesystem partition (LittleFS or SPIFFS) instead of the embedded Preact UI. This lets you ship your own branding, framework, and content without recompiling firmware — just rewrite the filesystem image and re-flash. ## Three Serving Modes The Web UI source is selected at build time by two Kconfig keys: | Mode | Kconfig | Source of files | |---|---|---| | **Embedded Web UI** (default when enabled) | `WIFI_CFG_ENABLE_WEBUI=y`, `WIFI_CFG_WEBUI_CUSTOM_PATH=""` | Bundled Preact app (~10 KB gzipped) linked into the firmware via `EMBED_FILES` | | **Custom filesystem UI** | `WIFI_CFG_ENABLE_WEBUI=y`, `WIFI_CFG_WEBUI_CUSTOM_PATH="/littlefs"` | Files on the configured filesystem path | | **Simple fallback page** | `WIFI_CFG_ENABLE_WEBUI=n` | A built-in minimal HTML page with inline JS, just enough to add a network and connect | :::caution Custom path replaces embedded — no fallback When `WIFI_CFG_WEBUI_CUSTOM_PATH` is set, the embedded Preact assets are **excluded from the build entirely** (the CMake `EMBED_FILES` list drops them and the corresponding C handler is `#ifndef`-gated). If a file isn't found on the filesystem, the request fails — there is no runtime fallback to embedded content. Test your filesystem image contains the expected files before flashing. ::: ## Required File Layout The HTTP server serves exactly three URL paths. Your filesystem image must provide files that match: | URL | Filesystem path (under `WEBUI_CUSTOM_PATH`) | Notes | |---|---|---| | `/` | `/index.html` | Main HTML document. `/` is internally remapped to `/index.html`. | | `/assets/app.js` | `/assets/app.js` or `/assets/app.js.gz` | JS bundle. Single-file output required (no code splitting). | | `/assets/index.css` | `/assets/index.css` or `/assets/index.css.gz` | CSS stylesheet. | ### Gzip handling The server checks for both the plain file and a `.gz` sibling for every request: - If only the plain file exists → serve plain. - If only the gzipped file exists → serve gzipped, add `Content-Encoding: gzip`. - If **both** exist → prefer the gzipped variant (smaller response, same `Content-Type`). Gzipping is recommended for `app.js` and `index.css` — the embedded Preact build ships both as `.js.gz` and `.css.gz` and saves around 70% on the wire. ### Content types The server auto-sets `Content-Type` from the file extension. Supported extensions: `.html`, `.css`, `.js`, `.json`, `.svg`, `.png`. Unknown extensions fall back to `application/octet-stream`. ## Minimal HTML Template A custom frontend can use any framework or none at all. The only hard requirements are: - Mount JS at `/assets/app.js`. - Mount CSS at `/assets/index.css`. - Output a single JS bundle (no dynamic imports / code splitting). ```html My Device Setup
``` The DOM structure inside `` is entirely up to you — the library serves the files but does not require any particular markup. ## Vite Build Configuration If your frontend uses Vite (the bundled Preact app does), the relevant rollup output settings are: ```typescript // vite.config.ts import { defineConfig } from 'vite'; import preact from '@preact/preset-vite'; import { compression } from 'vite-plugin-compression2'; export default defineConfig({ plugins: [ preact(), compression({ algorithm: 'gzip' }), // emit .gz alongside originals ], build: { rollupOptions: { output: { inlineDynamicImports: true, // force single JS bundle entryFileNames: 'assets/app.js', // fixed filename, no hash assetFileNames: 'assets/[name].[ext]', // index.css (no hash) }, }, }, }); ``` The non-negotiable bits are: - **`inlineDynamicImports: true`** — the HTTP server only serves three fixed paths; dynamic chunks would be unreachable. - **Fixed `app.js` / `index.css` output names** — no content hashes, since the server has hardcoded handlers for those exact URLs. The bundled frontend lives at [`frontend/`](https://github.com/thorrak/esp_wifi_config/tree/main/frontend); its `vite.config.ts` is the canonical reference. ## Deployment: Custom Frontend on LittleFS A complete worked example lives at [`examples/with_webui_customize/`](https://github.com/thorrak/esp_wifi_config/tree/main/examples/with_webui_customize) — this section summarises the moving parts. ### 1. sdkconfig ``` CONFIG_WIFI_CFG_ENABLE_WEBUI=y CONFIG_WIFI_CFG_WEBUI_CUSTOM_PATH="/littlefs" CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" ``` ### 2. Partition table Add a LittleFS data partition (here 512 KB at the tail of flash): ```csv # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 1M, storage, data, littlefs,, 512K, ``` Adjust `factory` size for your firmware and pick a `storage` size that comfortably fits your assets (compressed). The bundled Preact UI is ~10 KB gzipped, but a richer custom app can easily reach 100–200 KB. ### 3. Place frontend output ``` www/ ├── index.html └── assets/ ├── app.js.gz └── index.css.gz ``` ### 4. Wire LittleFS into CMake In the project's top-level `CMakeLists.txt`: ```cmake littlefs_create_partition_image(storage www FLASH_IN_PROJECT) ``` `storage` must match the partition name from `partitions.csv`. The component `joltwallet/littlefs` (or the in-tree LittleFS port) needs to be in `idf_component.yml` for this CMake function to exist. ### 5. Build and flash ```bash idf.py build flash ``` The library mounts the partition automatically when the HTTP server starts and serves files from `/littlefs/...` for any request matching the three fixed URLs. ## Iterating on the Frontend Because the filesystem image is independent of the firmware binary, frontend-only changes don't require rebuilding firmware. After editing your frontend: ```bash # Rebuild frontend → www/ npm --prefix frontend run build # Re-flash just the LittleFS partition idf.py littlefs-flash ``` (The exact target name depends on your LittleFS component; some expose `storage-flash` or similar.) ## Captive-Portal Behaviour Captive-portal detection probes are handled regardless of which Web UI mode is active: | Path | Platform | |---|---| | `/generate_204`, `/gen_204` | Android | | `/hotspot-detect.html`, `/library/test/success.html` | iOS / macOS | | `/ncsi.txt`, `/connecttest.txt` | Windows | | `/success.txt`, `/canonical.html` | Firefox | All probes return an HTTP 302 redirect to `http:///`, which the phone/laptop then renders inside its captive-portal popup. This works whether `/` is served from the embedded UI, your custom filesystem UI, or the simple fallback page. ## Talking to the Backend The custom frontend communicates with the device over the REST API documented in [REST API Reference](../api/rest-api). Base path defaults to `/api/wifi` and is configurable through `wifi_cfg_http_config_t`: ```c .http = { .api_base_path = "/api/wifi", // default .enable_auth = false, // set true for HTTP Basic Auth .pre_request_hook = my_hook, // optional; see HTTP Server Sharing }, ``` If you need to host other endpoints alongside the WiFi Config API on the same server, see [HTTP Server Sharing](./http-server-sharing). ## Reference Example The full deployment lives at [`examples/with_webui_customize/`](https://github.com/thorrak/esp_wifi_config/tree/main/examples/with_webui_customize) — it copies the bundled Preact frontend into the example's `www/` directory and demonstrates flashing both firmware and LittleFS image from a single `idf.py build flash`. ================================================================================ Provisioning Modes Source: https://configwifi.com/docs/provisioning/modes ================================================================================ # Provisioning Modes The `provisioning_mode` field controls when ESP WiFi Config automatically starts provisioning interfaces (SoftAP, BLE, and/or Improv WiFi). ## Modes | Mode | Value | Behavior | |---|---|---| | `WIFI_PROV_ON_FAILURE` | 0 | Start provisioning when no networks are saved or all saved networks fail to connect. **The default**, supplied by `WIFI_CFG_DEFAULTS`. | | `WIFI_PROV_WHEN_UNPROVISIONED` | 1 | Start provisioning only if no networks exist in NVS | | `WIFI_PROV_MANUAL` | 2 | Never auto-start provisioning; the application calls `wifi_cfg_start_ap()` explicitly | | `WIFI_PROV_ALWAYS` | 3 | **Disabled** — kept in the API for compatibility but currently treated as `WIFI_PROV_MANUAL` at boot. See note below. | :::info Numeric values changed in 0.2.0 `WIFI_PROV_ALWAYS` used to be 0, so a config that omitted `provisioning_mode` silently selected the one mode that does nothing. The working mode is now the zero value. Code that uses the enumerator names only needs a recompile; anything that **stored or transmitted the number** must be migrated — see [MIGRATION.md](https://github.com/thorrak/esp_wifi_config/blob/main/MIGRATION.md). ::: :::caution `WIFI_PROV_ALWAYS` is disabled `wifi_cfg_start_provisioning()` eventually calls `wifi_prov_mgr_start_provisioning()`, which calls `nimble_port_init()`. If the application has already initialised the BLE stack, that call fails — so the always-on path is bypassed (with a warning log) and treated as `WIFI_PROV_MANUAL`. The enum value is preserved so existing configs still compile. May be re-enabled when the library has its own BLE provisioning path independent of Espressif's `wifi_provisioning` component. ::: ## Choosing a Mode - **`WIFI_PROV_ON_FAILURE`** (recommended for most devices) — The device tries its saved networks first. If it can't connect to any of them, it opens provisioning so the user can reconfigure. This is the most common pattern for consumer IoT devices. - **`WIFI_PROV_WHEN_UNPROVISIONED`** — Only provision on first boot. Once the user has configured at least one network, provisioning never starts automatically again. Use this for devices that should be configured once and left alone. - **`WIFI_PROV_MANUAL`** — Full control. Provisioning only starts when your code calls `wifi_cfg_start_ap()` (e.g., on a button press or GPIO event). Use this when you have a physical provisioning trigger. ## Configuration Example ```c wifi_cfg_init(&(wifi_cfg_config_t){ WIFI_CFG_DEFAULTS, // provisioning_mode is already WIFI_PROV_ON_FAILURE from the macro; // name it explicitly only when you want a different mode. .stop_provisioning_on_connect = true, .provisioning_teardown_delay_ms = 5000, .enable_ap = true, // BLE interfaces are enabled at compile time via Kconfig // (mutually exclusive — pick one): // CONFIG_WIFI_CFG_ENABLE_NETWORK_PROVISIONING=y (ESP-IDF Wi-Fi Provisioning) // CONFIG_WIFI_CFG_ENABLE_IMPROV_BLE=y (Improv standard) }); ``` ## Post-Connect Behavior ### Provisioning Teardown When `stop_provisioning_on_connect` is `true`, the library stops AP/BLE/Improv after the STA obtains an IP address. The `provisioning_teardown_delay_ms` value (default: 0) adds a delay before teardown so the Web UI can display connection results to the user. ``` STA gets IP → wait provisioning_teardown_delay_ms → emit PROVISIONING_STOPPED → stop AP, BLE, Improv → transition HTTP per http_post_prov_mode ``` ### HTTP Post-Provisioning Mode Controls the HTTP server after provisioning stops: | Mode | Behavior | |---|---| | `WIFI_HTTP_FULL` | Keep the full HTTP server running (Web UI + API) | | `WIFI_HTTP_API_ONLY` | Keep only REST API endpoints, remove Web UI and captive portal routes | | `WIFI_HTTP_DISABLED` | Stop the HTTP server entirely | ### Reconnect Exhaustion After a post-connect disconnect, the library retries up to `max_reconnect_attempts` times (0 = infinite). When attempts are exhausted: | `on_reconnect_exhausted` | Value | Behavior | |---|---|---| | `WIFI_ON_RECONNECT_EXHAUSTED_RESTART` | 0 | Reboot the device via `esp_restart()`. **The default**, supplied by `WIFI_CFG_DEFAULTS`. | | `WIFI_ON_RECONNECT_EXHAUSTED_PROVISION` | 1 | **Disabled** — kept in the API for compatibility but currently treated as "continue retrying indefinitely" (equivalent to `max_reconnect_attempts = 0`). See note below. | These two swapped numeric values in 0.2.0, for the same reason as the provisioning modes above. :::caution `WIFI_ON_RECONNECT_EXHAUSTED_PROVISION` is disabled The re-enter-provisioning path called `wifi_prov_mgr_start_provisioning()` → `nimble_port_init()`, which fails when the application owns the BLE stack. The library now logs a warning and falls through to normal exponential-backoff retry instead. Use `WIFI_ON_RECONNECT_EXHAUSTED_RESTART` or leave `max_reconnect_attempts = 0` for indefinite retry. See [MIGRATION.md](https://github.com/thorrak/esp_wifi_config/blob/main/MIGRATION.md). ::: ### Reboot After BLE Provisioning When the BLE provisioning channel is enabled, the device reboots automatically once a session completes — both because `wifi_provisioning` does not expose a clean BLE-stack tear-down/rebuild path, and because a cold boot is the most reliable way to hand off from "BLE-owned" to "STA-owned" state. The reboot is governed by two fields on `wifi_cfg_prov_config_t`: | Field | Default | Effect | |---|---|---| | `disable_reboot_on_provisioning_success` | `false` (reboot **on**) | Set true if the application handles the BLE/Wi-Fi handoff itself. | | `reboot_max_wait_ms` | `0` → 15000 ms | Backstop window after `WIFI_PROV_EVT_CRED_SUCCESS` before the forced reboot. The actual reboot fires on whichever happens first: the BLE client disconnecting after `WIFI_PROV_EVT_CRED_RECV`, or this timer expiring. | While reboot-on-success is active, `stop_provisioning_on_connect` / `provisioning_teardown_delay_ms` / `prov_ble.stop_after_success` are bypassed for the BLE flow — the reboot supersedes any in-place teardown. SoftAP and Improv flows still observe the in-place teardown lifecycle. See [BLE Provisioning → Reboot on success](./ble-gatt.md#reboot-on-successful-provisioning) for the full discussion. ================================================================================ SoftAP & Captive Portal Source: https://configwifi.com/docs/provisioning/softap-captive-portal ================================================================================ # SoftAP & Captive Portal When provisioning starts with `enable_ap = true`, the device creates a WiFi access point with a captive portal that automatically opens in the user's browser. ## How It Works 1. Device starts a SoftAP (e.g., "ESP32-AABBCC") 2. User connects their phone/laptop to the AP 3. The OS detects the captive portal and automatically opens a browser popup 4. User configures WiFi via the Web UI or REST API 5. Device connects to the selected network 6. Provisioning stops (if `stop_provisioning_on_connect = true`) after `provisioning_teardown_delay_ms` ## AP Configuration ```c wifi_cfg_init(&(wifi_cfg_config_t){ WIFI_CFG_DEFAULTS, // provisioning_mode is already WIFI_PROV_ON_FAILURE from the macro. .stop_provisioning_on_connect = true, .provisioning_teardown_delay_ms = 5000, .enable_ap = true, // Customize the SoftAP. Naming .default_ap replaces the whole // sub-struct; wifi_cfg_init() backfills password (open), // ip/gateway (192.168.4.1), netmask, max_connections and the DHCP range. .default_ap = { .ssid = "MyDevice-{id}", // {id} is replaced with last 3 bytes of MAC }, }); ``` The `{id}` placeholder in the SSID is replaced with the last 3 bytes of the device's MAC address (e.g., "MyDevice-AABBCC"). This ensures each device has a unique AP name. If you omit `default_ap` entirely, `WIFI_CFG_DEFAULTS` supplies the AP defaults, which are also available to your code as public macros: | Macro | Value | |---|---| | `WIFI_CFG_DEFAULT_AP_SSID` | `"ESP32-Config"` | | `WIFI_CFG_DEFAULT_AP_PASSWORD` | `""` (open network) | | `WIFI_CFG_DEFAULT_AP_IP` | `"192.168.4.1"` (also the gateway) | The remaining defaults are `netmask` `"255.255.255.0"`, `max_connections` 4, and a DHCP range of `"192.168.4.2"`–`"192.168.4.20"`. ## Starting AP Manually In `WIFI_PROV_MANUAL` mode, start the AP from your application code: ```c // Start with default config (from wifi_cfg_config_t) wifi_cfg_start_ap(NULL); // Or start with custom config wifi_cfg_start_ap(&(wifi_cfg_ap_config_t){ .ssid = "MyDevice", .password = "12345678", .ip = "192.168.10.1", }); // Stop the AP wifi_cfg_stop_ap(); ``` ## AP Status ```c wifi_ap_status_t ap_status; wifi_cfg_get_ap_status(&ap_status); if (ap_status.active) { ESP_LOGI(TAG, "AP: %s, IP: %s, Clients: %d", ap_status.ssid, ap_status.ip, ap_status.sta_count); } ``` ## Captive Portal Detection The library responds to captive portal detection probes from all major platforms: | Platform | Detection URLs | |---|---| | Android | `/generate_204`, `/gen_204` | | iOS / macOS | `/hotspot-detect.html` | | Windows | `/ncsi.txt`, `/connecttest.txt` | | Firefox | `/success.txt`, `/canonical.html` | All detection probes redirect to the Web UI at the AP's IP address. ## Web UI Enable the embedded Web UI with `CONFIG_WIFI_CFG_ENABLE_WEBUI=y` in your sdkconfig. The Web UI is a Preact-based responsive interface (~10KB gzipped) that provides: - WiFi network scanning and selection - Saved network management - Connection status display - Custom variable editing - AP configuration To ship your own frontend instead — branded HTML, a different framework, or extra screens served from LittleFS/SPIFFS — see the [Custom Web UI guide](../guides/custom-webui.md). ================================================================================ BLE Provisioning Source: https://configwifi.com/docs/provisioning/ble-gatt ================================================================================ # BLE Provisioning ESP WiFi Config integrates with ESP-IDF's official **Wi-Fi Provisioning** manager (`wifi_prov_mgr` on IDF 5.4, `network_prov_mgr` on IDF 6.x) over the BLE scheme. This replaces the previous custom JSON-over-GATT service (UUID `0xFFE0`) which has been removed — see [MIGRATION.md][migrate] for how to update existing client tools. [migrate]: https://github.com/thorrak/esp_wifi_config/blob/main/MIGRATION.md ## Why Network Provisioning - Standard, audited protocol — works with **Espressif's official mobile apps** out of the box ("ESP BLE Provisioning" on iOS / Android). - Encrypted handshake (Security 1 / 2) instead of plaintext JSON. - Bluedroid and NimBLE supported via the same code path. - Library still owns the higher-level lifecycle: provisioning mode, retry/backoff, multi-network store, custom variables, post-prov HTTP. ## Enabling ### 1. Kconfig ```kconfig CONFIG_WIFI_CFG_ENABLE_NETWORK_PROVISIONING=y CONFIG_WIFI_CFG_NETWORK_PROVISIONING_BLE=y ``` That's the whole Kconfig surface. Security version, PoP, device-name template, and the rest of the runtime parameters are set on `wifi_cfg_prov_config_t` in step 3 below. Mutually exclusive with `CONFIG_WIFI_CFG_ENABLE_IMPROV_BLE` — both want to own the BLE GAP advertising and the host stack. ### 2. Bluetooth Stack ```kconfig CONFIG_BT_ENABLED=y CONFIG_BT_NIMBLE_ENABLED=y # recommended CONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=6144 # Or: # CONFIG_BT_BLUEDROID_ENABLED=y ``` ### 3. Runtime Config ```c wifi_cfg_init(&(wifi_cfg_config_t){ WIFI_CFG_DEFAULTS, // provisioning_mode is already WIFI_PROV_ON_FAILURE from the macro. .stop_provisioning_on_connect = true, .provisioning_teardown_delay_ms = 5000, // Left at their defaults here: device_name ("PROV_{id}", supports {id}), // security (Security 1), memory_policy (FREE_BTDM, see c-api.md), // max_failed_attempts (3), cleanup_delay_ms (1000), // reboot_max_wait_ms (15000). .prov_ble = { .pop = "1234abcd", // Security 1 PoP (NULL/"" → no PoP) .wifi_conn_attempts = 5, // 0 = infinite .reset_on_failure = true, // accept retries without reboot .firmware_version = "1.0.0", }, }); ``` `provisioning_mode` controls **when** wifi_prov_mgr is started: | Mode | Behaviour | |------|-----------| | `WIFI_PROV_ON_FAILURE` | start when no networks saved or all failed (**the default**) | | `WIFI_PROV_WHEN_UNPROVISIONED` | start only if no networks saved | | `WIFI_PROV_MANUAL` | only via explicit API call | | `WIFI_PROV_ALWAYS` | **Disabled** — treated as `WIFI_PROV_MANUAL`. See [Provisioning Modes](./modes.md#modes). | `stop_provisioning_on_connect` and `provisioning_teardown_delay_ms` still control the in-place teardown lifecycle for SoftAP and Improv, but the BLE channel reboots the device once provisioning completes (see [Reboot on successful provisioning](#reboot-on-successful-provisioning) below), so the BLE-specific teardown they would otherwise drive is bypassed. ## Reboot on successful provisioning When the BLE channel is enabled, the device reboots automatically once a provisioning session completes. The reboot is **on by default** and is not optional in the strict sense — it's how the library avoids a class of latent post-handoff BLE bugs. ### Why Espressif's `wifi_provisioning` component does not expose a clean way to tear down and rebuild the BLE/NimBLE stack in place. Any in-place teardown leaves enough residual state (stale GATT db, lingering controller state, suspended supervision timers) that subsequent BLE operations behave unpredictably. A cold boot is the only state that is reliably consistent. ### Trigger ordering The reboot fires on whichever happens first: 1. The BLE client disconnecting after `WIFI_PROV_EVT_CRED_RECV` — the well-behaved client path. The library logs `Provisioning complete; client disconnected, rebooting`, waits ~50 ms for the final protocomm response to drain, and calls `esp_restart()`. 2. A backstop timer started on `WIFI_PROV_EVT_CRED_SUCCESS`. Default 15000 ms; tunable via `prov_ble.reboot_max_wait_ms`. Catches clients that simply disappear (force-quit, lost link) and never deliver a clean disconnect. It must comfortably exceed the client's status-poll interval (~5 s for Espressif's ESPProvision SDK) plus associate and DHCP, or the device can reboot between two polls and the client reports a false failure. Both paths call `esp_restart()`. The first to fire wins; the second is moot because `esp_restart()` does not return. ### Configuration ```c .prov_ble = { // Default: reboot enabled (false → reboot on). // Set true ONLY if the application owns the BLE/Wi-Fi handoff itself. .disable_reboot_on_provisioning_success = false, // Backstop wait between CRED_SUCCESS and the forced reboot, in ms. // 0 → 15000 ms. Ignored when disable_reboot_on_provisioning_success is true. .reboot_max_wait_ms = 15000, } ``` The field uses negative polarity (`disable_reboot_...`) to match the existing `disable_disconnect_restart` knob — both are default-on `wifi_prov_mgr`/NimBLE workarounds, so a `false` value gives the safer behaviour. ### Implications for application code - **`prov_ble.on_credentials_success` and the `WIFI_CFG_EVENT_PROV_CRED_SUCCESS` bus event still fire** — the callback runs before the reboot is scheduled. Anything that must persist needs to land in NVS inside that callback. If you need a wider window for the work to complete, extend `prov_ble.reboot_max_wait_ms`. - **`prov_ble.stop_after_success` is bypassed** while reboot-on-success is active — the reboot supersedes any in-place stop. - **`stop_provisioning_on_connect` / `provisioning_teardown_delay_ms` still drive SoftAP and Improv** if they are enabled alongside BLE. - **`disable_reboot_on_provisioning_success = true`** is intended for apps that consciously own the BLE/Wi-Fi handoff — e.g., a flow that morphs from BLE provisioning into a BLE companion link, or an app that takes over the BLE stack via `disable_disconnect_restart` and `keep_ble_on_after_stop`. Opting out accepts the `wifi_prov_mgr` in-place teardown sharp edges as a trade-off. ## Security versions | Version | Handshake | Setup cost | |---------|-----------|-----------| | Security 0 | none (plaintext) | none — testing only | | Security 1 | Curve25519 + AES-CTR with PoP | set `prov_ble.pop` (or leave NULL for no-PoP mode) | | Security 2 | SRP6a (salted authenticated key exchange) | requires pre-computed `salt` + `verifier` | For Security 2, derive the `salt` / `verifier` offline using `esp-idf-provisioning`'s helper or the `esp_prov` Python tool, embed the bytes in firmware, and pass them via `wifi_cfg_prov_config_t`: ```c extern const uint8_t my_salt[]; extern const size_t my_salt_len; extern const uint8_t my_verifier[]; extern const size_t my_verifier_len; wifi_cfg_init(&(wifi_cfg_config_t){ WIFI_CFG_DEFAULTS, .prov_ble = { .security = WIFI_CFG_PROV_SECURITY_2, .security2_username = "device-fleet-2", .security2_salt = my_salt, .security2_salt_len = my_salt_len, .security2_verifier = my_verifier, .security2_verifier_len = my_verifier_len, }, }); ``` If `prov_ble.security` is set to `WIFI_CFG_PROV_SECURITY_2` but no salt/verifier is provided, `wifi_cfg_init()` returns `ESP_ERR_INVALID_ARG` — the library does not silently fall back. ## Custom protocomm endpoints Alongside the standard `prov-config` / `prov-scan` endpoints, the library registers four custom protocomm endpoints. They are minimal by design — the goal is to expose the library's higher-level state, not to recreate the broad pre-Wi-Fi management surface that previously lived in the 0xFFE0 service. | Endpoint | Direction | Purpose | |----------|-----------|---------| | `esp-wifi-config-version` | read | library/IDF/firmware versions | | `esp-wifi-config-capabilities` | read | feature flags + limits | | `esp-wifi-config-vars` | read/write | the custom variable store | | `esp-wifi-config-network-policy` | read | `provisioning_mode`, retries | The `vars` endpoint accepts a small JSON request: ```json {"op": "list"} {"op": "get", "key": "server_url"} {"op": "set", "key": "server_url", "value": "https://api.example.com"} {"op": "del", "key": "server_url"} ``` ## Recommended clients - **iOS / Android — "ESP BLE Provisioning"** (Espressif official app) - **`esp-prov` Python tool** (in any ESP-IDF checkout under `tools/esp_prov/`) — useful for CI and headless setup - **`esp-idf-provisioning-android` / `-ios`** SDKs if you ship a custom mobile app ## iOS "ESP BLE Provisioning" app — "Encrypted Communication" toggle Espressif's iOS "ESP BLE Provisioning" app exposes a setting that the app does **not** auto-negotiate with the device. It is on the Settings screen reached via the gear icon in the upper-left of the device-list screen, and is labelled **"Encrypted Communication"**: | Toggle state | Wire protocol used by the app | Required device build | |--------------|--------------------------------|------------------------| | Off ("Unsecured") | plaintext protocomm — no handshake | `.prov_ble.security = WIFI_CFG_PROV_SECURITY_0` | | On ("Secured") | Security 2 (SRP6a) — app prompts for a username at connect time | `.prov_ble.security = WIFI_CFG_PROV_SECURITY_2` with a valid salt/verifier | The toggle is sticky across sessions and is not adjusted based on what the device advertises in its BLE service data. The app gives no in-app indication that it exists or that it has to match the firmware. ### Confirmed behaviour - **Security 0 device, app in "Unsecured" mode** — works. Credentials transfer in plaintext as expected. - **Security 0 device, app in "Secured" mode** — the app hangs after the device is tapped. No PoP prompt appears, no Wi-Fi scan list is rendered, and there is no error toast or log entry that surfaces the mismatch. The fix is to flip the toggle to "Unsecured" and reopen the device. ### Not yet confirmed The interaction between this toggle and **Security 1 (PoP)** has not yet been validated against this app. Neither toggle position obviously corresponds to Security 1 on the wire — "Unsecured" is plaintext and "Secured" is SRP6a — so a Security 1 device may not be reachable from this app at all. If you require Security 1 specifically, use the Android app, `esp_prov`, or your own client built on the `esp-idf-provisioning-*` SDKs until this is confirmed. ### Practical guidance - Pick the security version at build time and document the matching toggle position in any user-facing setup instructions you ship. - If you support a mixed fleet that uses different security versions, prefer the Android app or `esp_prov` for QA — the iOS toggle becomes a per-device manual step. - The same caveat does not appear in Espressif's `esp_prov` Python tool: that one is told the security version via a CLI flag (`--sec_ver`) and will not silently hang on mismatch. ## Coexistence with Improv `CONFIG_WIFI_CFG_ENABLE_IMPROV_BLE` is mutually exclusive with `CONFIG_WIFI_CFG_ENABLE_NETWORK_PROVISIONING` — they cannot both be enabled in a single firmware build. If you need to support both ecosystems, ship two firmware variants. Improv Serial (`CONFIG_WIFI_CFG_ENABLE_IMPROV_SERIAL`) is independent of BLE and remains safe to enable alongside Network Provisioning. ## ESP-IDF version requirements - **Minimum**: ESP-IDF 5.4 (uses the in-tree `wifi_provisioning` component). - **6.x**: works via the external `espressif/network_provisioning` managed component, declared in this library's `idf_component.yml` with an `idf_version >=6.0` rule. The implementation switches between the two via a thin compatibility shim in `esp_wifi_config_prov_ble.c`. ================================================================================ Improv WiFi Source: https://configwifi.com/docs/provisioning/improv-wifi ================================================================================ # Improv WiFi [Improv WiFi](https://www.improv-wifi.com/) is an open standard by ESPHome for provisioning IoT devices over BLE or Serial using web browsers and companion apps. :::note Mutually exclusive with Network Provisioning BLE `CONFIG_WIFI_CFG_ENABLE_IMPROV_BLE` and `CONFIG_WIFI_CFG_ENABLE_NETWORK_PROVISIONING` cannot both be enabled in a single firmware build — they each want to own the BLE GAP advertising and the NimBLE/Bluedroid host. Pick the protocol that matches your provisioning client tooling, or ship two firmware variants. Improv **Serial** (`CONFIG_WIFI_CFG_ENABLE_IMPROV_SERIAL`) is independent of BLE and remains safe to enable alongside Network Provisioning BLE. ::: ## Enabling Improv ### Kconfig ```kconfig # BLE transport (requires Bluetooth enabled) CONFIG_WIFI_CFG_ENABLE_IMPROV_BLE=y # Serial transport (optional) CONFIG_WIFI_CFG_ENABLE_IMPROV_SERIAL=y CONFIG_WIFI_CFG_IMPROV_SERIAL_UART_NUM=0 CONFIG_WIFI_CFG_IMPROV_SERIAL_BAUD=115200 ``` ### Runtime Config ```c wifi_cfg_init(&(wifi_cfg_config_t){ WIFI_CFG_DEFAULTS, // provisioning_mode is already WIFI_PROV_ON_FAILURE from the macro. .stop_provisioning_on_connect = true, .enable_ap = true, // Transports selected via Kconfig (CONFIG_WIFI_CFG_ENABLE_IMPROV_BLE / _SERIAL) .improv = { .firmware_name = "my_project", .firmware_version = "1.0.0", .device_name = "My Device", .on_identify = my_identify_callback, // Optional: flash LED/make noise on Identify }, }); ``` ## How to Provision ### Via Web Bluetooth (Chrome/Edge) 1. Open [improv-wifi.com](https://www.improv-wifi.com/) in Chrome or Edge 2. Click "Connect device via Bluetooth" 3. Select the device from the browser pairing dialog 4. Enter WiFi credentials — the device connects and returns its IP ### Via ESPHome Companion App 1. Install the ESPHome app (Android/iOS) 2. The device appears automatically for Improv provisioning 3. Tap and enter WiFi credentials ### Via Web Serial (if enabled) 1. Open [improv-wifi.com](https://www.improv-wifi.com/) in Chrome or Edge 2. Click "Connect device via Serial" 3. Select the serial port and enter WiFi credentials ## Supported RPC Commands | Command | ID | Description | |---|---|---| | Send WiFi Settings | 0x01 | Provide SSID + password, device connects | | Identify | 0x02 | Flash LED / beep (calls `on_identify` callback) | | Get Device Info | 0x03 | Returns firmware name, version, chip, device name | | Get WiFi Networks | 0x04 | Triggers a WiFi scan and returns results | ### How many networks a scan returns The two Improv specifications shape this command differently, and the library follows each transport's own. **Serial** sends one response per network and then an empty one to mark the end, so the list is not bounded by anything the protocol imposes. **BLE** sends a single response holding every network, which the format caps: a result's length field is one byte, so no response can carry more than 255 bytes of payload — roughly eleven networks at three strings each. The library asks for an ATT MTU of 517, so on a typical link that 255-byte ceiling is what you hit; a client that negotiates a smaller MTU gets a shorter list still, because the response is also kept inside what one notification can carry. Whichever bound bites first, the list is cut to fit. What survives the cut is chosen for you: scan results arrive strongest-first and are already deduplicated by SSID, so the networks that drop off are the faintest ones. A device in a crowded band will not show a user every SSID over BLE, and cannot — but it will show them the nearby ones. ## BLE Stack Requirements Improv BLE requires `CONFIG_BT_ENABLED=y` and a NimBLE or Bluedroid host stack. The BLE stack is initialised automatically when Improv BLE is enabled — the library does not need any other Kconfig opt-in. See the [with_improv example](https://github.com/thorrak/esp_wifi_config/tree/main/examples/with_improv) for a complete sdkconfig. If you need the official ESP-IDF provisioning protocol instead of Improv (e.g. for use with the Espressif "ESP BLE Provisioning" mobile apps), see [BLE Provisioning](./ble-gatt.md). The two are mutually exclusive at compile time. ================================================================================ C API Reference Source: https://configwifi.com/docs/api/c-api ================================================================================ # C API Reference All functions are declared in `esp_wifi_config.h` and return `esp_err_t` unless otherwise noted. ## Initialization ```c // Initialize WiFi Config with the given configuration esp_err_t wifi_cfg_init(const wifi_cfg_config_t *config); // Deinitialize WiFi Config, stop all interfaces, free resources. // deinit_wifi = false keeps WiFi connected and the netifs alive. esp_err_t wifi_cfg_deinit(bool deinit_wifi); ``` ## Status ```c // Check if STA is connected bool wifi_cfg_is_connected(void); // Get the current WiFi state enum wifi_state_t wifi_cfg_get_state(void); // Get detailed status (SSID, IP, RSSI, etc.) esp_err_t wifi_cfg_get_status(wifi_status_t *status); // Block until connected or timeout (0 = wait forever) esp_err_t wifi_cfg_wait_connected(uint32_t timeout_ms); ``` ## Events The push half of the section above: `wifi_cfg_get_status()` answers when you ask, events tell you when something changes. The library publishes them on ESP-IDF's **default event loop** under its own base, alongside `WIFI_EVENT` and `IP_EVENT`. ```c // The event base. Declared in esp_wifi_config.h; register against it as you // would for any IDF event source. ESP_EVENT_DECLARE_BASE(WIFI_CFG_EVENT); // Every event id the library posts, as wifi_cfg_event_t. // WIFI_CFG_EVENT_MAX is the count, not an event. esp_event_handler_register(WIFI_CFG_EVENT, WIFI_CFG_EVENT_GOT_IP, on_got_ip, NULL); // ESP_EVENT_ANY_ID for a catch-all. esp_event_handler_register(WIFI_CFG_EVENT, ESP_EVENT_ANY_ID, on_any, NULL); // Human-readable name for an event id, for logging. // Static string, "unknown" if out of range. Never NULL. const char *wifi_cfg_event_name(wifi_cfg_event_t event); ``` `wifi_cfg_init()` creates the default loop itself. Call `esp_event_loop_create_default()` and register first only if you need to catch events emitted *during* init; creating it twice is harmless. Handlers run on the system event loop task, shared with IDF's own networking callbacks — keep them short and do heavy work on your own task. **[Events guide](../guides/events.md)** has the full id-to-payload table, the handler signature, and worked examples. It is not repeated here: one table that drifts is worse than one table that is looked up. ## Connection ```c // Connect to a specific SSID, or pass NULL for auto-connect (highest priority) esp_err_t wifi_cfg_connect(const char *ssid); // Disconnect from the current network esp_err_t wifi_cfg_disconnect(void); // Scan for available WiFi networks esp_err_t wifi_cfg_scan(wifi_scan_result_t *results, size_t max, size_t *count); ``` ## Network Management ```c // Add a new network to NVS esp_err_t wifi_cfg_add_network(const wifi_network_t *network); // Update an existing network (matched by SSID) esp_err_t wifi_cfg_update_network(const wifi_network_t *network); // Remove a network from NVS esp_err_t wifi_cfg_remove_network(const char *ssid); // Look one up by SSID. ESP_ERR_NOT_FOUND if it is not stored. esp_err_t wifi_cfg_get_network(const char *ssid, wifi_network_t *network); // List all saved networks esp_err_t wifi_cfg_list_networks(wifi_network_t *networks, size_t max, size_t *count); ``` ## SoftAP ```c // Start SoftAP with the given config, or NULL for defaults esp_err_t wifi_cfg_start_ap(const wifi_cfg_ap_config_t *config); // Stop SoftAP esp_err_t wifi_cfg_stop_ap(void); // Get AP status (active, SSID, IP, connected clients) esp_err_t wifi_cfg_get_ap_status(wifi_ap_status_t *status); // Read the stored SoftAP config esp_err_t wifi_cfg_get_ap_config(wifi_cfg_ap_config_t *config); // Update it and persist to NVS. Applied immediately if the AP is running. esp_err_t wifi_cfg_set_ap_config(const wifi_cfg_ap_config_t *config); ``` `wifi_cfg_start_ap()` takes a config for one run; `wifi_cfg_set_ap_config()` stores one. Use the setter when the change should outlive a reboot. ## Custom Variables ```c // Set a key-value variable (creates or updates in NVS) esp_err_t wifi_cfg_set_var(const char *key, const char *value); // Get a variable value esp_err_t wifi_cfg_get_var(const char *key, char *value, size_t max_len); // Delete a variable from NVS esp_err_t wifi_cfg_del_var(const char *key); ``` ## Factory Reset ```c // Erase all saved networks, variables, and AP config from NVS esp_err_t wifi_cfg_factory_reset(void); ``` ## HTTP Server ```c // Get the HTTP server handle (for registering custom endpoints) httpd_handle_t wifi_cfg_get_httpd(void); // Stop the HTTP server (only if library-owned and provisioning not active) esp_err_t wifi_cfg_stop_http(void); ``` ## Configuration Struct The `wifi_cfg_config_t` struct controls all behavior. **Always start from `WIFI_CFG_DEFAULTS`** — see [Starting from the defaults](#defaults) below for why, and for the full list of what the macro sets. ```c wifi_cfg_config_t config = { WIFI_CFG_DEFAULTS, // Default networks (seed data for first boot when NVS is empty) .default_networks = networks, .default_network_count = 2, // Default variables (seed data for first boot) .default_vars = (wifi_var_t[]){ {"server_url", "https://api.example.com"}, {"device_name", "my-device"}, }, .default_var_count = 2, // SoftAP config ({id} = last 3 bytes of MAC). A designated initialiser // replaces the whole sub-struct; wifi_cfg_init() backfills the rest. .default_ap = { .ssid = "MyDevice-{id}", }, // Provisioning behavior. provisioning_mode is already // WIFI_PROV_ON_FAILURE from the macro. .stop_provisioning_on_connect = true, .provisioning_teardown_delay_ms = 5000, .enable_ap = true, // Reconnect exhaustion. on_reconnect_exhausted is already // WIFI_ON_RECONNECT_EXHAUSTED_RESTART from the macro — // _PROVISION is currently disabled (treated as infinite retry). .max_reconnect_attempts = 10, // 0 = infinite (the default) // HTTP post-provisioning mode (the macro sets WIFI_HTTP_FULL) .http_post_prov_mode = WIFI_HTTP_API_ONLY, // HTTP interface. api_base_path ("/api/wifi") and auth_username // ("admin") already come from the macro. .http = { .enable_auth = true, .auth_password = "secret", }, // ESP-IDF Network Provisioning over BLE // (requires CONFIG_WIFI_CFG_ENABLE_NETWORK_PROVISIONING=y; mutually // exclusive with Improv BLE). Omitted here and left at their defaults: // device_name ("PROV_{id}"), security (Security 1), // memory_policy (FREE_BTDM), max_failed_attempts (3), // cleanup_delay_ms (1000), reboot_max_wait_ms (15000). .prov_ble = { .pop = "1234abcd", // Security 1 PoP .wifi_conn_attempts = 5, // 0 = infinite (legacy default) .reset_on_failure = true, // accept retries without reboot .firmware_version = "1.0.0", }, // Improv WiFi (transports gated by Kconfig: CONFIG_WIFI_CFG_ENABLE_IMPROV_BLE / _SERIAL). // `ble_device_name` is the BLE GAP advertised name (what scanners show); // `device_name` is the human-readable name reported via the Improv // Device-Info RPC (what the Improv companion app shows after connect). .improv = { .ble_device_name = "ESP32-WiFi-{id}", // also the Kconfig default .firmware_name = "my_project", .firmware_version = "1.0.0", .device_name = "My Device", .on_identify = my_identify_callback, }, }; wifi_cfg_init(&config); ``` ## Starting from the defaults {#defaults} `wifi_cfg_init()` does **not** patch fields you leave at zero. Two macros in `esp_wifi_config.h` supply the documented defaults as a value: ```c // Compound-literal style — the house style in the examples wifi_cfg_init(&(wifi_cfg_config_t){ WIFI_CFG_DEFAULTS, .enable_ap = true, }); // Struct-value style, when you want to compute fields wifi_cfg_config_t cfg = WIFI_CFG_DEFAULT_CONFIG(); cfg.enable_ap = true; cfg.auto_reconnect = false; // means false, because you started here wifi_cfg_init(&cfg); // Or take them unmodified wifi_cfg_init(NULL); ``` What the macros set: | Field | Default | |-------|---------| | `max_retry_per_network` | `CONFIG_WIFI_CFG_DEFAULT_RETRY` (3) | | `retry_interval_ms` | `CONFIG_WIFI_CFG_RETRY_INTERVAL_MS` (5000) | | `retry_max_interval_ms` | 60000 | | `auto_reconnect` | `true` | | `max_reconnect_attempts` | 0 (retry forever) | | `on_reconnect_exhausted` | `WIFI_ON_RECONNECT_EXHAUSTED_RESTART` | | `provisioning_mode` | `WIFI_PROV_ON_FAILURE` | | `http_post_prov_mode` | `WIFI_HTTP_FULL` | | `default_ap.ssid` | `WIFI_CFG_DEFAULT_AP_SSID` (`"ESP32-Config"`) | | `default_ap.password` | `WIFI_CFG_DEFAULT_AP_PASSWORD` (`""` — open) | | `default_ap.max_connections` | 4 | | `default_ap.ip` / `.gateway` | `WIFI_CFG_DEFAULT_AP_IP` (`"192.168.4.1"`) | | `default_ap.netmask` | `"255.255.255.0"` | | `default_ap.dhcp_start` / `.dhcp_end` | `"192.168.4.2"` / `"192.168.4.20"` | | `http.api_base_path` | `"/api/wifi"` | | `http.auth_username` / `.auth_password` | `"admin"` / `"admin"` | | `improv.serial_uart_num` | `CONFIG_WIFI_MGR_IMPROV_SERIAL_UART_NUM` (0) | | `improv.serial_baud_rate` | `CONFIG_WIFI_MGR_IMPROV_SERIAL_BAUD` (115200) | | `prov_ble.cleanup_delay_ms` | 1000 | | `prov_ble.reboot_max_wait_ms` | 15000 | | `prov_ble.max_failed_attempts` | 3 | Everything not listed defaults to zero / `false` / `NULL`, which is the intended value for that field. :::warning `wifi_cfg_init()` returns `ESP_ERR_INVALID_ARG` if `retry_interval_ms` or `retry_max_interval_ms` is zero — `retry_interval_ms << retry` is the backoff, so a zero base retries with no delay at all. Building a config without `WIFI_CFG_DEFAULTS` is the usual way to hit this. ::: :::note A designated initialiser for a nested struct replaces the **whole** sub-struct, so `{ WIFI_CFG_DEFAULTS, .default_ap = {.ssid = "x"} }` blanks the other `default_ap` fields. `wifi_cfg_init()` backfills the per-field AP defaults for exactly this reason; set members individually (`cfg.default_ap.ssid`) if you want to be certain. The same applies to `.http`, `.improv` and `.prov_ble`. ::: ## Network Provisioning configuration (`wifi_cfg_prov_config_t`) {#prov-network-provisioning} The `.prov_ble` sub-struct carries every runtime parameter for the ESP-IDF `wifi_provisioning` manager (BLE scheme). Only `CONFIG_WIFI_CFG_ENABLE_NETWORK_PROVISIONING` and `CONFIG_WIFI_CFG_NETWORK_PROVISIONING_BLE` are set via Kconfig; everything below is plain runtime configuration. Zero/NULL fields fall back to the library defaults documented in the table. | Field | Purpose | |-------|---------| | `device_name` | BLE GAP advertised name template. Supports `{id}` (expanded to the last 3 bytes of the STA MAC). NULL → `"PROV_{id}"`. | | `service_uuid128` | Optional 16-byte 128-bit GATT service UUID. NULL → IDF default. Espressif recommends a product-specific UUID. | | `manufacturer_data` / `_len` | Optional bytes appended to the BLE scan response. Total must fit in 31 bytes alongside the device name. | | `random_addr` | Optional 6-byte static random BLE address. | | `security` | `WIFI_CFG_PROV_SECURITY_{0,1,2,DEFAULT}`. DEFAULT → Security 1. | | `pop` | Security 1 proof-of-possession. NULL or empty → no PoP. | | `security2_username` | Security 2 SRP6a username. Metadata only — the username never flows into `wifi_prov_mgr` from the device side, so no default is substituted. | | `security2_salt` / `_verifier` (+ lens) | Pre-computed SRP6a parameters. Required when Security 2 is selected — `wifi_cfg_init()` returns `ESP_ERR_INVALID_ARG` if missing. | | `memory_policy` | Bluetooth memory cleanup policy on provisioning deinit. See below. | | `keep_ble_on_after_stop` | If true, BLE stays advertising after the manager stops. Useful when the app takes over BLE post-provisioning. | | `cleanup_delay_ms` | Grace period the manager observes between stop and protocomm teardown. 0 → 1000 ms. Min 100 ms. | | `wifi_conn_attempts` | STA connection attempts before CRED_FAIL. 0 → infinite (legacy default). A bounded value gives the manager a chance to report failure cleanly. | | `stop_after_success` | Stop the manager on CRED_SUCCESS even when `stop_provisioning_on_connect` is false (useful in MANUAL mode). Ignored while reboot-on-success is active — the reboot supersedes any in-place stop. | | `disable_reboot_on_provisioning_success` | **Default false** (reboot enabled). Set true only when the app handles the BLE/Wi-Fi handoff itself. See [Reboot on successful provisioning](../provisioning/ble-gatt.md#reboot-on-successful-provisioning). | | `reboot_max_wait_ms` | Backstop window after CRED_SUCCESS before the forced reboot, in ms. 0 → 15000 ms. Must comfortably exceed the client's status-poll interval (~5 s for Espressif's SDK) plus associate + DHCP, or the client can report a false failure. Ignored when `disable_reboot_on_provisioning_success` is true. | | `reset_on_failure` | If true, reset the state machine after `max_failed_attempts` consecutive credential failures so a fresh attempt can be accepted without rebooting. | | `max_failed_attempts` | Threshold used when `reset_on_failure` is true. 0 → library default (3). | | `firmware_version` | Surfaced via the built-in `esp-wifi-config-version` endpoint. | | `app_infos` / `_count` | Optional metadata published via the standard `proto-ver` endpoint (label "prov" is reserved). | | `custom_endpoints` / `_count` | Additional protocomm endpoints registered alongside the library's four built-in endpoints. | | `on_credentials_received` | Callback invoked when the client sends WiFi credentials. Receives `wifi_cfg_prov_creds_t *`. | | `on_credentials_failed` | Callback invoked on STA connect failure. Receives the `wifi_prov_sta_fail_reason_t` value as `int`. | | `on_credentials_success` | Callback invoked when STA connects with the supplied credentials. | | `event_ctx` | User pointer passed to every callback above. | All three credential callbacks also fire as library events (`WIFI_CFG_EVENT_PROV_CRED_RECV`, `_FAIL`, `_SUCCESS`) — pick whichever path fits the app. ### Bluetooth memory policy The wifi_provisioning manager hooks the Bluetooth controller's `disable` event and can release controller/host memory at deinit time. Pick the policy that matches what the rest of the application needs from Bluetooth **after** provisioning ends: | Policy | Frees | Use when | |--------|-------|----------| | `WIFI_CFG_PROV_MEM_FREE_BTDM` (default) | Classic BT + BLE | The device does not use Bluetooth post-provisioning. Reclaims the most RAM. | | `WIFI_CFG_PROV_MEM_FREE_BLE` | BLE only | The app still needs **Classic BT** (A2DP, SPP, HFP, etc.). Only valid on chips that support Classic BT (ESP32). | | `WIFI_CFG_PROV_MEM_FREE_BT` | Classic BT only | The app still needs **BLE** (custom GATT service, beacon, scanner). | | `WIFI_CFG_PROV_MEM_KEEP_ALL` | Nothing | The app brought up the BLE/BT stack itself before `wifi_cfg_init()` and owns the lifecycle. | The library auto-detects the "app already owns the stack" case (BT controller already enabled at start time) and forces `KEEP_ALL` with a log warning. Setting the wrong policy crashes the app — picking `FREE_BTDM` and then calling a Classic BT function afterwards will fault inside the controller. If your firmware uses **Classic Bluetooth (A2DP, SPP, HFP, etc.)** after WiFi is provisioned, set `memory_policy = WIFI_CFG_PROV_MEM_FREE_BLE` explicitly — the default reclaims Classic BT memory and will break those profiles. Likewise, the BLE-keep-alive pattern (custom GATT service or scanning after provisioning) needs `WIFI_CFG_PROV_MEM_FREE_BT` or `KEEP_ALL` to avoid freeing BLE host state. ================================================================================ REST API Reference Source: https://configwifi.com/docs/api/rest-api ================================================================================ # REST API Reference Base URL: `http:///api/wifi` (configurable via `api_base_path`) ## Authentication If `enable_auth = true` in the HTTP config, all endpoints require HTTP Basic Auth: ```bash curl -u admin:password http://192.168.4.1/api/wifi/status ``` ## Endpoints | Method | Endpoint | Description | |---|---|---| | GET | `/status` | Get connection status | | GET | `/scan` | Scan available networks | | GET | `/networks` | List saved networks | | POST | `/networks` | Add new network | | PUT | `/networks/:ssid` | Update network | | DELETE | `/networks/:ssid` | Remove network | | POST | `/connect` | Connect (auto or specific SSID) | | POST | `/disconnect` | Disconnect | | GET | `/ap/status` | Get AP status | | GET | `/ap/config` | Get AP configuration | | PUT | `/ap/config` | Update AP configuration | | POST | `/ap/start` | Start SoftAP | | POST | `/ap/stop` | Stop SoftAP | | GET | `/vars` | List custom variables | | PUT | `/vars/:key` | Set variable | | DELETE | `/vars/:key` | Delete variable | | POST | `/factory_reset` | Factory reset | ## Example Requests ### Get Status ```bash curl http://192.168.4.1/api/wifi/status ``` Response: ```json { "state": "connected", "ssid": "MyWiFi", "ip": "192.168.1.100", "gateway": "192.168.1.1", "netmask": "255.255.255.0", "dns": "192.168.1.1", "rssi": -65, "quality": 70, "channel": 6, "mac": "AA:BB:CC:DD:EE:FF", "hostname": "esp32-aabbcc", "uptime_ms": 123456, "ap_active": false } ``` `state` is one of: `"connected"`, `"connecting"`, `"disconnected"`. ### Scan Networks ```bash curl http://192.168.4.1/api/wifi/scan ``` Response: ```json { "networks": [ {"ssid": "MyWiFi", "rssi": -65, "auth": "WPA2"}, {"ssid": "Neighbor", "rssi": -80, "auth": "WPA/WPA2"}, {"ssid": "OpenNet", "rssi": -70, "auth": "OPEN"} ] } ``` `auth` is one of: `"OPEN"`, `"WEP"`, `"WPA"`, `"WPA2"`, `"WPA3"`, `"WPA/WPA2"`, `"UNKNOWN"`. ### Add Network ```bash curl -X POST http://192.168.4.1/api/wifi/networks \ -H "Content-Type: application/json" \ -d '{"ssid": "MyWiFi", "password": "secret123", "priority": 10}' ``` ### Connect ```bash # Connect to a specific network curl -X POST http://192.168.4.1/api/wifi/connect \ -H "Content-Type: application/json" \ -d '{"ssid": "MyWiFi"}' # Auto-connect (highest priority saved network) curl -X POST http://192.168.4.1/api/wifi/connect ``` ### Delete Network ```bash curl -X DELETE http://192.168.4.1/api/wifi/networks/MyWiFi ``` ### AP Status ```bash curl http://192.168.4.1/api/wifi/ap/status ``` Response: ```json { "active": true, "ssid": "ESP32-AABBCC", "ip": "192.168.4.1", "channel": 1, "sta_count": 2, "clients": [ {"mac": "AA:BB:CC:DD:EE:01", "ip": "192.168.4.2"}, {"mac": "AA:BB:CC:DD:EE:02", "ip": "192.168.4.3"} ] } ``` ### Custom Variables ```bash # List all variables curl http://192.168.4.1/api/wifi/vars # Set a variable curl -X PUT http://192.168.4.1/api/wifi/vars/device_name \ -H "Content-Type: application/json" \ -d '{"value": "Living Room"}' # Delete a variable curl -X DELETE http://192.168.4.1/api/wifi/vars/device_name ``` Variables list response: ```json { "vars": [ {"key": "server_url", "value": "https://api.example.com"}, {"key": "device_name", "value": "My ESP32"} ] } ``` ### Factory Reset ```bash curl -X POST http://192.168.4.1/api/wifi/factory_reset ``` ## Error Responses All errors return a JSON object with an `error` field: ```json {"error": "Error message"} ``` | HTTP Code | Description | |---|---| | 400 | Bad Request — Invalid JSON, missing required field | | 401 | Unauthorized — Authentication required | | 404 | Not Found — Network or variable does not exist | | 500 | Internal Error — Operation failed | ## CORS The REST API includes CORS headers, allowing browser-based clients to access the endpoints directly. ================================================================================ BLE Protocol Reference Source: https://configwifi.com/docs/api/ble-protocol ================================================================================ # BLE Protocol Reference :::caution Removed in 0.1.0 The custom JSON-over-GATT BLE service (UUID `0xFFE0` / characteristics `0xFFE1`–`0xFFE3`) that this page used to document has been **removed** in favour of ESP-IDF's official Wi-Fi Provisioning protocol. See [MIGRATION.md][migrate] for the protocol-level migration plan and the steps for updating downstream client tools. ::: [migrate]: https://github.com/thorrak/esp_wifi_config/blob/main/MIGRATION.md ## What runs over BLE now The library wraps Espressif's `wifi_prov_mgr` with the BLE scheme. The on-air protocol is the same one Espressif's official mobile apps speak, so any of the following work out of the box: - **iOS / Android**: "ESP BLE Provisioning" by Espressif Systems - **Python**: `esp_prov` tool (in `tools/esp_prov/` of any IDF checkout) - **Custom apps**: `esp-idf-provisioning-android` / `esp-idf-provisioning-ios` SDKs For the full Wi-Fi Provisioning over BLE specification (advertising format, GAP service UUIDs, protocomm framing) see [Espressif's docs][espressif-prov]. [espressif-prov]: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/provisioning/provisioning.html ## Library-specific protocomm endpoints Alongside the standard `prov-config`, `prov-scan`, `prov-session`, etc., the library registers four custom endpoints. These give the provisioning client read access to the higher-level state the library maintains, and read/write access to the custom variable store. | Endpoint | Direction | Notes | |----------|-----------|-------| | `esp-wifi-config-version` | read | JSON: `{lib, idf, app, fw_version, chip}` | | `esp-wifi-config-capabilities` | read | JSON: `{capabilities[], max_networks, max_vars}` | | `esp-wifi-config-vars` | read/write | JSON request, see schema below | | `esp-wifi-config-network-policy` | read | JSON: `{provisioning_mode, retries, …}`. `provisioning_mode` is a **string** (`"on_failure"`, `"when_unprovisioned"`, `"manual"`, `"always"`), never the raw enum value — so the 0.2.0 enum renumbering does not affect clients. | ### `esp-wifi-config-vars` request/response schema ```jsonc // list every saved variable → {"op": "list"} ← {"vars": [{"k": "server_url", "v": "..."}, …]} // fetch one → {"op": "get", "key": "server_url"} ← {"key": "server_url", "value": "..."} // upsert → {"op": "set", "key": "server_url", "value": "https://api.example.com"} ← {"ok": true} // delete → {"op": "del", "key": "server_url"} ← {"ok": true} ``` Errors are returned as `{"error": ""}` (e.g. `not_found`, `store_full`, `bad_json`, `unknown_op`). ## What was intentionally **not** ported The old custom protocol exposed a number of pre-Wi-Fi management operations directly over BLE (`scan`, `add_network`, `connect`, `start_ap`, `factory_reset`, etc.). Most of those are already covered by the standard provisioning protocol (`prov-scan` and `prov-config`); the remainder (factory reset, AP control) are intentionally left to the HTTP/REST API or local UI — the BLE provisioning surface is meant to get the device on the network, not act as a full management backdoor. If your application needs a richer command surface during provisioning, register additional protocomm endpoints by extending `esp_wifi_config_prov_ble.c`. ================================================================================ CLI Commands Source: https://configwifi.com/docs/api/cli-commands ================================================================================ # CLI Commands ESP WiFi Config provides a set of serial console commands via the ESP-IDF Console component. These are useful for development, debugging, and headless device configuration. ## Enabling the CLI Add to your `sdkconfig.defaults`: ```kconfig CONFIG_WIFI_CFG_ENABLE_CLI=y ``` Your application must initialize the ESP Console REPL. See the [with_cli example](https://github.com/thorrak/esp_wifi_config/tree/main/examples/with_cli) for a complete setup. ## Commands | Command | Description | |---|---| | `wifi status` | Show connection status | | `wifi scan` | Scan available networks | | `wifi list` | List saved networks | | `wifi add [password] [priority]` | Add network | | `wifi del ` | Remove network | | `wifi connect [ssid]` | Connect (auto or specific) | | `wifi disconnect` | Disconnect | | `wifi ap start` | Start SoftAP | | `wifi ap stop` | Stop SoftAP | | `wifi reset` | Factory reset | | `wifi var get ` | Get variable | | `wifi var set ` | Set variable | ================================================================================ Kconfig Options Source: https://configwifi.com/docs/api/kconfig ================================================================================ # Kconfig Options Configure via `idf.py menuconfig` → WiFi Config, or set in `sdkconfig.defaults`. ## Core Options | Option | Default | Description | |---|---|---| | `CONFIG_WIFI_CFG_MAX_NETWORKS` | 5 | Maximum number of saved networks | | `CONFIG_WIFI_CFG_MAX_VARS` | 10 | Maximum number of custom variables | | `CONFIG_WIFI_CFG_DEFAULT_RETRY` | 3 | Retries per network before moving to next | | `CONFIG_WIFI_CFG_RETRY_INTERVAL_MS` | 5000 | Base retry interval in milliseconds | ## CLI | Option | Default | Description | |---|---|---| | `CONFIG_WIFI_CFG_ENABLE_CLI` | n | Enable serial console CLI commands | ## Web UI | Option | Default | Description | |---|---|---| | `CONFIG_WIFI_CFG_ENABLE_WEBUI` | n | Enable the embedded Web UI | | `CONFIG_WIFI_CFG_WEBUI_CUSTOM_PATH` | "" | Path to custom frontend files (LittleFS/SPIFFS) | ## Network Provisioning (BLE) Only two Kconfig symbols gate Network Provisioning — everything else (security version, PoP, device name template, SRP6a username, auto-reset behaviour, retry threshold) is plain runtime configuration on `wifi_cfg_prov_config_t`. See [C API → `.prov_ble`](./c-api#prov-network-provisioning). | Option | Default | Description | |---|---|---| | `CONFIG_WIFI_CFG_ENABLE_NETWORK_PROVISIONING` | n | Enable ESP-IDF Wi-Fi/Network Provisioning manager | | `CONFIG_WIFI_CFG_NETWORK_PROVISIONING_BLE` | y | Use the BLE scheme (currently the only transport supported by this library) | The previous custom BLE GATT option (`WIFI_CFG_ENABLE_CUSTOM_BLE`) has been **removed** in 0.1.0. See [MIGRATION.md][migrate] for upgrade notes. [migrate]: https://github.com/thorrak/esp_wifi_config/blob/main/MIGRATION.md ## Improv WiFi | Option | Default | Description | |---|---|---| | `CONFIG_WIFI_CFG_ENABLE_IMPROV_BLE` | n | Enable Improv BLE transport (mutually exclusive with Network Provisioning) | | `CONFIG_WIFI_CFG_ENABLE_IMPROV_SERIAL` | n | Enable Improv Serial transport | | `CONFIG_WIFI_CFG_IMPROV_SERIAL_UART_NUM` | 0 | UART port for Improv Serial | | `CONFIG_WIFI_CFG_IMPROV_SERIAL_BAUD` | 115200 | Baud rate for Improv Serial | ## Common sdkconfig.defaults Combinations ### Basic WiFi (no extra features) ```kconfig # No extra config needed — defaults work ``` ### WiFi + Web UI ```kconfig CONFIG_WIFI_CFG_ENABLE_WEBUI=y ``` ### WiFi + Network Provisioning over BLE (NimBLE, recommended) ```kconfig CONFIG_BT_ENABLED=y CONFIG_BT_NIMBLE_ENABLED=y CONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=6144 CONFIG_WIFI_CFG_ENABLE_NETWORK_PROVISIONING=y CONFIG_WIFI_CFG_NETWORK_PROVISIONING_BLE=y CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y ``` Pair this sdkconfig with the runtime parameters (security version, PoP, etc.) in your `wifi_cfg_init()` call — see the [C API reference](./c-api#prov-network-provisioning). ### WiFi + Network Provisioning over BLE (Bluedroid) ```kconfig CONFIG_BT_ENABLED=y CONFIG_BT_BLUEDROID_ENABLED=y CONFIG_WIFI_CFG_ENABLE_NETWORK_PROVISIONING=y CONFIG_WIFI_CFG_NETWORK_PROVISIONING_BLE=y CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y ``` ### WiFi + Improv BLE Only ```kconfig CONFIG_BT_ENABLED=y CONFIG_BT_BLUEDROID_ENABLED=y CONFIG_WIFI_CFG_ENABLE_IMPROV_BLE=y CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y ``` ### WiFi + Improv Serial + Network Provisioning BLE ```kconfig # Improv Serial is independent of BLE and safe to combine with Network Provisioning. CONFIG_BT_ENABLED=y CONFIG_BT_NIMBLE_ENABLED=y CONFIG_WIFI_CFG_ENABLE_NETWORK_PROVISIONING=y CONFIG_WIFI_CFG_NETWORK_PROVISIONING_BLE=y CONFIG_WIFI_CFG_ENABLE_IMPROV_SERIAL=y CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y ``` ### Kitchen Sink (CLI + WebUI + Network Provisioning + Improv Serial) ```kconfig CONFIG_BT_ENABLED=y CONFIG_BT_NIMBLE_ENABLED=y CONFIG_WIFI_CFG_ENABLE_CLI=y CONFIG_WIFI_CFG_ENABLE_WEBUI=y CONFIG_WIFI_CFG_ENABLE_NETWORK_PROVISIONING=y CONFIG_WIFI_CFG_NETWORK_PROVISIONING_BLE=y CONFIG_WIFI_CFG_ENABLE_IMPROV_SERIAL=y CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y ``` ================================================================================ Examples Source: https://configwifi.com/docs/examples ================================================================================ # Examples All examples are complete ESP-IDF projects you can build and flash directly. Each includes a `main.c`, `CMakeLists.txt`, `sdkconfig.defaults`, and `idf_component.yml`. ```bash cd examples/ idf.py set-target esp32s3 idf.py build idf.py -p /dev/ttyUSB0 flash monitor ``` --- ## [basic](https://github.com/thorrak/esp_wifi_config/tree/main/examples/basic) Minimal setup with default networks, REST API, and SoftAP captive portal. This is the best starting point — it shows event subscriptions, custom variables, and the provisioning-on-failure pattern with no optional features enabled. --- ## [with_cli](https://github.com/thorrak/esp_wifi_config/tree/main/examples/with_cli) Adds serial console commands (`wifi status`, `wifi scan`, `wifi add`, etc.). Requires initializing the ESP Console REPL in your app code — the library registers its commands automatically when `CONFIG_WIFI_CFG_ENABLE_CLI=y`. Uses USB Serial JTAG console on ESP32-S3. --- ## [with_webui](https://github.com/thorrak/esp_wifi_config/tree/main/examples/with_webui) Enables the embedded Preact Web UI (~10KB gzipped) with `CONFIG_WIFI_CFG_ENABLE_WEBUI=y`. No additional code needed — the Web UI is served automatically at the device's IP (or `192.168.4.1` in AP mode). Supports dark mode and captive portal auto-open. --- ## [with_webui_customize](https://github.com/thorrak/esp_wifi_config/tree/main/examples/with_webui_customize) Serves a custom frontend from a LittleFS partition instead of the embedded UI. Requires a custom partition table with a 512 KB LittleFS partition and `CONFIG_WIFI_CFG_WEBUI_CUSTOM_PATH="/littlefs"`. With the custom path set, the embedded Preact assets are excluded from the build — your filesystem image must provide `index.html`, `assets/app.js` (or `.js.gz`), and `assets/index.css` (or `.css.gz`). See the [Custom Web UI guide](./guides/custom-webui.md) for the full workflow (partition table, Vite config, gzip handling, captive-portal interaction). --- ## [with_ble](https://github.com/thorrak/esp_wifi_config/tree/main/examples/with_ble) BLE provisioning using ESP-IDF's official Wi-Fi Provisioning manager (NimBLE host, Security 1 + Proof-of-Possession). Provision via the "ESP BLE Provisioning" mobile app (iOS / Android), the `esp_prov` Python tool, or any client built on the `esp-idf-provisioning-{android,ios}` SDKs. Requires `CONFIG_BT_ENABLED=y`, a NimBLE or Bluedroid host, and `CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y` (BLE adds ~100 KB flash). --- ## [with_ble_deinit](https://github.com/thorrak/esp_wifi_config/tree/main/examples/with_ble_deinit) Demonstrates app-owned BLE stack with NimBLE using the **Improv BLE** host bootstrap. The app initialises NimBLE first, then WiFi Config registers only its Improv GATT service (service-only mode). After provisioning, `wifi_cfg_deinit()` removes the WiFi Config service while NimBLE stays running for the app's own BLE services. Network Provisioning BLE manages its own host lifecycle and isn't suitable for this particular handoff pattern. --- ## [with_improv](https://github.com/thorrak/esp_wifi_config/tree/main/examples/with_improv) Improv WiFi standard provisioning over BLE and (optionally) Serial. Supports Web Bluetooth (Chrome/Edge at [improv-wifi.com](https://www.improv-wifi.com/)), the ESPHome companion app, and Web Serial. Mutually exclusive with Network Provisioning BLE — pick one BLE protocol per firmware build. Improv Serial is independent of BLE and remains safe alongside Network Provisioning.