BLE Wi-Fi provisioning (IOTMER GATT protocol v1)
This page specifies the IOTMER-owned BLE GATT interface for transferring Wi‑Fi STA credentials from a mobile app to an ESP32-family device. It is not compatible with Espressif’s wifi_prov_mgr phone apps or their byte-level provisioning protocol.
The ESP-IDF implementation lives in the optional component iotmer_ble_wifi_prov. Credentials are written to NVS only through iotmer_wifi_set_credentials() (same keys/namespace as the rest of the IOTMER SDK).
Minimum ESP-IDF: 5.0 (NimBLE host). Test against your target IDF release; enable NimBLE in menuconfig.
GATT layout
| Item | UUID (canonical string) | Properties | Notes |
|---|---|---|---|
| Service | 1d14d6ee-0001-4000-8024-b5a3c0ffee01 | Primary | |
| Control | 1d14d6ee-0101-4000-8024-b5a3c0ffee01 | Write | Single-byte opcode + optional future extensions |
| Data | 1d14d6ee-0202-4000-8024-b5a3c0ffee01 | Write | Raw credential fragment (UTF‑8 octets) |
| Events | 1d14d6ee-0303-4000-8024-b5a3c0ffee01 | Notify | v1 event frame (subscribe via CCCD) |
UUIDs: RFC strings, NimBLE BLE_UUID128_INIT, and centrals
- The table uses RFC 4122 canonical lowercase strings (
IOTMER_BLE_WIFI_PROV_UUID_*_STRiniotmer_ble_wifi_prov.h). Mobile and cloud docs should use these exact strings for UX and support. - In ESP-IDF NimBLE,
BLE_UUID128_INIT(a0,…,a15)fillsble_uuid128_t.value[]in stack wire order (time-low and following fields little-endian per Bluetooth Base UUID layout). That sequence must matchuuid.UUID(bytes_le=canonical_16_bytes)in Python when generated from the canonical string.
Important: the two clock-sequence octets that appear as8024in the RFC string must be stored as0x80, 0x24in the init list (not0x24, 0x80, which would present as2480on some centrals and break string-based clients). - macOS Core Bluetooth (and some other stacks) may display the same 128-bit UUID with a different hyphenated string than RFC. Implementations must compare 128-bit values (or normalise strings), not plain ASCII equality. The reference Bleak client in
examples/05_ble_wifi_prov/pc_ble_client/prov_client.pycanonicalises OS-reported UUIDs before matching.
Advertising and scan response (legacy 31-octet limit)
Legacy Advertising and Scan Response PDUs are each limited to 31 octets of AD payload. Placing Flags + Complete Local Name + full 128-bit service UUID in a single PDU exceeds that limit and yields BLE_HS_EMSGSIZE on the peripheral.
The reference firmware therefore puts Flags + 128-bit IOTMER service UUID in the advertising PDU and the GAP device name in the scan response PDU. Centrals that filter on service UUID still see the service in the advertisement; they may need an active scan to obtain the full friendly name from scan response.
Byte order and encoding (payloads)
- SSID and password payload octets are UTF‑8 (no BOM). Embedded NUL bytes in the middle of a fragment are not supported.
- Multi-byte fields in the Events frame are little-endian.
- Control opcodes are a single uint8 in the first octet of each write.
Control opcodes (v1)
| Opcode (hex) | Name | Behaviour |
|---|---|---|
0x01 | PING | Device sends a progress notification (pong payload). |
0x02 | BEGIN_SSID | Clears the in-RAM SSID accumulator; subsequent Data writes append to SSID until BEGIN_PASS. |
0x03 | BEGIN_PASS | Requires a non-empty SSID; clears password accumulator; Data writes append to password until COMMIT. |
0x04 | COMMIT | Validates lengths, calls iotmer_wifi_set_credentials(), then notifies DONE or ERROR. No partial NVS commit before this. |
0x05 | ABORT | Clears in-RAM buffers and resets the transfer state. |
Data writes
- After BEGIN_SSID, each Data write appends octets to the SSID (max 32 octets total, IEEE 802.11 SSID).
- After BEGIN_PASS, each Data write appends octets to the password (max 63 octets total, aligned with WPA2-PSK passphrase length used by this SDK’s NVS strings).
- Each ATT Write payload must fit in the negotiated ATT MTU (default 23 → 20 user octets per write after ATT overhead unless MTU is exchanged). The central should request a larger MTU (e.g. 185–247) for fewer round-trips.
Events notification (v1 frame)
All integer fields little-endian.
| Offset | Size | Field |
|---|---|---|
| 0 | 1 | Protocol version (must be 1) |
| 1 | 1 | Event code (see below) |
| 2 | 2 | iotmer_ble_wifi_prov_err_t application error code |
| 4 | 2 | Payload length N |
| 6 | N | Optional payload (often short ASCII hints for logs; not a second API surface) |
Event codes (byte offset 1)
| Value | Meaning |
|---|---|
0 | STATE (informational) |
1 | PROGRESS |
2 | DONE — COMMIT succeeded (esp_err implied OK) |
3 | ERROR — see error code; NVS failure may include 4-byte esp_err_t LE in payload |
Structured error codes (bytes 2–3)
Matches iotmer_ble_wifi_prov_err_t in iotmer_ble_wifi_prov.h (e.g. SSID_TOO_LONG, SESSION_TIMEOUT, ADV_TIMEOUT, NVS, …).
Session rules and timeouts (Kconfig)
Typical symbols (see components/iotmer_ble_wifi_prov/Kconfig.projbuild; menu path Component config → IOTMER BLE Wi‑Fi provisioning when the component is part of the project):
| Symbol | Role |
|---|---|
CONFIG_IOTMER_BLE_ADV_TIMEOUT_MS | Max time advertising while not connected (0 = until stop()). |
CONFIG_IOTMER_BLE_CONN_IDLE_MS | Connected idle timeout without writes resetting the timer. |
CONFIG_IOTMER_BLE_SESSION_MAX_MS | Hard cap after connect. |
CONFIG_IOTMER_BLE_RESTART_ADV_ON_DISCONNECT | Resume advertising after disconnect while start() window is active. |
Advertising also ends when the stack reports ADV_COMPLETE (e.g. duration elapsed).
Security and threat model
Default posture
- LE pairing with NimBLE SMP defaults and
CONFIG_IOTMER_BLE_SMP_IO_CAP(default NO_IO → effectively Just Works). - Short provisioning window and physical proximity are the primary mitigations.
Threats accepted by default
- Passive observation of encrypted air traffic is mitigated by LE encryption after pairing; MITM during Just Works pairing within RF range is not ruled out.
- A nearby attacker could attempt to connect during the provisioning window; users should verify the device identity out-of-band (QR, printed label, NFC, UI confirmation).
- No application-layer encryption of SSID/password inside GATT writes in v1 — rely on LE link encryption and operational process.
Optional hardening (Kconfig / firmware)
- Tighter timeouts, IO capabilities that require passkey/display (needs product UI), shorter advertising windows.
BLE + Wi‑Fi STA coexistence
- Use a supported Espressif coexistence configuration; follow IDF documentation for your chip (antenna layout, RF calibration).
- After COMMIT, the integrator typically calls
iotmer_wifi_reconnect()or reboots; starting heavy Wi‑Fi traffic while BLE is still connected may reduce throughput — consideriotmer_ble_wifi_prov_stop()after success. - Power save: review
esp_wifi_set_ps()for your product; BLE scanning on a phone plus STA may require tuning.
Public C API (firmware)
See components/iotmer_ble_wifi_prov/include/iotmer_ble_wifi_prov.h:
iotmer_ble_wifi_prov_init/deinit/start/stop- Optional callbacks for state and result (
esp_err_tfromiotmer_wifi_set_credentials).
The module does not assume ordering relative to MQTT or HTTPS provisioning.
Manual QA (nRF Connect)
- Flash firmware with
CONFIG_IOTMER_BLE_WIFI_PROV=y, NimBLE enabled, and callinit+start. - Scan for GAP name prefix
IOTMER-(suffix is MAC-derived hex). - Connect, discover services, enable Notifications on Events (
…0303…). - Write Control
0x02(BEGIN_SSID). - Write Data with SSID UTF‑8 octets (may require multiple writes if MTU is small).
- Write Control
0x03(BEGIN_PASS). - Write Data with password octets.
- Write Control
0x04(COMMIT). - Observe Events:
DONEwith error code0, then verify NVS /iotmer_wifi_reconnect()on device.
Negative tests
- COMMIT with empty SSID or password → structured error, no NVS change.
- Disconnect mid-transfer → buffers discarded; reconnect and repeat from BEGIN_SSID.
- Wait for idle/session timeout → connection dropped, error notified.
Mobile app team checklist
- MTU exchange requested early (target ≥ 185).
- Parse Events v1 frame (version, endianness).
- Implement opcode sequence: BEGIN_SSID → Data* → BEGIN_PASS → Data* → COMMIT.
- Handle
ADV_COMPLETE/ timeout: prompt user to tap “Retry” (callsstart()again on device side if exposed). - Document pairing UX (proximity, short window).
- OOB device identity confirmation designed in product.
Customer integration summary
- Add
components/iotmerandcomponents/iotmer_ble_wifi_provtoEXTRA_COMPONENT_DIRS(or use Component Manager manifests that pull both). - Set
CONFIG_IOTMER_BLE_WIFI_PROV=yand enable NimBLE + Bluetooth controller insdkconfigfor a BLE-capable target. - Add
REQUIRES iotmer_ble_wifi_prov(andiotmer,nvs_flash, Wi‑Fi /esp_netifas needed) in your applicationCMakeLists.txt. - Call
nvs_flash_init(),esp_netif_init(), create the default event loop, and start Wi‑Fi / TCP/IP as required by your product. - Call
iotmer_ble_wifi_prov_init(), theniotmer_ble_wifi_prov_start()when you want a provisioning window (andstop/deinitwhen done). - On success (
on_resultwithESP_OK), calliotmer_wifi_reconnect()or reboot; calliotmer_ble_wifi_prov_stop()if you want the radio free for STA while BLE is no longer needed. - Optionally
iotmer_ble_wifi_prov_deinit()on factory-reset paths (see header for lifetime notes).
Build note (CMake and bt)
The component CMakeLists.txt always registers REQUIRES bt (and PRIV_REQUIRES bt) so NimBLE headers and link dependencies resolve reliably. Optional behaviour is selected with #if CONFIG_IOTMER_BLE_WIFI_PROV inside the C translation unit, not by omitting bt from CMake (doing so breaks header paths on some IDF / Kconfig orders).
Desktop test client (Python + Bleak)
For lab use without a phone, see examples/05_ble_wifi_prov/pc_ble_client/README.md. It performs MTU exchange, subscribes to Events, and writes Control / Data in order. On macOS, if you change UUID-related firmware, remove the peripheral from System Settings → Bluetooth once so Core Bluetooth does not serve a stale GATT cache.