Skip to main content
Updates are handled by the f_ota/ package and f_lib/firmware_* modules, with two delivery transports (BLE and WiFi) feeding one install path. On current firmware (v5.0.3, release_code 5.0.3 / release_id 339 in f_ota/system.py) the WiFi path runs over station WiFi — OTA Hotspot has been sunset — with the hotspot join kept only as a legacy path for older firmware.

Delivery paths

Via app (BLE)

The image streams over the chunked GATT transport (ota_ble.py, svc_ble_transfer.py): Connected to OTA BLE, cb_start_ota.

Via WiFi

Current WiFi OTA runs over station WiFi: the release is resolved via http://api.totemportal.com, the package is fetched from datapeak S3, and the update is command-triggered over a WebSocket channel. The legacy hotspot path (OTA Hotspot has been sunset) — join totemupdate, download over HTTP — remains only for older firmware. See WiFi OTA.
A demi-god broadcast (demigod_gen_ota_update) can also tell a group of nearby devices to update at once. Locally, a triple tap of the physical SOS button calls start_ota (sw_sos.cb_triple_tap, wired in compass; confirmed, identical in v5.0.2 and v5.0.3). The SOS button is a button.AsyncButton, not the capacitive Touch Crystal (touch_button_v2) — earlier revisions of this page had that wrong. See physical buttons.

Package formats

The updater accepts two package types, one per update method — firmware vs preview — selected by the update method field (Getting release details | update method: {} | endpoint_id: {} | version: {}): If the expected package is absent the update aborts cleanly: .tgz package not found in repo, cannot perform OTA, .bin package not found in repo, cannot perform OTA. A package index (contents.json, Downloading content.json) — a JSON array of filenames — describes what to fetch; the device picks the entry ending in .bin (firmware) or .tgz (preview). Assets and preferences can be pulled alongside (Download Compass Preferences, Download Developer Options, Downloaded preview).

OTA server contract (WiFi path)

The station-WiFi update is a plain-HTTP REST + fetch exchange against http://api.totemportal.com (API_ENDPOINT — no TLS, no auth). A custom server that answers these exchanges can push firmware to a device you own: there is no API key, client certificate, HMAC, or image signature anywhere on this path (see Integrity).
1

Device polls for a release

POST http://api.totemportal.com/devices/{MAC}/ota with a JSON body (get_release_from_api). {MAC} is the uppercase-hex device MAC (get_mac_addr):
(version and release_id are the device’s own release_code / release_id from f_ota/system.py: 5.0.3 / 339 on v5.0.3, 5.0.2 / 335 on v5.0.2. The other values are illustrative.)
2

Server returns a release object

The reply (get_release_details) must expose these fields:
Logged as Product:.......{}, Branch:........{}, Release Code:..{}, Release ID:....{}, OTA URL:.......{}.
3

Device fetches the package index

GET {ota_url}/contents.json (?uid= appended when uncached) returns a JSON array of filenames (contents.json syntax err, cannot parse on bad JSON). The device picks the entry ending in .bin (firmware) or .tgz (preview) — e.g. ["firmware_v5.0.3.bin"]. The version is parsed as the substring between _v and the extension.
4

Device downloads, verifies, flashes, reboots, acks

The picked file is downloaded (.bin streamed to the OTA slot; .tgz unpacked to VFS), verified by SHA-256 only, flashed, and the device reboots. It then reports success with POST http://api.totemportal.com/devices/{MAC}/ota?updated (record_release, Recording OTA release) — a JSON body carrying boot_count, branch, device_age, device_type_id, product, release_code, release_id, lat, lon, gnss_time. A custom server need only accept it with 200.

Install flow

Rollback & slot management

The image uses the standard ESP-IDF OTA data partition with two app slots plus a factory image. f_lib/firmware_rollback.py drives recovery. In the frozen bytecode the module is small — its only string literal is OTA rollback unsupported — and the recovery logic is expressed as code, not log strings: mark_app_valid_cancel_rollback confirms a good image (cancelling the IDF anti-rollback watchdog); the app-driven manual trigger Manually rolling back firmware lives in f_ota/main.py, alongside this module’s force() path and the IDF partition mechanism. A failed or non-self-confirming update reverts to the previous good slot. The “orange blink” failure indicator is described in the user-facing update guide; no firmware string binds an orange blink specifically to OTA failure or rollback.
The strings ota data invalid, no current app. Assuming factory, not found otadata, Rollback is not possible…, and Running firmware is factory do not appear in any of the 94 frozen modules of v5.0.3 (nor in v5.0.2’s 96) and are not part of f_lib/firmware_rollback.py; the module’s decision logic is carried in bytecode as shown above.

Integrity

  • App-level SHA-256 verification lives on the BLE transfer path, not the WiFi install. f_ble/chunking.py (fed by ota_ble.py) compares a digest: File integrity confirmed! on match, SHA256 hash does NOT match on mismatch (SHA256 final : {}, SHA256 origin: {}; qstrs sha256_expected, sha256_feed, sha256_hash). In the v5.0.3 disassembly sha256 appears in f_ble_chunking.dis, ota_ble.dis, f_ble_file_upload.dis, and f_lib_file_mgr.dis — the last is new in v5.0.3, where f_lib/file_mgr.py gains an async SHA-256 helper gen_file_hash(path, buff=None, yield_every=4) — plus unrelated references in mip and peer_helpers (present in both versions). There is no SHA-256 in f_ota/install_ota.py or in the WiFi streaming writer f_lib/firmware_ota.py.
  • The WiFi .bin path relies on the ESP-IDF bootloader image hash. f_lib/firmware_ota.py (BlockDevWriter / Ota.from_firmware_file) streams blocks to the inactive slot and sets the boot partition (Will boot from '{}' partition on next boot.) with no app-level digest of its own; integrity of a WiFi firmware image therefore rests on the stock ESP-IDF bootloader’s image-hash check at boot (Image hash failed - image is corrupt — a native bootloader string, not present in the frozen bytecode, so this wiring is inferred). The expected SHA-256 that the BLE path checks is carried in the transfer metadata, not derived from transport security.
  • There is no firmware signature on either OTA path. The ECDSA with SHA256 / ECP_VERIFY_FAILED machinery in the image belongs to the bundled mbedTLS/TLS stack and is not wired to the OTA install, so a custom OTA server needs no signing key.
The download itself was observed over HTTP (not HTTPS). Update integrity therefore relies on image-level verification and rollback rather than transport security. See WiFi OTA.