Skip to main content
WiFi exists on this device for one job: pulling over-the-air updates. It is never active during normal navigation. The code lives in f_lib/wifi.py, f_lib/requests.py, f_lib/firmware_ota.py and the f_ota/ package, and it only runs on a boot that follows an OTA trigger — see an OTA is two boots.
Everything on this page was read out of the frozen MicroPython bytecode in re/v5.0.3/mpy/*.dis; citations are file.dis:line. Claims that could not be read directly are marked inferred or not recoverable.

What actually starts a WiFi OTA

Earlier revisions of this page claimed the device “opens a client WebSocket to the API host and can be told to update over it.” That is wrong. The only WebSocket code in the image is a server in f_ota/hotspot.py — the device’s own soft-AP web UI — and that module is imported by nothing in v5.0.3 (grep f_ota.hotspot matches only itself). There is no WebSocket client, no persistent connection to api.totemportal.com, and no polling loop: the device only talks to the API during an update it was already told to perform.
A trigger (SOS triple tap, the phone app over BLE, an ESP-NOW/demi-god broadcast, or the REPL) writes a perform.ota parameter file and soft-resets. The next boot notices the soft reset plus the file and calls f_ota.main.start_ota, which is where WiFi first comes up. The full sequence, the cmd numbers and the perform.ota schema are documented under OTA & rollback.

Battery gate

OTA is battery-gated at the trigger, before any reboot:
(compass.dis:6904.) This is an asyncio.Event flag maintained by the battery monitor, not a voltage or percentage compared inline — the threshold that sets and clears the event lives in the power subsystem, not here. Once the reboot has happened there is no further battery check on the download path.

Choosing and joining a network

Known networks

f_ota.config hard-codes two credentials (f_ota_config.dis, PERM_NETWORKS): get_known_networks() (f_ota_config.dis) merges them with runtime state:
Two things follow. First, the hard-coded pair always wins over anything supplied at runtime with the same SSID. Second, because d is the module-level PERM_NETWORKS dict rather than a copy, every call permanently accumulates the caller’s networks into it for the life of the process. cfg.networks is whatever a trigger put in perform.ota under networks; user_cfg.wifi_ssid / wifi_key come from user-config.json, the file the (now dead) hotspot web UI wrote.
totemupdate / totem1234 is not legacy. It is in PERM_NETWORKS, so the current station-mode OTA path will join it on every update attempt if it is in range. Anything broadcasting that SSID with that password, in range of a device that has been told to update, gets to answer its HTTP requests.

prep_wlan() and the connect loop

prep_wlan() (f_ota_main.dis:437) logs Preparing WLAN protocols for update (v2)... and then:
ending with WLAN prep completed. Despite the (v2) in that string, f_ota/main.py imports ConnErr, WiFi, CLIENT from f_lib.wifi, not f_lib/wifi_v2.py (f_ota_main.dis:162). WiFi.__init__ defaults to protocol=7, pm=1 and sets self.reconnects = 3 (f_lib_wifi.dis:234). WiFi.connect_to_known(networks, hostname, max_n=10, attempts=2, priority_nw=None) (f_lib_wifi.dis:550, defaults at :183):
  1. Empty networks → ConnErr('No network creds were provided', ErrCode.invalid_auth).
  2. Up to max_n = 10 scan rounds, each await asyncio.sleep_ms(200) then get_nearby_networks(), stopping as soon as anything is seen. Still nothing → ConnErr('No networks within range', ErrCode.conn_failed).
  3. Intersect the scan with networks. Nothing in common → ConnErr('Known-network not found within range', ErrCode.conn_failed).
  4. If priority_nw (i.e. user_cfg.wifi_ssid) is among the matches it wins; otherwise the first match in scan order.
  5. attempts = 2 tries of asyncio.wait_for(self.connect(ssid, key, hostname), 15), printing Connecting to: {} | Attempt {} of {}.... A timeout prints Timed out connecting to WiFi, trying again....
  6. if not wlan.isconnected(): raise ConnErr('Unable to establish connection with {}', ErrCode.conn_failed).
The retry branch assigns a backoff value of 30 to a local that is never read again — the wait_for timeout is the literal 15 on both attempts. Dead code. wifi_update passes hostname = '{}_{}'.format(cfg.hostname, cfg.mac[-4:]), e.g. mytotem_a1b2 (cfg.hostname defaults to 'mytotem'). Any ConnErr is re-raised by wifi_update as OtaErr('{} | code: {}'.format(e.msg, e.code), ErrCode.conn_failed), which aborts the update and — with cfg.is_reboot defaulting to True — reboots the device.

The HTTP exchange

Two update kinds share one path, distinguished by the cmd number the trigger supplied, not by cfg.update_method: cfg.update_method selects where the URL comes from: 1 = use cfg.ota_url as given, 2 = ask the API first. start_ota sets it to 2 whenever cfg.endpoint_id and cfg.version are both set, which the standard perform.ota defaults guarantee (endpoint_id = 2, version = 'latest'). The full request and response shapes — the seven-key release poll, the resp['body'] release object, contents.json, and the ten-key ?updated completion report — are documented once, under OTA server contract. In summary:
{mac} is get_mac_addr() = ''.join('{:02x}'.format(b) for b in machine.unique_id()), i.e. 12 lower-case hex characters with no separators (f_lib_bitwise.dis:158). Earlier revisions said upper-case; that was wrong. f_lib/requests.py speaks HTTP/1.0 with Connection: close (b'%s /%s HTTP/1.0\r\n', f_lib_requests.dis). It can do TLS — port 443 is in its scheme table — but every Totem host literal in the image is http://.

Transport security

Both hops are cleartext HTTP with no authentication, and the firmware image carries no app-level hash or signature check on the WiFi path — the only verification is that the number of bytes written equals the server’s own Content-Length (Received {} bytes (expected {})., f_lib_firmware_ota.dis:639). Anything that can answer for api.totemportal.com, or for whatever host the API’s endpoint field names, or that can stand up a totemupdate / magiceye access point near a device that has been told to update, can flash arbitrary code onto it. See OTA integrity.

LED feedback

The ota_callback task drives the ring and Touch Crystal for the whole update (ota_callback.dis:497). Earlier revisions of this page said no firmware string bound a colour to an OTA state, and concluded the mapping was unconfirmed. That was wrong — the mapping is in bytecode, not in strings: colors.orange = (255,128,0) and GLOBAL_BRT = 0.6 (project_data.dis:1902, :515). There is no pink OTA state: the “can’t join” animation uses colors.white, same as the progress ring. Full detail in LED feedback during an OTA.

The two things called “hotspot”

These are unrelated and earlier revisions conflated them.

1. totemupdate, a network the device joins — still live

A phone personal hotspot (or any AP) named totemupdate with the key totem1234, on 2.4 GHz. The device joins it in station mode via connect_to_known, exactly like any other entry in get_known_networks(). Documented above; not deprecated.

2. f_ota/hotspot.py, the device as an access point — dead code

Historically the device could become the AP and serve a web UI to a phone. The module is still compiled in but nothing imports it, and ota_mgr refuses the command that used to reach it:
(f_ota_main.dis:1076.) For the record, what it did (f_ota_hotspot.dis:601): It also set ota_status.step_id = 3 — the only assignment of that value anywhere, which is why the LED callback’s step_id >= 3 test reads oddly on the surviving path.

WiFi capabilities present

f_lib/wifi.py exposes exactly seven methods (f_lib_wifi.dis:183): __init__, connect, connect_to_known, disconnect(power_off=True), get_nearby_networks, power_on, scan(max_n=10). scan powers the interface on, calls wlan.scan() and sorts the result in reverse (signal-strongest first); get_nearby_networks turns that into the nearby_networks list connect_to_known intersects against.
The string WLAN settings changed to (pwr, proto, txpower) ({}, {}) lives in espnow_conn_v2.dis:2735, not in the WiFi driver — it belongs to the ESP-NOW radio manager, which owns the same 2.4 GHz interface during normal operation. Earlier revisions of this page attributed it to the WiFi stack.
The radio is shared with BLE and ESP-NOW, which is why prep_wlan() tears it down with disconnect(power_off=True) and brings it back up with power save disabled before an update, and why stop_ota disconnects again on the way out.