desklock is a three-component repo and only the gateway had a Makefile, so
make test meant "the gateway suite" or "no such target" depending on which
directory you happened to be standing in. One root Makefile makes it mean the
same thing everywhere (D-27), and gateway/Makefile is removed rather than
delegated to, so there is one place to look.
The firmware targets now source ~/esp-idf/export.sh themselves. Verified that
idf.py does not resolve on PATH without it and does after — the same class of
failure that has cost time on four other tools on this host, and the reason
D-10 puts path resolution in the Makefile rather than in callers. They fail
loudly with a hint when the toolchain is absent instead of reporting command
not found.
make test never reports green for the firmware. It has no suite, so it prints
undetermined rather than skipping silently — a no-op target that exits 0 would
claim a pass for something never run (D-26).
Verified: make test runs the real 9-test gateway suite, make lint passes, the
missing-toolchain guard fires, and make help lists every target. The firmware
build itself was not run.
Co-Authored-By: Claude <noreply@anthropic.com>
Network migration: the gateway's default Tatlock URL is now the docker
container name (http://tatlock:8000); the retiring tatlock.schweitz.internal
domain is gone from config and docs. Deployments that set
DESKLOCK_TATLOCK_BASE_URL are unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The homelab is retiring the *.schweitz.internal domain; in-network
machine-to-machine traffic uses docker container names on the
docker-dataplane network. The deployed tatlock-ui stack already overrides
DESKLOCK_TATLOCK_BASE_URL (Tatlock runs on the host), so only the
fallback default changes.
Docs follow: AGENTS.md M2M guidance now points at container names with
*.schweitz.net reserved for browsers, architecture.md drops the retired
domain (the registry name now matches what CI actually pushes since
c477019), and the README diagram loses the stale hostname.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes: volume voice command no longer hangs in the thinking spinner (gong
stops holding the busy state), and the butler filler is reworded to avoid
a text-to-speech mid-phrase pause.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKPbR6DY2JygHbyLjxm7Uu
Kokoro inserts an unnatural ~0.2s pause before "for you", so "Let me check
on that for you, sir." came out as two phrases. Reword to "Let me check on
that, sir." — same intent, clean pacing (measured: no internal silence gap).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKPbR6DY2JygHbyLjxm7Uu
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
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
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
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>
- 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>
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>
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>
- 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>
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>