Commit Graph
45 Commits
Author SHA1 Message Date
jpmschweitzerandClaude Opus 4.8 24e9375095 fix(firmware): don't let the feedback gong swallow return-to-idle
A spoken volume command left the face stuck in the thinking spinner. The
gateway sends state:thinking -> command -> state:idle, but the device's
state:idle handler is gated on !audio_is_playing(), and the feedback gong
had been setting s_playing for its (up to 5 s) duration — so the idle was
ignored and nothing re-sent it. The gong is a UI cue, not reply playback,
so it no longer sets s_playing. This also drops the 5 s wake-gate the gong
was imposing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKPbR6DY2JygHbyLjxm7Uu
2026-07-15 21:50:53 +02:00
jpmschweitzerandClaude Opus 4.8 e3021baf4d release v0.2.0
Test, Build and Push / test-gateway (push) Successful in 10s
Test, Build and Push / release (push) Successful in 3s
Test, Build and Push / build-gateway (push) Successful in 1m48s
Volume goes to eleven (0–11 scale, gong feedback, mute), a voice command
service that handles volume/mute without the LLM, and an immediate butler
filler line with a spinner during the Tatlock wait.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKPbR6DY2JygHbyLjxm7Uu
v0.2.0
2026-07-15 21:32:31 +02:00
jpmschweitzerandClaude Opus 4.8 09f8afcadd feat(gateway): butler filler line while Tatlock works
Tatlock turns take 10-25s, which is a long silence after a request. Speak
a canned "Let me check on that for you, sir" immediately (synthesized once
and cached), then re-assert the thinking state so the device keeps its
effort-face spinner up until the real reply arrives. On the device, guard
the playback-done handler so the filler audio finishing doesn't drop the
spinner back to idle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKPbR6DY2JygHbyLjxm7Uu
2026-07-15 21:29:35 +02:00
jpmschweitzerandClaude Opus 4.8 6b7fbb60c9 feat(gateway): voice command service
Recognize simple device commands in the transcript and act on them without
a Tatlock round-trip. commands.match() maps volume up/down, mute/unmute,
"set volume to N", and "goes to eleven"/max to a "command" message sent
straight to the device; the utterance never reaches the LLM. Matching is
deliberately precise so real requests ("set an alarm for a quarter to
eleven") are not hijacked. Adds the "command" message to the device
protocol in docs/architecture.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKPbR6DY2JygHbyLjxm7Uu
2026-07-15 21:29:00 +02:00
jpmschweitzerandClaude Opus 4.8 cdb23bda05 feat(firmware): volume control goes to eleven
Rework the speaker volume from a 0-100 percentage to an 11-step level
(0..11, mapped to the codec's percent), with mute that remembers the
prior level so unmute restores it. Any non-silent change plays the gong
as feedback; a new change cuts the in-flight gong off and restarts it
rather than queueing another. The tap overlay shows the level number and
0..11 bar, and a "11" drifts up off the bar when you hit maximum.

Also lands the device side of gateway volume commands: gw_client routes a
"command" message to face_volume_command, which applies the change and
shows a compact auto-hiding volume HUD (a centered bar) — the gateway
half that sends these lives in a following commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKPbR6DY2JygHbyLjxm7Uu
2026-07-15 21:27:48 +02:00
jpmschweitzerandClaude Opus 4.8 9521deddc5 feat(dev): device screenshot over USB
Adds a way to capture the live LVGL screen off the device and rebuild it
as a PNG on the host, so UI changes can be verified remotely without a
camera. A watcher task polls the USB-serial-JTAG RX for a trigger byte and
streams the current screen as raw RGB565 straight to the USB FIFO (framed
by ###SHOT_BEGIN/END### with a CRC); firmware/tools/device_shot.py and the
device-screenshot skill drive it from the host.

Writing straight to the USB FIFO bypasses the primary UART console, which
at 115200 baud would take ~37s per frame. The whole capability is behind
DESKLOCK_DEVMODE (off by default, enable at deploy time with
-DDESKLOCK_DEVMODE=ON) so production spends no internal RAM on the watcher
and nothing extra runs on the render path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKPbR6DY2JygHbyLjxm7Uu
2026-07-15 20:24:41 +02:00
jpmschweitzerandClaude Opus 4.8 23f001ceb5 feat(face): tap-to-reveal volume controls
Tapping the face now brings up an overlay with a microphone button and
volume down/up, plus a live level bar showing the current output volume.
Icons are drawn from a subset of the Phosphor glyph font. Tapping the dim
scrim behind the controls dismisses them; they also auto-hide after a few
seconds of inactivity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKPbR6DY2JygHbyLjxm7Uu
2026-07-15 20:24:04 +02:00
jpmschweitzerandClaude Opus 4.8 ebdb1f0457 Cycle rain streams through the sprite pool for a sense of change
Test, Build and Push / test-gateway (push) Successful in 10s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
Each stream periodically swaps to a different one of the pooled streak
sprites on a staggered timer, so the columns keep changing rather than
sitting statically tiled — the rain reads as alive in the corner of the
eye. The pool stays at 5: each sprite is ~15 KB of internal RAM and more
starves esp-hosted's internal-RAM task stacks into a boot loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKPbR6DY2JygHbyLjxm7Uu
2026-07-15 18:34:17 +02:00
jpmschweitzerandClaude Opus 4.8 b42d8216cc Render matrix rain as pooled internal-RAM sprites
Test, Build and Push / test-gateway (push) Successful in 12s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
The rain was 40 live text labels; moving a label re-rasterizes its
katakana glyphs every frame, reading the font from PSRAM and blending
against the framebuffer. That per-frame load starved the DSI's
framebuffer read and flashed the panel blue while a voice reply
downloaded (confirmed: with the rain removed the flash vanished).

Pre-render a small pool of streak sprites once into internal RAM and
blit them; movement is now a cheap opaque copy that never touches the
PSRAM bus. Also drain the rain during audio (listening/speaking) rather
than freezing it. This clears the last of the load flicker while keeping
the dense look.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKPbR6DY2JygHbyLjxm7Uu
2026-07-15 18:02:59 +02:00
jpmschweitzerandClaude Opus 4.8 728c460d36 Raise the DSI bridge FIFO refill threshold for underrun margin
Bump raw_buf_almost_empty_thrd 512 -> 1024 right after display start so
the bridge demands a DMA refill with more slack still in the FIFO,
letting it ride out a PSRAM-bus latency spike instead of draining to the
blue underrun colour. Symptom mitigation for the bus-arbitration
starvation; the register isn't otherwise exposed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKPbR6DY2JygHbyLjxm7Uu
2026-07-15 18:02:59 +02:00
jpmschweitzerandClaude Opus 4.8 88a7f3e9cc Lower DSI lane rate to 720 Mbps to smooth framebuffer reads
The stock 1500 Mbps (the P4 maximum) makes the DSI transmit each line in
a tight burst, so the framebuffer reads are bursty and lose the PSRAM
bus to the Wi-Fi SDIO DMA under load. 720 spreads the read out and
tolerates the contention. Espressif's documented underrun mitigation;
the floor at this pixel clock is ~480 Mbps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKPbR6DY2JygHbyLjxm7Uu
2026-07-15 18:02:59 +02:00
jpmschweitzerandClaude Opus 4.8 d4a9fd7265 Add CHANGELOG (Keep a Changelog format)
Test, Build and Push / test-gateway (push) Successful in 11s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
Start tracking notable user-facing changes. Seed the Unreleased section
with the DSI flicker mitigation from this round.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKPbR6DY2JygHbyLjxm7Uu
2026-07-15 14:44:37 +02:00
jpmschweitzerandClaude Opus 4.8 7abf9e3fdd Record the esp-hosted PSRAM-mempool famine dead-end
Comment-only. Moving the esp-hosted transport buffers off PSRAM
(MEMPOOL_PREFER_SPIRAM=n), to keep the radio's SDIO DMA off the DSI's
bus and stop it starving the framebuffer read, was tried against the
flicker — but it exhausts internal RAM and boot-loops in esp-hosted init
before Wi-Fi comes up. Documented so it isn't retried; the setting stays
=y.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKPbR6DY2JygHbyLjxm7Uu
2026-07-15 14:43:55 +02:00
jpmschweitzerandClaude Opus 4.8 4c3718411e Ease DSI PSRAM write bursts: bigger inv buffer + half-rate rain
Two secondary mitigations for the DSI underrun (the primary fix is the
reduced DPI clock in the BSP). Raise LV_INV_BUF_SIZE 32 -> 128 so the
busy rain states stay partial-redraw instead of collapsing into a
full-screen redraw, which dumps a ~1.3 MB PSRAM write burst that
competes with the DSI's continuous framebuffer read. Advance the matrix
rain every other tick (doubled step, visually identical) to halve how
often its ~80 labels invalidate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKPbR6DY2JygHbyLjxm7Uu
2026-07-15 14:43:32 +02:00
jpmschweitzerandClaude Opus 4.8 d1e1250016 Add DSI load-test harness; give the LVGL task a 16 KB stack
Start the display via bsp_display_start_with_config with a 16 KB LVGL
task stack. The 8 KB default overflows once LV_INV_BUF_SIZE is raised to
128 (the partial-flush path's stack frame scales with it) — the guard
fires as a "Stack protection fault" boot loop otherwise.

Add FACE_LOADTEST (gated off): a load-emulation harness that cycles
isolated load types — a render-only face ladder, light/heavy render plus
a network stream, render plus playback, and all three stacked. Each
phase logs a marker and shows an on-screen label. It pinned the blue
flicker to a DSI PSRAM-bandwidth underrun (the driver logs "underrun
happens"), which is serial-verifiable without eyes on the screen. Kept
for future bus-contention debugging.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKPbR6DY2JygHbyLjxm7Uu
2026-07-15 14:42:19 +02:00
jpmschweitzerandClaude Opus 4.8 67de636ab4 Fix DSI blue flicker under load: lower DPI clock to 40 MHz
The blue flicker during voice activity is a MIPI-DSI framebuffer-read
underrun on the PSRAM bus, not tearing. The DSI DMA reads the 800x800
framebuffer continuously (~138 MB/s at the stock 80 MHz clock); under
load the Wi-Fi (esp-hosted SDIO) and audio-playback DMA on the same
PSRAM bus starve the DSI FIFO, which the hardware then paints blue
("lcd.dsi.dpi: can't fetch data from external memory fast enough").

Drop the DPI pixel clock to 40 MHz (~54 fps): halves the continuous
read and gives the FIFO slack to ride out bursty SDIO latency spikes.
Still smooth for the face UI, and lowers display power.

Vendored as a local component because managed_components/ is gitignored
(same pattern as the esp_hosted fork) — a local component overrides the
same-named waveshare/ managed dependency, so the fix survives a clean
build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKPbR6DY2JygHbyLjxm7Uu
2026-07-15 14:41:34 +02:00
jpmschweitzerandClaude Fable 5 6b3b9ce0ed Fix load-dependent DSI flicker: TRIPLE_PARTIAL -> TRIPLE_FULL tear-avoid
Test, Build and Push / test-gateway (push) Successful in 11s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
Root cause (investigation): the BSP default tear-avoid mode is
TRIPLE_PARTIAL. On ESP-IDF 5.5 the MIPI-DSI driver has no
on_frame_buf_complete callback (added in IDF 6.0 -> the boot warning
"buffer-switch...may not function on MIPI DSI"), so the adapter falls
back to on_refresh_done, which fires every refresh (~60Hz) as a fake
vsync. In PARTIAL mode that release path is NOT submit-gated, so when
one LVGL frame takes >1 refresh to render it over-releases the buffer
that is still being scanned out -> LVGL draws into the live front
buffer -> tearing/flicker.

Why it only started with the wake word, and only under load: at idle
(sparse rain) a frame renders in <16.6ms so exactly one submit per
refresh -> harmless. The always-on WakeNet added constant CPU/PSRAM
load that pushed the heavy-rain frames (listening=16, thinking=40
streams) past one refresh -> triggered the over-release. User
correctly identified it as a resource starve exposing the latent bug.

Fix: switch to TRIPLE_FULL, which IS submit-gated even without the
callback (one release per actual submit). Same 3 framebuffers, zero
memory cost, app-side one-liner via bsp_display_start_with_config so
the managed component is untouched. Trade-off: full-screen redraw per
frame; if the rain gets choppy under load, cheaper rain rendering
(canvas / half-rate tick) is the follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 11:31:25 +02:00
jpmschweitzerandClaude Fable 5 b462dff082 Phase 5: hands-free "Computer" wake word (esp-sr WakeNet + AFE VAD)
Test, Build and Push / test-gateway (push) Successful in 10s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
Say "Computer" -> chime + listening -> speak -> AFE VAD detects you
stopped -> auto-sends. No taps. Touch still works as a manual override.

- esp-sr 2.4.6 added; wn9_computer_tts model packed into a new "model"
  flash partition (MODEL_IN_FLASH). App moved to 8M, model 4M.
- audio.c: replaced the on-demand capture_task with an AFE pipeline —
  feed_task is the SOLE mic reader (-> afe->feed); detect_task fetches,
  watches wakeup_state for the wake word and vad_state for end of
  speech, and forwards AFE-cleaned audio upstream during an utterance.
  One mic reader ever.
- short rising chime acknowledges the wake audibly.

Fixes from adversarial review before trusting it:
1. utterance framing (blocking WS sends) moved OFF the AFE fetch thread
   onto an app_task event queue (EV_TOUCH/EV_WAKE/EV_SPEECH_END) — a
   1.5s send could stall fetch and drop the first ~1.5s of speech.
2. app_task is now the single serializer of start/end -> no TOCTOU
   double-start (was: two utterance_start on a tap during wake).
3. VAD accounting resets on every streaming (re)start (wake OR tap),
   not just wake -> a tapped utterance can no longer end instantly on
   stale silence.
4. chime/reply set s_playing (+DMA tail hold) and detect_task skips the
   mic while s_playing -> our own audio no longer streams into STT or
   false-triggers the wake at a playback boundary (no AEC yet).
5. NULL-checked AFE create + feed buffer; tasks only start if AFE is up.

Verified on hardware: model loads, AFE inits with the Computer word,
boots and connects clean, no crash/wedge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 10:52:41 +02:00
jpmschweitzerandClaude Fable 5 cb5826e02b Fork fix: the SDIO wedge is FIXED (esp-hosted-mcu #167)
Test, Build and Push / test-gateway (push) Successful in 12s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
Root cause (verified against our exact IDF tree, not the community guess):
the "258" in "sdio_write_task: Failed to send data: 258" is NOT a timeout
(that is 263). 258 = 0x102 = ESP_ERR_INVALID_ARG. On the ESP32-P4, block-
mode CMD53 writes require the SOURCE buffer to be 64-byte (cache-line)
aligned; the IDF sdmmc driver rejects a misaligned source with INVALID_ARG
BEFORE any bus activity. esp_hosts write loop then declares "Unrecoverable
host sdio state" and reboots the whole P4. The audio TX payload is not
64-aligned, so streaming mic audio wedged on the very FIRST frame (which is
exactly what we saw: listening -> instant Failed to send -> reboot).

This also explains why buffer/queue/clock/retry tuning all did nothing: the
write never reached the bus. And why our symptom was instant, not after
~100 writes (the community block-mode-desync theory) — it is the first
misaligned buffer, every time.

Fix: vendored esp_hosted 2.12.11 as an editable local component (overrides
the registry copy) and bounce a misaligned TX payload through one aligned
DMA scratch buffer in hosted_sdio_write_block (port_esp_hosted_host_sdio.c).
TX is serialized by the bus lock so a single static bounce buffer is safe;
freed in hosted_sdio_deinit. Host-only change — no C6 reflash.

VERIFIED ON HARDWARE (autonomous self-test): 40s of continuous mic-audio
upstream streaming — the traffic that previously wedged on the first frame
— ran clean, zero timeouts, zero reboots. A guarded SDIO_TX_SELFTEST harness
is kept (compiled out) for future SDIO stress testing.

Credit: root cause + patch designed via multi-agent investigation; the
precise 258=INVALID_ARG decode (correcting the upstream community timeout
assumption) came from checking our actual esp_err.h.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 09:50:27 +02:00
jpmschweitzerandClaude Fable 5 ac9d24e0f9 SDIO tuning for sustained audio-upload TX (the real wall)
Test, Build and Push / test-gateway (push) Successful in 11s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
Root cause of the "blue screen on tap": NOT a firmware bug. The touch
fix worked (reached "listening"), and the mic audio then streamed
upstream — which instantly triggered the upstream SDIO wedge (#167):

  sdio_write_task: Failed to send data: 258 (timeout) -> host restart

i.e. the same wedge we fought overnight, but now provoked reliably by
the sustained ~32KB/s mic upload instead of waiting minutes. The wedge
is a host->slave SDIO write timeout: the slave RX buffers fill because
its Wi-Fi TX cannot drain fast enough.

Attempt: restore the slave Wi-Fi TX path shrunk during the RAM hunt and
enlarge the SDIO queues so the burst is absorbed and drained:
- WIFI_RMT_DYNAMIC_TX_BUFFER_NUM 24->64, STATIC_RX 8->16, RX 24->32
- WIFI_RMT_TX_BA_WIN 16->32
- ESP_HOSTED_SDIO_TX_Q_SIZE / RX_Q_SIZE = 32
- SDIO clock 10->20 MHz for drain headroom (still 1-bit)
Boot verified: no RAM famine, connects clean. Whether it survives the
audio burst is the open question upstream has not solved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 09:18:03 +02:00
jpmschweitzerandClaude Fable 5 5c6a6eba36 Fix touch crash + make rain a reachability signal + calm the alarm churn
Test, Build and Push / test-gateway (push) Successful in 10s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
TOUCH CRASH (the blue screen): app_on_touch ran a blocking WebSocket
send (up to 5s) directly in the LVGL touch callback, stalling the
MIPI-DSI flush into a garbage/blue frame + task-watchdog reboot on
every tap. Now touch_cb only gives a semaphore; a dedicated app_task
does the blocking sends, audio, and face changes off the render
thread. WS send timeout cut 5s to 1.5s as belt-and-braces.

RAIN = REACHABILITY (user request): rain now falls only while the
gateway WebSocket is live (gated on gw_connected in rain_tick). It
drains gracefully on disconnect, resumes on reconnect, a genuine
glanceable reachable signal. Idle density bumped 2 to 4 so
connected-idle reads distinctly from disconnected-black.

CALM THE WEDGE CHURN (user request): transient wifi/WS drops no longer
slam to the x_x error face or a CONNECTING banner. Boot goes straight
to the calm idle face (dry until connected). Only a sustained 30s+
outage escalates to x_x (clock_cb); the ~15s wedge-recovery just shows
a brief rain pause.

Two fixes from adversarial concurrency review before flashing:
- persistent single capture task (was xTaskCreate per utterance; a
  rapid re-tap or WS-stop-vs-app-start race could put two readers on
  one mic/I2S handle and corrupt the codec)
- reset s_talking on disconnect (app_on_disconnect) so the first tap
  after a mid-utterance drop starts fresh, not the stop branch

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 09:04:57 +02:00
jpmschweitzerandClaude Fable 5 795a1d4baf Remove boot-time diagnostic burst (fast re-wedge trigger)
Test, Build and Push / test-gateway (push) Successful in 10s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
Night-watch caught a 21s wedge (vs 5-13min baseline) landing exactly
on the boot ping-diagnosis + wifi-stats-dump burst, which hammered the
freshly-recovered SDIO link on every reboot. That scaffolding proved
connectivity long ago; removing it makes each recovery light and clean.
ping_target/ping_diag_task kept as no-op symbols for manual use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 02:44:04 +02:00
jpmschweitzerandClaude Fable 5 395e05343d gateway: handle websocket.disconnect in receive loop
Test, Build and Push / test-gateway (push) Successful in 10s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
Stops the RuntimeError spam ('Cannot call receive once a disconnect
message has been received') every time the device's watchdog reboots.
Redeployed to the tatlock-ui stack.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 02:29:10 +02:00
jpmschweitzerandClaude Fable 5 f156b640d8 Stop the C6 reflash loop: accept factory 2.12.11 slave (gate >=2)
Test, Build and Push / test-gateway (push) Successful in 10s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
The BT-off custom slave (2.12.12) can't survive the C6's OTA rollback
protection: it flashes, the C6 boots it, reverts to 2.12.11, host
reflashes -> ~30s reboot loop, worse than the wedge. BLE-coex
hypothesis is UNTESTABLE via OTA (custom slaves won't stick; needs
direct C6 UART flash, out of scope tonight). Reverted to the
self-healing 2.12.11 baseline: ~5-13min MTBF, 15s auto-recovery.
That is the shippable overnight state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 02:27:58 +02:00
jpmschweitzerandClaude Fable 5 ca2144694a Test BLE-coex wedge hypothesis: C6 slave rebuilt with Bluetooth off
Test, Build and Push / test-gateway (push) Successful in 10s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
The wedge clock is immune to every network-layer change (~300s
regardless of bus width, clock, power-save, DHCP). A slave-side
periodic starver remains; the C6's BLE controller (BT_LE_SLEEP_ENABLE=y
in stock slave, shares the radio/RF arbiter with Wi-Fi) is the prime
suspect. DeskLock uses no Bluetooth, so the stock slave carries a
coexistence scheduler we can delete outright. Custom slave 2.12.12
(CONFIG_BT_ENABLED=n, ESP_HOSTED_CP_BT=n), OTA'd over SDIO. If the
metronome stops, BLE coexistence was starving the SDIO peripheral.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 02:24:31 +02:00
jpmschweitzerandClaude Fable 5 02d91461b8 Upstream reproduction pack for the SDIO wedge (esp-hosted-mcu#167)
Test, Build and Push / test-gateway (push) Successful in 10s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
Four controlled experiments characterize it: 72-byte frame times out
at CMD53 after the slave advertised credits; bus width/clock/power
save have no effect; removing dhcpc stretches MTBF ~5min -> ~13min.
Slave-side peripheral stall, upstream bug. Device self-heals in ~15s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 02:12:11 +02:00
jpmschweitzerandClaude Fable 5 fe82a7a22d Test DHCP-renewal wedge hypothesis: fully static IP on 2.12 pair
Test, Build and Push / test-gateway (push) Successful in 10s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
The wedge metronome (~300s: 297/297/322/437s across bus widths,
clocks, and power modes) matches FRITZ DHCP T1 renewal at half of a
10-min lease. Every wedging build ran an active dhcp client. Static
addressing removes the renewal TX path entirely; if the metronome
dies, root cause #2 is the DHCP renew packet through hosted's write
path. (2.9.7 experiment abandoned: slave image bootloops/rolls back
on this C6; version matrix documented for upstream.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 01:57:16 +02:00
jpmschweitzerandClaude Fable 5 e5c8a38f94 Stability round 2: WIFI_PS_NONE + 10MHz SDIO
Test, Build and Push / test-gateway (push) Successful in 10s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
1-bit bus alone did not stop the sdio_write timeout wedge (recurred
at ~5min uptime). Modem power save is the new prime suspect: the C6
napping mid-SDIO-transaction matches the minutes-scale periodicity.
Always-powered appliance -> PS_NONE costs ~50mW and removes the
entire failure class if correct. SDIO clock also halved to 10MHz.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 01:29:33 +02:00
jpmschweitzerandClaude Fable 5 7943e0e72a 1-bit SDIO to kill the wedge-reboot loop; gong on cold boot only
Test, Build and Push / test-gateway (push) Successful in 10s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
Night-watch caught the instability in the act at 7min uptime:
  E H_SDIO_DRV: sdio_write_task: Failed to send data: 258 (timeout)
  E H_SDIO_DRV: Unrecoverable host sdio state -> SW_CPU_RESET
i.e. esp-hosted-mcu#167. Known mitigation: 1-bit SDIO bus (#148),
still ~80x voice bandwidth. Also: recovery reboots no longer ring
the gong (esp_reset_reason gate) - a butler doesn't bong at 3am.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 01:19:52 +02:00
jpmschweitzerandClaude Fable 5 b8974ec84f Wi-Fi data path FIXED: esp_hosted 2.12 on both chips + C6 SDIO auto-OTA
Test, Build and Push / test-gateway (push) Successful in 10s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
Root cause of the dead data path (assoc/scan/RPC fine, zero data
frames): esp-hosted 1.4.x is formally incompatible with IDF 5.5
(esp-hosted-mcu#47) and the factory C6 slave firmware was ancient
(couldn't even answer a version RPC). Waveshare's examples pin 1.4.* —
do not follow them.

Fix:
- host: espressif/esp_hosted ^2.12 (+ esp_wifi_remote 1.6)
- slave: 2.12.11 network_adapter.bin embedded in the app (c6_ota.c
  streams it to the C6 over the SDIO RPC channel at boot when the
  reported version is < 2.x; ~10s, no wires, idempotent)
- RAM diet for hosted 2.x's footprint (MEMPOOL_PREFER_SPIRAM,
  reduced WIFI_RMT buffers) — without it internal SRAM famine
  boot-loops in xTaskCreateStaticPinnedToCore before app_main
- conservative 20MHz SDIO clock for first verified data path

Verified on hardware: DHCP lease (even that healed), 8/8 pings to
router and tower-of-joy at 1-5ms, WebSocket to the gateway connected,
device registered at /devices as desklock-p4 with fw version.

Also: wifi_diag.c L1 SoftAP diagnostic mode (DESKLOCK-DIAG) with
review fixes, boot ping ladder in net.c, static-IP fallback,
face_status() line, docs for the whole failure taxonomy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 00:26:28 +02:00
jpmschweitzerandClaude Fable 5 3a402220b0 Device registry (/devices + hello), static-IP fallback, WS keepalive, uncompressed fonts
Test, Build and Push / test-gateway (push) Successful in 11s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
- gateway: GET /devices registry (per-IP connect count, timestamps,
  device/fw from new 'hello' protocol event); graceful utterance
  errors -> error event; uvicorn ws-ping-timeout 120s
- firmware: hello on WS connect; static 192.168.86.53 fallback after
  15s without DHCP; scan-on-boot removed (kept for diagnostics);
  ping_interval 8s; fonts regenerated --no-compress (compressed
  glyphs render blank with LV_USE_FONT_COMPRESSED off); 8MB factory
  partition; montserrat_28; LVGL lock timeout semantics fixed
- docs: sauron endpoint plan, power ladder, versioning policy

Known issue: 'Outside' AP association succeeds but no L2 traffic
flows (no DHCP, no ARP) — worked once at 22:05 then never again;
ToJ side verified clean. Investigation ongoing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 22:34:57 +02:00
jpmschweitzerandClaude Fable 5 ed0e8221f1 Phase 2: full LVGL face + Wi-Fi + gateway WebSocket + touch-to-talk
Test, Build and Push / test-gateway (push) Successful in 10s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
The device now runs the complete face contract on glass: seven states
with glyph expressions (custom 140px face font), matrix rain as pooled
label streams (22px DejaVu+Noto katakana font), rage kaomoji frames
(64px), orbit arc + elapsed counter wait cues, blink/talk/thought
animations, idle clock (SNTP, Europe/Amsterdam), and the power ladder
(active/ambient 35%/dormant 5% with rain parked).

Voice path: ES7210 mic capture task streams 16k PCM over
esp_websocket_client to the gateway; reply PCM buffers to PSRAM and
plays via the audio task; touch-to-talk (tap to speak, tap to send).
Protocol mapping per docs: thinking->effort, audio->speaking,
error event->rage (auto-composes after 2 loops), WS loss->error face.

Bring-up fixes: 8MB factory partition (fonts overflowed 1.5M),
bsp_display_lock(0) is try-lock in this adapter (use UINT32_MAX),
gong synth yields to feed IDLE0, wifi scan diagnostics (found SSID
case mismatch), esp_websocket_client pinned ~1.3 (1.4 needs IDF>5.5).

Verified on hardware: boots, connects to Wi-Fi and holds an open
WebSocket to the gateway. Sauron (iMac TTY endpoint + ops console)
planned in architecture.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 21:42:51 +02:00
jpmschweitzerandClaude Fable 5 ae9f6e0abb Approved gong voicing: 220Hz fundamental, fatter mids, -7dBFS at vol 75
Test, Build and Push / test-gateway (push) Successful in 11s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
User verdict: audible and butler-non-intrusive on the bare 2W speaker.
Recorded as the house sound signature in architecture.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 20:44:30 +02:00
jpmschweitzerandClaude Fable 5 bfe792f006 Replace beeps with a cathedral gong
Test, Build and Push / test-gateway (push) Successful in 10s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
Additive gong synthesis: seven inharmonic partials with per-partial
decay (fundamental 165Hz rings 2.6s, highs die in 0.2s), a detuned
pair for shimmer, 45ms distant attack, and three feedback-comb
reflections (95/210/370ms, net gain .93) for a dense stone-space tail.
Normalized to -12dBFS at codec volume 58: quiet but voluminous.
Synthesized into PSRAM at boot (~4s), plays 5s, blocking is fine for
bring-up — audio moves to its own task in phase 2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 20:41:02 +02:00
jpmschweitzerandClaude Fable 5 60c73e135c Boot chime through ES8311: audio-out path verified on hardware
Test, Build and Push / test-gateway (push) Successful in 10s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
C5->E5 half-second chime with fade-out at boot, right after the face
renders. Codec opens in slave mode, esp_codec_dev write path works.
Phase 1 bring-up is now complete: display, touch, audio, 200MHz PSRAM.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 20:34:59 +02:00
jpmschweitzerandClaude Fable 5 450bac1c81 First successful flash: placeholder face live; fix 20MHz PSRAM trap; power ladder
Test, Build and Push / test-gateway (push) Successful in 10s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
- firmware boots to a rendered LVGL face in 1.57s (eyes + smile + tag)
- sdkconfig: CONFIG_IDF_EXPERIMENTAL_FEATURES=y unlocks SPIRAM 200MHz
  (silently degraded to 20MHz before -> MIPI-DSI underrun -> LVGL lock
  starvation -> task watchdog); mirrors official 08_lvgl_demo_v9 config
- architecture.md: power management section (user prime concern) —
  active/ambient/dormant/night ladder, levers, hard edges, <=300ms wake
- AGENTS/CLAUDE: replace stale bring-up warnings with verified commands

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 20:31:25 +02:00
jpmschweitzerandClaude Fable 5 2d5cbd1174 Document versioning policy: 0.x per roadmap phase, 1.0.0 = wall milestone
Test, Build and Push / test-gateway (push) Successful in 10s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 20:17:55 +02:00
jpmschweitzerandClaude Fable 5 550db5a0ef CI: use WATCHTOWER_HTTP_API_TOKEN secret name as set on the repo
Test, Build and Push / test-gateway (push) Successful in 11s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 20:04:29 +02:00
jpmschweitzerandClaude Fable 5 79d43ec199 Deployment home is the tatlock-ui Portainer stack, not a standalone file
Test, Build and Push / test-gateway (push) Successful in 10s
Test, Build and Push / release (push) Successful in 2s
Test, Build and Push / build-gateway (push) Failing after 8s
The gateway service now lives in system-admin-toj/containers/stacks/
tatlock-ui.yml (shared docker-dataplane network: speaches by service
name, host-run Tatlock by LAN IP). Drop deploy/desklock-gateway.yml.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v0.1.0
2026-07-14 19:08:50 +02:00
jpmschweitzerandClaude Fable 5 15b8a1b900 Strip Tatlock <think> reasoning before TTS and history
Test, Build and Push / test-gateway (push) Successful in 10s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
Live smoke test showed /v1/chat/completions replies open with the
Steward's <think> block (Open WebUI convention) — without stripping,
the device would speak the internal monologue aloud.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 18:15:36 +02:00
jpmschweitzerandClaude Fable 5 22f00fda0f Drop audioop: request 16 kHz directly via Speaches sample_rate extension
Test, Build and Push / test-gateway (push) Successful in 33s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
Settles the Python version question: floor >=3.11, no ceiling.
Container moves to python:3.13-slim, CI tests on 3.13. The embedded
Piper fallback resamples with numpy (already present via [speech]).
Verified with a live round trip at 16 kHz.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 18:13:58 +02:00
jpmschweitzerandClaude Fable 5 f5df5049db Add rage table-flip state; wire gateway to live Speaches; add CI pipeline
Test, Build and Push / test-gateway (push) Successful in 1m2s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped
- Face: new 'rage' state — 3-frame kaomoji loop (stare, flip the table,
  put it back) for in-flight request failures; 'error' stays the quiet
  persistent face for a dead link. Sim + artifact + design doc updated.
- Gateway: stt.py/tts.py are now pluggable backends. Default 'speaches'
  talks OpenAI-format HTTP to the live container on :8601
  (faster-whisper-small STT, Kokoro bm_george TTS with 24->16 kHz
  audioop resample); 'embedded' fallback kept behind the [speech] extra.
  Verified with a live TTS->STT round trip (warm: STT 0.27s, TTS 1.9s).
  Docker image is now slim (no CUDA/ML deps). Python pinned to 3.12
  (system 3.8 too old, audioop gone in 3.13).
- CI: .gitea/workflows/build.yml — lint+test on main pushes; on v* tags
  test, build gateway image, push to registry, release, and trigger
  Watchtower (tatlock pattern; needs REGISTRY_USER/REGISTRY_PASSWORD/
  WATCHTOWER_TOKEN secrets). Runtime stack in deploy/desklock-gateway.yml.
- architecture.md: measured speech latencies, deployed-Speaches status,
  CI & deployment section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 17:54:30 +02:00
jpmschweitzerandClaude Fable 5 42b988b7ef Add face design: ASCII expressions + matrix rain simulator, wait cues, fresh latency numbers
- sim/face/index.html: browser simulator of the 800x800 round panel with
  six states (idle/listening/pensive/effort/speaking/error), state-driven
  rain density, blink/talk/thought animations, idle clock
- effort state gets hard-required wait cues: orbiting bezel arc, elapsed
  counter, max rain (Tatlock turns run 10-25s)
- rain driven by setInterval, not rAF: renders under the screenshot
  tool's --virtual-time-budget and mirrors LVGL lv_timer
- architecture.md: Face design contract (state table, protocol mapping,
  LVGL port notes) + latency table updated to GPU-era benchmarks
  (Steward ~6s warm, full flow 11-25s; old CPU-era figures retired)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 17:32:16 +02:00
jpmschweitzerandClaude Fable 5 0886c84d5e Document Speaches speech layer, on-device processing limits, latency constraints
- STT/TTS move to a shared Speaches container (OpenAI-format, GPU, port
  8601 proposed); gateway becomes a thin orchestrator with pluggable
  speech backends (speaches default, embedded fallback)
- Record the on-device ceiling: WakeNet wake word, VAD, ES7210 AEC,
  optional MultiNet fixed commands; open-vocabulary STT permanently out
- Record the real latency bottleneck (Tatlock ~2 min full local flow):
  gateway must stream chat tokens and synthesize sentence-by-sentence
- Plan reply_delta + barge-in protocol additions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 17:14:44 +02:00
jpmschweitzerandClaude Fable 5 576fd7d237 Initial scaffold: ESP32-P4 firmware + voice gateway for Tatlock endpoint
DeskLock gives the Tatlock butler a face and voice on a Waveshare
ESP32-P4-WIFI6-Touch-LCD-3.4C round display in the living room.

- firmware/: ESP-IDF project targeting esp32p4 with the Waveshare XC BSP
- gateway/: FastAPI voice bridge (faster-whisper STT, Tatlock chat, Piper TTS)
- docs/architecture.md: component design and device<->gateway WS protocol

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 16:58:59 +02:00