- 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
6.7 KiB
DisplayIdleService & Screensaver Disable-When-Charging + MCP Persistence
Files
Tactility/Source/service/displayidle/DisplayIdle.cppTactility/Private/Tactility/service/displayidle/DisplayIdleService.hTactility/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):- MCP active check OUTSIDE LVGL lock (
3629ffef):bool isMcpActive = (drawArea != nullptr || overrideActive)understate.mutexBEFORElvgl::lock()— avoids AB/BA deadlock lvgl::lock(100)else returnlv_display_get_default()==nullptr→ unlock returnsettingsReloadRequested.exchange(false)→ reload under lockinactive_ms = lv_display_get_inactive_time(nullptr)— always read, for all displays (50d86de4: not gated by supportsBacklight)- If
displayDimmed && overlay && !stopRequested && !isMcpActive: auto-off counter orupdateScreensaver() lvgl::unlock()- If
stopScreensaverRequested→stopScreensaver() - Get display:
bool supportsBacklight = display != nullptr && display->supportsBacklightDuty() - Timeout logic runs for ALL displays (
50d86de4):- If
!timeoutEnabledortimeoutMs==0: ifdisplayDimmed && !isMcpActive→ ifsupportsBacklightrestore 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)
- If
- Only
setBacklightDuty()calls guarded bysupportsBacklight && display != nullptr
- MCP active check OUTSIDE LVGL lock (
-
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. Switchelse {→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()inensureOverrideScreen() - Old tick():
lvgl::lock() -> state.mutex - Fix: read
isMcpActiveBEFORE LVGL lock. Rule: MCP mutex first, never LVGL->MCP. - Verify:
mcp::getState()pos <lvgl::lock()pos. Buildwaveshare-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::handleApiScreenshotusedlvgl::lock(100)— RLCD ST7305 SPI 10MHz full_refresh 120k pixels ~100ms/copy, 100ms too shortScreenshotTask::makeScreenshotused 50msmcp_video_stream_taskloop 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.mdfor 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() < 100mskills MCP if guard missing — guard all auto-stop with!isMcpActive- Forgetting
settingsReloadRequestedcheck races overlay creation findDevicescallback signaturebool(const shared_ptr<T>&)— clangd may false-report; callback form silences itbacklightOffafter auto-off stays off until wake — charging_blocks can wake but only if!isMcpActive