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
A trigger (SOS triple tap, the phone app over BLE, an ESP-NOW/demi-god broadcast, or the REPL) writes aperform.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:
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.
prep_wlan() and the connect loop
prep_wlan() (f_ota_main.dis:437) logs
Preparing WLAN protocols for update (v2)... and then:
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):
- Empty
networks→ConnErr('No network creds were provided', ErrCode.invalid_auth). - Up to
max_n = 10scan rounds, eachawait asyncio.sleep_ms(200)thenget_nearby_networks(), stopping as soon as anything is seen. Still nothing →ConnErr('No networks within range', ErrCode.conn_failed). - Intersect the scan with
networks. Nothing in common →ConnErr('Known-network not found within range', ErrCode.conn_failed). - If
priority_nw(i.e.user_cfg.wifi_ssid) is among the matches it wins; otherwise the first match in scan order. attempts = 2tries ofasyncio.wait_for(self.connect(ssid, key, hostname), 15), printingConnecting to: {} | Attempt {} of {}.... A timeout printsTimed out connecting to WiFi, trying again....if not wlan.isconnected(): raise ConnErr('Unable to establish connection with {}', ErrCode.conn_failed).
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 thecmd 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
LED feedback
Theota_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.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.