BLE Operational Protocol
After provisioning, BLE stays active for direct mobile ↔ device communication — with or without WiFi.
Principle
BLE and MQTT carry identical JSON. Route by payload fields (type, ch), not transport.
MQTT → {workspace_slug}/{device_key}/{stream}
BLE → same JSON object, no topic path
One handler per message type. Switching transport mid-session requires no format change.
Transport comparison
| MQTT | BLE | |
|---|---|---|
| Routing | Topic suffix + JSON type/ch | JSON type/ch only |
| Auth | Connection (username/password) | auth message |
| Config | Full protocol (gzip, sha256) | Simplified (no gzip) |
| Direction | Cloud ↔ device | Mobile ↔ device |
Discovery
- Scan for service UUID
1d14d6ee-1001-4000-8024-b5a3c0ffee01 - Match GAP name to
device_key(e.g.MER-C782)
Before provisioning the name is MAC-based (MER-XXXX). See BLE JSON channel.
Session
Connect → negotiate MTU → enable TX notify → auth → operational messages
Non-auth messages before successful auth are rejected.
MTU
uint16_t mtu = iotmer_ble_get_att_mtu() ?: 23;
uint16_t max_payload = mtu > 3 ? mtu - 3 : 0;
Split payloads larger than max_payload into multiple notifications.
Auth (BLE only)
No MQTT equivalent — MQTT auth is at connection level.
Request:
{"type":"auth","token":"dht_abc123..."}
Success:
{"type":"auth.ok","mode":"online"}
mode: "online" (MQTT connected) or "offline".
Failure:
{"type":"auth.err","reason":"invalid_token"}
reason | Cause |
|---|---|
missing_token | No token field |
invalid_token | Token mismatch |
not_authenticated | Message sent before auth |
Session timeout: 60 s. Re-auth on timeout. Disconnect clears session.
Mode notification (BLE only)
Device sends when MQTT state changes:
{"type":"mode","mode":"offline"}
Commands
MQTT: publish to …/cmd · BLE: write to RX
{"type":"cmd","ch":"relay_1","cmd":"set","state":true}
| Field | Description |
|---|---|
type | "cmd" |
ch | Channel ID from device template |
cmd | e.g. "set", "reset_energy" |
state | For set: true = on |
Device-wide commands use ch: "device". Firmware OTA is MQTT-triggered ("cmd":"ota"); do not start OTA over BLE. See OTA.
Telemetry
MQTT: publish to …/telemetry · BLE: TX notify
{"type":"telemetry","ch":"ch_1","voltage":22050,"current":1234,"ts":1748000000}
| Field | Required | Notes |
|---|---|---|
type | Yes | "telemetry" |
ch | Per channel | Omit for device-scope metrics |
ts | Recommended | Unix seconds UTC |
Integer scaling is defined per device template in the console — not hard-coded in the transport.
Push
Sent on timer, significant change, or boundary events. MQTT publishes when connected. BLE may mirror on the same schedule or only on demand (product-specific).
Pull: telemetry.get
BLE (after auth, write to RX):
{"type":"telemetry.get","rid":"550e8400-…"}
Device responds with one or more type:"telemetry" notifications. Optional ack: telemetry.get.ok.
MQTT (publish to …/cmd):
{"type":"cmd","ch":"device","cmd":"telemetry.get"}
Device publishes telemetry to …/telemetry. Some products use …/cmd/device with {"cmd":"telemetry.get"} — follow the device template.
State
MQTT: …/state, QoS 1, retain · BLE: TX notify
{"type":"state","ch":"relay_1","state":true,"relay_status":"closed","ts":1748000000}
| Field | Meaning |
|---|---|
state | Logical: energized |
relay_status | Physical: "closed" / "open" |
Event
MQTT: …/event, QoS 1 · BLE: TX notify
{"type":"event","key":"fault","value":3,"fault_bits":{"over_current":true},"ts":1748000000}
Common fault_bits: over_current, over_voltage, under_voltage, energy_low, reset_overflow.
This is live notify. On-device history is event.get (command ack), not this topic. See Event logging.
Config (BLE)
Get
No MQTT equivalent for this direction — MQTT config flows cloud → device. See MQTT Config Protocol.
// request
{"type":"config.get","rid":"abc123"}
// response
{"type":"config.resp","rid":"abc123","data":{"protection":{"overcurrent_a":63}}}
Large responses use chunked config.resp.part:
{"type":"config.resp.part","rid":"abc123","i":0,"n":3,"chunk":"{\"protection\":…"}
Reassemble: group by rid, sort by i, concatenate chunk strings, parse JSON. Timeout: 5 s. Cap concurrent transfers (2–4).
If config not loaded: "data": null, "note": "config_not_loaded".
Set
Applies to MCU immediately. Does not persist to cloud.
{"type":"config.set","rid":"abc124","data":{"protection":{"over_voltage_v":260}}}
{"type":"config.ok","rid":"abc124"}
When MQTT reconnects, cloud config overwrites local values. To persist, update config in the Control Plane.
SDK integration
Register one handler per command — called identically from MQTT or BLE:
iotmer_ble_ops_register_cmd(type, ch, handler);
Outbound — build JSON once, send on both channels:
iotmer_ble_ops_notify(payload); /* BLE TX */
iotmer_publish(client, payload); /* MQTT */
Related
- BLE JSON channel — GATT UUIDs, security
- BLE JSON provisioning — WiFi over BLE
- Telemetry — SDK MQTT helpers
- MQTT Config Protocol — Cloud → device config
- Event logging —
event.gethistory (app contract) - MQTT topics & ACL