Skip to content

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.

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.

The device advertises over Bluetooth LE:

  • Name (scan response): <bluetooth_root>-<serial>, e.g. ACME-AIR-7K3M9Q2X, at most 29 characters. bluetooth_root is 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.

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.

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.

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):

  1. App → S2SessionCmd0 { client_username = serial, client_pubkey = A }
  2. Device → S2SessionResp0 { device_pubkey = B, device_salt = s }
  3. App → S2SessionCmd1 { client_proof = M }
  4. 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.

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.

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 serial and product are the ones in the QR code before it sends anything.
  • nonce is 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.
  • set stores both in the device’s NVS. Limits: token at most 1024 bytes, label at most 32 bytes. Error codes: bad_request, too_long, storage.

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.

  • 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 for CONFIG_DC_SETUP_AFTER_BOOT_S seconds 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.