Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
269a9f1
fix post-release automation findings
BrettKinny Jul 12, 2026
fc960b6
fix firmware onboarding and release docs
BrettKinny Jul 16, 2026
2626077
fix: serialize PiVoice RPC turns
0Downtime Aug 11, 2026
d9ceb71
fix(test): preserve continuous microphone evidence in bounded AV sess…
BrettKinny Sep 12, 2026
780cf03
test(av): measure acoustic quality and clarify live feature contracts
BrettKinny Sep 12, 2026
b3123b7
test(av): verify quiet replies and separate cold wake from warm follo…
BrettKinny Sep 12, 2026
bb04055
fix: keep ambient sound turns out of active conversations
BrettKinny Sep 12, 2026
391fbca
test(av): treat missing quiet-speech transcripts as inconclusive
BrettKinny Sep 12, 2026
e670613
test: exercise all voice tools with isolated synthetic memory
BrettKinny Sep 12, 2026
dfd72b8
test(av): bind pre-rendered prompts to exact text and waveform hashes
BrettKinny Sep 12, 2026
41080f2
test: make response VAD experiments explicit and reproducible
BrettKinny Sep 12, 2026
0344837
fix: unwrap ASR content before correction and routing
BrettKinny Sep 12, 2026
6381f46
docs: align sleep acceptance with firmware privacy contract
BrettKinny Sep 12, 2026
9493575
fix: preserve ASR intent instead of fuzzy-matching commands
BrettKinny Sep 12, 2026
1f3e9da
fix(voice): reject explicit negated and quoted state commands
BrettKinny Sep 12, 2026
74a6cfa
fix: guard voice camera access with live child policy
BrettKinny Sep 12, 2026
3f46836
test: require an explicit overnight deployment host
BrettKinny Sep 12, 2026
2a51e06
test(av): capture optional float PCM and require explicit listening r…
BrettKinny Sep 12, 2026
a0c2657
test(av): report native-channel prompt and response quality separately
BrettKinny Sep 12, 2026
50c68e2
test(av): require fresh listening status before warm capture
BrettKinny Sep 13, 2026
258b9e2
wip: pre-migration snapshot (CachyOS reinstall prep)
BrettKinny Sep 17, 2026
da4a6d3
fix(voice): clear abort flag when a new user turn is accepted
BrettKinny Oct 5, 2026
f0e6cda
fix(asr): drop WhisperLocal transcripts with a high no-speech score
BrettKinny Oct 5, 2026
f8f200a
chore(pi_voice): sync provider with the code deployed from PR #173
BrettKinny Oct 5, 2026
bcfc225
fix(pi_voice): give pi a Dotty system prompt on the live voice path
BrettKinny Oct 5, 2026
baf85d7
test(av): let the overnight runner open the mic and stop scoring eval…
BrettKinny Oct 5, 2026
1892510
fix(dashboard): relay perception feed, drop dead waits, disable sound…
BrettKinny Oct 5, 2026
bfe69b6
fix(persona): tell the voice persona to answer briefly and stop
BrettKinny Oct 5, 2026
2114148
feat(dashboard): report voice turns and filter hits from PiVoiceLLM
BrettKinny Oct 5, 2026
d42b146
test(av): set aside cases the evidence cannot settle; a misheard prom…
BrettKinny Oct 5, 2026
e6badc3
feat(av): portrait crop option for clip exports
BrettKinny Oct 5, 2026
94530f1
Merge branch 'pr-173' into release/server-v0.2.0-rc.1
BrettKinny Oct 5, 2026
fda3b5d
Merge branch 'pr-175' into release/server-v0.2.0-rc.1
BrettKinny Oct 5, 2026
92fc7af
Merge branch 'pr-184' into release/server-v0.2.0-rc.1
BrettKinny Oct 5, 2026
b6dc2ba
docs(modes): reword historical-pin note so the onboarding doc check p…
BrettKinny Oct 5, 2026
ec04a31
chore(av): drop unused import flagged by ruff
BrettKinny Oct 5, 2026
8356a90
Merge test/overnight-av-20260912 (bfe69b6) into web-dashboard-fixes
BrettKinny Oct 5, 2026
d3a320e
docs(changelog): record the voice fixes and runner changes going into…
BrettKinny Oct 5, 2026
08b1f69
Merge remote-tracking branch 'origin/web-dashboard-fixes' into releas…
BrettKinny Oct 5, 2026
11eb4d5
fix(memory): keep prompt scaffolding out of memory and stop speaking …
BrettKinny Oct 5, 2026
39975e4
docs(changelog): record the memory scaffolding fix (#186)
BrettKinny Oct 5, 2026
7844b30
fix(think_hard): outlast the reasoner's cold load and speak before wa…
BrettKinny Oct 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,21 @@ sbom-server.json
htmlcov/
.pytest_cache/

# Local UAT evidence. Keep compact manifests/results/draft issue summaries
# visible for selective review, but never offer raw household media, event
# streams, health snapshots, or container logs for an accidental commit.
/uat-sessions/**/video/
/uat-sessions/**/clips/
/uat-sessions/**/logs/
/uat-sessions/**/ndjson/
/uat-sessions/**/snapshots/

# Generated audit/session outputs. These can contain machine-local paths,
# prompts, or deployment observations; regenerate or curate before publishing.
/AUDIT-REPORT.md
/audit-draft-issues.md
/pi-session-*.html

# Frontend build deps for the dashboard's vendored Tailwind bundle.
# package.json + tailwind.config.js are committed; build artifacts and
# the npm install state are not. Re-run `npm install && npm run build:css`
Expand Down
2 changes: 2 additions & 0 deletions :memory:.ses
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
1789215087076
80298cc7-4cef-98c0-2eea-5f395ca28db5
1,286 changes: 1,286 additions & 0 deletions AUDIT-REPORT.md

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]

### Added
- **Overnight A/V runner can run unattended** (#182) — opt-in `--mic-opener admin_say` opens the microphone through `/xiaozhi/admin/say` before a warm case; playback is also proven by the robot's own recognition; reference-microphone slips and extra speech in the room are `INCONCLUSIVE` rather than failures; a case the evidence cannot settle is set aside after three attempts. `dotty_av_clips.py export --crop` fills the 9:16 frame from a portrait source region. See `docs/dotty-av-tests.md`.
- **Kid Mode: blocked-words content filter on the live voice path** (#157, closes the gap tracked in #138) — the pure matcher (three severity tiers + the kid-safe replacement) moved from `bridge/text.py` into the shared `custom-providers/textUtils.py`, the single source of truth both containers import; the bridge keeps its metrics/safety-ring/logging wrapper on top with unchanged behaviour. Both live LLM providers (`PiVoiceLLM`, `OpenAICompat`) now wrap their TTS-bound streams in `filter_tts_stream()`. In Kid Mode it drains and checks the complete response before TTS, then emits the original clean chunks or atomically replaces a blocked turn; outside Kid Mode it remains a transparent streaming passthrough. Full-turn consumption also prevents PiVoiceLLM from abandoning the RPC iterator before `agent_end` and leaking stale frames into the next turn. Honest caveat (see `docs/faq.md`): a word-level blocklist is a weak, bypassable backstop — prompt steering remains the primary defence. **Bench: needs on-device red-team verification before release sign-off.**
- **Admin-API auth: `X-Admin-Token` across the whole stack** (#149, #150, #151, #152) — xiaozhi-server's `/xiaozhi/admin/*` routes now accept an `X-Admin-Token` shared secret (`DOTTY_ADMIN_TOKEN`, timing-safe compare, permissive when unset), and all three callers — the bridge dashboard, dotty-behaviour's `XiaozhiAdminClient`, and the dotty-pi voice tools' `adminFetch` — send it. The secret is now **plumbed end-to-end**: `make setup` generates one into `.env`, the compose template / all-in-one pass it to xiaozhi-server, the three service compose files document where their copy lives, and `.env.example` + `SETUP.md` §10 describe the enable-everywhere-or-nowhere semantics. Previously the code shipped with no config path, so every deploy silently stayed permissive.
- **PersonResolver — one answer to "who is this?"** (`dotty-behaviour/household/resolver.py`) — identity resolution was smeared across consumers, and the 2026-06-06 audit found four separate identity bugs because of it. All resolution now funnels through one module with `Person.id` as the canonical key space. Fixed by the consolidation: **room_view roster recognition silently failing whenever `id != display_name`** (the VLM echoes display names; validation compared ids — confirmed 3/3, both the core and greeter paths), **multi-word display names never matching** (the NAME parser was single-token, so "Mary Anne" was a 100% silent miss — confirmed 3/3), **the greeter's calendar lookup dropping a person's own events on a case mismatch** (`[Hudson]` ≠ `hudson` — confirmed 2/3), and **bracketless `calendar_prefix:` YAML never matching**. `summarize_for_prompt` now matches person tags case-insensitively and accepts the resolver's tag set; the room_view test fakes were also fixed to carry real ids (the old fakes re-derived ids from display names, which is exactly what masked the bug).
Expand All @@ -26,6 +27,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- **`docs/multi-daemon-split.md`, `docs/advanced/multi-host.md`** — both documented ZeroClaw-host topologies that no longer exist.

### Fixed
- **Voice: one abort no longer silences every later reply** (#176) — a device or admin abort set `conn.client_abort` and nothing on the `nointent` chat path cleared it, so ASR and the LLM kept running but no TTS was sent until the robot reconnected. `startToChat` now clears the flag when a new user turn is accepted. Reproduced and verified on the physical robot.
- **Live voice path now has a Dotty system prompt** (#177) — `PiClient` spawned pi with no `--system-prompt`, so pi's built-in coding-assistant prompt was the only identity the model saw ("I'm just a tiny coding assistant…"). `personas/pi_voice.md` is now passed via `--system-prompt` (`DOTTY_PI_SYSTEM_PROMPT_FILE` overrides; a missing file keeps the old behaviour). The persona also tells the 4B model to answer briefly and stop.
- **WhisperLocal drops near-silence hallucinations** (#105) — transcripts whose mean `no_speech_prob` exceeds `no_speech_threshold` (default 0.6, `1.0` disables) are discarded instead of being answered ("Thank you." after the robot stops speaking).
- **Memory: prompt scaffolding is no longer stored or spoken** (#186) — every voice turn was logged to `brain.db` with the per-turn tool-routing and HARD CONSTRAINTS text attached, and an explicit recall ("do you remember…") spoke `memory_lookup`'s raw rows. The turn logger now stores only what the person said, `memory_lookup` cleans older rows at read time, and recall passes the search results to the model as context so the answer is phrased naturally.
- **think_hard no longer fails on a cold start** (#187) — its request timeout defaulted to 30 s, shorter than the reasoner's 30–50 s cold load, so the first think-hard after an idle period was cancelled mid-load. Default is now 90 s (as the pre-cutover bridge unit had it), the direct tool runner allows 105 s, and Dotty says "Let me think hard about that." before the wait.
- **PiVoice RPC turns are serialized** (#175, thanks @0Downtime) — one connection can no longer interleave two pi transactions and consume each other's frames.
- **No-GPU ASR path no longer crash-loops on first run** (#124, #136) — `make fetch-models` was requesting two SenseVoiceSmall filenames that don't exist on Hugging Face (`tokens.json` and `chn_jpn_yue_eng_ko_spectral.fbank.conf.yaml`); the real SentencePiece tokenizer is `chn_jpn_yue_eng_ko_spectok.bpe.model`. Both 404s were silently saved as 15-byte "Entry not found" stubs (curl had no `--fail`), so funasr loaded with `bpemodel=None` and `xiaozhi-esp32-server` crash-looped on every GPU-less host. The file list is corrected; **all `fetch-models` downloads now fail loudly** (`curl --fail --retry` + a size floor + delete-on-failure) instead of saving junk; and **`make doctor` now size-checks the required SenseVoice assets** so a corrupt download FAILs instead of passing. Huge thanks to **[@miltieIV2](https://github.com/miltieIV2)** — a meticulous bug report *and* a self-driven root-cause that pinned it on the `HAS_CUDA=0` FunASR switch. A lighter int8 sherpa-onnx SenseVoice runtime (no PyTorch) for Pi-class hosts has landed as the opt-in `SenseVoiceOnnx` provider (#135; see the Added section above).

## [server-v0.1.0] - 2026-05-17
Expand Down
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ The voice path runs through a single LLM provider — `PiVoiceLLM`, selected via

```
StackChan hardware → configured persona
│ ESP32-S3, xiaozhi firmware (built from m5stack/StackChan source)
│ ESP32-S3, xiaozhi firmware (pinned BrettKinny/StackChan@dotty fork)
│ WiFi / WebSocket (Xiaozhi protocol)
▼
xiaozhi-esp32-server (Docker)
Expand Down Expand Up @@ -197,7 +197,8 @@ For hardware specs, protocol details, model internals, latent capabilities, and

- xiaozhi-esp32-server: https://github.com/xinnan-tech/xiaozhi-esp32-server
- xiaozhi-esp32 firmware (upstream): https://github.com/78/xiaozhi-esp32
- StackChan (hardware + firmware patches): https://github.com/m5stack/StackChan
- StackChan upstream (hardware + base firmware): https://github.com/m5stack/StackChan
- Dotty firmware fork (the pinned build source): https://github.com/BrettKinny/StackChan/tree/dotty
- Emotion protocol: https://xiaozhi.dev/en/docs/development/emotion/

## Agent skills
Expand Down
38 changes: 22 additions & 16 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,16 @@ For protocol wire formats see `docs/protocols.md`.

| Component | Current Version | Protocol / Interface | Breaking Change Policy |
|---|---|---|---|
| StackChan firmware (m5stack/StackChan v1.2.4) | v1.2.4 | Xiaozhi WebSocket protocol, MCP over WS (JSON-RPC 2.0) | Pin firmware to a known-good build; do not OTA-update without verifying server compatibility first |
| StackChan firmware (`BrettKinny/StackChan@dotty`) | `fw-v1.3.3` release; submodule `969c2b2` | Xiaozhi WebSocket protocol, MCP over WS (JSON-RPC 2.0), StateManager event contract | Build the pinned submodule; do not substitute the official upstream tree or OTA-update without coordinated verification |
| xiaozhi-esp32-server (local build) | `xiaozhi-esp32-server-piper:local` | Custom LLM provider API, `.config.yaml` schema, Xiaozhi WS server | Rebuild image only after checking upstream changelog for provider API or config schema changes |
| dotty-pi (pi agent) | `dotty-pi:0.1.0` | pi RPC (JSONL over stdio), the five `dotty-pi-ext` voice tools | Pin the image tag; pi-version or model changes need end-to-end cutover testing |
| dotty-pi (pi agent) | `dotty-pi:0.1.0` | pi RPC (JSONL over stdio), the seven `dotty-pi-ext` voice tools | Pin the image tag; pi-version or model changes need end-to-end cutover testing |
| dotty-behaviour | `dotty-behaviour:0.1.0` | HTTP API (`/api/perception/*`, `/api/vision/*`, `/api/audio/*`, `/health`) | Endpoint signatures stable; perception event-schema changes require firmware + xiaozhi review |
| bridge.py (dashboard) | unversioned (HEAD) | `/ui` dashboard, `/admin/*`, `/health` | Dashboard/admin service only post-#36; admin route changes require updating dashboard callers |
| bridge.py (dashboard) | `dotty-bridge:0.1.0` image from repo HEAD | `/ui` dashboard, `/admin/*`, `/health` | Dashboard/admin service only post-#36; admin route changes require updating dashboard callers |

The public `fw-v1.3.3` superproject tag (commit `24a009c`, 2026-07-12) is the
current coordinated release pointer. It pins firmware submodule `969c2b2` and
the matching server-side patches. Draft PRs and a dirty submodule are not a
released compatibility set.

## What counts as a breaking change

Expand All @@ -42,27 +47,28 @@ Any of the following require coordinated updates across components:

## Versioning strategy

No formal versioning is adopted yet (tracked in
[ROADMAP.md](ROADMAP.md#community-wishlist) under "Firmware/server
compatibility matrix"). When adopted, the plan is:
The repo uses separate tag namespaces:

- `server-vX.Y.Z` for server-only release milestones.
- `fw-vX.Y.Z` for coordinated firmware release pointers in this superproject.

- Separate tag namespaces: `server-vX.Y.Z` and `fw-vX.Y.Z`.
- This matrix will document which server versions are compatible with which
firmware versions.
- The bridge will carry its own version once it moves to a tagged release
cadence.
Container image tags remain `0.1.0` today and are not sufficient by themselves
to identify the exact source revision; retain the Git commit/tag alongside a
deployment record.

## Upgrade guidance

1. **Check this matrix first.** Confirm the component you are upgrading is
compatible with the versions of the other components you are running.
2. **Back up before upgrading.** Run `scripts/backup.sh` (or the equivalent
manual steps) to snapshot config, persona files, and bridge state.
2. **Back up before upgrading.** Manually snapshot `.env`, rendered
`data/.config.yaml`, persona/household files, `brain.db` (including WAL/SHM
companions), and the bridge state directory. This repo does not currently
ship an automated backup script.
3. **Upgrade one component at a time.** Validate with a health check
(`curl http://<XIAOZHI_HOST>:8090/health` and `:8081/health`) plus a live
voice turn before moving to the next component.
4. **Tail logs during validation.** Watch both the xiaozhi-server container
logs and the bridge journal simultaneously to catch mismatches early.
4. **Tail logs during validation.** Watch the xiaozhi-server, dotty-pi,
dotty-behaviour, and bridge container logs together to catch mismatches.
5. **Roll back if broken.** Restore from the backup taken in step 2 and
revert to the previous image or binary.

Expand Down Expand Up @@ -106,4 +112,4 @@ versions work with which firmware versions.

---

Last verified: 2026-05-22.
Last verified against the repository and `fw-v1.3.3` pin: 2026-07-16.
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ FROM ghcr.io/xinnan-tech/xiaozhi-esp32-server@sha256:3accd82a7d1a6c01c58f32f6199

RUN pip install --no-cache-dir piper-tts scipy numpy mido faster-whisper sherpa-onnx==1.13.2

# Patch upstream TTS consumers to treat explicitly registered server-push
# sentence IDs as independent from chat-turn stale-message arbitration (#104).
COPY scripts/patch-tts-server-push.py /tmp/patch-tts-server-push.py
RUN python /tmp/patch-tts-server-push.py /opt/xiaozhi-esp32-server/core/providers/tts

# fluidsynth + General MIDI soundfont for runtime rendering of dance/song MIDI
# files to Opus. Installed as the LAST layer so iteration on Python deps above
# doesn't invalidate the soundfont download (~141MB).
Expand Down
7 changes: 4 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,10 @@ setup: _preflight-compose ## Interactive first-run wizard (re-runnable; remember
echo -e "$(GREEN)$(BOLD)Setup complete.$(RESET)"; \
echo ""; \
echo "Next steps:"; \
echo " 1. Flash the StackChan firmware (see SETUP.md or m5stack/StackChan repo)."; \
echo " 2. In the device's Advanced Options, set the OTA URL to:"; \
echo " http://$$XIAOZHI_HOST:8003/xiaozhi/ota/"; \
echo " 1. Build and flash StackChan firmware with this compiled setting:"; \
echo " CONFIG_OTA_URL=\"http://$$XIAOZHI_HOST:8003/xiaozhi/ota/\""; \
echo " See SETUP.md. The on-device Settings app has no OTA URL editor."; \
echo " 2. Provision the robot's 2.4 GHz Wi-Fi using its displayed setup flow."; \
echo " 3. Run 'make doctor' to verify everything is healthy."; \
echo ""

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Full policy: [`AI_TRANSPARENCY.md`](./AI_TRANSPARENCY.md).
- **Streaming responses** — the bridge streams LLM output to the voice pipeline for lower perceived latency.
- **Emoji expressions** — every response starts with an emoji that the firmware maps to a face animation (smile, laugh, sad, surprise, thinking, angry, love, sleepy, neutral).
- **Voice tools** — the pi agent can search its memory, escalate hard questions to a bigger model, take a photo, and play songs, all mid-conversation.
- **States, toggles & LEDs** — a six-state mutex (`idle / talk / story_time / security / sleep / dance`) plus two orthogonal toggles (`kid_mode`, `smart_mode`), all owned by the firmware StateManager and surfaced on the 12-pixel LED ring. Shipped on the active firmware fork (commit `d78118b`, 2026-04-27); the `firmware/firmware/` submodule pin in this repo lags, so flash from the active fork to get it. See "States, Toggles & LEDs" below and [`docs/modes.md`](./docs/modes.md).
- **States, toggles & LEDs** — a six-state mutex (`idle / talk / story_time / security / sleep / dance`) plus two orthogonal toggles (`kid_mode`, `smart_mode`), all owned by the firmware StateManager and surfaced on the 12-pixel LED ring. Shipped on the active firmware fork (commit `d78118b`, 2026-04-27) and included in the release pin checked on 2026-09-12 (`969c2b2`). Later fixes may still differ between the release pin and active fork; see [`docs/modes.md`](./docs/modes.md).
- **Vision (camera)** — the robot's built-in camera can capture images for multimodal LLM queries.
- **Privacy LEDs** — hardware-bound mic (green) and camera (red) indicators on the LED ring. They light from the codec/camera enable signals via RAII guards, so a misbehaving server or model can't capture with the lights off.
- **Calendar context** — optional calendar integration feeds upcoming events into the conversation context.
Expand All @@ -46,7 +46,7 @@ Full policy: [`AI_TRANSPARENCY.md`](./AI_TRANSPARENCY.md).

Behaviour is a **six-state mutex** (`idle / talk / story_time / security / sleep / dance`) plus two orthogonal toggles (`kid_mode`, `smart_mode`), all owned by the firmware StateManager (shipped on the active fork in commit `d78118b`, 2026-04-27; bench checks tracked in [#38](https://github.com/BrettKinny/dotty-stackchan/issues/38)). Voice phrases, camera edges, and dashboard controls all flow through it.

> Note: the `firmware/firmware/` submodule pin in this repo deliberately lags the active fork — flashing from the submodule won't give you Phase 4 yet. See the "Firmware iteration" section in [`CLAUDE.md`](./CLAUDE.md) and the submodule-pin caveat in [`docs/modes.md`](./docs/modes.md).
> Note: `firmware/` is a release pointer, not the active development checkout. The historical `35f701a` pin predates Phase 4; the pin checked on 2026-09-12 (`969c2b2`) includes it. Check your actual revision for subsequent fixes. See the "Firmware iteration" section in [`CLAUDE.md`](./CLAUDE.md) and the submodule-pin caveat in [`docs/modes.md`](./docs/modes.md).

The 12-pixel LED ring shows the current state at a glance. **Left ring 0-5 is the state arc** — all six pixels paint the state colour, matching the dashboard's state buttons:

Expand All @@ -56,12 +56,12 @@ The 12-pixel LED ring shows the current state at a glance. **Left ring 0-5 is th
| 🟢 | `talk` — conversation engaged. |
| 🟠 | `story_time` — long-running interactive story. |
| ⚪ | `security` — watching the room (1 Hz white flash). |
| 🔵 | `sleep` — quiescent, mic open for "wake up". |
| 🔵 | `sleep` — privacy sleep: face detection, voice processing, and wake-word detection off; wake by head touch or explicit dashboard state change. |
| 🟣 | `dance` — rainbow sweep + choreography. |

On the right ring, **indices 8-9 are toggle pips** for kid_mode (salmon pink) and smart_mode (orange), and **index 11 (bottom) lights red while you have the turn** (LISTENING). The `idle → talk` transition fires on `face_detected` from the firmware; VLM identity recognition runs in parallel and feeds the LLM context.

> Heads up: that right-ring layout is the active-fork StateManager. On the firmware **submodule pinned in this repo**, pixels 6 and 11 instead drive the **privacy LEDs** — 6 = mic (green), 11 = camera (red) — and the StateManager pips arrive once the submodule catches up to the active fork.
> Heads up: the historical pre-StateManager build (`35f701a`) used a different right-ring layout: 6 = mic (green), 11 = camera (red). The release pin checked on 2026-09-12 already includes StateManager. Interpret indicators against the actual firmware revision, not the old pre-StateManager layout.

Full state taxonomy, colour palette, transition diagram, and per-state backing architecture: [`docs/modes.md`](./docs/modes.md).

Expand Down
Loading
Loading