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.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 theupdate 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 againsthttp://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 (Logged as
get_release_details) must expose these fields: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 byota_ble.py) compares a digest:File integrity confirmed!on match,SHA256 hash does NOT matchon mismatch (SHA256 final : {},SHA256 origin: {}; qstrssha256_expected,sha256_feed,sha256_hash). In the v5.0.3 disassemblysha256appears inf_ble_chunking.dis,ota_ble.dis,f_ble_file_upload.dis, andf_lib_file_mgr.dis— the last is new in v5.0.3, wheref_lib/file_mgr.pygains an async SHA-256 helpergen_file_hash(path, buff=None, yield_every=4)— plus unrelated references inmipandpeer_helpers(present in both versions). There is no SHA-256 inf_ota/install_ota.pyor in the WiFi streaming writerf_lib/firmware_ota.py. - The WiFi
.binpath 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_FAILEDmachinery 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.