Boot sequence
- The bootloader validates the appended image SHA-256 and the OTA-data partition, then jumps into the selected app slot. Failed or rolled-back updates land on the previous good slot (see OTA).
_boot.pymountsbdevat/throughflashbdev.py, and onOSErrorfalls back toinisetup.setup(), which formats LittleFS2 and writesboot.pyandmain.py.boot.py(a VFS file, distinct from the frozen_boot.py) is the unmodified MicroPython template and does nothing. Both the frozenmain.pyand themain.pyinisetupwrites to the VFS contain the same three lines.main.pyis, in its entirety,try: from ota_daemon import */except ImportError: pass.ota_daemon.pycallsf_lib.firmware_rollback.cancel()— marking the running slot valid and cancelling rollback — then doesfrom project_main import *inside atrythat printsProject Exception Occurredand a traceback on failure.- Importing
project_mainruns its module body, which callsdisable_ble(),rtc_mem.reload(), and thenstart(1). Everything after that is the application: event bus, task manager, LEDs, radios, GNSS, and the compass state machine. In v5.0.3compass.startlaunches the ESP-NOW comms task viaenow_v2.communicate_supervised, which restarts it on a crash (v5.0.2:communicate_v2directly).
project_main.start(cmd, opt) (CONFIRMED)
Before dispatching on cmd, start drains the RTC category-0 mailbox: if a frame is
present it takes cmd_id = frame[3], deletes the frame, and for cmd_id in (1, 2) runs
asyncio.run(svc_ble_transfer.svc_manager(cmd_id=cmd_id)) — this is how a BLE command
reboots the device into the transfer service (see module reference).
Modes 2, 4, 5, 8, 9 and 11 import
mpy_dev.* or web_bluetooth, none of which is
frozen into the image or present on the VFS. They raise ImportError on a production
device. They are reachable only by calling start(n) by hand from the REPL, because the
module body hard-codes start(1).Power / boot modes
device_power.py tracks an application-level boot_mode and a battery-health state. Observed
states and transitions (from log strings and symbols):
Reset cause vs. boot-loop protection
The appboot_mode above is separate from the hardware reset cause the ESP reports
(Reset Cause: {} — e.g. WDT_RESET, DEEPSLEEP_RESET). A boot-failure / crash-counter
mechanism guards against boot loops: it tracks boot_count, boots_failed
(with boots_failed_max / boots_failed_min) and boots_wdt, and on a bad boot runs a WDT
recovery path (check_bootsec, calc_recovery_risk, check_recovery_cache,
Performing immediate WDT Recovery) before rebooting (Rebooting from: {} in 3sec,
Is Reboot a Continued Session: {}).
Remote power control (demigod)
Power state can be commanded remotely over the mesh. A peer can send a demigod power-control command (Power control demigod command received, demigod_gen_pwr_control) that changes the
power mode or triggers power-down (Sending demigod command to power down), subject to a
qualification gate (Device disqualified from demigod command).
Sleep and power-down
Light sleep
Real and heavily used.
espnow_conn_v2.py imports machine.lightsleep and duty-cycles
the radio around it, tracking lightsleep_ms / dev_total_lightsleep_ms:
Awoke from lightsleep | Slept for: {} of {} | Total Sleep: {} | sleep duty: {:.3f}.
Sleep is skipped when the radio is due on (Not sleeping due radio needing to turn on soon), for GNSS RTC sync (Block sleep for GNSS RTC Sync), and after invalid traffic
(Lightsleep skipped due to invalid messages).Power-down
Not sleep —
device_power.shut_down_tasks(method) either cuts hardware power or
resets the chip. See the table below.device_power.shut_down_tasks(method, is_save_config) (CONFIRMED)
The single power-down path. It sets the BOOT_SHUTDOWN watchdog condition, optionally
bumps the boot counters and persists config, saves BLE secrets unless method == 2,
tears down the task set (tasks.cleanup(), then tasks.stop(...) over thirteen named
tasks — compassing.start, sound_react, start_comms, enow_recv, gnss_stream,
imu_fusion, monitor_battery, low_battery, esp_comms, backend_checks,
gnss_debug_printout, stream_gnss_msgs, debug_active), awaits
enow_v2.power_off(force=True), and then:
For methods 1 and 2 the LEDs are turned off first (
leds.crystal.off(),
leds.ring.off(), then a 100 ms pause) unless modes.is_silent_reboot is set.
About
Awake from Deep Sleep | {} | Free mem: {}. The string exists, in
project_main.dis. It sits in the cmd == 2 branch of project_main.start, guarded by
a reset-cause check against DEEPSLEEP_RESET (4), and the branch continues into
from mpy_dev.dev_comms import start — a module that is not in the image. Since the
module body hard-codes start(1), production firmware never enters that branch, and it
would ImportError if it did. A DEEPSLEEP_RESET cause is something the ESP can
report; it is not evidence that this firmware ever calls machine.deepsleep().What RTC memory actually holds
f_lib/rtc_mem.py is the allocator, and it survives resets, not sleep. It carries three
categories: a reboot-into-service command, a mirror of the BLE pairing secrets
(ble_keys.bin), and a 7-byte device-state snapshot. The frame format and all three
category ids are documented on the module reference. f_lib/rtc_v2.py
is unrelated — it is a machine.RTC subclass providing calendar maths.
The navigation values snapshot_coords / snapshot_ticks / snapshot_travel and the
is_rtc_snapshot flag live on the project_data config object; only the two flag bits
described in the module reference are written into RTC memory.
Touch sensitivity adapts to the power state — [touch] Low battery mode ON/OFF adjusts
the capacitive baseline so the Touch Crystal still works as the battery drains. This
low-battery touch state (constant BATT_POOR_TOUCH) is part of the touch subsystem, not a
value of the battery-health enum above.
Watchdog
wdt_manager.py is not a plain task-WDT wrapper but a condition/blocker engine
(WdtManager / WdtConditions / WdtBlockers): an async feed loop (Starting WDT feed loop)
feeds the native task watchdog (mpy_machine_wdt / task_wdt), and is disabled while any
condition or blocker is active — _should_enable returns False ([WDT {}] cond=[{}] blk=[{}]).
The v5.0.3 blockers are ('log rotate', 'vfs write', 'wlan kick'); WLAN_KICK = 2 is new,
set and cleared by ble_manager around its WiFi-driver “kick” (v5.0.2 had only the first
two). It supports dynamic threshold changes
(Updating WDT threshold from: {} to {}) and drives a recovery/reboot path
(Performing immediate WDT Recovery, restore_wdt_reboot) that ties into the boot-failure
counters above.
A separate BLE handoff watchdog (ble_handoff_watchdog, [BLE Watchdog] Handoff stall detected)
arbitrates BLE TX-priority ownership between the phone app and the ESP
(last_handoff_to_app / last_handoff_to_esp); when a handoff stalls it reclaims TX priority.