Skip to main content

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 only 12-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: {}.
GNSS RTC sync can hold off sleep (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 when ble_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 via touch_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 native c_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.