Skip to main content
The Totem Compass uses the ESP32’s single 2.4 GHz radio in three roles. They are time-multiplexed by the WiFi/BT coexistence layer, never simultaneously active in a way that conflicts on-air.

BLE

Phone app ↔ Totem. GATT peripheral, chunked transfer, (cat_id, cmd_id) messages.

ESP-NOW mesh

Totem ↔ Totem “Unity Mesh”. Peer sync and relay — cleartext, no encryption.

WiFi

Station-mode OTA. Joins a WiFi network, downloads firmware over plain HTTP (api.totemportal.com / S3). The device-hosted hotspot OTA path is sunset.

One message model across radios

The most important protocol finding: BLE and the mesh share a single application message model. Every message is addressed by a two-byte tuple:
The BLE link logs it directly — [BLE] DataXfer cat_id: {} | cmd_id: {} and Conn Status Update | cat_id: {} | cmd_id: {} — and the same categories drive peer sync over ESP-NOW. Large BLE file/OTA transfers are fragmented by a shared chunking layer; mesh frames are single fixed-size packets (57 B / 69 B, well under the 250-byte ESP-NOW MTU). The full model is on the message format page.
The (cat_id, cmd_id) addressing is shared, but the two transports bind each key to a different payload layout — BLE uses the gen_* builders in ble_manager.py / ble_core.py, ESP-NOW uses TOTEM_MSG_MAP / EXTENDED in espnow_conn_v2.py. Same key, different bytes: BLE gen_live_data (0x03, 0x01) packs <bfi3fb4Bi3b2hbiffb3ibHBBb (69 B), while TOTEM_MSG_MAP (3,1) is <BBHH4b4BHHBb (20 B). A client must pick the format for the transport it is on.

Capture completeness

This table is the direct answer to “is the 2.4 GHz protocol fully captured?” It grades each element by how firmly the firmware binary supports it.

Recovered from bytecode

Frozen-MicroPython disassembly (v1.25.0 / .mpy v6.3) has since decoded the three formerly-pending items. TOTEM_MSG_MAP is a flat dict built in espnow_conn_v2.py, keyed by the 2-byte (cat_id, cmd_id); every value is a payload struct format — the mesh frame layout for that message.
An earlier pass decoded the qstr-immediate operands with >>2 and produced bogus “handler names” (disconn_animation, device_power, dev_info, dev_total_lightsleep_ms, disabled, dev_sampling, …). Those were decode artifacts and are wrong. ESP32 MicroPython’s REPR_A tags a qstr-immediate as (o & 7) == 2 with value o >> 3; under the correct >>3 decode every entry resolves to a struct format string, shown below. There are no handler-name values in the map.
Every <BB… format leads with the echoed (cat_id, cmd_id). The (0x00, *) 23-byte family is the mesh peer beacon (2 floats lat/lon + flags + a 6-byte MAC). Field-by-field mesh semantics are partial — the layouts are confirmed but the fields are not all labelled. A parallel EXTENDED map holds schema-versioned variants, keyed (cat, cmd) → {schema_id → fmt}: The 57-B and 69-B (0x00, 0x00) schemas are the fixed-size mesh frames. Category 0x06 (Peer Sync) is absent from this registry — it is handled by separate peer-sync logic, not the payload-format map. Chunk headers. chunking.gen_transfer_buff packs the 16-byte transfer header <HBBBiHiB at offset 2 of a [0x02, 0x02, …] frame: file_id (H), status_id (B), action_id (B), file_type_id (B), byte_pos (i), chunk_no (H), file_size (i), then a byte that is literal 0 in v5.0.2 and pack_flags(is_last_chunk, is_origin_compass) in v5.0.3. The name CHUNK_HDR_FMT belongs to a different header. v5.0.3 binds it to the 12-byte per-chunk header '<HHiH' streamed on …-0003. v5.0.2 imported the name without defining it, so its uploader was dead code. See chunking.
Verdict. The protocol’s transport, addressing scheme, category taxonomy, GATT identifiers, connection state machine, chunking/file-transfer, and the demi-god command set are fully captured. The three items that were previously pending in bytecode — the per-category cmd_id map (TOTEM_MSG_MAP), the struct-to-message bindings, and the chunk-header layout — have now been recovered by disassembling the frozen MicroPython modules: TOTEM_MSG_MAP and its bindings live in espnow_conn_v2.py, and the chunk headers are packed in f_ble/chunking.py and f_ble/file_upload.py (see the tables above).Crucially, there is no confidentiality or integrity layer to defeat: ESP-NOW is cleartext on both broadcast and unicast (no PMK/LMK ever programmed), the ESP-NOW frame carries no CRC or checksum (SyncWord + length + size-check only), BLE needs no pairing, and no message path carries a token, HMAC, or signature. A client interoperates by matching the wire format alone. See methodology.

Coexistence

ESP-IDF’s WiFi/BT coexistence arbitrates the radio. The mesh runs on a fixed home channel; a peer whose channel differs is rejected (Peer channel is not equal to the home channel, send fail!). During a hotspot OTA the device is a WiFi station on the phone’s channel, and BLE/mesh traffic is suspended for the duration of the download.

Protocol lineage & prior art

Totem’s protocol is not a single named standard, and it is not a novel scheme either. It is a custom application layer assembled on top of standard, open transports, using conventional design patterns rather than anything proprietary or exotic.

Standard / open building blocks (reused)

Custom (Totem-authored) layer

  • Wire framing — a SyncWord (0xA774) + (cat_id, cmd_id) + struct.pack-ed binary payloads. This is the conventional “magic + type + fixed struct” idiom; there is no serialization library on the air (no Protobuf, CBOR, msgpack, or JSON — JSON appears only in the HTTP/OTA config, and WebSocket only for the cloud OTA trigger).
  • Mesh (“Unity Mesh”) — a custom managed-flooding design over ESP-NOW broadcast: flood + relay suppression (overhearing / slots) + message-UID dedup + hop limit. The pattern is well known (the same family as BLE Mesh’s managed flooding); the implementation is Totem’s own Python.
  • Chunked file transfer — a generic file_id / chunk_no / resume / ACK / SHA-256 scheme, not a named protocol.

What it is explicitly NOT

The disassembly rules out the usual suspects: not Bluetooth BLE Mesh; not Espressif esp-wifi-mesh (the SDK’s WIFI_MESH_EVENT symbols are present but unused); not Thread, Zigbee, Matter, or 6LoWPAN; not painlessMesh; not MQTT or CoAP. There is also no application-layer cryptography — traffic is cleartext, and the only obfuscation is a trivial rot13 (not encryption).
In short: a bespoke command-and-mesh protocol built from standard, open components, with a Totem-specific binary message layer on top. A client interoperates by speaking the standard transports plus Totem’s custom framing — there is no proprietary or exotic protocol to reimplement. See Building a custom client.