The protocol
How the Device Configs app puts a device on Wi-Fi. Version 2 is Espressif’s provisioning protocol
(ESP-IDF wifi_provisioning over protocomm, security scheme 2) plus one Device Configs endpoint.
The firmware kit (firmware/components/deviceconfigs) is the device side; the Android app
(android/.../ble/protocomm) is the phone side. Anything that speaks Espressif’s protocol with
security 2 (their esp_prov.py, their phone libraries) can provision a kit device too; only the
dc-device endpoint is ours.
Espressif ships the device side in two builds: wifi_provisioning (ESP-IDF 5.x) and its successor
network_provisioning (ESP-IDF 6, Arduino-ESP32 3.x). The kit uses whichever the build has; their
Wi-Fi messages have the same field numbers and values, so the app can’t tell them apart.
Version 1 (the first devices set up with the app: one plaintext write of token,ssid,password,label, success =
the device drops the connection) stays supported in the app for devices that have not been reflashed.
What is on the device’s label
Section titled “What is on the device’s label”https://deviceconfigs.com/p/<product>/<serial>#<setup code>| Part | Example | What it is |
|---|---|---|
product |
airmon-2 |
The product record (Firestore devices/<product>): name, maker, branding. |
serial |
7K3M9Q2X |
This unit. 8 characters of Crockford base32 (no I, L, O, U). |
setup code |
4TQ8N-HV2KB |
This unit’s secret: 10 Crockford base32 characters (50 bits), printed with a dash. |
The setup code sits after #, so a browser that opens the link never sends it to a server. The
device does not store it either: it stores an SRP verifier made from it at the factory
. Reading a code: drop dashes and spaces, upper-case, then O→0 and
I/L→1, so a hand-typed code still matches.
A QR code without a serial and setup code (/p/<product> only) is a version-1 product.
Finding the device
Section titled “Finding the device”The device advertises over Bluetooth LE:
- Name (scan response):
<bluetooth_root>-<serial>, e.g.ACME-AIR-7K3M9Q2X, at most 29 characters.bluetooth_rootis the product record’s field, as in version 1. With a serial in the QR code the app connects to that unit only, even if several identical devices are nearby. - Service (advertisement):
898cb7ac-c83e-42a1-bae2-e5a6bae527bd, the same for every Device Configs device. Each endpoint is a characteristic of that service; the app finds them by reading each characteristic’s User Description descriptor (0x2901), which holds the endpoint name.
Each exchange is a write to the endpoint’s characteristic followed by a read of the same one.
Endpoints
Section titled “Endpoints”| Endpoint | Encrypted | What it does |
|---|---|---|
proto-ver |
no | JSON version info (below). |
prov-session |
(sets it up) | The security 2 handshake. |
prov-scan |
yes | The device scans for Wi-Fi networks and lists what it can see. |
dc-device |
yes | Device Configs: who the device is, and the token and label. |
prov-config |
yes | Wi-Fi name and password, apply, and the result of joining. |
proto-ver
Section titled “proto-ver”Write any text, read JSON:
{"prov": {"ver": "v1.1", "sec_ver": 2, "sec_patch_ver": 1, "cap": ["wifi_scan"]}, "dc": {"ver": "2", "cap": ["token", "label"]}}The app requires prov.sec_ver = 2 and a dc entry. sec_patch_ver decides the nonce rule below.
Security 2 session (prov-session)
Section titled “Security 2 session (prov-session)”SRP-6a (RFC 5054 3072-bit group, g = 5, SHA-512) with username = the serial and password = the
setup code, then AES-256-GCM. Messages are protobuf SessionData (session.proto, sec2.proto in
ESP-IDF components/protocomm/proto):
- App →
S2SessionCmd0 { client_username = serial, client_pubkey = A } - Device →
S2SessionResp0 { device_pubkey = B, device_salt = s } - App →
S2SessionCmd1 { client_proof = M } - Device →
S2SessionResp1 { device_proof = H(A, M, K), device_nonce }; the app checks the proof.
A wrong setup code fails at step 4 (the device refuses the proof), and the device logs
PROTOCOMM_SECURITY_SESSION_CREDENTIALS_MISMATCH. The numbers follow Espressif’s reference client
(tools/esp_prov/security/srp6a.py), including that integers are hashed as their shortest
big-endian bytes, while k and u hash values padded to 384 bytes.
After the handshake every message on an encrypted endpoint is AES-GCM(key = K[0..32], iv = nonce),
ciphertext followed by the 16-byte tag, no associated data. With sec_patch_ver 1 (ESP-IDF 5.1 and
later) the last 4 bytes of the nonce are a big-endian counter that both sides step after every
message, in either direction; with patch version 0 the nonce never changes.
prov-scan
Section titled “prov-scan”WiFiScanPayload (wifi_scan.proto in ESP-IDF components/wifi_provisioning/proto): start a
blocking scan, read the count, read the results in pages of 4. The app shows the device’s own list:
it is what the device can actually reach, 2.4 GHz included.
dc-device
Section titled “dc-device”JSON, UTF-8, encrypted like the other endpoints.
| Request | Reply |
|---|---|
{"cmd":"info"} |
{"v":2,"serial":"7K3M9Q2X","product":"airmon-2","fw":"1.4.0","nonce":"<32 hex>","has_token":false} |
{"cmd":"set","token":"<token>","label":"Kitchen"} |
{"ok":true}, or {"ok":false,"error":"<code>"} |
- The app checks that
serialandproductare the ones in the QR code before it sends anything. nonceis fresh for each session. The token service (to come) binds the token it signs to this serial and nonce, so a token cannot be replayed onto another device.setstores both in the device’s NVS. Limits: token at most 1024 bytes, label at most 32 bytes. Error codes:bad_request,too_long,storage.
prov-config
Section titled “prov-config”WiFiConfigPayload (wifi_config.proto): CmdSetConfig { ssid, passphrase }, CmdApplyConfig,
then CmdGetStatus once a second until the state settles:
| Device says | App shows |
|---|---|
Connected with ip4_addr |
Done. |
Connecting |
Still joining. |
ConnectionFailed, AuthError |
Wrong password: type it again (Bluetooth stays connected). |
ConnectionFailed, NetworkNotFound |
The device can’t see that network: closer to the router, or 2.4 GHz. |
Disconnected + attempt_failed { attempts_remaining } |
Still trying. |
After a failure the device resets its provisioning state and waits for new settings in the same session. Once connected it keeps Bluetooth up for 5 seconds, so the app reads the result, then the kit ends setup and frees the Bluetooth memory.
When setup is open
Section titled “When setup is open”- Not set up yet: setup is open until the device is on Wi-Fi.
- Already set up: setup opens only when the maker’s firmware asks for it (
dc_setup_open(), e.g. a button held at power-on), or forCONFIG_DC_SETUP_AFTER_BOOT_Sseconds after each power-on when the maker enables it. Opening setup on a working device needs the setup code, so a neighbour cannot take the device over. - Factory reset (
dc_factory_reset()): forgets the Wi-Fi network, token and label; the serial, verifier and product, in the factory partition, stay.