DocsBluetooth LE Open APIProtocol & Commands

Protocol & Commands

One message envelope for every command, response, and event — plus the full command reference.

Requests

Write a message to CMD_IN. Every request has the same shape, shown here decoded (on the wire the keys are the integers 0–4 and ns / prop are dictionary ids; see Wire Format):

{
  "kind":  1 | 2,            // 1 = get (read), 2 = set (write)
  "ns":    "deviceState",    // namespace (see below)
  "prop":  "PLAYER_SHIELD",  // property within the namespace
  "value": 100,              // the new value (for set); omit for get
  "id":    "abc123"          // your correlation id (echoed in the response)
}

Give every request a unique id. Responses arrive on CMD_OUT carrying the same id, so you can match a reply to the request that produced it.

Responses

Responses are delivered as notifications on CMD_OUT:

// success: kind 3
{ "kind": 3, "ns": "deviceState", "prop": "PLAYER_SHIELD", "value": 100, "id": "abc123" }

// error: kind 4
{ "kind": 4, "key": "invalidValue", "msg": "…", "data": "prop", "id": "abc123" }
Error keyMeaning
unknownValueThe namespace or property is not recognised.
invalidValueThe value was the wrong type or out of range for that property.
missingValueA required field (kind / ns / prop) was missing.
missingInputThe written value was empty.
decodeThe message could not be decoded: not a CBOR map, a dictionary id the device does not know, or malformed CBOR. The message text says which.

Events

Events are unsolicited notifications on EVENTS_OUT, with kind 5. They use the same ns / prop / value fields and carry no id — treat them as a fire-and-forget stream.

nsShapeWhen
deviceState{ "ns": "deviceState", "prop": "<KEY>", "value": <any> }A state value changed. Fires only while DEVICE_APP_CONNECTED is true. High-frequency values are rate limited (GPS ~2s, battery ~5s).
fsm{ "ns": "fsm", "prop": "state", "value": "<state>" }The device entered a new game / UI state (e.g. IN_GAME → DEAD).
game{ "ns": "game", "prop": "config", "value": { "radarType": 1, "trackerRangeM": 100, … } }The game configuration, pushed when an app connects and again whenever the device applies a config. Read radarType to know whether this game has a motion tracker at all before drawing one.
players{ "ns": "players", "prop": "update", "value": { "playerId", "name", "teamId", "lives", "connected", "kills", "deaths", "points" } }One player's scoreboard row changed. Sent per player rather than as a whole board, and only for rows that actually moved. Kills and deaths are counted from kill events, so they are always present; points are only meaningful when the game config says scoreBased is true, and are zero otherwise.
tracker{ "ns": "tracker", "prop": "contact", "value": { "playerId", "teamId", "lat", "lon" } }Another player broadcast their position. Relayed from the mesh, so this describes a different player and is not deviceState. Only arrives while that player is moving, and only when the game's radar is on — a player who stops moving simply stops appearing.
diagnostics{ "ns": "diagnostics", "prop": "timing", "value": { "windowMs", "loop": { "min", "max", "avg", "count" }, "items": [ { "id", "min", "max", "avg", "count" } ] } }One loop timing window, once a second while diagnostics/timing is on: the whole loop pass and every timed item, in microseconds. Items are FSM, DISPLAY, BLE, WIFI, NFC, AUDIO, INPUT, LORA, GPS, LIGHTING, ACCEL, INTERVALS, COMMS, BATTLESCRIPT and BATTLESCRIPT_EVENT.
ota{ "ns": "ota", "prop": "progress", "value": { "phase", "status", "percent", "bytes", "total" } }Firmware update progress during an OTA.
raw{ "ns": "raw", "prop": "ir" | "esp-now", "value": { "tp", "raw", "state", … } }The debug tap, while esp-bridge is enabled: every captured IR frame per sensor and every ESP-NOW frame, before any deduplication. The value is the tap object itself.

Command reference

Commands are grouped by namespace (ns). Unless noted, the response value mirrors the request.

deviceState

Read, write, and subscribe to the device's live state. prop is any state key (see the Device State Reference). Any value that changes is also pushed as an event.

MethodPropertyValueDescription
get<KEY>—Read a value, e.g. PLAYER_SHIELD. GET GAME_TIME returns the live game clock rather than the stored field.
set<KEY>new valueWrite a value, e.g. set PLAYER_NAME to a string, or DEVICE_APP_CONNECTED to true. The value is coerced to the key's type; a type mismatch returns invalidValue.

audio

Play and stop audio, and set the master volume.

MethodPropertyValueDescription
setstartTrackfile path (string)Play an audio file, e.g. "/audio/default/pickup.wav".
setmasterVolume0–30Set the master volume.
setstopTracktrack numberStop a playing track.
getisTrackPlaying—Returns whether audio is currently playing.

rgb

Drive the addressable RGB LEDs directly.

MethodPropertyValueDescription
setsetAll[[r,g,b,brightness], …]Set every pixel at once. One [r, g, b, brightness] array per pixel.
setsetOne[index,r,g,b,brightness]Set a single pixel by index.

players

The scoreboard. Every tagger derives this independently from the gossiped mesh event log rather than being told it by a host, so it keeps working if the host leaves the game.

MethodPropertyValueDescription
getroster—The whole board as an array: playerId, name, teamId, lives, connected, kills, deaths, points. Read this once for the initial paint — sixteen full records do not fit in a single BLE write, so ongoing changes arrive as players/update events rather than by re-reading.

game

The configuration of the game currently loaded. Every field is also readable individually through deviceState, but those tell you what changed rather than what things are — this returns the whole set at once, which is what you need to decide what to display.

MethodPropertyValueDescription
getconfig—Returns the game config as an object. Field names match the game's config file: gameId, bstId, name, isTeamBased, gameDurationMins, minPlayers, respawnMode, respawnTimeSecs, playerLives, mapType, radarType, trackerRangeM, trackerSensitivity, gpsTransmit, gpsMinMoveM, scoreBased.

fsm

The device's state machine — its current game / UI state. State transitions are also pushed as events.

MethodPropertyValueDescription
getstate—Returns the current state name, e.g. HOME, IN_GAME, DEAD.
setstatestate nameForce a state transition (advanced — most integrations only read this).

wifi

WiFi used by the device for firmware updates.

MethodPropertyValueDescription
setcredentials{ ssid, password }Store the WiFi network the device should use for OTA updates.
getchannel—Current radio channel.

provisioning

Fleet grouping and firmware-update triggers.

MethodPropertyValueDescription
setgroupgroup name (string)Assign this device to a named update group. Group update triggers only affect devices in the same group.
setcheckUpdate—Check this device for a firmware update AND broadcast the trigger to every device in the same group over the mesh.
setcheckUpdateSelf—Check only this device for a firmware update (no mesh broadcast).

Advanced

Lower-level namespaces used internally by the official app. Included for completeness.

MethodPropertyValueDescription
getscreenController · currentScreenName—The name of the UI screen currently shown.
setdiagnostics · timingbooleanLoop timing on or off. Off by default; while on, a diagnostics/timing event arrives once a second. A get of the same prop returns { enabled, last }.
setesp-bridge · enabledbooleanRelay ESP-NOW mesh traffic over the BLE event channel.
getdevice · crashReport—Whether a core dump from a crash is waiting in flash, and what it says: present, sizeBytes, attempts, resetReason, task, pc, cause, corrupted, elfSha, backtrace. The device normally sends a dump to the server by itself on the next boot when it has WiFi credentials, then erases it; this is for the app to show what is pending.
setdevice · crashReport{upload: true}Reboot into the crash-report boot now and send the waiting dump whatever the automatic attempt count says (three boots per dump). {discard: true} erases it instead. {trailReport: true} sends the breadcrumb trail alone, the way a brownout or hardware watchdog reset does by itself; the get also reports trailPending.
getdevice · diag—The breadcrumb trail from the diag partition, oldest first: {count, records: [{seq, ms, kind, text}]}. Kinds: boot (with the reset reason), state, command, wifi, reset, note, game. A set with {note: "..."} adds a line of your own. The newest forty ride with every crash report.
setbench · crash"hang" | "null" | "abort"Crash on purpose, to prove the report path: hang stops the main loop so the task watchdog panics after three seconds, null dereferences a null pointer, abort calls abort(). Test units only.
setirSender · 1 / 2—Fire IR emitter 1 or 2.

Worked example

Read the player's current ammo, then halve it. Decoded form first, then the bytes of the first request as they leave CMD_IN:

// request  -> CMD_IN
{ "kind": 1, "ns": "deviceState", "prop": "WEAPON_AMMO", "id": 1 }

// response <- CMD_OUT
{ "kind": 3, "ns": "deviceState", "prop": "WEAPON_AMMO", "value": 48, "id": 1 }

// request  -> CMD_IN
{ "kind": 2, "ns": "deviceState", "prop": "WEAPON_AMMO", "value": 24, "id": 2 }

// response <- CMD_OUT
{ "kind": 3, "ns": "deviceState", "prop": "WEAPON_AMMO", "id": 2 }

// ...and because the value changed, an event arrives on EVENTS_OUT:
{ "kind": 5, "ns": "deviceState", "prop": "WEAPON_AMMO", "value": 24 }

// the first request on the wire: frame header, then a 4-pair map
C1 01 00 01  A4 00 01 01 0B 02 18 A8 04 01