chore: move firmware skills in-repo + AGENTS.md

- Migrates tactility-firmware and tactility-bluetooth from
  ~/.hermes/skills/ into .claude/skills/ for repo co-location
- Adds AGENTS.md with local workstation context, board table,
  board-direct build rule, Hermes PYTHONPATH pitfall, common
  Tactility pitfalls, and in-repo skill index

Refs: personal Gitea mirror
This commit is contained in:
Adolfo Reyna
2026-07-17 09:51:08 -04:00
parent e7fb5fb38f
commit 12035f2f39
22 changed files with 2362 additions and 0 deletions
@@ -0,0 +1,105 @@
# DisplayIdleService & Screensaver Disable-When-Charging + MCP Persistence
## Files
- `Tactility/Source/service/displayidle/DisplayIdle.cpp`
- `Tactility/Private/Tactility/service/displayidle/DisplayIdleService.h`
- `Tactility/Source/service/displayidle/Screensaver.h` (base) + BouncingBalls, MatrixRain, Mystify, StackChan, McpScreensaver
Only compiled `#ifdef ESP_PLATFORM` — not in simulator.
tick()` flow (final — includes 50d86de4 wake fix for RLCD + 6f095081 no-auto-screensaver for monochrome):
- `onStart()`:
- `srand(time(nullptr))`
- `cachedDisplaySettings = loadOrGetDefault()` — always, even on RLCD/no-backlight (pre-2026-07-11 early-return killed timer + settings, broke MCP)
- If `!supportsBacklightDuty()` → log "Monochrome/RLCD detected: idle timer will run but auto-backlight off is disabled" but continue
- Timer 50ms `TICK_INTERVAL_MS`, Lower priority, start
- `tick()` flow (final):
1. **MCP active check OUTSIDE LVGL lock (3629ffef)**: `bool isMcpActive = (drawArea != nullptr || overrideActive)` under `state.mutex` BEFORE `lvgl::lock()` — avoids AB/BA deadlock
2. `lvgl::lock(100)` else return
3. `lv_display_get_default()==nullptr` → unlock return
4. `settingsReloadRequested.exchange(false)` → reload under lock
5. `inactive_ms = lv_display_get_inactive_time(nullptr)` — always read, for all displays (50d86de4: not gated by supportsBacklight)
6. If `displayDimmed && overlay && !stopRequested && !isMcpActive`: auto-off counter or `updateScreensaver()`
7. `lvgl::unlock()`
8. If `stopScreensaverRequested` → `stopScreensaver()`
9. Get display: `bool supportsBacklight = display != nullptr && display->supportsBacklightDuty()`
10. **Timeout logic runs for ALL displays (50d86de4)**:
- If `!timeoutEnabled` or `timeoutMs==0`: if `displayDimmed && !isMcpActive` → if `supportsBacklight` restore duty, `displayDimmed=false` (wake for RLCD too)
- Else:
- `charging_blocks = disableScreensaverWhenCharging && isDeviceCharging()`
- Not dimmed && inactive>=timeout && !charging_blocks → activate (creates screensaver even on RLCD)
- Dimmed && !isMcpActive && inactive<100ms → stop (wake on touch/button, for RLCD too)
- Dimmed && !isMcpActive && charging_blocks → stop (wake when plugged)
- Only `setBacklightDuty()` calls guarded by `supportsBacklight && display != nullptr`
- `stopScreensaverLocked()` requires LVGL lock.
## RLCD Wake Lock Bug (50d86de4) + Permanent Freeze (6f095081) — MUST READ
**Symptom 50d86de4:** "screen seem to be locked..." — RLCD frozen after dim, buttons do nothing, looks crash but serial no Guru Meditation.
**Root Cause 50d86de4:**
Previous MCP fix changed tick() to `if (supportsBacklight) { if (!timeoutEnabled) restore else { activate/wake } }`. RLCD `supportsBacklightDuty()==false` → entire state machine skipped → once `displayDimmed=true`, wake branch `inactive<100ms` never runs → screensaver stuck forever → appears locked.
**Fix 50d86de4:**
Run timeout logic for all displays, guard only duty calls. See final flow above. Build RLCD `0x2b78b0 32% free`.
**Symptom 6f095081:** "screen is still showing the freezed image" after 3770ac89 + 50d86de4 flashed.
**Root Cause 6f095081:**
Even with wake logic fixed, `BouncingBalls` screensaver `full_refresh=true` 300x400 120k pixels SPI 10MHz ~100ms/frame hogs LVGL lock. If SD has `display.properties` with `backlightTimeoutEnabled=1` 60s + BouncingBalls (carried from ES3C28P SD), RLCD auto-enters heavy animation → looks frozen stuck on weird image. RTS reset doesn't clear ST7305 RAM, frozen image persists until power cycle.
**Fix 6f095081:**
- For monochrome/RLCD `!supportsBacklight`: don't auto-enter screensaver, only handle wake. Switch `else {` → `else if (supportsBacklight) {` + `else { // monochrome don't auto-enter }`
- Video stream idle 50→100ms, streaming 5→30ms
- Advise user: unplug USB 3s power cycle to clear frozen RAM, set Timeout=Never or None on RLCD
Verification: outer `if (supportsBacklight)` gates full handling, `else` monochrome only wake, `setBacklightDuty` guarded, `!isMcpActive>=3`.
## MCP Flash-Then-Back Bug (6235fa05)
**Symptom:** "when the text tool is called it is shown on the screen for a small time (less than a second) and back to the previous screen."
**Cause:** After `startMcpScreensaver()` sets `displayDimmed=true`, next `tick()` sees `inactive_ms < 100ms` → `stopScreensaver()` deletes overlay.
**Fix 6235fa05:** Guard all auto-stop with `!isMcpActive` (count >=3). MCP remains closable via `stopScreensaverCb` CLICKABLE overlay.
## Lock Inversion Deadlock (3629ffef)
**Symptom suspected as crash:** RLCD freeze after text tool, serial clean boot to AP `Tactility-5C7D`.
- Video task: `state.mutex -> lvgl::lock()` in `ensureOverrideScreen()`
- Old tick(): `lvgl::lock() -> state.mutex`
- Fix: read `isMcpActive` BEFORE LVGL lock. Rule: MCP mutex first, never LVGL->MCP.
- Verify: `mcp::getState()` pos < `lvgl::lock()` pos. Build `waveshare-esp32-s3-rlcd 0x2b78b0`.
## Screenshot Lock Contention (2026-07-14 observed, fixed 2026-07-14 6f095081)
**Symptom:** "Screenshot failed: could not acquire LVGL lock" from `/api/screenshot` (WebServerService 1522 logs `could not acquire LVGL lock within 100ms`) and `ScreenshotTask` 50ms. Also "screen is still showing the freezed image" when video stream hogs.
**Root Causes:**
- `WebServerService::handleApiScreenshot` used `lvgl::lock(100)` — RLCD ST7305 SPI 10MHz full_refresh 120k pixels ~100ms/copy, 100ms too short
- `ScreenshotTask::makeScreenshot` used 50ms
- `mcp_video_stream_task` loop 5ms streaming hogs LVGL lock → UI appears frozen
**Fixes 6f095081:**
- Web handler 100→500ms + error msg update
- ScreenshotTask 50→300ms
- Video stream idle 50→100ms, streaming 5→30ms + comment "RLCD ST7305 SPI is slow and shared"
- DisplayIdle: for monochrome/RLCD don't auto-enter screensaver (bouncing balls heavy)
- Verification 14 checks PASS hermes-verify-jw_55oxm.py, build 0x2b79e0
- See `references/rlcd-initialization.md` for full init + contention audit
## Charging Guard
Callback overload as in Launcher/Statusbar avoids clangd false-positive. Must combine with `!isMcpActive`.
## Pitfalls
- Early return on `!supportsBacklightDuty()` breaks MCP entirely — always load settings + start timer
- Outer `if (supportsBacklight)` gating timeout/wake breaks RLCD — run for all, guard only duty
- `lv_display_get_inactive_time() < 100ms` kills MCP if guard missing — guard all auto-stop with `!isMcpActive`
- Forgetting `settingsReloadRequested` check races overlay creation
- `findDevices` callback signature `bool(const shared_ptr<T>&)` — clangd may false-report; callback form silences it
- `backlightOff` after auto-off stays off until wake — charging_blocks can wake but only if `!isMcpActive`