12035f2f39
- 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
7.0 KiB
7.0 KiB
MCP System — Screensaver + WebServer + Video Streaming
File Map
Private/Tactility/mcp/McpSystem.h— globalMcpSystemStatesingletonSource/mcp/McpSystem.cpp— drawing (clear/text/RGB565/BMP/PBM), screenshot PBM base64, audio, sensors/battery/ble/SD file ops, video stream task, app listingSource/service/webserver/McpHandler.cpp— TOOLS_JSON, handle_rpc, handleApiMcp, handleApiScreenRawSource/service/webserver/WebServerService.cpp— wildcard/api/*dispatches to MCP before auth, screenshot handler/api/screenshot(500ms timeout after fix)Source/service/screenshot/ScreenshotTask.cpp—makeScreenshotuses lvgl::lock(300ms after fix)Source/service/displayidle/McpScreensaver.{h,cpp}— canvas full-screen overlay, framebuffer SPIRAM, registers in McpSystemStateInclude/Tactility/settings/McpSettings.h+Source/settings/McpSettings.cpp— persisted atgetUserDataPath()+"/settings/mcp.properties"=/sdcard/tactility/...on SD boards,/data/tactility/...internal. Old hardcoded/data/service/mcp/settings.propertiesbug fixed 2026-07-13.
State Flow
- WebServer
onStart()loadswebServerSettings+mcpSettingsunderg_settingsMutex,serverEnabled = webEnabled || mcpEnabled setEnabled(true)→startServer()→HttpServer(80, handlers, 12288)→httpd_start- If
mcpEnabledafter HTTP up →startVideoStreamServer()→xTaskCreatePinnedToCore(mcp_video_stream, 8192, prio5, core1)allocates mono 15000 + color 153600 buffers SPIRAM - LLM draw →
drawRgb565/clear/drawText→ensureOverrideScreen()→displayidle::findService()->startMcpScreensaver()→ LVGL lock 200ms → create overlay canvas → registerdrawArea+framebuffer - Video stream client connects 8081/8083 → raw recv loop →
ensureOverrideScreen()+put_pixel+lv_obj_invalidate
Screensaver Integration (RLCD + MCP Persistence + Deadlock + Wake 50d86de4)
- Previously
DisplayIdle::onStart()early returned on!supportsBacklightDuty()(ST7305) — killed timer + settings → MCP manual activation broken - Now timer always starts, settings always loaded,
supportsBacklightDuty()check only gates auto-backlight logic and duty calls in tick() andstartMcpScreensaver() - Wake fix
50d86de4: tick() timeout/wake logic must run for ALL displays, not gated bysupportsBacklight. OnlysetBacklightDuty()guarded. Prevents RLCD locked state after dim. - Critical guard
6235fa05— MCP flash-then-back <1s:tick()every 50ms afterstartMcpScreensaver()setsdisplayDimmed=true, ifinactive_ms < 100ms→stopScreensaver()deletes overlay. Fix:bool isMcpActive = (drawArea != nullptr || overrideActive)under mutex, guard all auto-stopif (displayDimmed && !isMcpActive). Count!isMcpActive>=3. - Deadlock fix
3629ffef: MCP video task locksstate.mutex -> lvgl, tick must NOT dolvgl -> mutex. ReadisMcpActiveOUTSIDE LVGL lock. - See
display-idle.mdfor full tick flow final.
RLCD 2-Button UX
User: "if i press the select button first it gets lock in there, so i need to press the change button first".
- Change GPIO18 KEY = LV_KEY_NEXT cycles focus
- Select GPIO0 BOOT = LV_KEY_ENTER activates
- No focus yet → Select does nothing → appears locked. Mitigation: auto-focus first btn on onShow.
Serial Log Debugging — RLCD Crash Triage Recipe
import serial
port="/dev/cu.usbmodem101"
ser=serial.Serial(port, 115200, timeout=1)
ser.dtr=False; ser.rts=False
buf=""
start=time.time()
while time.time()-start < 15:
data=ser.read(2048)
if data:
txt=data.decode('utf-8', errors='replace')
buf+=txt
print(txt, end='')
ser.close()
open("/tmp/serial.log","w").write(buf)
Look for:
- ESP-ROM + rst:0x15 = normal hard reset after flash
- Tactility v0.8.0-dev on WaveShare ESP32-S3-RLCD-4.2 = correct target
- Monochrome/RLCD detected = RLCD timer fix applied
- Mono TCP Video stream server started 8081 + Color 8083 = video OK
- Guru Meditation / Backtrace / Stack canary / abort() = crash, decode via
xtensa-esp32s3-elf-addr2line -pfiaC -e Tactility.elf <addr> i2c: CONFLICT! driver_ng= legacy i2c vs i2c_master both registered in platform-esp32, benign after migration toesp32-i2c-masterDTS but still warns; ES8311 now initializes after fix- File lock function not set = benign
- No crash 15s + AP started + launcher → freeze likely deadlock not crash, check lock order (
3629ffef) WaveshareRLCD: ES8311 codec initialized= I2C master fix3770ac89worksHal: ST7305 startedtime: should be ~2160ms with 150ms reset delay, vs 1828ms before (no delay) — freeze on fast boot fixed
Screenshot Lock Contention — "could not acquire LVGL lock" (fixed 2026-07-14 6f095081)
API: GET /api/screenshot in WebServerService.cpp:1522 — httpd_resp_send_err(..., "could not acquire LVGL lock") — this is what MCP reports as "Screenshot failed: could not acquire LVGL lock"
Root causes:
- Handler used
lvgl::lock(100)— RLCD ST7305 SPI 10MHz full_refresh 120k pixels ~100ms/copy, 100ms too short ScreenshotTask::makeScreenshotused 50msmcp_video_stream_taskloop 5ms streaming hogs LVGL lock → UI frozen appearance "screen is still showing the freezed image"
Fixes 6f095081 (build 0x2b79e0):
WebServerService.cpp100→500ms:lvgl::lock(pdMS_TO_TICKS(500))+ error log "within 500ms"ScreenshotTask.cpp50→300msMcpSystem.cppvideo stream 50ms idle→100ms, 5ms streaming→30ms with comment "RLCD ST7305 SPI is slow and shared"- DisplayIdle: for monochrome don't auto-enter screensaver (BouncingBalls heavy)
- ST7305 RAM retains frozen image until power cycle — RTS reset not enough, need USB unplug 3s
- Verification 14 checks PASS hermes-verify-jw_55oxm.py
- See also
references/rlcd-initialization.md+references/display-idle.md6f095081
Video Stream Protocol
- Mono: TCP 8081, 15000-byte frames, each byte 8 monochrome pixels RLCD packing, unpacked black/white with x_off/y_off centering
- Color: TCP 8083, packet: 16-byte header
magic=RAW\x01 (4) + x BE u16 + y BE u16 + w BE u16 + h BE u16 + payload_len BE u32, payload BE RGB565 - Stats:
framesDrawn,tcpBytesReceived,lastDrawMs,lastFps,overrideActive
Adding New MCP Tool
- Add function in
McpSystem.h/cpp - Add JSON schema entry in
TOOLS_JSONinMcpHandler.cpp - Add
if (strcmp(name,"my_tool")==0)branch - PSRAM malloc handling — use
heap_caps_malloc(SPIRAM)+ fallbackmalloc, free withheap_caps_free
Known Gotchas
psram_mallocfallback:heap_caps_malloc(..., SPIRAM)→ if nullmalloc, free must beheap_caps_freebase64_decodeusesmallocnot PSRAM — limit checked viaMCP_JSON_BODY_LIMIT 256KBandMCP_RAW_BODY_LIMIT 512KBensureOverrideScreen()blockingvTaskDelay(50)*10=500mspoll waiting for canvas — must not be called from LVGL task or deadlock- Audio
audioBusyspin flag prevents concurrent play/record - Screenshot API
/api/screenshotrequiresTT_FEATURE_SCREENSHOT_ENABLED— disabled saves build size but breaks MCP screenshot tool? Check sdkconfig