> ## Documentation Index
> Fetch the complete documentation index at: https://totem-cb8b3887.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Navigation & compass

> u-blox GNSS, AssistNow, IMU fusion, and World Magnetic Model declination.

The navigation stack turns GNSS position, IMU orientation, and magnetic heading into a
"point me to my friend / to the meetup" compass. It spans `compass.py`, `compassing.py`,
`ubx_gnss.py`, `imu_fusion_auto.py`, `mag_wmm_data.py`, `task_mag_declination.py`,
`nav_helpers.py`, `nav_logger.py`, and `peer_helpers.py`.

## GNSS (u-blox)

The receiver is a **u-blox** module spoken to over the **UBX** binary protocol.

| Element           | Evidence                                                                                                                                            |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| UBX framing       | `UBLOX_MSG_PREFIX`, `UBLOX_MSG_SIZE`, `calc_ubx_checksum`                                                                                           |
| Position solution | `NAV-PVT` (`last_nav_pvt`, `last_nav_pvt_raw`); parsed with `<IHBBBBBBIiBBBBiiiiIIiiiiiIIHHBBBBihH` (calcsize 92) → u-blox M8/M9/M10-class receiver |
| Version query     | `MON-VER`                                                                                                                                           |
| Ack / nak         | `ACK-ACK`, `ACK-NAK`                                                                                                                                |
| Rate control      | `modify_nav_rate`                                                                                                                                   |
| Fix quality       | `pAcc` (position accuracy, metres) in log line below                                                                                                |

A representative fix log:

```text theme={null}
{} | Gnss time: {} | {:.7f}, {:.7f} | pAcc: {:.2f} | 
```

(7-decimal latitude/longitude ≈ centimetre resolution; `pAcc` is the receiver's own
accuracy estimate used to gate whether a fix is trusted.)

## AssistNow (A-GNSS)

To get a fast first fix, the device uses u-blox **AssistNow** (MGA) assistance data,
delivered to the GNSS chip and cached in the filesystem.

| Element              | Evidence                                                                                                                                       |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| Assistance data file | `assist-now-b.bin`, `load_assist_now`, `Loading AssistNow from: {}`                                                                            |
| MGA acknowledgement  | `MGA-ACK`, `assist_ack_received`                                                                                                               |
| Transfer             | `AssistNow transfer completed successfully: {} chunks sent`                                                                                    |
| Skip when unneeded   | `A-GNSS file not needed. Location already known`                                                                                               |
| Result               | `Assistance used by GNSS - InfoCode: {}` / `NOT used`                                                                                          |
| Phone GNSS fallback  | with no on-board fix, the paired phone's location is used, chosen by hAcc (`Using Phone's GNSS location`, `is_phone_gnss_used`, `phone_h_acc`) |

Within the GNSS driver, assistance data is only ever loaded from the on-device filesystem
(`Checking VFS for assist now files`) and streamed to the receiver in
chunks (`Sending AssistNow chunk {}/{} ({} bytes), attempt {}`). Delivery of that data over BLE/mesh is
covered by the [message-format subsystem](/protocols/message-format), not the GNSS driver.

## IMU fusion

`imu_fusion_auto.py` fuses accelerometer and gyroscope data for orientation and motion.
In v5.0.3 it is the **sole** fusion module (byte-identical to v5.0.2), imported by
`compass`, `compassing`, `ble_manager`, `espnow_conn_v2`, `nav_logger`, and `debugger`.
v5.0.2 also froze an older `imu_fusion.py` — an unused Madgwick `Fusion`/`Cal`
implementation that nothing imported — which v5.0.3 removes. The fusion engine provides:

* **Orientation filter** is a Madgwick AHRS with an adaptive beta gain (`madgwick`,
  `is_adaptive_beta`, `_BETA_MIN_SCALE`).
* **Tilt/orientation** feeds the 2D↔3D compass mode switch.
* **Shake detection** (`_SHAKE_ACCEL_DEV`, `_shake_count`, `_shake_start_ms`) triggers a
  manual magnetometer bias reset (`Manual bias reset triggered via shake pattern`,
  `is_shake_reset`).
* Accel deviation bounds gate magnetometer calibration (`_CAL3D_MAX_ACCEL_DEV`).

## Magnetic compass & calibration

Heading comes from a magnetometer, corrected for hard/soft-iron errors and for magnetic
declination.

### Calibration

| Concept               | Evidence                                                                                                                            |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| 2D and 3D calibration | `_switch_to_3d`, `Bias handoff 2D->3D`, `_CAL3D_MAX_ACCEL_DEV`                                                                      |
| Quality metric        | `gap_xy={:.1f}uT` (residual field gap in microtesla)                                                                                |
| Persistence           | `2D bias restored from config \| age={}s \| cals={}`                                                                                |
| Finish / fail         | `_finish_calibration`, `_last_calibrated`; a rejected 3D fit logs `3D cal rejected: mag={:.1f} std={:.1f} cov={:.2f} change={:.1f}` |

The 2D→3D "bias handoff" upgrades from a flat-plane heading to a full 3D solution once
enough motion is seen: `Bias handoff 2D->3D ({}) | gap_xy={:.1f}uT | 2D age={}s | cals={}`.
All of this calibration telemetry lives in `imu_fusion_auto`; it is unrelated to Vibe Mode
(`new_vibe.py`, a sound-reactive LED effect — see [Power, input & sensors](/subsystems/power)).
The `--- Magnetic Calibration Samples ---` string cited in earlier revisions existed only in
the unused v5.0.2 `imu_fusion.py` and is gone in v5.0.3.

### Declination (World Magnetic Model)

`mag_wmm_data.py` embeds the **World Magnetic Model** — specifically **WMM 2025** (valid
2025–2030; `WMM_2025`) — so the device converts magnetic heading to true heading based on
location, refreshed periodically:

```text theme={null}
Declination check unnecessary. Last check was {} sec ago and {}m away
```

`task_mag_declination.py` recomputes declination only when the device has moved far
enough or enough time has passed — a power optimization.

The embedded WGS84 axes (`6378.137` / `6356.7523142` km) and the `6371.2` km geomagnetic
reference radius — all present as IEEE-754 doubles in the binary — confirm a genuine
spherical-harmonic declination computation rather than a lookup table.

## Heading output

The compass surface the UI consumes:

| Output                   | Evidence                                                                                                                                                                                                                                  |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Tilt-compensated heading | `heading_tilt_compensated`, `heading_tilt_compensated_deg` (degrees)                                                                                                                                                                      |
| Motion heading           | `get_heading_mot` — course-over-ground computed by `get_azimuth` between the previous and current GNSS fix (`location_prev`→`location`), gated by `odometer` ≥ 10000 (there is no NAV-PVT `headMot` field); `avg_azimuth`, `peer_azimuth` |
| Compass lock             | `is_compass_lock`, `toggle_compass_lock`; logs `Compasss Lock Enabled`, `Turning ON compass lock` / `Turning OFF compass lock`                                                                                                            |

## Navigation logging

`nav_logger.py` records navigation events, schedulable remotely:

| Symbol                                  | Role                                                       |
| --------------------------------------- | ---------------------------------------------------------- |
| `is_nav_log`, `is_nav_ready`            | logging state                                              |
| `evt_nav_log_scheduled`, `rec_nav_logs` | scheduled recording                                        |
| `demigod_gen_add_nav_log_rule`          | push a logging rule via [demi-god](/protocols/demigod)     |
| `cloud_nav_peer`                        | upload nav data to the cloud                               |
| `is_nav_log_fast_rotate`                | v5.0.3: fast log rotation while BLE log uploads are active |

`nav_logger.check_log_storage` normally rotates `events.bin` at 7100 B. In v5.0.3, when
`modes.is_nav_log_fast_rotate` is set — by the BLE log uploader when no `events-*.gz`
backlog exists — it rotates at **≥1024 B** instead
(`Nav log fast rotation at {} B (BLE uploads active, backlog empty)`).

## Peer staleness & proximity display

How a bonded peer is shown on the ring depends on whether its coordinates are fresh.
v5.0.3 changes both the staleness timeout and how stale peers are displayed:

| Behavior                                                                | v5.0.3                                                                                                                            | v5.0.2                      |
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------- |
| `peer_helpers.is_peer_stale` sets `is_unknown` after no coordinates for | **7200 s** (2 h)                                                                                                                  | 14400 s (4 h)               |
| Stale peers in `compassing.get_peer_led_prox`                           | proximity path: RSSI ≥ −41 → shown "nearby"; within the accuracy distance → `is_unknown=1`, then nearby; no distance → RSSI tiers | always a directional spread |

## Promo activation

`compass.activation_check` creates a **time-limited promo point of interest** (log strings
`Add Promo Activation: {} | Secs until start: {}`, `Creating promo activation in: {}`,
`No promo activation`, `[activation_check] Activation expired`,
`[activation_check] Activation doesn't start for 12hrs`). In v5.0.2 the function existed but
was never called; v5.0.3 launches it from `backend_checks`.

|               | v5.0.3                     | v5.0.2                                     |
| ------------- | -------------------------- | ------------------------------------------ |
| Promo name    | `Totem Secret Meetup`      | `Three City Stages: Tinie Tempah` (unused) |
| Active window | 2026-09-21 03:20–04:00 UTC | n/a (`activation_check` never called)      |

The promo's coordinates are embedded in the firmware and are intentionally not reproduced
here.

## Receiver lifecycle & auto-reset

The GNSS subsystem manages the receiver end-to-end and can self-heal:

| Concept             | Evidence                                                                                                                                        |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Baud auto-detection | `Auto-detecting correct baud...`, `GNSS baud rate identified: {}`                                                                               |
| RAM configuration   | `CFG-VALSET` / `CFG-MSGOUT` frames enable NAV-PVT output on UART1 (key `0x20910007`, decoded per u-blox spec as `CFG-MSGOUT-UBX_NAV_PVT_UART1`) |
| Start modes         | `CFG-RST` drives cold / warm / hot start (`GNSS Cold Start`, `GNSS Warm Start`, `GNSS Hot Start`)                                               |
| BBR persistence     | `Saving GNSS config to BBR for faster reboots`; hot/cold start state cached in `hotcold.bin`                                                    |
| Auto-reset          | `Auto-Reset GNSS`, `Cannot communicate with GNSS module` — an unresponsive receiver is power-cycled/reinitialized                               |

The embedded config frames all begin `B5 62` and their trailing checksum bytes verify, so
the frame contents are confirmed; the CFG-MSGOUT key→message-name mapping is inferred from
the u-blox protocol spec.
