Part of: PureLife Device Protocol Version: 1.0 (Draft 0.1, 2026-09-17)
Warning
This specification is currently in draft status. The definitions, procedures and data structures described herein are subject to change without notice.
The interfaces documented here are not yet implemented and are provided for design and planning purposes only.
This document specifies how a Device is provisioned: how it receives its network configuration and the address of its Server, how the Provisioner verifies that the Device can reach the Server, and how the Device is paired with a Customer.
The protocol builds on the
Unified Provisioning
framework of Espressif (protocomm and network_provisioning). Everything
that framework already defines (session establishment, security schemes,
Wi-Fi configuration, Bluetooth LE transport) is used unchanged and is only
referenced here. This document specifies what is added on top of it.
| Reference | Location |
|---|---|
| Espressif Unified Provisioning | https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/provisioning/provisioning.html |
Espressif Protocol Communication (protocomm) |
https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/provisioning/protocomm.html |
Espressif network_provisioning component |
https://components.espressif.com/components/espressif/network_provisioning |
Espressif provisioning tool esp_prov |
https://github.com/espressif/esp-idf/tree/master/tools/esp_prov |
| Espressif provisioning library for Android | https://github.com/espressif/esp-idf-provisioning-android |
| Espressif provisioning library for iOS | https://github.com/espressif/esp-idf-provisioning-ios |
| Protocol Buffers | https://protobuf.dev/ |
The Espressif components are licensed under Apache-2.0. Their protobuf definitions are not reproduced in this specification. Implementers take them from the component sources.
A Device does not have to be built with ESP-IDF. Any implementation that behaves as specified by the references above and by this document is compliant.
Provisioning happens over Bluetooth LE between the Provisioner and the Device. The Provisioner also talks to the Server over HTTPS to finish the pairing. The Device talks to the Server during the Server check and the pairing step.
flowchart LR
P["Provisioner<br/>(app or tool)"]
D["Device"]
S["Server<br/>(PureLife Cloud)"]
P <-->|"Bluetooth LE (protocomm)<br/>this specification"| D
P -->|"HTTPS<br/>PureLife Cloud API (pairing)"| S
D -->|"HTTP(S)<br/>Device Messaging Protocol"| S
The Espressif framework contributes these endpoints:
| Endpoint | Provided by | Purpose |
|---|---|---|
proto-ver |
network_provisioning |
Version and capability information, readable without a secure session. |
prov-session |
protocomm |
Establishes the secure session. |
prov-scan |
network_provisioning |
Wi-Fi scan. |
prov-config |
network_provisioning |
Wi-Fi credentials, apply, connection status. |
prov-ctrl |
network_provisioning |
Reset and re-provision of the Wi-Fi state machine. |
This specification adds the following endpoints. All of them are custom endpoints in the sense of the Espressif framework and are only reachable inside the secure session.
| Endpoint | Purpose |
|---|---|
pld-info |
Read Device identity and versions. |
pld-config |
Set and read the pending PLD configuration (Server URL, NTP, DNS). |
pld-check |
Run the Server check and read its result. |
pld-ctrl |
Commit the configuration and finish provisioning. |
pld-pair |
Obtain a pairing token from the Server. |
protocomm BLE transport.PLD_<Device ID> (see
section 9.1 for the Device ID format). The name is at most
28 bytes; the protocomm BLE transport allows 29.protocomm does. The Provisioner takes the service
UUID from the advertisement and discovers the endpoints through the
Characteristic User Description descriptor of each characteristic. The
Provisioner MUST NOT rely on a fixed service UUID.The security scheme is the protocomm security scheme announced in
proto-ver (prov.sec_ver).
| Scheme | Mechanism | Requirement |
|---|---|---|
| 1 | Curve25519 key exchange, AES-256-CTR, authenticated with a Proof of Possession (PoP) | Production Devices MUST support Security 1 with PoP. |
| 2 | SRP6a (3072-bit group, SHA-512), AES-256-GCM, authenticated with username and password | Devices MAY support Security 2. If they do, the credentials are printed on the label instead of the PoP. |
| 0 | No encryption, no authentication | Devices MAY support Security 0. |
Rules:
no_pop
capability MUST NOT be present in proto-ver).no_sec
capability in proto-ver. With Security 0 all provisioning data,
including the Wi-Fi passphrase, is transmitted unencrypted, and anyone in
Bluetooth range can provision the Device while it is in Provisioning.
A Provisioner SHOULD inform the user before it continues with an
unsecured session.CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_0 /
..._VERSION_1).A-Z, a-z, 0-9. 12 characters are RECOMMENDED.The proto-ver endpoint returns a JSON document. The framework fills the
prov object. The Device MUST add an object with the key pld:
{
"prov": { "ver": "...", "sec_ver": 1, "sec_patch_ver": 0, "cap": ["wifi_scan"] },
"pld": { "ver": "1.0", "cap": ["ipv6"] }
}
The values inside prov are set by the Espressif component and are not
specified here.
| Key | Meaning |
|---|---|
pld.ver |
Version of the Device Provisioning Protocol implemented by the Device, MAJOR.MINOR. This document is version 1.0. |
pld.cap |
Optional capabilities from the closed list below. |
Defined capabilities:
| Capability | Meaning |
|---|---|
ipv6 |
The Device accepts IPv6 literals as DNS server addresses. |
The key pld and the contents of the pld object are fixed by this
specification. A Device MUST NOT add other keys to the pld object or
other capabilities than the ones listed. The prov object is defined by
the Espressif component; the Provisioner does not depend on any other
top-level key.
The Provisioner MUST refuse to continue if pld is missing or if
pld.ver has a MAJOR version it does not support.
stateDiagram-v2
direction LR
[*] --> Provisioning : power on<br/>(delivery state)
Provisioning --> Operating : Finish, or<br/>timer expiry after Commit
Provisioning --> Off : timer expiry<br/>without Commit
Off --> Provisioning : power cycle
Operating --> Provisioning : Factory Reset<br/>(button 20 s)
| State | Behaviour |
|---|---|
| Provisioning | The Device advertises over Bluetooth LE and accepts provisioning. The Device does not perform its normal function and does not communicate with the Server except for the Server check and the pairing step. |
| Operating | Normal operation as defined by the Device Messaging Protocol. Bluetooth LE advertising is stopped. |
| Off | The Device has switched itself off. If the hardware cannot power itself off, it MUST enter its lowest power state and stop all radio activity. A power cycle is required to leave this state. |
Rules:
disable_auto_stop) and MUST stop the provisioning
service only on a Finish command or on timer expiry.The status LED shows the following states.
| Situation | LED |
|---|---|
| Provisioning, no Provisioner connected | blue, blinking 1 Hz |
| Provisioning, secure session established | blue, steady |
| Wi-Fi connection failed (after Apply, or in Operating) | red, blinking 1 Hz |
| Wi-Fi connected, Server not reachable | yellow, blinking 1 Hz |
| Connection to Server established | green, steady for 5 seconds, then off |
| Operating, everything fine | off |
| Server rejected Device (see Device Messaging Protocol) | red, steady |
| Factory Reset pending (button held for 3 s, not yet 20 s) | yellow, blinking 4 Hz |
| Factory Reset triggered (after the button was held for 20 s) | red, 3 short flashes, then the Provisioning pattern |
| Off | off |
"Connection to Server established" applies whenever the Device establishes its connection to the Server, both during the Server check and in Operating after every reconnect.
The Provisioner performs the following steps in order. Steps marked (framework) use Espressif endpoints unchanged.
| # | Step | Endpoint |
|---|---|---|
| 1 | Read the Device label: scan the QR code, or enter Device ID and PoP by hand. Obtain device name, PoP and security scheme. | – |
| 2 | Find the Device by its advertised name and connect. | – |
| 3 | Read version and capabilities. Refuse on unsupported pld.ver. (framework) |
proto-ver |
| 4 | Establish the secure session with the PoP. (framework) | prov-session |
| 5 | Read Device identity. Verify that the Device ID matches the label. | pld-info |
| 6 | Send the PLD configuration: Server URL and, if the network requires it, NTP and DNS servers. | pld-config |
| 7 | Scan Wi-Fi networks. (framework, optional) | prov-scan |
| 8 | Send Wi-Fi credentials, apply, poll the connection status. (framework) | prov-config |
| 9 | Start the Server check and poll its result. | pld-check |
| 10 | Commit the configuration. This is allowed even if step 8 or 9 failed. | pld-ctrl |
| 11 | Start pairing, poll for the pairing token. | pld-pair |
| 12 | Send the pairing token to the Server (Server API). | – |
| 13 | Finish. The Device enters Operating. | pld-ctrl |
Deviations:
prov-ctrl (CtrlReset) and repeat step 8 with different
credentials, or it MAY commit anyway (step 10) and leave the Device to
retry. Committing with failing Wi-Fi is explicitly allowed so that an
installer can finish work when the network is not yet available.All PLD endpoints follow the same pattern as the Espressif endpoints: each
endpoint has one payload message with a msg type field and a oneof
containing the command or response. Each response carries a Status.
| Value | Meaning |
|---|---|
STATUS_UNSPECIFIED |
Not set. A response with this status is invalid. |
STATUS_OK |
The command succeeded. |
STATUS_INVALID_ARGUMENT |
A field is missing, malformed or out of range. detail names the field. |
STATUS_INVALID_STATE |
The command is not allowed in the current state (for example Finish before Commit). |
STATUS_NOT_SUPPORTED |
The Device does not support this command or capability. |
STATUS_BUSY |
An operation is still running (for example a check or pairing). |
STATUS_INTERNAL_ERROR |
Any other error inside the Device. |
Every response message contains Status status = 1 and
string detail = 2 (human readable, English, at most 64 bytes).
detail MUST be empty when status is STATUS_OK and SHOULD name the
cause otherwise. It is meant for logs and support, not for display to end
users.
The .proto files are the reference for field names, numbers, enum
values and the conventions used (see the header of
pld_constants.proto). The tables in this
document use the short forms of enum values where the prefix is obvious.
pld-infoReads the identity of the Device. Read-only.
| Message | Direction | Fields |
|---|---|---|
GetInfoCommand |
Provisioner → Device | – |
GetInfoResponse |
Device → Provisioner | status, detail, device_id, manufacturer, model, hardware_revision, firmware_version |
| Field | Constraints |
|---|---|
device_id |
Device ID. 8 to 24 characters from A-Z, 0-9 and -. Unique per Manufacturer and stable for the lifetime of the Device. RECOMMENDED: the 12 hex digits of the primary MAC address in upper case without separators. |
manufacturer |
Manufacturer identifier agreed with PureSec. 1 to 32 characters from a-z, 0-9 and -. |
model |
Model name, 1 to 32 bytes UTF-8. |
hardware_revision |
Free text, 1 to 32 bytes UTF-8. |
firmware_version |
Free text, 1 to 32 bytes UTF-8. RECOMMENDED: semantic version. |
The Provisioner MUST compare device_id with the Device ID from the label
and abort if they differ.
pld-configSets and reads the pending PLD configuration. Values sent here are
held in memory only. They are persisted by Commit (pld-ctrl).
| Message | Direction | Fields |
|---|---|---|
SetConfigCommand |
Provisioner → Device | server_url (optional string), ntp_servers (StringList), dns_servers (StringList) |
SetConfigResponse |
Device → Provisioner | status, detail |
GetConfigCommand |
Provisioner → Device | – |
GetConfigResponse |
Device → Provisioner | status, detail, config (the pending values), committed (bool) |
Semantics of SetConfigCommand:
STATUS_INVALID_ARGUMENT if any field is invalid. In that case no
pending value is changed.| Field | Constraints |
|---|---|
server_url |
Absolute URL, scheme https or http, no userinfo, no query, no fragment, at most 128 bytes. A path prefix is allowed. A trailing slash is invalid (STATUS_INVALID_ARGUMENT), so that the URL can be compared byte for byte. Example: https://cloud.smart-altern.de. The Device appends the paths of the Device Messaging Protocol (/pld/v1/...) to it. With http the connection to the Server is unencrypted; the operator of the Server is responsible for the security of the network in that case. |
ntp_servers |
0 to 3 entries. Each entry a host name or IPv4 literal, at most 40 bytes. |
dns_servers |
0 to 3 entries. Each entry an IPv4 literal (at most 15 bytes), or an IPv6 literal (at most 45 bytes) if the Device announces the ipv6 capability. |
Only server_url is required for a Commit. The lists are empty until
the Provisioner sets them.
pld-ctrl| Message | Direction | Fields |
|---|---|---|
CommitCommand |
Provisioner → Device | – |
CommitResponse |
Device → Provisioner | status, detail |
FinishCommand |
Provisioner → Device | – |
FinishResponse |
Device → Provisioner | status, detail |
Commit
STATUS_INVALID_ARGUMENT otherwise, detail names the missing field).Finish
STATUS_INVALID_STATE otherwise).pld-checkSee section 10.
| Message | Direction | Fields |
|---|---|---|
StartCheckCommand |
Provisioner → Device | – |
StartCheckResponse |
Device → Provisioner | status, detail |
GetCheckStatusCommand |
Provisioner → Device | – |
GetCheckStatusResponse |
Device → Provisioner | status, detail, state, stages |
pld-pairSee section 11.
| Message | Direction | Fields |
|---|---|---|
StartPairingCommand |
Provisioner → Device | – |
StartPairingResponse |
Device → Provisioner | status, detail |
GetPairingStatusCommand |
Provisioner → Device | – |
GetPairingStatusResponse |
Device → Provisioner | status, detail, state, error, http_status, token, expires_at |
The Server check verifies from the Device's point of view that the Server is reachable with the pending configuration. It is asynchronous: the Provisioner starts it and polls the result.
Preconditions: the pending configuration MUST be complete and Wi-Fi MUST be
connected. Otherwise StartCheckCommand answers STATUS_INVALID_STATE. A running
check answers STATUS_BUSY.
The check runs the following stages in order and stops at the first
failure. Later stages are reported as SKIPPED, as is a stage that does
not apply.
| Stage | What is verified |
|---|---|
WIFI |
The station interface is connected and has an IP address. |
DNS |
The host of server_url resolves using the DNS servers in effect (configured, or from DHCP). |
NTP |
The clock is synchronised with one of the NTP servers in effect (configured, from DHCP, or Manufacturer default). |
TLS |
A TLS connection to the Server host is established and the certificate chain validates against the Device's store of public CAs. Reported as SKIPPED when server_url uses the scheme http. |
SERVER |
Authentication (POST /pld/v1/auth) and bootstrap (GET /pld/v1/bootstrap) of the Device Messaging Protocol both succeed with HTTP status 200. As a side effect the Device is registered on the Server. |
MQTT |
A connection to the broker named in the bootstrap response succeeds with the returned credentials (CONNECT, CONNACK with success), and is closed again normally. Nothing is published. This is the only stage that needs a port other than the Server's HTTP port, which is why it is tested here. |
The overall check MUST finish within 60 seconds. Each stage MUST fail with
CHECK_ERROR_TIMEOUT if it exceeds its share.
The Server check does not check for or install firmware updates; that
happens in the start-up sequence after Finish. The test connection of
stage MQTT is not an operating connection.
GetCheckStatusResponse:
| Field | Meaning |
|---|---|
state |
CheckState: IDLE (never started), RUNNING, DONE. |
stages |
One StageResult per stage, in order. |
StageResult:
| Field | Meaning |
|---|---|
stage |
Stage identifier. |
result |
StageState: PENDING, RUNNING, OK, FAILED, SKIPPED. |
error |
CheckError, see below. CHECK_ERROR_NONE unless result is STAGE_STATE_FAILED. |
http_status |
HTTP status code, only set for CHECK_ERROR_HTTP_STATUS, 0 otherwise. |
detail |
Human readable, at most 32 bytes, optional. For example the resolved address or the TLS error string of the platform. |
CheckError:
| Value | Stage | Meaning |
|---|---|---|
CHECK_ERROR_NONE |
– | No error. |
CHECK_ERROR_WIFI_NOT_CONNECTED |
WIFI |
No connection or no IP address. |
CHECK_ERROR_DNS_FAILED |
DNS |
Name resolution failed. |
CHECK_ERROR_NTP_UNREACHABLE |
NTP |
No NTP server in effect answered. |
CHECK_ERROR_TLS_CONNECT_FAILED |
TLS |
TCP or TLS handshake failed for a reason other than certificate validation. |
CHECK_ERROR_TLS_CERT_UNTRUSTED |
TLS |
Certificate chain does not validate against the CA store. |
CHECK_ERROR_TLS_CERT_INVALID_TIME |
TLS |
Certificate not yet valid or expired according to the Device clock. |
CHECK_ERROR_TLS_HOSTNAME_MISMATCH |
TLS |
Certificate does not match the host name. |
CHECK_ERROR_HTTP_CONNECT_FAILED |
SERVER |
Request could not be sent or no response was received. |
CHECK_ERROR_HTTP_STATUS |
SERVER |
Response received with a status other than 200. http_status contains it. |
CHECK_ERROR_HTTP_INVALID_RESPONSE |
SERVER |
Response received but not a valid authentication or bootstrap response. |
CHECK_ERROR_MQTT_CONNECT_FAILED |
MQTT |
TCP or TLS connection to the broker failed, or no CONNACK. |
CHECK_ERROR_MQTT_NOT_AUTHORIZED |
MQTT |
The broker refused the credentials (CONNACK reason code other than success). |
CHECK_ERROR_TIMEOUT |
any | Stage exceeded its time budget. |
CHECK_ERROR_INTERNAL |
any | Any other error. detail SHOULD explain. |
The Provisioner SHOULD present the failed stage and error to the user in plain language and SHOULD offer to commit anyway.
Pairing links the Device to the Customer of the Provisioner's user. The Device proves to the Server that it is online, and the Provisioner proves to the Server that it has physical access to the Device (it holds the secure session that required the PoP).
sequenceDiagram
participant P as Provisioner
participant D as Device
participant S as Server
P->>D: StartPairingCommand
D->>S: request pairing token (Device Messaging Protocol, HTTPS)
S-->>D: token, expires_at
P->>D: GetPairingStatusCommand (poll)
D-->>P: DONE, token, expires_at
P->>S: pair Device (PureLife Cloud API): device_id, token
S-->>P: 200 OK
P->>D: FinishCommand
Rules:
StartPairingCommand requires a Commit (STATUS_INVALID_STATE otherwise) and
answers STATUS_BUSY while a request is running.POST /pld/v1/pairing-token). The request MUST finish within
30 seconds. On 401 the Device authenticates again and retries the
request once within that budget; on 429 or 503 it does not retry but
reports PAIR_ERROR_SERVER_REJECTED with the status, and the
Provisioner MAY start a new pairing after the Retry-After time.expires_at is the expiry time as Unix
milliseconds, set by the Server; the default lifetime is 10 minutes.StartPairingCommand requests a new token. The Server invalidates the
previous one.GetPairingStatusResponse:
| Field | Meaning |
|---|---|
state |
PairState: IDLE, RUNNING, DONE, FAILED. |
error |
PairError, see below. PAIR_ERROR_NONE unless state is PAIR_STATE_FAILED. |
http_status |
Only set for PAIR_ERROR_SERVER_REJECTED. |
token |
Only set when state is DONE. |
expires_at |
Only set when state is DONE. |
PairError:
| Value | Meaning |
|---|---|
PAIR_ERROR_NONE |
No error. |
PAIR_ERROR_SERVER_UNREACHABLE |
The Server could not be reached. Run the Server check for details. |
PAIR_ERROR_SERVER_REJECTED |
The Server answered with an error status. http_status contains it: 401 after one failed re-authentication, 403 when the Device is blocked, 429 or 503 when the Server asks to retry later, 5xx otherwise. |
PAIR_ERROR_TIMEOUT |
No answer within 30 seconds. |
PAIR_ERROR_INTERNAL |
Any other error. |
Every Device carries a label with, in human readable form:
The label SHOULD additionally carry a QR code as specified below. The
Provisioner MUST support both ways: scanning the QR code and entering the
Device ID and the secret by hand. From the Device ID the Provisioner
derives the advertised name (PLD_<Device ID>).
The QR code contains a JSON document in the format used by the Espressif provisioning libraries, so that those libraries can parse it directly:
{"ver":"v1","name":"PLD_9923003A4BE3","pop":"K7fT2pQ9xL4m","transport":"ble","security":1}
| Key | Value |
|---|---|
ver |
Always v1 (format version of the Espressif QR code). |
name |
The advertised Bluetooth LE name, PLD_<Device ID>. |
pop |
The PoP. Security 1 only. |
transport |
Always ble. |
security |
0, 1 or 2. |
For Security 2 the keys username and password are used instead of
pop, as defined by the Espressif provisioning libraries. For Security 0
the key pop is omitted.
The Device ID is not a separate key. It is name without the PLD_
prefix. After the session is established the Provisioner compares it with
pld-info.
The protocomm BLE transport limits one transfer to 512 bytes, including
the overhead of the security scheme (16 bytes for the AES-GCM tag of
Security 2). Every PLD request and every PLD response MUST therefore be at
most 450 bytes after protobuf serialisation and before encryption.
The field limits in this document are chosen so that every message stays
within that limit even when all fields have their maximum length. The
largest messages are SetConfigCommand and GetConfigResponse with a
full configuration (about 410 bytes). This relies on the rule that
detail is empty on success; a 64-byte detail in a GetConfigResponse
with a full configuration would exceed the limit. The Provisioner MAY
split SetConfigCommand over several commands.
Some steps of this protocol are specified elsewhere:
| Step | Specified in |
|---|---|
Server check, stage SERVER: authentication and bootstrap of the Device |
Device Messaging Protocol, sections 6 and 8 |
| Pairing: the request that returns the pairing token | Device Messaging Protocol, section 10 |
| Pairing: redeeming the token (Provisioner only) | PureLife Cloud API, operation Pair a sensor |
The messages of the PLD endpoints are defined in proto/:
| File | Endpoint |
|---|---|
pld_constants.proto |
shared enums |
pld_info.proto |
pld-info |
pld_config.proto |
pld-config |
pld_ctrl.proto |
pld-ctrl |
pld_check.proto |
pld-check |
pld_pair.proto |
pld-pair |
The framework's own definitions (session.proto, sec0.proto,
sec1.proto, sec2.proto, constants.proto, wifi_config.proto,
wifi_scan.proto, wifi_ctrl.proto) are taken from the protocomm and
network_provisioning sources.