Files
tactility/.claude/skills/tactility-firmware/references/display-idle.md
T
Adolfo Reyna 12035f2f39 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
2026-07-17 09:51:08 -04:00

6.7 KiB

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 stopScreensaverRequestedstopScreensaver()
    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 < 100msstopScreensaver() 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