> ## 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.

# LEDs & animations

> The halo ring and Touch Crystal: APA106 driver, animation set, and status colors.

The Totem's visible output is a ring of addressable RGB LEDs (the **halo**) plus the
**Touch Crystal**. Driven by `apa106.py` / `neopixel.py` / `f_lib/neopixel_v2.py`, with
behavior in `leds.py` and `animations.py`.

## Driver

| Element     | Evidence                                                                                                                                                                                                                                          |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| LED type    | **APA106** (`APA106`, `apa106.py`) — WS2812-like, single-wire; `APA106` subclasses `NeoPixel` with byte `ORDER = (0, 1, 2, 3)` (**confirmed**)                                                                                                    |
| Transport   | RMT bit-stream via `machine.bitstream` — imported as `bitstream` in `neopixel.py` / `f_lib/neopixel_v2.py` (**confirmed**; there is no `bitstream_channel` symbol)                                                                                |
| Pixel count | **`RING_PX_COUNT` = 60**, **`CRYSTAL_PX_COUNT` = 7** — module-level ints in `project_data.py` (**confirmed**). The ring/crystal split matches the `_ring_leds`/`is_ring` and `_crystal_leds`/`is_crystal` symbol pairs                            |
| Palette     | `Colors` class (13 named colors, `project_data.py`). `COLOR_LIST` is a *separate*, OTA-BLE-only 3-color gradient `((200,0,200),(100,0,200),(0,0,200))` in `ota_ble.py`, not the main palette                                                      |
| Controller  | `DeviceLeds` (`peripherals.py`) owns `_ring_leds` / `_crystal_leds`; the low-level strip driver class is `LightStrip` (`leds.py`). Rotation uses `_spin_deg`, which is defined in `imu_fusion_auto` (the fusion engine), not an LED-module symbol |

APA106 is driven via the ESP32 RMT peripheral as a precise bit-stream, the standard
technique for these single-wire addressable LEDs.

### Colors

The `Colors` class (`project_data.py`) defines **13 named colors** (**confirmed**): `red`,
`orange`, `yellow`, `yl_green`, `green`, `bl_green`, `teal`, `white`, `aqua`, `blue`,
`indigo`, `magenta`, `hot_pink`. That is also the order of the internal 13-tuple the class
uses as its color→id table, so it doubles as the module's `COLOR_LIST`-style index order.
(There is no `pink` or `cyan` qstr; `hot_pink` and `teal` fill those roles. There *is* a
`red` and a `green` qstr — the earlier claim that these were absent was wrong.) The
name→RGB pairings are **confirmed**: the class binds each name to its tuple with an explicit
`STORE_ATTR` in the disassembly, so these are no longer inferred:

| Name       | RGB             | Grade     |
| ---------- | --------------- | --------- |
| `red`      | (255, 0, 0)     | confirmed |
| `orange`   | (255, 128, 0)   | confirmed |
| `yellow`   | (255, 255, 0)   | confirmed |
| `yl_green` | (128, 255, 0)   | confirmed |
| `green`    | (0, 255, 0)     | confirmed |
| `bl_green` | (0, 255, 128)   | confirmed |
| `teal`     | (0, 255, 255)   | confirmed |
| `white`    | (255, 255, 255) | confirmed |
| `aqua`     | (0, 128, 255)   | confirmed |
| `blue`     | (0, 0, 255)     | confirmed |
| `indigo`   | (128, 0, 255)   | confirmed |
| `magenta`  | (255, 0, 255)   | confirmed |
| `hot_pink` | (255, 0, 128)   | confirmed |

Note: the disassembly settles the earlier `teal`/`aqua` ambiguity — `teal` is `(0, 255, 255)`
and `aqua` is `(0, 128, 255)` (the previous table had these two swapped). There is **no**
named `OFF` color; the "off" / clear state is the literal `(0, 0, 0)` used by
`LightStrip.off` / `fill`, not a palette entry.

When Totems auto-bond, peers are assigned colors by `shuffle_bond_colors`, invoked right
after the log `Setting new auto-bond colors` (**confirmed** — the log immediately precedes
the call). It shuffles a **9-color** subset given as color indices
`[12, 4, 9, 2, 10, 1, 6, 7, 11]` — i.e. hot\_pink, green, blue, yellow, indigo, orange, teal,
white, magenta (**confirmed**; the earlier "8-color: …, orange, aqua, indigo, teal" list was
wrong on both count and order).

## Animation set

Animations are event-driven — subsystems raise events and the LED task plays the
matching effect:

| Animation                                                                              | Trigger                                                                                                                                                                               |
| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `boot_animation`, `powerup_animation`                                                  | boot / power-up                                                                                                                                                                       |
| `anim_boot_batt_pct`                                                                   | boot battery-percentage indicator                                                                                                                                                     |
| `anim_gnss_search`                                                                     | acquiring a GNSS fix                                                                                                                                                                  |
| `pairing_animation`                                                                    | pairing                                                                                                                                                                               |
| `anim_add_peer`, `add_peer_animation`, `bonded_animation`                              | a new Totem bonded                                                                                                                                                                    |
| `anim_bonding_ctdwn`                                                                   | bonding countdown                                                                                                                                                                     |
| `anim_peer_del_ctdwn`                                                                  | peer removal countdown                                                                                                                                                                |
| `disconn_animation`                                                                    | peer / link disconnect                                                                                                                                                                |
| `wifi_animation`                                                                       | WiFi activity                                                                                                                                                                         |
| `crystal_sos`, `crystal_sos_blink`, `sos_led`, `start_sos`, `enable_sos`, `toggle_sos` | SOS alert blink; peer-in-SOS event (`evt_peer_in_sos`); mute/unmute (logs `Muting SOS` / `Un-muting SOS`)                                                                             |
| `demi_god_blink`, `demi_god_countdown`                                                 | a [demi-god](/protocols/demigod) command landed                                                                                                                                       |
| downloading / progress ring (`PROGRESS_RGB`)                                           | OTA in progress                                                                                                                                                                       |
| spin / rotation (`_spin_deg`, `Dial rotation`)                                         | dial/heading feedback                                                                                                                                                                 |
| Vibe Mode (`new_vibe.new_vibe`, task `'sound_react'`)                                  | sound-reactive ring effect: microphone samples (`mic.read_u16`) written straight into the ring buffer (`ring.buf`) — see [Vibe Mode](/subsystems/power#vibe-mode-sound-reactive-leds) |

All rows above are **confirmed symbol names**; the exact frame data and timings live in
undisassembled frozen bytecode. The underlying effect primitives (confirmed qstrs) are
`breathe_effect` / `breathe_gen` / `breathe_v2`, `twinkle` / `twinkle_crystal`, and
`fade_step`. Vibe Mode's v5.0.3 tuning (`newvibe-1.10`) lowers its brightness base from
0.1 to 0.07 — about 30% dimmer (inferred) — and raises the auto-gain up rate from 13 to 25.

The compass heading itself is rendered as a lit point on the ring that rotates to point
toward the target (`_spin_deg`, `Dial rotation cancelled`).

## Status colors

<Note>
  Identifying the **firmware version** by the power-up Touch Crystal color (e.g. "5.0 →
  Sparkly Pink") comes from the official update guide, **not** from firmware evidence. No
  version→color table was found in the binary, and no `sparkly` or `pink` string exists
  (only `hot_pink` is a defined color), so this mapping cannot be verified here.
</Note>

What the firmware *does* show is that the crystal color is a **configurable default that is
also mapped per bonded peer**, not derived from `__version__`:

| Symbol / string                          | Evidence                                                                                                                       |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `crystal_default`, `powerup_crystal_clr` | the power-up crystal color is a stored default                                                                                 |
| `Change default crystal color: {}`       | that default is user-changeable (log string)                                                                                   |
| `PEER_CRYSTAL_MAP` = (1, 2, 4, 6)        | crystal color is mapped per bonded peer — a 4-tuple of color-list indices (→ orange, yellow, green, teal), **value confirmed** |
| `Can't change color - in bond group`     | color changes are blocked while in a bond group                                                                                |

In v5.0.3, when `compass.start_peer_management` runs while the device is vertical it now
sleeps 300 ms and then restores the default crystal colour (`crystal_default()`).

OTA/update state→color mappings are described in the [WiFi OTA](/protocols/wifi-ota) guide.
The disassembly of `ota_callback.py` resolves the two color constants (previously "values
undetermined"), so two of the four guide claims are now **confirmed in firmware** and two
remain guide-only:

| Guide claim                                 | Firmware evidence                                                                                           |
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| white progress ring while downloading       | **confirmed** — `PROGRESS_RGB = dim_leds(colors.white, 0.3)` (white at 30% brightness)                      |
| orange blink on update failure              | **confirmed** — `ERROR_RGB = dim_leds(colors.orange, GLOBAL_BRT)`, applied via `ring.fill(ERROR_RGB, True)` |
| full white fill on success                  | guide only — no matching literal or state→color binding found                                               |
| pink spinning when hotspot can't be reached | guide only — no `pink` / `spinning` literal found                                                           |

`dim_leds(rgb, brt)` returns the RGB tuple with each channel scaled by `brt`. A third
constant, `LOG_RGB = dim_leds(colors.white, GLOBAL_BRT)`, is defined alongside these.

## Power interaction

LEDs are one of the largest current draws, so the LED task is **likely** gated by power
state. A general `lightsleep` mechanism exists (`dev_total_lightsleep_ms`, log `Awoke from
lightsleep …`), but no LED-task-specific sleep-gating symbol was found, so the LED-task
suspension is an inference rather than a confirmed binding.

Brightness is independently controllable: `GLOBAL_BRT`, `dim_leds`, `toggle_brightness`
(with `toggle_brightness_timeout`), `update_brightness`, and the log `Revert to full
brightness`. `toggle_brightness` is wired to a single tap of the physical power button
(`sw_power.cb_single_tap`, **confirmed**; see
[physical buttons](/subsystems/power#physical-buttons-power--sos)). Note that the binary's
**low-battery mode is evidenced for touch sensitivity, not LED brightness** — the
low-battery symbols (`_low_batt_sensitivity`, `_LOW_BATT_FLOOR`, `_LOW_BATT_MULTIPLIER`)
and logs (`[touch] Low battery mode ON | base_sens: {:.3f}`) all
tie to touch, with nothing linking low-battery state to LED brightness in the evidence.
