Skip to main content
WiFi is used almost exclusively for over-the-air updates when the Bluetooth path is unavailable (for example, when the device is on firmware older than the app supports). Driven by f_lib/wifi.py / wifi_v2.py, f_lib/firmware_ota.py, f_lib/requests.py, and ota_daemon.
In v5.0.3 (as in v5.0.2) the phone-hotspot OTA path is sunset (OTA Hotspot has been sunset, still in f_ota/main.py). The live path is station-mode WiFi (updates performed “via WiFi”), kicked off by an OTA command received over the device’s API WebSocket or locally by a triple tap of the physical SOS button (start_ota). The hotspot section lower down is retained as legacy: its credentials and soft-AP machinery are still compiled in, but it is no longer the current update mechanism.

The current path: station-mode WiFi OTA

The device connects to a saved/nearby WiFi network in station mode (conn_to_wifi, Searching for WiFi network) and performs the update over HTTP. Two update methods exist, distinguished in the logs, with different package formats: The payload for either method is described by a downloaded contents.json — a JSON array of filenames (Downloading content.json), not a fixed filename; the device picks the entry ending in .bin (firmware) or .tgz (preview). The image streams into the inactive OTA slot via ota_block_writer.py, its SHA-256 is verified (File integrity confirmed! / SHA256 hash does NOT match), and on success the device reboots into the new slot (OTA completed successfully!). See OTA server contract for the full request/response exchange.

Command-triggered over the API WebSocket

The device opens a client WebSocket to the API host and can be told to update over it. OTA can also be started on the device itself: a triple tap of the physical SOS button calls start_ota (see the trigger note below). After the command is received, the device runs the station-WiFi download described above.

Battery gate

OTA is battery-gated: Battery too low for OTA update (low_battery / monitor_battery / battery_volt). The exact voltage/percent threshold is not present as a literal in this firmware — it lives in bytecode and cannot be recovered from strings. Only the gate itself is confirmed.

OTA server contract

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 (or a spoofed api.totemportal.com via DNS/ARP) is sufficient to push firmware to a device you own: there is no API key, client certificate, HMAC, or image signature anywhere on this path.
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:
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 (no signature), 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.

Where the firmware comes from

The device fetches release metadata from the Totem API and downloads the package from S3. On-device, the payload is resolved through contents.json (a JSON array of filenames) rather than a fixed filename: version is tracked as a release code (release_code / release_id / MPY_OTA_VER). The running version appears only as the release_code 5.0.3 / release_id 339 constants in f_ota/system.py (v5.0.2: 5.0.2 / 335), never as a path or filename. The confirmed on-device host literal uses http:// (see Transport security below):
A phone-app-style URL such as http://datapeak-developer.s3.us-east-1.amazonaws.com/totem_compass/totem/5.0.3/firmware_v5.0.3.bin is a phone-app construction, not a device literal. The filename component firmware_v<code>.bin does not appear anywhere in this firmware (zero matches across all segments seg0seg6 and the ELF; re-checked on the v5.0.3 segments). What is confirmed on-device is the bucket / product / branch dirs (totem_compass, totem_compass/totem) plus the templated host …/{}; the concrete filename is built app-side. See download URL for the full breakdown.
The device-side endpoints are http://api.totemportal.com/devices/{MAC}/ota (release poll) and the …/ota?updated success callback (get_release_from_api / record_release). {MAC} is the uppercase-hex device MAC (get_mac_addr) — confirmed by bytecode, not merely symbol adjacency. See OTA server contract for the request/response bodies.

Transport security

The observed OTA download URLs use HTTP, not HTTPS (http://datapeak-developer.s3.us-east-1.amazonaws.com/{}, http://api.totemportal.com). Integrity therefore rests on the image-level checks (appended SHA-256 and the ESP-IDF OTA validation / rollback), not on TLS. The mbedTLS stack is compiled in and used elsewhere, but the update fetch itself was seen over cleartext. There is no image signature on the OTA path — integrity is the SHA-256 digest only (checked on the BLE transfer path and by the ESP-IDF bootloader, not in f_ota/install_ota.py, which contains no SHA-256; see OTA integrity); the ECDSA/mbedTLS machinery in the image is unused here, so a custom OTA server needs no signing key.

The hotspot method (legacy / sunset)

This path is sunset in v5.0.3, as it was in v5.0.2 (OTA Hotspot has been sunset). The credentials and soft-AP machinery are still present in the image, but it is no longer the active update mechanism. Documented here for completeness.
Historically the device joined a phone personal hotspot configured with fixed credentials, then pulled the update over plain HTTP.
1

Trigger

The SOS button is a physical push button driven by button.AsyncButton (sw_sos = AsyncButton(0, hold_ms=800, long_hold_ms=10000) in compass), not the capacitive Touch Crystal (touch_button_v2, a separate input). Its triple tap is wired to start_ota (sw_sos.cb_triple_tap) — confirmed, identical in v5.0.2 and v5.0.3; SOS itself is started by holding the button (start_sos). Earlier revisions of this page said the triple tap belonged to the Touch Crystal and SOS rather than OTA; that was wrong. There is no update mode / updater string in the image: the gesture calls the same compass.start_ota entry point that the ESP-NOW (cb_start_ota) and BLE (cb__start_ota) triggers use. The “searching for WiFi” halo animation is supported (anim_gnss_search, Searching for WiFi network).
2

Join totemupdate

Station mode connects to the totemupdate SSID on 2.4 GHz (conn_to_wifi).
3

Download

Over HTTP via f_lib/requests.py, streaming into the inactive OTA slot (ota_block_writer.py).
4

Verify & reboot

The image SHA-256 is checked (File integrity confirmed! / SHA256 hash does NOT match); on success the device reboots into the new slot, on failure it reverts.
The specific OTA-state LED colors and ring/spinner animations earlier drafts described (pink spinning ring = can’t join, white progress ring = downloading, ring fills white = success, orange blink = failure) are not confirmed by the WiFi/OTA strings. Only palette color symbols like hot_pink and orange (and PROGRESS_RGB / ERROR_RGB) exist — there is no ring/spinner/progress-ring animation string binding a color to an OTA state. The one supported state reference is the “searching for WiFi” halo (anim_gnss_search); any color-to-state mapping would need separate LED/animation analysis to confirm.

WiFi capabilities present

Beyond OTA, the WiFi driver exposes standard station/AP features (scan, connect, protocol/tx-power control — WLAN settings changed to (pwr, proto, txpower)). These share the single 2.4 GHz radio with BLE and ESP-NOW and are not used during normal navigation, only for updates and any cloud sync.