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 callsstart_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 againsthttp://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 throughcontents.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 seg0–seg6 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.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 hotspot method (legacy / sunset)
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.