# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
## [Unreleased]
## [0.2.1] — 2026-08-22 - The SoftAP survives BLE provisioning
### Fixed
- **The SoftAP no longer disappears when BLE provisioning starts.**
`wifi_cfg_start_provisioning()` raises the SoftAP and then starts ESP-IDF's
`wifi_prov_mgr`, whose `wifi_prov_mgr_start_provisioning()` calls
`esp_wifi_set_mode(WIFI_MODE_STA)` unconditionally — "necessary for scanning
to work" — and takes the AP with it about twelve milliseconds later. The
manager has no way to know an AP was meant to stay up; this library is the
only layer that knows both were started, so reconciling is its job. With
`enable_ap = true` and Network Provisioning both enabled, the captive portal
simply never appeared, and the library logged the teardown at INFO and
carried on with a portal DNS server bound to an interface that no longer
existed.
IDF forces station-only mode in three places, not one — at start, when
credentials arrive, and when provisioning stops — so a single re-assert
after startup would have fixed the reported symptom and left the AP
disappearing mid-provisioning instead. The library now reconciles on the
`AP_STOP` it already observes, bounded to three restarts per provisioning
session, and logs a warning each time saying what took the AP down.
- **A shared HTTP server that runs out of URI slots now says so.** The twenty
`httpd_register_uri_handler()` calls behind the REST API discarded their
return value. On a server the library creates this cannot happen — it sizes
its own — but on one passed in through `config.http.httpd` it happens
readily: `HTTPD_DEFAULT_CONFIG()` allows eight handlers and these routes need
about twenty-two. Registration stopped partway with nothing logged, so the
device answered `/api/wifi/status` and 404'd `/api/wifi/scan`, which reads as
a library that half works. `wifi_cfg_http_register_api_handlers()` now
returns `ESP_ERR_NO_MEM` and logs how many routes were lost and why.
### Added
- **`examples/with_shared_httpd`** — the `config.http.httpd` flow as a
buildable project. It is the only configuration where the application
touches the network before `wifi_cfg_init()`, which makes three things its
responsibility: `esp_netif_init()`, a large enough `max_uri_handlers`, and
`httpd_uri_match_wildcard`. Every other example leaves `.http.httpd` unset,
so nothing in CI built this path until now.
### Documentation
- The header's `config.http.httpd` field, the HTTP-sharing guide and the
Kconfig help all now state the requirements above.
- Three code samples cast event payloads to types that do not exist —
`wifi_got_ip_t` in the Quick Start and the AI integration guide,
`wifi_var_changed_t` in the custom variables guide. They now use
`esp_netif_ip_info_t` and `wifi_var_t`, which is what the header documents.
- `esp_wifi_config.h` carried 66 lines of Vietnamese documentation, including
the field comments an IDE shows on hover. Translated, and the seven event
names among them corrected — they referred to `WIFI_EVENT_*`, which is
ESP-IDF's own base, rather than this library's `WIFI_CFG_EVENT_*`.
## [0.2.0] — 2026-08-22 - Events on esp_event, defaults as a value
### Breaking Changes
- **The `esp_bus` dependency is removed; events move to `esp_event`.**
The library now publishes its events on ESP-IDF's default event loop
under a new base, `WIFI_CFG_EVENT` — the same loop it already consumed
`WIFI_EVENT` and `IP_EVENT` from, and which it creates itself in
`wifi_cfg_init()`. Register with `esp_event_handler_register()`, using
`ESP_EVENT_ANY_ID` for a catch-all; handlers take the standard
esp_event signature. Each `WIFI_CFG_EVT_<NAME>` string macro becomes a
`WIFI_CFG_EVENT_<NAME>` enumerator of the new `wifi_cfg_event_t`.
`WIFI_EVT()`, `WIFI_REQ()`, `WIFI_MODULE` and every `WIFI_ACTION_*`
macro were removed — the bus's action surface was a string-dispatch
shim over the public C API, so call the matching `wifi_cfg_*` function
directly. See MIGRATION.md.
To catch events emitted during startup, call
`esp_event_loop_create_default()` and register before
`wifi_cfg_init()`; creating the loop twice is harmless.
Delivery stays asynchronous, so handlers do not run on the WiFi state
machine's task or on the httpd task. One failure mode is fixed:
`esp_bus_emit()` enqueued with a zero timeout and all seventeen call
sites ignored the error, so a full queue dropped events silently. The
library still posts with a zero timeout — blocking the state machine
would be worse — but now checks the result and logs a warning naming
the event.
Measured on esp32s3 / IDF 5.5.3: **~5.9 KB of flash** saved across
every example configuration (`basic` 855,120 → 849,232 bytes) and
about **5.4 KB of heap**, since `esp_bus` ran a 4 KB-stack task and a
16 × 64-byte queue while `esp_event`'s loop already exists and is
already paid for. Static RAM is unchanged. The library also drops from
one third-party dependency to none.
- **Every `wifi_cfg_config_t` initialiser must now start from
`WIFI_CFG_DEFAULTS`.** The new `WIFI_CFG_DEFAULTS` (a
designated-initialiser list) and `WIFI_CFG_DEFAULT_CONFIG()` (the same
thing as a complete struct value) carry every documented default, so
the defaults exist as a *value* rather than as init-time patching.
Passing `NULL` to `wifi_cfg_init()` uses them unmodified.
- **`wifi_cfg_init()` no longer patches unset fields, and rejects a
zero backoff.** It used to rewrite `max_retry_per_network`,
`retry_interval_ms` and `retry_max_interval_ms` when they were zero.
Patching is the only way to express "unset" without an initialiser,
and it works for a scalar whose zero is nonsensical while failing for
everything else — a `bool` has no spare value, so `auto_reconnect`
could not be defaulted at all. Zero now means zero;
`retry_interval_ms == 0` or `retry_max_interval_ms == 0` returns
`ESP_ERR_INVALID_ARG` (`retry_interval_ms << retry` is the backoff, so
a zero base retries with no delay), and `max_retry_per_network == 0`
logs a warning.
- **`wifi_provisioning_mode_t` and `wifi_reconnect_exhausted_action_t`
renumbered.** Both had a `[DISABLED]` member on zero, so an omitted
field selected the one value that does nothing. The working value is
now zero: `WIFI_PROV_ON_FAILURE` (0), `WIFI_PROV_WHEN_UNPROVISIONED`
(1), `WIFI_PROV_MANUAL` (2), `WIFI_PROV_ALWAYS` (3); and
`WIFI_ON_RECONNECT_EXHAUSTED_RESTART` (0),
`WIFI_ON_RECONNECT_EXHAUSTED_PROVISION` (1). Source using the
enumerator names just needs a recompile. Anything that **stored or
transmitted the numeric value** (NVS blobs, config files, custom
protocomm/MQTT payloads) must be versioned or migrated. The library's
own `esp-wifi-config-network-policy` endpoint reports the mode as a
string, so stock provisioning clients are unaffected.
- **`wifi_cfg_config_t` layout changed.** Source-compatible for
designated initialisers — which is what every example and doc uses —
but dependents must be rebuilt against the new header (automatic for
an IDF component build).
- **`WIFI_CFG_DEFAULT_AP_SSID` / `_PASSWORD` / `_IP` moved to the public
header.** `WIFI_CFG_DEFAULTS` expands in the caller's translation
unit, so everything it names has to be reachable from
`esp_wifi_config.h`. Applications can now compare against them
instead of retyping the literals.
### Security
- **A ~109-byte HTTP request could reboot the device, and ~409 bytes
could wedge it until physically reset.** `read_json_body()` bounded
the request body's *length* but not its JSON *nesting depth*, and
cJSON parses by recursive descent — two characters buy one level, so
a body far inside the 2048-byte limit reached depths that overflowed
the HTTP task's 4 KB stack. Measured on an ESP32-S3 with IDF 5.5.4:
depth 40 answered normally, depth 50 rebooted, depth 200 stopped the
device answering on *any* channel. Basic Auth is off by default and
the provisioning SoftAP is open by default, so this was reachable by
anyone in radio range of a device being set up. Bodies nested deeper
than `WIFI_CFG_JSON_MAX_DEPTH` (16) are now refused with `400` before
cJSON sees them. No evidence was found of anything beyond a crash,
and no investigation of exploitability beyond that was done.
### Fixed
- **Improv BLE dropped its own scan response when the neighbourhood packed
flush with the buffer.** An RPC result is `[command][length][payload]` and
the length is one byte, so `GET_WIFI_NETWORKS` packed up to 255 payload
bytes — but `send_rpc_result()` assembled it in a 256-byte buffer, which is
two of header plus room for 254, and silently returned when it did not fit.
Measured on a bench with thirteen APs: the device scanned, packed and
checksummed an answer, and the Improv BLE client waited out its whole
30-second timeout with nothing to show. It is not a random landing either —
the total is a function of which SSIDs are on the air, so it stays wrong for
as long as the neighbourhood does, which is why it read as flakiness. The
buffer is now sized to the format's own ceiling and the guard logs when it
refuses.
Alongside it, the packer is now told what the transport can carry, as
`max_payload` on the internal RPC entry point. This library asks for an ATT
MTU of 517 and gets it from desktop and phone clients, so on a normal link
the format's 255-byte ceiling is reached long first — but a client that
settles on a smaller MTU carries less than a result can express, and a
response packed past it is clipped to fit by the host stack and arrives
claiming bytes that never came. NimBLE reads the negotiated value with
`ble_att_mtu()`; Bluedroid was not tracking it at all and now does.
Networks past the budget are still dropped and always will be — no MTU
changes a one-byte length field. `wifi_cfg_scan()` returns strongest-first
and deduplicates by SSID, so the cut falls on the faintest neighbours.
Improv Serial is unaffected: it answers one response per network, which
cannot overflow. Three silent failure paths in the BLE transport now log.
- **Improv Serial's `driver` dependency is now declared unconditionally.**
`CMakeLists.txt` appended `driver` to `PRIV_REQUIRES` inside an
`if(CONFIG_WIFI_CFG_ENABLE_IMPROV_SERIAL)` block, which never reaches
ESP-IDF's early dependency-resolution pass — so `driver/uart.h` was never on
the include path. It compiled anyway because `esp_bus` declared
`REQUIRES driver` publicly and this component depended on `esp_bus`, so the
include path arrived transitively. Removing `esp_bus` removed the accident
and every `improv_serial` build broke. The bug is older than the removal;
it was simply unobservable. Found by the HIL matrix build (all three boards
× both IDF series) during pre-merge validation. Costs nothing where Improv
Serial is off — `examples/basic` measures 0xcf550 bytes either way — since
the linker drops the unreferenced archive.
- **Improv BLE never checked the RPC command checksum.** The Improv
packet is `[command, length, ...data, checksum]` in both directions,
and the checksum is the only integrity check the protocol has above
the link layer. The library appended a correct one to every result —
its own comment cites the spec requiring it — while the command path
handed the raw GATT write straight to `wifi_cfg_improv_handle_rpc()`,
which validates the length field and nothing else. A corrupted
`SEND_WIFI_SETTINGS` was therefore executed: the mangled SSID and
passphrase were written to NVS and connected to.
`improv_ble_frame_valid()` now checks length and checksum in
`improv_ble_cmd_task()`, which is the one path both NimBLE and
Bluedroid reach the core through, and a bad frame answers
`IMPROV_ERROR_INVALID_RPC`. **Note for client authors:** the length
check is exact, so a frame that omits the trailing checksum byte is
now rejected rather than accepted with its last payload byte read as
the checksum. Conforming clients always send it; one that had been
getting away without it will start seeing `INVALID_RPC`.
- **`esp-wifi-config-network-info` was unreachable over BLE
provisioning.** It was registered but never created:
`wifi_prov_mgr_endpoint_register()` attaches a handler to a name,
`wifi_prov_mgr_endpoint_create()` allocates the GATT characteristic,
and only four of the five built-in endpoints got the second call. The
handler was live with nothing able to reach it — including the
`network-info` command in this repo's own `tools/wifi_ble_cli`.
Endpoint discovery on a device returned nine characteristic names and
that one was not among them. Nothing logged, because registering a
handler succeeds whether or not a characteristic exists to route to
it; only the create can tell. Both calls now go through checked
wrappers that log the failure and name the endpoint.
- **The bundled Web UI was compiled out of every build that used it**,
so `/` returned 404 — and that is where all eight captive-portal
detection handlers redirect. A device would come up, advertise its
SoftAP, hijack DNS and return a correct 302 to the OS, which then
opened the portal and found nothing there.
`CONFIG_WIFI_CFG_WEBUI_CUSTOM_PATH` is a Kconfig `string` with
`default ""`, so it is *always defined* and the `#ifndef` guarding
the embedded asset table was always false. CMake reached the opposite
conclusion — an empty string is falsy there — and embedded the assets
anyway, so they shipped and nothing could reach them. CMake now
defines `WIFI_CFG_WEBUI_EMBEDDED` when it populates `EMBED_FILES`,
and the C guards test that. No Kconfig change, no flash cost, and
builds that set a real custom path are unaffected.
- **Every HTTP error response tore down the connection.**
`send_error()` returned `ESP_FAIL` after successfully sending its
response, and `esp_http_server` closes the socket whenever a handler
returns non-OK. Any client that collected a few 4xx responses — a Web
UI polling an endpoint that 404s, say — burned one connection per
error against a pool of seven. It now returns `ESP_OK`, because the
response *was* sent.
- **The 8th concurrent connection hung instead of being refused.**
`wifi_cfg_http_init()` left `lru_purge_enable` at its default of
false, so once `max_open_sockets` (7) were in use, further
connections were accepted at the TCP layer and never answered — the
client waited out its own timeout while the device looked healthy. A
browser alone opens up to six connections to one origin, so the Web
UI plus an OS captive-portal probe was already at the limit. LRU
purging is now enabled.
- **An oversized request body left the connection unusable.**
`read_json_body()` refused bodies over `WIFI_CFG_HTTP_MAX_CONTENT`
without reading them, so the unread bytes were parsed as the *next*
request and the connection was reset — meaning the intended `400`
reached the client only about half the time, and a keep-alive
client's following request never did. The body is now drained before
the refusal.
- **A request body split across TCP segments was truncated.**
`httpd_req_recv()` returns what a single read produced;
`read_json_body()` assumed it returned everything. It now loops until
`content_len` bytes have arrived.
- **`auto_reconnect` now actually defaults to true**, via
`WIFI_CFG_DEFAULTS`. `wifi_cfg_init()` applied defaults for
`max_retry_per_network`, `retry_interval_ms` and
`retry_max_interval_ms` only, and never touched `auto_reconnect` —
a `bool` has no spare value to mean "unset". Any application that
zero-initialised its config — the documented "everything omitted takes
the library default" style — silently got `auto_reconnect == false`,
and the whole reconnect block at `esp_wifi_config.c` was gated on it.
Measured on an ESP32-S3: after a post-connect disconnect (beacon
timeout when the AP goes away) the device emitted one
`wifi:disconnected` event and never retried — no `wifi:connecting`,
ever. `auto_reconnect` stays a plain `bool`: start from the macro and
`.auto_reconnect = false` means false.
- **`wifi_cfg_init(NULL)` finally means "all defaults".** The header
claimed this for a long time and did not deliver, because the defaults
did not exist as a value anywhere. `NULL` now copies
`WIFI_CFG_DEFAULT_CONFIG()` verbatim.
- **Partial `default_ap` configs now get the per-field AP defaults the
header documents.** `set_default_ap_config()` ran only when there was
no AP config at all, so supplying `.default_ap = { .ssid = "X" }`
(or loading a legacy NVS blob) left `ip`, `netmask`, `gateway`,
`max_connections` and the DHCP range blank. A new
`normalize_ap_config()` backfills every documented default after the
AP config is resolved, whichever source it came from. `password` is
left alone — empty means "open network", a real choice.
- **The auto-reconnect backoff no longer blocks the manager task.** The
reconnect branch ended in `vTaskDelay(pdMS_TO_TICKS(delay))`, so the
task that owns the reconnect state machine slept through the entire
backoff and could not service its own queue — with the default
5 s → 60 s schedule that is up to a minute of deafness per retry, and
the queue is 10 deep with `xQueueSend(..., 0)`, so events arriving in
that window were silently dropped. Measured on an ESP32-S3 with a 30 s
backoff: the retry fired at 29.99 s regardless of what happened in
between. The delay is now a *deadline* — the task's existing
`xQueueReceive()` uses whatever is left of it as its timeout, so the
backoff is a maximum that a relevant message can cut short. Cancelled
by `STA_CONNECTED`, `GOT_IP`, a disconnect request, and any call to
`wifi_cfg_start_connect_sequence()`; deliberately *not* by scan
completion or soft-AP events, which say nothing about the upstream
network and would let an HTTP client collapse the backoff into a
busy-loop. The interval schedule (`retry_interval_ms << retry`, capped
at `retry_max_interval_ms`) is unchanged. Side effects: an explicit
`wifi_cfg_connect()` during a backoff is no longer undone by the retry
that was already scheduled, a `wifi_cfg_disconnect()` during a backoff
is no longer reversed, and `wifi_cfg_deinit()` no longer has to wait
out the backoff before the task observes `WM_INT_EVT_STOP`.
### Changed
- **`wifi_cfg_init()` warns when a `[DISABLED]` provisioning enum value
is selected.** `WIFI_PROV_ALWAYS` and
`WIFI_ON_RECONNECT_EXHAUSTED_PROVISION` still behave as
`WIFI_PROV_MANUAL` and "retry indefinitely" respectively. They are no
longer the zero value, so reaching them takes a deliberate choice
rather than an omission — but the warning stays, because the choice
does not do what its name suggests. The `on_reconnect_exhausted`
warning also flags the live side effect: with `enable_ap` set and
`max_reconnect_attempts > 0`, that value makes `wifi_cfg_stop_http()`
return `ESP_ERR_INVALID_STATE`.
### Documentation
- Every documented default now lives in one place, `WIFI_CFG_DEFAULTS`,
so the header's field docs and the values init actually applies can no
longer drift apart.
- `MIGRATION.md` gained a **0.2.0** section covering the macros, the
removal of init-time patching, and the enum renumbering (with the
old→new numeric tables).
- Every example and every documented config sample now starts from
`WIFI_CFG_DEFAULTS` and shows only its non-default settings.
- Every field of `wifi_cfg_config_t` now states its default (or states
that it has none) in the header, including the previously silent
`stop_provisioning_on_connect`, `provisioning_teardown_delay_ms`,
`http_post_prov_mode`, `always_use_ap_defaults` and `enable_ap`.
- `prov_ble.security2_username` no longer claims a `"wificfg"` default.
The username never reaches `wifi_prov_mgr` from the device side; the
field is application metadata and no default is substituted.
- `wifi_cfg_ap_config_t.dhcp_start` / `.dhcp_end` now note that they are
stored, defaulted and reported over the API but are **not** programmed
into the AP netif's DHCP server.
- `improv.serial_uart_num` / `.serial_baud_rate` now point at the
Kconfig options that actually supply their defaults, and note that
because `0` means "unset", `UART_NUM_0` cannot be selected in
preference to a non-zero Kconfig value.
## [0.1.0] — 2026-05-31 - ESP-IDF Network Provisioning over BLE
### Breaking Changes
- **Custom BLE GATT service removed.** The hand-rolled JSON-over-GATT
service (UUID `0xFFE0` / characteristics `0xFFE1`–`0xFFE3`) and the
`CONFIG_WIFI_CFG_ENABLE_CUSTOM_BLE` Kconfig option are gone. ESP-IDF's
official `wifi_provisioning` manager (BLE scheme) is now the
recommended secure BLE provisioning path. See `MIGRATION.md` for the
protocol-level migration plan and the steps for updating downstream
client tools.
- **Minimum ESP-IDF bumped to 5.4.** `idf_component.yml`'s
`idf.version` is now `>=5.4`. Prior versions are not supported.
- **Network Provisioning and Improv BLE are mutually exclusive at
Kconfig level.** Both want to own the BLE GAP advertising and the
NimBLE/Bluedroid host. Pick one per firmware build. Improv Serial is
independent and remains safe alongside Network Provisioning.
- **`wifi_cfg_ble_config_t` removed; `.ble.device_name` moved to
`.improv.ble_device_name`.** With the custom 0xFFE0 service gone the
field only applied to the Improv BLE host bootstrap, but its
top-level placement implied it controlled BLE for Network
Provisioning too (it never did — `wifi_prov_mgr` owns its own GAP
name). Move it under `.improv` where it actually applies, alongside
`.improv.device_name` (which is the post-connect Device-Info RPC
value, not the GAP advertised name).
### Added
- `esp_wifi_config_prov_ble.c` — wraps `wifi_prov_mgr_*` with the BLE
scheme, integrates with the library's lifecycle, and registers four
custom protocomm endpoints (`esp-wifi-config-version`,
`esp-wifi-config-capabilities`, `esp-wifi-config-vars`,
`esp-wifi-config-network-policy`).
- `wifi_cfg_prov_config_t` carries the full runtime configuration:
`device_name`, `security` version, `pop`, `security2_username`,
Security 2 salt/verifier, `reset_on_failure`, `max_failed_attempts`,
`firmware_version`, and the rest of the BLE / lifecycle / endpoint
knobs. Only two Kconfig options remain:
`WIFI_CFG_ENABLE_NETWORK_PROVISIONING` (gates the code) and
`WIFI_CFG_NETWORK_PROVISIONING_BLE` (selects the BLE transport).
- ESP-IDF 6.x compatibility shim — uses the in-tree
`wifi_provisioning` component on 5.4 and the external
`espressif/network_provisioning` managed component on 6.x via a
conditional `idf_component.yml` rule.
- `esp-wifi-config-network-info` protocomm endpoint — reports the
device's current network state (SSID, IP, connection status) to the
provisioning client.
- **Automatic reboot after successful BLE provisioning** (default on).
Espressif's `wifi_provisioning` component has no clean way to tear
down and rebuild the BLE/NimBLE stack in place after credentials are
delivered, so the library now reboots on whichever happens first:
the BLE client disconnecting after `WIFI_PROV_EVT_CRED_RECV`, or a
backstop timer set on `WIFI_PROV_EVT_CRED_SUCCESS` (default 15000 ms,
tunable via the new `wifi_cfg_prov_config_t.reboot_max_wait_ms`).
Opt out with `wifi_cfg_prov_config_t.disable_reboot_on_provisioning_success
= true` if the application handles the BLE/Wi-Fi handoff itself.
`prov_ble.stop_after_success` is now ignored while reboot-on-success
is active. See `MIGRATION.md` for migration guidance.
### Changed
- `examples/with_ble` rewritten to demonstrate Network Provisioning.
- `examples/with_ble_deinit` switched to Improv BLE (the host-stack
handoff pattern doesn't fit `wifi_prov_mgr` cleanly).
- BLE backends (`esp_wifi_config_ble_nimble.c` /
`esp_wifi_config_ble_bluedroid.c`) slimmed to the Improv host
bootstrap only — they no longer carry the `0xFFE0` service.
- **`WIFI_PROV_ALWAYS` and `WIFI_ON_RECONNECT_EXHAUSTED_PROVISION`
disabled.** Both code paths called `wifi_prov_mgr_start_provisioning()`,
which in turn calls `nimble_port_init()` — a fatal collision when the
application has already initialised the BLE stack. The enum values
remain in the public API (existing configs still compile) but the
underlying provisioning-start is bypassed with a warning log.
`WIFI_PROV_ALWAYS` now behaves like `WIFI_PROV_MANUAL` at boot;
`WIFI_ON_RECONNECT_EXHAUSTED_PROVISION` now keeps retrying
indefinitely (equivalent to `max_reconnect_attempts = 0`). The
original code is preserved behind `#if 0` for re-enablement once a
BLE provisioning path that doesn't depend on Espressif's
`wifi_provisioning` component is in place. See `MIGRATION.md`.
### Fixed
- **BLE provisioning recovers from client disconnects.** Worked around
an IDF 5.5.3 NimBLE bug where only the first BLE client to connect
after boot could complete a provisioning session — subsequent
reconnects accepted at the link layer then timed out at supervision,
and the wedged state only cleared on a full reboot. The library now
subscribes to `PROTOCOMM_TRANSPORT_BLE_DISCONNECTED` and tears
down + re-initialises the provisioning manager whenever a client
drops before credentials are delivered. Opt out with the new
`wifi_cfg_prov_config_t.disable_disconnect_restart = true` if you
prefer to debug the underlying IDF bug or drive teardown yourself.
- **NimBLE flow-control regression handled.** Added error handling for
the ESP-IDF 5.5.3 NimBLE flow-control regression encountered during
provisioning sessions.
## [0.0.4] — 2026-04-26 - BLE KConfig Updates
### Changed
- Added dependencies on the necessary NimBLE configuration options being set when using BLE support
## [0.0.3] — 2026-04-25 - BLE Interface Segregation
### Breaking Changes
- **Renamed `WIFI_CFG_ENABLE_BLE` to `WIFI_CFG_ENABLE_CUSTOM_BLE`** — The Kconfig option now explicitly refers to the custom BLE GATT service (UUID 0xFFE0). Update `sdkconfig.defaults` files accordingly.
- **Removed `ble.enable` from `wifi_cfg_ble_config_t`** — BLE interfaces are now enabled entirely at compile time via Kconfig. Remove `.ble.enable = true` from your `wifi_cfg_config_t` initializer. The `.ble.device_name` field remains.
- **Improv BLE no longer selects custom BLE** — Enabling `WIFI_CFG_ENABLE_IMPROV_BLE` no longer implicitly enables `WIFI_CFG_ENABLE_CUSTOM_BLE`. Each BLE interface is independently configurable. The BLE stack is initialized automatically when either is enabled.
### Changed
- Decoupled the custom BLE GATT interface from Improv WiFi BLE at the Kconfig level, allowing each to be enabled independently.
## [0.0.2] — First release as esp_wifi_config
This is the first release since hard-forking from [tuanpmt/esp_wifi_manager](https://github.com/tuanpmt/esp_wifi_manager). It includes a full rename of the library, numerous bug fixes, significant BLE improvements, and new provisioning capabilities. See the [Migration Guide](MIGRATION.md) for upgrading from esp_wifi_manager.
### Breaking Changes
- **Renamed from `esp_wifi_manager` to `esp_wifi_config`.** All public symbols have been renamed:
- Functions: `wifi_manager_` → `wifi_cfg_`
- Types: `wifi_mgr_` → `wifi_cfg_` (config struct is now `wifi_cfg_config_t`)
- Events: `WIFI_MGR_EVT_` → `WIFI_CFG_EVT_`
- Kconfig: `WIFI_MGR_` → `WIFI_CFG_`
- Header: `esp_wifi_manager.h` → `esp_wifi_config.h`
- **`wifi_cfg_deinit()` now takes a `bool deinit_wifi` parameter.** Pass `true` for the old behavior (full teardown), or `false` to tear down the manager while keeping WiFi connected — useful after provisioning completes.
- **Built-in mDNS integration removed.** If you need mDNS, initialize it directly in your application after receiving `WIFI_CFG_EVT_GOT_IP`. See the Migration Guide for details.
- **Provisioning booleans replaced with a unified provisioning mode system.** The separate boolean flags for enabling provisioning methods have been consolidated.
- **Several Kconfig options removed** in favor of runtime config struct fields: `AP_SSID`, `AP_PASSWORD`, `AP_IP`, and `BLE_DEVICE_NAME` are now set via `wifi_cfg_config_t` rather than menuconfig.
- **`esp_bus` dependency** has moved from `tuanpmt/esp_bus` to `thorrak/esp_bus`.
### New Features
- **Improv WiFi support.** Full implementation of the [Improv WiFi](https://www.improv-wifi.com/) standard, with both BLE and Serial transports. Includes a new `with_improv` example.
- **NimBLE Bluetooth stack support.** Choose between Bluedroid and NimBLE for BLE provisioning, enabling use on memory-constrained devices.
- **BLE interface at feature parity with SoftAP.** Added `update_network`, `list_vars`, and `del_var` BLE commands, plus missing status fields.
- **BLE response chunking.** Large BLE responses are now automatically chunked, with an increased SSID buffer size for reliability.
- **BLE command queue.** Commands are now processed via a dedicated task and queue rather than inline in the BLE callback, improving stability.
- **BLE service advertisement.** Service UUID 0xFFE0 is now advertised in BLE broadcasts for easier device discovery.
- **Network upsert.** "Add network" commands now update existing entries instead of failing if the network already exists.
- **Continued BLE use after deinitialization.** BLE can remain active after calling `wifi_cfg_deinit(false)`, with a new `with_ble_deinit` example demonstrating the pattern.
- **Pre-request hook and variable validation** for HTTP API endpoints, giving applications more control over provisioning requests.
- **Option to ignore NVS AP config** and always use the default AP settings provided at init time.
- **HTTP handler registration improvements** for provisioning and API management, including proper URI deregistration when using a shared HTTP server.
- **Mock HTTP test server** (`tools/test_server/`) — a Flask-based server that emulates the esp_wifi_config API for frontend development without hardware.
### Bug Fixes
- **Defer `WIFI_STATE_CONNECTED` until `GOT_IP`** — previously the connected state was emitted before an IP address was assigned, causing race conditions in application code.
- **Fix double AP start/stop events** — AP lifecycle events are now emitted exactly once.
- **Fix reconnect logic** — stale disconnect events from previous connections no longer trigger spurious reconnect attempts. Connected SSID is now properly tracked.
- **Hide hidden networks** from WiFi scan results.
- **Fix AP config loading** to properly prioritize provided defaults over stale NVS data.
- **Fix Web UI initialization order** — fallback page registration now happens at the correct time.
- **Fix custom WebUI builds** — embedded files are no longer included when a custom WebUI path is configured.
- **Fix `set_var` callback** — the "variable changed" callback now fires correctly when setting variables programmatically.
- **Fix gzipped asset serving** — file serving logic now correctly prefers gzipped assets.
### Infrastructure & Documentation
- **Documentation site** at [configwifi.com](https://configwifi.com) built with Docusaurus, including AI-friendly `llms.txt`.
- **GitHub Actions CI** — automated builds for all examples on every push.
- **ESP Component Registry** publishing via GitHub Actions on release.
- **PlatformIO Library Registry** support with `library.json`.
- **Migration Guide** (`MIGRATION.md`) documenting all breaking changes with find-and-replace instructions.