BLE Provisioning for IoT Devices via Mobile Applications
BLE Provisioning — transmitting Wi-Fi credentials and configuration to an IoT device through Bluetooth Low Energy. This is the best method for user-friendly UX: no need to switch Wi-Fi networks on the phone, more reliable than SmartConfig, and doesn't depend on router settings. For ESP32, nRF52, Nordic chips — the preferred choice.
GATT Architecture for Provisioning
The device in provisioning mode advertises a BLE service. The mobile application connects as a GATT client and writes data to service characteristics. Standard scheme for ESP-IDF:
-
Service UUID:
021a9004-0382-4aba-aa36-ec4d15d65e0e(Espressif Provisioning) - Configuration characteristic: write (SSID, password, auth mode)
- Status characteristic: notify (device Wi-Fi connection result)
After writing credentials, the device attempts to connect to Wi-Fi and notifies the phone via the notify characteristic of success or failure.
Android BLE API: What Goes Wrong
BLE on Android is a source of pain. Different manufacturers implement the stack differently. BluetoothGatt.writeCharacteristic() may return true on call, but onCharacteristicWrite arrives with GATT_ERROR (133) status — the most common unexplained error.
The correct pattern is a command queue. BLE doesn't support parallel GATT operations:
class BleCommandQueue {
private val queue: LinkedList<() -> Unit> = LinkedList()
private var isExecuting = false
fun enqueue(command: () -> Unit) {
queue.add(command)
if (!isExecuting) executeNext()
}
fun onCommandComplete() {
isExecuting = false
executeNext()
}
private fun executeNext() {
if (queue.isEmpty()) return
isExecuting = true
queue.poll()?.invoke()
}
}
Every writeCharacteristic, readCharacteristic, setNotification — goes through the queue. onCharacteristicWrite callback → queue.onCommandComplete(). Without this, parallel operations cause the GATT stack to hang and disconnect.
MTU negotiation. Default MTU = 23 bytes (20 bytes payload). Long credentials may not fit. Request expansion immediately after connection:
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
gatt.requestMtu(512) // up to 517 bytes
}
}
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
// Now can write data up to mtu - 3 bytes
startProvisioning()
}
Espressif provisioning-android SDK
Espressif provides a ready-made SDK that hides low-level GATT work:
val device = ESPProvisionManager.getInstance(context)
.createESPDevice(
ESPConstants.TransportType.TRANSPORT_BLE,
ESPConstants.SecurityType.SECURITY_1
)
device.connectBLEDevice(scanResult) { connected ->
if (!connected) return@connectBLEDevice
device.scanNetworks { networks, error ->
// networks — list of Wi-Fi networks visible to the device
}
}
// After user selects a network
device.provision(selectedSsid, password) { status ->
when (status) {
ProvisioningStatus.SUCCESS -> navigateToSuccess()
ProvisioningStatus.FAILURE -> showError(status.toString())
}
}
The SDK implements channel encryption via SRP6a (Security 2) or Curve25519+AES (Security 1). Credentials are never transmitted in plain text.
iOS: CoreBluetooth + ESPProvision
On iOS — ESPProvision Swift Package from Espressif or native CoreBluetooth for custom protocols.
import ESPProvision
ESPProvisionManager.shared.searchESPDevices(devicePrefix: "PROV_", transport: .ble, security: .secure) { devices, error in
guard let device = devices?.first else { return }
device.connect(delegate: self) { status in
if case .connected = status {
device.provision(ssid: selectedSSID, passPhrase: password) { status in
// handle result
}
}
}
}
On iOS, there's no GATT stack fragmentation — CoreBluetooth works identically across all devices. But there's a limitation: background BLE scanning only works for devices with known Service UUIDs predefined in Info.plist.
Typical Provisioning UX Mistakes
No progress feedback. Device connects to Wi-Fi in 5–15 seconds. Without a progress indicator, users think the app froze and press back.
Not handling wrong password error. Device returns AUTH_ERROR status via notify characteristic. Show "Incorrect Wi-Fi password" — not "Connection error".
Not exiting provisioning mode after success. Device stops advertising BLE services after connecting to Wi-Fi — normal behavior. App should close the BLE connection and move to the next step.
Implementing BLE Provisioning based on Espressif SDK: 2–3 weeks. Custom GATT protocol with encryption for another chip: 4–6 weeks.







