The gate had no `select`, so it linted with whatever the installed ruff version defaults to. `dev` pins only `ruff>=0.6` and CI installs that extra fresh on every run, which made the rule set a function of when pip last resolved rather than of this code. Two developers on one commit could get different answers, and so could CI and a laptop. This surfaced when T-47's converged setup reinstalled ruff and pulled 0.16.3: `make lint` failed on UP017 and BLE001 in main.py, a file the commit before it had not touched. Under the previous install the same code passed. Nothing about the code changed — only the linter's idea of what to look at, which had grown to 413 rules with nobody choosing them. Naming the families fixes that; pinning the version would only have frozen the symptom and moved the surprise to whoever unpinned it. 217 rules now, selected on purpose, and a future ruff release becomes a decision instead of a broken push. ASYNC is included deliberately — this is a websocket gateway, and it is the family whose findings would be real bugs rather than style. BLE is deliberately excluded: main.py catches bare Exception when a device disappears mid-send, which is correct there, and selecting BLE would mean a noqa on every such site to say so. UP017 is fixed rather than suppressed (datetime.timezone.utc -> UTC, identical semantics, and requires-python is already >=3.11); isort then reordered the import, which is the whole of the main.py diff. Verified the selection is load-bearing rather than decorative: a probe file with a mutable default argument fails the explicit set (B006, exit 1) and passes ruff's minimal default set (exit 0), so the rules named here are doing work the fallback would not. Probe deleted; lint, typecheck and the 9-test suite all green after. Co-Authored-By: Claude <noreply@anthropic.com>
DeskLock
A living-room visual/audio endpoint for Tatlock, the homelab butler. DeskLock gives Tatlock a face and a voice on a Waveshare round touch display: you talk to it, it listens, thinks, and answers — the first line of contact with the butler backend running on tower-of-joy.
Hardware
Waveshare ESP32-P4-WIFI6-Touch-LCD-3.4C
| Component | Details |
|---|---|
| SoC | ESP32-P4NRW32 — dual-core RISC-V @ 400 MHz + LP core |
| Memory | 32 MB PSRAM (in-package), 32 MB NOR flash |
| Display | 3.4" round IPS, 800×800, MIPI-DSI 2-lane, capacitive touch |
| Radio | ESP32-C6-MINI-1 (Wi-Fi 6 + BLE 5) over SDIO via ESP-Hosted |
| Audio in | Dual onboard microphones + ES7210 echo-cancellation ADC |
| Audio out | ES8311 codec, PH2.0 2-pin speaker connector (8Ω 2W recommended) |
| Flashing | USB-C (hold BOOT during reset for download mode) |
The device is currently connected over USB-C directly to tower-of-joy, so build/flash happens on this server.
Architecture
┌──────────────────────┐ WebSocket: PCM audio + JSON events
│ DeskLock device │◄───────────────────────────────────┐
│ (ESP32-P4) │ │
│ • LVGL face │ ┌──────────────────────────────┴───────────┐
│ • touch / wake word │ │ DeskLock Gateway (container, :8600) │
│ • mic capture + AEC │ │ thin orchestrator — no ML dependencies │
│ • TTS playback │ └───────┬──────────────────┬───────────────┘
└──────────────────────┘ │ │ OpenAI-format HTTP
│ ▼
HTTP (LAN) │ ┌─────────────────────────────┐
▼ │ Speaches (container, GPU) │
┌────────────────────┐ │ • STT: faster-whisper │
│ Tatlock (butler) │ │ • TTS: Kokoro / Piper │
│ http://tatlock │ │ also usable by Open WebUI, │
│ :8000 │ │ Home Assistant, … │
└────────────────────┘ └─────────────────────────────┘
Tatlock stays a text-only brain. The gateway orchestrates speech-to-text, chat, and text-to-speech; the Speaches container owns the actual STT/TTS models on the GPU, shared homelab-wide. The device firmware stays thin: audio transport, wake word, and face rendering only. Everything runs on the LAN — no cloud in the voice path.
See docs/architecture.md for the full design.
Repository layout
firmware/— ESP-IDF (C, LVGL 9) application for the ESP32-P4gateway/— Python FastAPI voice gateway, deployed as a container on tower-of-joysim/face/— browser simulator of the face (design source of truth; serve withpython3 -m http.serverand openindex.html, or use?state=…&nochrome=1for screenshots)docs/— architecture and design notes
Roadmap
Bring-up✅ — display, touch, audio, 200 MHz PSRAM, cathedral gong- Face + voice loop (in hardware test) — full LVGL face (7 states, rain, orbit, power ladder), Wi-Fi, WebSocket, touch-to-talk
- Voice (touch-to-talk) — tap to talk → gateway → Tatlock → spoken reply
- Wake word — esp-sr WakeNet on-device, echo cancellation, barge-in
- Polish — Tatlock-initiated notifications, presence, OTA updates