# AGENTS.md — Tactility Firmware This file is for AI agents (and humans) working in this repo. It documents local workflow, owned hardware, and in-repo skills. ## Repo Tactility OS firmware — C++23, ESP-IDF 5.3, LVGL, FreeRTOS, NimBLE. Supports 40+ boards via `Devices//`. Upstream: `https://github.com/TactilityProject/Tactility` Personal Gitea mirror: `https://git.reynafamily.com/adolforeyna/tactility` (`personal` remote) Branch: `feature/waveshare-esp32-s3-rlcd` (active dev), `main` tracks upstream. ## Local Workstation - Host: M2 Air (14,2) macOS 26.5.2 - Project: `/Users/adolforeyna/Projects/Tactility/firmware` - ESP-IDF: `/Users/adolforeyna/esp/esp-idf`, env `idf5.3_py3.9_env` at `~/.espressif/python_env/idf5.3_py3.9_env` - Full clean via `idf.py fullclean` — `rm -rf build` is blocked by Hermes env guard. ### Owned Boards | Board | ID | Port / IP | Notes | |-------|-----|-----------|-------| | ES3C28P 2.8" color 240x320 IPS | `es3c28p` | `/dev/cu.usbmodem101` (USB) | 16MB flash, OCT PSRAM 120MHz, SD, BAT_ADC GPIO9 ADC1_CH8 200K/200K divider, TP4054 charger, CHRG LED only (IsCharging via heuristic) | | Waveshare RLCD 4.2" ST7305 mono 400x300 | `waveshare-esp32-s3-rlcd` | `/dev/cu.usbmodem1101` (USB) | Reflective, `DefaultDark` theme, mono thresh 60 default (user tested), no Bayer, fontSize 18+invert, 2-button Nav (KEY=GPIO18 next, BOOT=GPIO0 select) | | ES3C28P x2 WiFi | `es3c28p` | `192.168.68.112` + `192.168.68.111` | OS 0.8.0-dev, SDK 0.8.0-dev | > When user says "color board" they mean ES3C28P. When they say "ending in 112" they mean WiFi board `.112`. Don't flash RLCD firmware onto color board — check `grep CONFIG_TT_DEVICE_ID sdkconfig`. ### Build — Board-Direct Rule > **User rule (2026-07-12): "Don't compile for simulator is a waste of time, build for the board directly."** > For any driver/display/power/BT change, build for real hardware, not POSIX simulator. Simulator hides HAL bugs (ST7305 invert, GPIO conflicts, BT stack). #### Correct env wrapper (Hermes PYTHONPATH pollution fix) Hermes desktop runs Python 3.11 with `pydantic_core` .so that leaks into IDF 3.9 venv and breaks `idf.py` and `tactility.py` (`TypeError: |` or `ModuleNotFoundError: pydantic_core`). ```bash cd firmware unset PYTHONPATH; unset PYTHONHOME export IDF_PYTHON_ENV_PATH=/Users/adolforeyna/.espressif/python_env/idf5.3_py3.9_env source /Users/adolforeyna/esp/esp-idf/export.sh python device.py # es3c28p or waveshare-esp32-s3-rlcd idf.py fullclean # required when switching envs or font size idf.py -p /dev/cu.usbmodem101 flash # color idf.py -p /dev/cu.usbmodem1101 flash # RLCD idf.py -p /dev/cu.usbmodem101 flash monitor ``` Simulator (only when explicitly requested, no IDF): ```bash env -u ESP_IDF_VERSION -u IDF_PATH -u PYTHONPATH cmake -B /tmp/buildsim -G Ninja . ninja -C /tmp/buildsim ``` ### Common Tactility Pitfalls in This Repo - **Board config cleanliness**: User prefers clean `Devices/es3c28p/Source/Configuration.cpp` — no custom battery logic outside board config. Battery uses native statusbar icon. - **ES3C28P battery**: GPIO9 BAT_ADC via `adc_oneshot` + curve-fitting, 2x divider. Official spec in `~/Downloads/2.8inch_IPS_.../5-原理图_Schematic/` + Example_13. `supported: ChargeLevel, BatteryVoltage, IsCharging` (heuristic: <500mV or >4350mV → IsCharging=true). - **RLCD ST7305 mono**: `components/espressif__esp_lvgl_port/` not `managed_components/` else hash mismatch. Mono conversion `luma=(77R+150G+29B)>>8` threshold (no Bayer dithering). Default theme `DefaultDark` — `Mono` hides focus UI (breaks 2-button nav). - **Settings persistence**: Never hardcode `/data/...` — use `getUserDataPath()` which is `/sdcard/tactility` when `storage.userDataLocation=SD` (ES3C28P). Pattern: `getSettingsFilePath() = getUserDataPath()+"/settings/.properties"`. - **DisplayIdle/MCP coexistence**: `DisplayIdleService::onStart()` must NOT early-return on `!supportsBacklightDuty()` — RLCD needs timer for MCP. MCP manual activation via `startMcpScreensaver()`. WebServer + DevService `ctrl_port` collision + `CONFIG_HTTPD_MAX_URI_HANDLERS=20` required. - **Two-button nav**: KEY first (next/Change), BOOT second (Enter/Select). Blank focus on fresh screen = expected LVGL group behavior, not crash. - **Serial debug**: Hermes `terminal(background=true)` has no TTY — use `pyserial` script to capture 15s logs for Guru Meditation triage. ## In-Repo Skills Skills live under `.claude/skills//` — auto-discovered by Claude Code / Hermes. Each has `SKILL.md` + `references/*.md`. | Skill | When to use | |-------|-------------| | `tactility-firmware` | Power/battery `IsCharging`, display settings persistence, screensaver `DisplayIdle` lifecycle, statusbar icons, WebServer/MCP overlay, device `PowerDevice` drivers (AXP192/AXP2101/M5PM1/EstimatedPower), RLCD init race, LVGL theme/font/threshold, build-env wrapper | | `tactility-bluetooth` | BLE HID Host (central) keyboards/mice/combo touchpads (Fosmon B00BX0YKX4), Report ID stripping, mouse report parsing (std vs Logitech 12-bit), NimBLE scan cache nameless/RPA, PUBLIC vs RANDOM addr_type, auto-connect retry loop, `hid_host_active` guard | ### Key references inside skills - `tactility-firmware/references/power-system.md` — IsCharging matrix + EstimatedPower + ES3C28P GPIO9 spec - `tactility-firmware/references/display-idle.md` — tick flow final + charging guard + deadlock 3629ffef - `tactility-firmware/references/build-env-and-flash.md` — Hermes PYTHONPATH fix, wrapper, ADC REQUIRES, idf5.3_py3.10_env pitfall - `tactility-firmware/references/mcp-system.md` — MCP screensaver + video stream 8081/8083 + /api/mcp 404 triage - `tactility-firmware/references/rlcd-initialization.md` — I2C conflict + ES8311 + ST7305 reset + wake logic - `tactility-firmware/references/es3c28p-battery-debug.md` — why battery N/A, PowerDevice add, no-exceptions pitfall - `tactility-bluetooth/references/mouse-report-parsing.md` — dual-heuristic code + test vectors - `tactility-bluetooth/references/scan-and-reconnect.md` — inclusive cache + addr_type matrix + timer pattern Load a skill in a session: `skill_view(name='tactility-firmware')` etc. Reference docs via `skill_view(name=..., file_path='references/power-system.md')`.