Skip to main content
The Totem’s visible output is a ring of addressable RGB LEDs (the halo) plus the Touch Crystal. Both are driven by f_lib/neopixel_v2.py through the LightStrip subclass in leds.py, wired up in peripherals.py, with the animation library split between peripherals.py and animations.py. The ESP32 emulator models this: the 60-pixel ring, the 7-pixel crystal, the palette below, and the animation each event plays.
Everything on this page was read out of the v5.0.3 frozen bytecode (re/v5.0.3/mpy/*.dis). Claims are graded confirmed (read in the disassembly), inferred, or called out as unverifiable. leds.py, animations.py and f_lib/neopixel_v2.py are byte-identical between v5.0.2 and v5.0.3; peripherals.py differs only in the battery lookup table. No LED behavior changed in this release.

Driver

APA106 is dead code

The page previously claimed the LEDs are APA106 parts driven by apa106.py with ORDER = (0, 1, 2, 3). That is wrong for v5.0.3:
  • apa106.py imports NeoPixel from the generic neopixel.py and defines class APA106(NeoPixel): ORDER = (0, 1, 2, 3) — that is its entire body (apa106.dis, 49 lines).
  • Nothing imports apa106, and nothing imports the generic neopixel except apa106 itself (confirmed — the only IMPORT_NAME neopixel in all 94 modules is apa106.dis:23, and no module names apa106).
Both files are frozen into the image but unreachable. The live path is f_lib/neopixel_v2.py, whose order is GRB, so ORDER = (0, 1, 2, 3) describes nothing the hardware ever sees. The physical part number is not recoverable from the bytecode — only that it is a single-wire chain clocked at WS2812 800 kHz timings.

Geometry and pins

All confirmed: _ring_leds = const(18) / _crystal_leds = const(21) (peripherals.dis:173-181), and DeviceLeds.__init__ builds LightStrip(Pin(21), 7, 'rgb') and LightStrip(Pin(18), 60, 'rgb', pwr_gate_pin=19) (peripherals.dis:268-295). A single module-level instance leds = DeviceLeds() is the device-wide handle (peripherals.dis:187-189), so every module refers to leds.ring and leds.crystal. Two details worth knowing:
  • The pixel counts are hardcoded at the call site. project_data.py defines RING_PX_COUNT = 60 and CRYSTAL_PX_COUNT = 7 (project_data.dis:521-524), but DeviceLeds.__init__ passes the literals 60 and 7 instead of importing them. The constants are used elsewhere; the strips do not depend on them.
  • The mode='rgb' argument is accepted and discarded. LightStrip.__init__ never stores it and calls super().__init__(pin, n) with two arguments (leds.dis:239-274). Changing it would not change the wire order.
  • Only the ring is power-gated. off(pwr_off=True) drives GPIO 19 low and clears is_powered; power_on(is_force=False) drives it high (leds.dis:677-730, :980-1003). The crystal has no gate, so it can only be blanked, not de-powered.

LightStrip primitives

Every animation in the firmware is built from these seven coroutines plus fill/off (confirmed, leds.dis:152-238 for the signatures, bodies at the cited lines): off() swallows every exception and logs LED err with is_write=False (leds.dis:706-720), so a failing strip never takes down the caller. fade_step is a parameter of twinkle, not a function — the earlier claim that it was an effect primitive was wrong.

Colors

The Colors class (project_data.py) defines 13 named colors (confirmed, project_data.dis:1881-1944). The same 13-tuple, in this order, is the palette that integer color IDs index into (project_data.dis:1890): All 13 name→RGB bindings are confirmed: the class binds each with an explicit STORE_ATTR. There is no named OFF color — the clear state is the literal (0, 0, 0). Two helpers do all the color math (confirmed):
  • dim_leds(rgb, brt) → (int(r*brt), int(g*brt), int(b*brt)) (project_data.dis:2151-2178).
  • colors.rgb(color, brt=None) (project_data.dis:2097-2149) — brt defaults to config.led_brt, not to GLOBAL_BRT. color may be a tuple (used directly) or an integer palette index. Any exception logs Invalid color provided and returns (0, 0, 0), so a bad color id silently blacks out a strip rather than raising.
  • GLOBAL_BRT = 0.6 (project_data.dis:515-516) is the compile-time default that config.led_brt also starts at (project_data.dis:1353-1355).

Peer and bond colors

Three different color sources exist; they are easy to confuse. Colors.next_peer_clr has a confirmed off-by-one: the guard is if len(config.peers) > len(tuple) (i.e. > 8), so a peer count of exactly 8 falls through to tuple[8] and raises IndexError; the magenta fallback is only reachable at 9 or more. config.max_bonds = 8 and config.max_peers = 9 (project_data.dis:964-969), so the boundary is reachable in principle. Whether any caller hits it in practice is not determined here.

Peer pixels on the crystal

PEER_CRYSTAL_MAP = (1, 2, 4, 6) was previously documented as “a 4-tuple of color-list indices (→ orange, yellow, green, teal)”. That is wrong. They are the four crystal pixel slots handed out to bonded peers:
  • peer_helpers.py (get_next_peer_pixel / update_peer_pixels) allocates one slot per bonded peer from that tuple.
  • compassing.collect_all_peers then does leds.crystal[peer.pixel] = colors.rgb(peer.rgb, brt=led_brt) (compassing.dis:1273-1310).
So each peer lights its own bonded color on its own assigned pixel. The tuple fixes where on the 7-pixel crystal a peer shows up, never what color it is.

Animation set

Most of the names previously listed as “animations” are not functions. Three categories (confirmed): asyncio.Event() objects on modes (project_data.dis:1018-1037) — anim_add_peer, anim_bonding_ctdwn, anim_boot_batt_pct, anim_peer_del_ctdwn, anim_gnss_search. These are signals, not effects; something else waits on or sets them. Task names passed to tasks.launch(coro, 'name'), which is how the code stops an effect later: Real coroutines. peripherals.py (peripherals.dis:253): animations.py (animations.dis:89) holds five more:

shuffle_countdown has an off-by-one

shuffle_countdown builds four colors — hot_pink, yellow, blue, green — but its inner change_clr generator wraps with if i >= len(clrs) - 1: i = 0 (animations.dis:229-238). The index resets one iteration early, so index 3 is never yielded: green is constructed and never displayed. The effective cycle is hot_pink → yellow → blue (confirmed).

Boot battery gauge (powerup_animation)

Confirmed, peripherals.dis:830-1001:
  1. Clear modes.evt_led_override_off (the “an animation owns the LEDs” flag).
  2. volts defaults to 4.0; pct = get_batt_pct(max_volts=config.batt_max_volts, cur_volts=volts).
  3. n_lit = int(60 * pct / 100).
  4. Colors — body / leading tip:
  5. Sweep pixel by pixel, writing every frame and sleeping 1 ms on even indices.
  6. Hold 2000 ms, ring.off(), then set modes.anim_boot_batt_pct and modes.evt_led_override_off.
powerup_animation is the anim_boot_batt_pct implementation — the event is what it raises on completion, not a separate effect. crystal_sos_blink (peripherals.dis:704-829) is confirmed to be real Morse code, not a plain repeating flash:
Three flashes at 300 ms, three at 900 ms, three at 300 ms — · · · — — — · · ·. The timing generator is duplicated: once as f_lib.generators.sos_timing (f_lib_generators.dis:100-117), once nested inside crystal_sos_blink itself (peripherals.dis:812-829), and a third identical copy is the module-level generator leds.toggle_sos (leds.dis:1325-1342) — despite the name, it yields 300, 900, 300 and toggles nothing. Each flash also blocks on two events: evt_brt_adjusted (so a brightness change pauses the blink) and evt_ble_cntg_done. Mute/unmute is handled by the caller (logs Muting SOS / Un-muting SOS in compass.py); a running blink does not re-check is_sos_mute.

Bond celebration (peer_bonded)

Confirmed, peripherals.dis:1067-1373 — a four-act sequence:
  1. Blank both strips, 50 ms pause.
  2. Ring and crystal fade up together to brt over 6 steps × 50 ms (two tasks, both awaited).
  3. Hold 1000 ms, then run two tasks at once: collapse_px(start_px=0) on the ring and a crystal fade-out over 5 steps × 80 ms — on pixels=(0, 1, 2, 3, 4, 6), deliberately skipping crystal pixel 5.
  4. Pixel 5 and ring pixel 30 (the point opposite the seam) then cross-fade in 5 steps of 0.2 at 100 ms: the ring dims 1.0 → 0.0 while the crystal brightens 0.0 → 1.0, so the peer color visibly moves from the halo into the crystal. Ends with ring.off().

Breathing generators

The page previously listed breathe_effect, breathe_gen and breathe_v2 as three primitives. They are two duplicate generators plus one strip method (confirmed):
  • f_lib.generators.breathe_effect (f_lib_generators.dis:55-99) and compassing.breathe_gen (compassing.dis:1192-1234) are the same generator written twice: start at 1.0, step ±0.1, bounce off 0.8 at the top and 0.2 at the bottom, yielding a value rounded to 2 dp. They produce brightness levels; they touch no pixels.
  • LightStrip.breathe_v2 is the method that actually drives a strip (see the primitives table).
twinkle_crystal is a Compass method, not an animation-module function (compass.dis:4635-4828): an eco-mode-only crystal shimmer that wakes every 400 ms, waits on evt_orien_vertical, evt_brt_adjusted and evt_battery_charged, skips while BLE is connected (ble_conn.status_id == 2) or a peer is in SOS, then fills the crystal at 0.5 and drops randrange(3, 6) pixels to either black or 0.05. The whole effect is scaled by 1.0 when config.led_brt >= 0.5 and by 0.2 below that.

Status colors

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 exists in the binary, and no sparkly or pink string exists (only hot_pink is a defined color), so this mapping cannot be verified here.
The crystal color is a stored, user-changeable setting, not a function of __version__: In v5.0.3, compass.start_peer_management gained a tail that sleep_ms(300)s and then calls crystal_default() when modes.evt_orien_vertical is set (confirmed — compass.dis:7795-7813; the v5.0.2 build of the same function ends without it).

OTA colors

OTA/update state→color mappings are described in the WiFi OTA guide. ota_callback.py defines three constants at import time (confirmed, ota_callback.dis:250-267): The purple-to-blue gradient ((200, 0, 200), (100, 0, 200), (0, 0, 200)) shows up twice (confirmed):
  • ota_callback twinkles it on the crystal during a WiFi OTA: crystal.twinkle(colors=[…], per_ms=80, fade_step=1, bright=0.3, single=False, min_brt=10) (ota_callback.dis:545-560).
  • ota_ble.COLOR_LIST (ota_ble.dis:286-287) is the same three colors as a tuple, used to paint each of the 7 crystal pixels with colors.rgb(COLOR_LIST[randrange(3)], 0.6) during a BLE OTA (ota_ble.dis:1352-1385). This is a separate constant from the main palette.

Power interaction

Brightness is a single persisted value, config.led_brt, which defaults to 0.6 (project_data.dis:1353-1355) and is what colors.rgb() uses when no brt is passed. update_brightness(brt) writes it and refreshes compassing.default_rgb (confirmed, compass.dis:10302-10319). Eco mode blanks the ring. Compass.change_power_mode(power_mode) (confirmed, compass.dis:8446-8639): Either way it also clears evt_brt_adjusted, re-runs init_touch(False), stops the sound_react task (Vibe Mode does not survive a brightness change), launches toggle_brightness_timeout, repaints the crystal from config.color_id, and calls update_brightness(brt=…). Compass.toggle_brightness (confirmed, compass.dis:8386-8445) is the single-tap handler on the power button (sw_power.cb_single_tap; see physical buttons). It:
  • returns immediately if modes.is_power_down or if modes.evt_battery_charged is not set — so the tap does nothing on a device that has not reported a charged battery;
  • XOR-toggles the current mode (cur = 2 if is_eco_mode else 0; nxt = cur ^ 2) and delegates to change_power_mode(power_mode=…).
toggle_brightness_timeout(timeout_ms=2500, original_val=…) polls every 300 ms against self.last_brt_adjusted and, once the window lapses, blanks both strips, sets evt_brt_adjusted, and restores either crystal_default() (if vertical) or touch plus the sound_react task. Logs Toggle Brightness Timeout Started: {} / Completed: {}. The log Revert to full brightness does not belong to toggle_brightness. It is emitted by compassing.crystal_brightness() (confirmed, compassing.dis:1235-1261), which returns 0.2 until modes.evt_touch_active fires and 1 thereafter — a touch-gated crystal dimmer, latched by modes.is_touch_ready_blink. modes.evt_led_override_off is the mutual-exclusion flag: powerup_animation and blink_all_leds clear it on entry and set it on exit (peripherals.dis:835-839, :993-998, :1478-1482, :1578-1582), marking the window in which an animation owns both strips. A general lightsleep mechanism exists (dev_total_lightsleep_ms, log Awoke from lightsleep …), but no LED-task-specific sleep gating was found — LED suspension during lightsleep remains an inference. Separately, the binary’s low-battery mode is evidenced for touch sensitivity, not LED brightness: _low_batt_sensitivity, _LOW_BATT_FLOOR, _LOW_BATT_MULTIPLIER and the log [touch] Low battery mode ON | base_sens: {:.3f} all tie to touch, with nothing linking low-battery state to LED brightness.

Vibe Mode writes the raw buffer

Vibe Mode (new_vibe.py, task 'sound_react') caches leds.ring, leds.ring.buf and leds.ring.write as locals and writes bytes straight into the pixel buffer, bypassing __setitem__ (confirmed, new_vibe.dis:449-457). It therefore has to handle the GRB byte order itself. Its v5.0.3 tuning (newvibe-1.10) lowers the brightness base from 0.1 to 0.07 — about 30 % dimmer — which is confirmed by comparing the module constant tables across versions (new_vibe.dis:70 vs the v5.0.2 build). See Vibe Mode.

Not LED symbols

  • _spin_deg is defined only in imu_fusion_auto.py (confirmed — it appears in no other module). It is fusion-engine state, not an LED symbol. The compass heading is rendered as a lit point that rotates toward the target, but the rendering code was not located in this pass and the mapping from _spin_deg to a pixel index is not recovered.
  • apa106.py and neopixel.py — dead code, see above.