> ## Documentation Index
> Fetch the complete documentation index at: https://totem-cb8b3887.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# WiFi (OTA updates)

> The third 2.4 GHz role: station-mode WiFi used to pull firmware/preview updates over HTTP.

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`.

<Note>
  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.
</Note>

## 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:

| Method   | Log string                            | Package format                                                                       |
| -------- | ------------------------------------- | ------------------------------------------------------------------------------------ |
| Firmware | `Performing Firmware update via WiFi` | `.bin` (`.bin package not found in repo`)                                            |
| Preview  | `Performing Preview update via WiFi`  | `.tgz` gzipped tar (`.tgz package not found in repo`, `Downloaded file is not .tgz`) |

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](#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).

| Signal           | Evidence                                                                                                          |
| ---------------- | ----------------------------------------------------------------------------------------------------------------- |
| WebSocket to API | `WebSocket Connection initiated...` / `WebSocket Init Request...`, sitting beside `http://api.totemportal.com`    |
| OTA command      | `Sending OTA command to update nearby devices`, `Demigod to update nearby devices`, qstr `demigod_gen_ota_update` |
| OTA daemon       | symbols `ota_daemon` / `ota_cmd` (`from ota_daemon import *`)                                                     |
| Local gesture    | SOS button triple tap → `start_ota` (`sw_sos.cb_triple_tap`, **confirmed**, same in v5.0.2 and v5.0.3)            |

After the command is received, the device runs the station-WiFi download described above.

### Battery gate

<Note>
  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.
</Note>

## 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.

<Steps>
  <Step title="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`):

    ```json theme={null}
    {"version": "5.0.3", "endpoint_id": 1, "device_type_id": 1,
     "lat": 0.0, "lon": 0.0, "gnss_time": 0, "release_id": 339}
    ```

    (`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.)
  </Step>

  <Step title="Server returns a release object">
    The reply (`get_release_details`) must expose these fields:

    ```json theme={null}
    {"ota_url": "http://<host>/repo/*", "version": "5.0.3",
     "product": "totem_compass", "branch": "totem",
     "release_code": "5.0.3", "release_id": 123}
    ```
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

## 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):

```text theme={null}
http://datapeak-developer.s3.us-east-1.amazonaws.com/{}
```

<Note>
  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](/reference/download-url) for the full breakdown.
</Note>

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](#ota-server-contract) for the request/response bodies.

## Transport security

<Warning>
  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](/subsystems/ota#integrity));
  the ECDSA/mbedTLS machinery in the image is unused here, so a custom OTA server needs no
  signing key.
</Warning>

## The hotspot method (legacy / sunset)

<Warning>
  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.
</Warning>

Historically the device joined a **phone personal hotspot** configured with fixed
credentials, then pulled the update over plain HTTP.

| Parameter | Value                                                                                       |
| --------- | ------------------------------------------------------------------------------------------- |
| SSID      | `totemupdate`                                                                               |
| Password  | `totem1234`                                                                                 |
| Band      | 2.4 GHz (iOS "Maximize Compatibility", Android "Compatibility Mode")                        |
| Trigger   | triple tap of the physical SOS button → `start_ota` (still wired in v5.0.3; see note below) |

<Steps>
  <Step title="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`).
  </Step>

  <Step title="Join totemupdate">
    Station mode connects to the `totemupdate` SSID on 2.4 GHz (`conn_to_wifi`).
  </Step>

  <Step title="Download">
    Over HTTP via `f_lib/requests.py`, streaming into the inactive OTA slot
    (`ota_block_writer.py`).
  </Step>

  <Step title="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.
  </Step>
</Steps>

<Note>
  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.
</Note>

## 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.
