Skip to main content

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

ItemUUID (canonical string)PropertiesNotes
Service1d14d6ee-0001-4000-8024-b5a3c0ffee01Primary
Control1d14d6ee-0101-4000-8024-b5a3c0ffee01WriteSingle-byte opcode + optional future extensions
Data1d14d6ee-0202-4000-8024-b5a3c0ffee01WriteRaw credential fragment (UTF‑8 octets)
Events1d14d6ee-0303-4000-8024-b5a3c0ffee01Notifyv1 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_*_STR in iotmer_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) fills ble_uuid128_t.value[] in stack wire order (time-low and following fields little-endian per Bluetooth Base UUID layout). That sequence must match uuid.UUID(bytes_le=canonical_16_bytes) in Python when generated from the canonical string.
    Important: the two clock-sequence octets that appear as 8024 in the RFC string must be stored as 0x80, 0x24 in the init list (not 0x24, 0x80, which would present as 2480 on 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.py canonicalises 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)NameBehaviour
0x01PINGDevice sends a progress notification (pong payload).
0x02BEGIN_SSIDClears the in-RAM SSID accumulator; subsequent Data writes append to SSID until BEGIN_PASS.
0x03BEGIN_PASSRequires a non-empty SSID; clears password accumulator; Data writes append to password until COMMIT.
0x04COMMITValidates lengths, calls iotmer_wifi_set_credentials(), then notifies DONE or ERROR. No partial NVS commit before this.
0x05ABORTClears 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.

OffsetSizeField
01Protocol version (must be 1)
11Event code (see below)
22iotmer_ble_wifi_prov_err_t application error code
42Payload length N
6NOptional payload (often short ASCII hints for logs; not a second API surface)

Event codes (byte offset 1)

ValueMeaning
0STATE (informational)
1PROGRESS
2DONE — COMMIT succeeded (esp_err implied OK)
3ERROR — 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):

SymbolRole
CONFIG_IOTMER_BLE_ADV_TIMEOUT_MSMax time advertising while not connected (0 = until stop()).
CONFIG_IOTMER_BLE_CONN_IDLE_MSConnected idle timeout without writes resetting the timer.
CONFIG_IOTMER_BLE_SESSION_MAX_MSHard cap after connect.
CONFIG_IOTMER_BLE_RESTART_ADV_ON_DISCONNECTResume 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 — consider iotmer_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_t from iotmer_wifi_set_credentials).

The module does not assume ordering relative to MQTT or HTTPS provisioning.

Manual QA (nRF Connect)

  1. Flash firmware with CONFIG_IOTMER_BLE_WIFI_PROV=y, NimBLE enabled, and call init + start.
  2. Scan for GAP name prefix IOTMER- (suffix is MAC-derived hex).
  3. Connect, discover services, enable Notifications on Events (…0303…).
  4. Write Control 0x02 (BEGIN_SSID).
  5. Write Data with SSID UTF‑8 octets (may require multiple writes if MTU is small).
  6. Write Control 0x03 (BEGIN_PASS).
  7. Write Data with password octets.
  8. Write Control 0x04 (COMMIT).
  9. Observe Events: DONE with error code 0, 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” (calls start() again on device side if exposed).
  • Document pairing UX (proximity, short window).
  • OOB device identity confirmation designed in product.

Customer integration summary

  1. Add components/iotmer and components/iotmer_ble_wifi_prov to EXTRA_COMPONENT_DIRS (or use Component Manager manifests that pull both).
  2. Set CONFIG_IOTMER_BLE_WIFI_PROV=y and enable NimBLE + Bluetooth controller in sdkconfig for a BLE-capable target.
  3. Add REQUIRES iotmer_ble_wifi_prov (and iotmer, nvs_flash, Wi‑Fi / esp_netif as needed) in your application CMakeLists.txt.
  4. Call nvs_flash_init(), esp_netif_init(), create the default event loop, and start Wi‑Fi / TCP/IP as required by your product.
  5. Call iotmer_ble_wifi_prov_init(), then iotmer_ble_wifi_prov_start() when you want a provisioning window (and stop / deinit when done).
  6. On success (on_result with ESP_OK), call iotmer_wifi_reconnect() or reboot; call iotmer_ble_wifi_prov_stop() if you want the radio free for STA while BLE is no longer needed.
  7. 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.