Power & battery
device_power.py owns the power state machine; wdt_manager.py the watchdogs.
Battery percentage
peripherals.get_batt_pct maps cell voltage to a percentage with a piecewise-linear
voltage table, optionally rescaled by a learned maximum voltage. v5.0.3 recalibrated it
(confirmed from the disassembly):
- The table’s top segment (4.38–4.13 V) was removed, so 4.12 V now reads 100%
(v5.0.2: 61%). The curve was renormalised (
/605). - The learned-max rescale applies only when the learned maximum is below 4.12 V.
Battery % therefore reads noticeably higher on v5.0.3 and reaches 100% at a normal full
charge.
Sleep
The device uses aggressive sleep to extend battery life. A specific “~12 h for v5.0” endurance figure is not evidenced in the binary (it comes from the v5.0 release notes/marketing); the only12-hour literal, [activation_check] Activation doesn't start for 12hrs,
concerns promo-activation timing (see Navigation),
not battery endurance.
Light sleep
Awoke from lightsleep | Slept for: {} of {} | Total Sleep: {} | sleep duty: {:.3f}.
Radios are gated off (Not sleeping due radio needing to turn on soon); LED gating
during light sleep is inferred, not directly evidenced. A dev_total_lightsleep_ms
counter tracks duty.Deep sleep
State persists in RTC memory (
f_lib/rtc_mem.py, rtc_v2.py).
Wake: Awake from Deep Sleep | {} | Free mem: {}.Block sleep for GNSS RTC Sync) so a time sync
completes before the device drops power.
Watchdogs
wdt_manager.py runs a condition/blocker model (classes WdtManager / WdtConditions
/ WdtBlockers): the WDT feed loop is enabled only while no condition and no
blocker is active — _should_enable returns False as soon as any is set.
The v5.0.3
WLAN_KICK blocker ('wlan kick') is set and cleared by ble_manager around
its new WiFi-driver “kick”, so the watchdog is held off while the WLAN driver is being
kicked.
A separate ble_handoff_watchdog guards BLE TX-ownership handoff between the phone app
and the ESP, reclaiming TX priority when a stall is detected
([BLE Watchdog] Handoff stall detected).
Scheduled BLE off-time (v5.0.3)
compass.ble_schedule_mgr turns BLE off on a schedule. In v5.0.3 it:
- waits for the BLE outbox to empty before a scheduled disconnect for at most 20 s
(
BLE outbox not empty after {} ms (App never confirmed the connection?) - disconnecting anyway), and skips that wait entirely whenble_conn.is_stalled; - also waits for the BLE log uploader to go idle
(
Waiting on BLE log upload to finish before scheduled disconnect); - after two or more consecutive hung links (
stall_count ≥ 2) keeps BLE off for 90 s instead of 30 s and skips the fast-reconnect window (BLE hung {} links in a row - staying off {} ms without fast-reconnect bursts).
start_ble now ignores a reconnect request when BLE is already active
([Compass.start_ble] BLE already active; reconnect request ignored).
Touch Crystal
Capacitive touch input viatouch_button_v2.py, which wraps a custom native driver
imported in Python as c_cap_touch (IMPORT_NAME / STORE_NAME c_cap_touch), built on
the ESP-IDF touch_pad API (touch_pad_init failed, etc.) — not the stock touch module. The Touch Crystal is a separate input from the two
physical buttons below. (v5.0.2 also froze an io_mgr.py, but it contained only
import io and had no importer; it is removed in v5.0.3.)
Touch sensitivity is recalibrated as the battery drains so the crystal keeps working at
low voltage. The two low-battery log forms differ: OFF reports a single value
(
[touch] Low battery mode OFF | sens: {:.3f}), while ON reports the base and scaled
values in one line ([touch] Low battery mode ON | base_sens: {:.3f} → adjusted: {:.3f})
via _LOW_BATT_FLOOR and _LOW_BATT_MULTIPLIER.
Native parameter validation bounds
The nativec_cap_touch driver validates its parameters against these literal ranges.
These are validation bounds, not defaults — the defaults are non-extractable
bytecode/C constants.
Physical buttons (power & SOS)
button.py (button.AsyncButton) drives both physical buttons with an async
press/hold/multi-tap model (long_hold_task, per-gesture cb_* callbacks). The default
wiring is set in compass and is identical in v5.0.2 and v5.0.3 (confirmed):
So the SOS alert is started by holding the SOS button, and a triple tap of the
SOS button starts an OTA update. (Earlier revisions of this page said no literal tied a
physical-button triple tap to any action and that
cb_triple_tap belonged to the Touch
Crystal — both wrong; the Touch Crystal is the separate touch_button_v2 input.) On debug
builds only (log._print_lvl == 1), v5.0.3 rewires the SOS single tap to
dev_trigger_upload, which requests a BLE log upload and then calls sos_mute.
v5.0.3 debounce change. v5.0.2 disabled the button for 50 ms after a press via a
debounce coroutine. v5.0.3 removes that coroutine and instead applies a 30 ms
timestamp lockout (_edge_ms) in the IRQ handler changed, on both press and release
edges. The effect — more reliable single/double/triple-tap detection — is inferred.
sos_timing is a generator in f_lib/generators.py (next to breathe_effect); it paces
the SOS indication (inferred) rather than defining the trigger gesture.
The SOS triple tap calls
start_ota. Firmware updates run over direct
WiFi OTA (Performing Firmware update via WiFi, is_wifi_ota).
The older hotspot OTA path is still sunset in v5.0.3 (OTA Hotspot has been sunset),
so it is not the active update path despite f_ota/hotspot.py still being present.Vibe Mode (sound-reactive LEDs)
new_vibe.py (“Vibe Mode”, version string newvibe-1.10; v5.0.2: newvibe-1.9) is a
microphone-driven, sound-reactive ring LED effect (confirmed): it samples the
microphone ADC (mic.read_u16) and writes the halo ring’s pixel buffer (ring.buf)
directly. It is not magnetometer calibration — earlier revisions of this page called it
“passive magnetic calibration”, which was wrong for both v5.0.2 and v5.0.3.
compass.vibe_mode launches new_vibe.new_vibe as a task still named 'sound_react';
v5.0.3 removed the dead in-compass sound_react / set_buff code. The task is gated by
evt_* events (qstrs such as evt_non_eco_mode, evt_led_override_off).
v5.0.3 tuning changes:
With the
_DBG telemetry gone, the v5.0.2 NEWVIBE … and #NEWVIBE-REC … log lines and
the task started, waiting on enable events / resumed after pause / task cancelled
strings no longer exist in v5.0.3. The magnetometer bias handoff and its gap_xy
residual (Bias handoff 2D->3D ({}) | gap_xy={:.1f}uT, 2D governs, gap_xy={:.1f}uT)
belong to the fusion engine (imu_fusion_auto), unrelated to Vibe Mode — see
Navigation.
Temperature sensor
ds18x20.py + onewire.py provide a DS18B20 1-Wire temperature driver
(_search_rom, DS18X20). The driver is compiled in and instantiable, but there is no
runtime-log evidence (no °C / celsius / temperature: {} string) that a sensor is
physically wired and polled, so its actual use for temperature compensation or environment
reporting is inferred, not confirmed.