- Migrates tactility-app-development from ~/.hermes/skills/ into .claude/skills/ for repo co-location - Adds AGENTS.md with local workstation context, hardware table, board-direct build/env wrapper (PYTHONPATH fix), ELF missing-symbol triage, app patterns (immersive, QVGA rail, tutorial 2-row, GameKit bubbling, screenshot rate-limit), and skill index Refs: personal Gitea mirror
28 KiB
name, description, version, author, license, metadata
| name | description | version | author | license | metadata | |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| tactility-app-development | Use when building, compiling, deploying, or debugging Tactility OS external ELF apps (ESP32-S3/P4). Covers SDK env, missing-symbol loader errors, asset packaging, and immersive LVGL patterns. | 1.0.0 | Hermes Agent | MIT |
|
Tactility OS App Development
Overview
Tactility loads third-party apps as dynamic ELF binaries at runtime. The host firmware exports a symbol table (g_esp_espidf_elfsyms / g_customer_elfsyms via ESP_ELFSYM_EXPORT / DEFINE_MODULE_SYMBOL). If your ELF references anything not exported, it fails at load time with E ELF: Can't find symbol X. The build system also requires ESP-IDF environment variables and a specific Python interpreter to avoid venv hijacking.
This skill consolidates the build-env fix, symbol hygiene, and asset/offline-data patterns discovered across sessions.
When to Use
- Building
Apps/MyAppviatactility.py ... build esp32s3 --local-sdk - Fixing
ELF: Can't find symbolon device serial - Packaging large offline assets (bibles, fonts, images) for constrained RAM
- Implementing immersive single-screen LVGL UI with tap-to-toggle chrome
- Handling settings/progress persistence on SD card
Don't use for:
- Simulator (
FirmwareSim) builds (statically linked C++tt::app::Appsubclass, different workflow) - Board support (devicetree, display drivers) — see
tactility-board-supportproject skill
Build Workflow (Local SDK)
Always source IDF first, export SDK path, and use IDF's Python binary directly:
. /Users/adolforeyna/esp/esp-idf/export.sh
export TACTILITY_SDK_PATH=/Users/adolforeyna/Projects/Tactility/firmware/release/TactilitySDK
# Use IDF python to avoid Hermes/host venv pollution (urllib3 | type error on 3.9):
/Users/adolforeyna/.espressif/python_env/idf5.3_py3.9_env/bin/python tactility.py Apps/MyApp clean
/Users/adolforeyna/.espressif/python_env/idf5.3_py3.9_env/bin/python tactility.py Apps/MyApp build esp32s3 --local-sdk --verbose
Manifest requirements (Apps/MyApp/manifest.properties):
[manifest]
version=0.1
[target]
sdk=0.8.0-dev
platforms=esp32s3,esp32p4
[app]
id=one.tactility.myapp
versionName=1.0.0
versionCode=1
name=My App
CMake component (main/CMakeLists.txt):
file(GLOB_RECURSE SOURCE_FILES Source/*.c)
idf_component_register(SRCS ${SOURCE_FILES} REQUIRES TactilitySDK)
# Relax noisy werrors that other apps also ignore
target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-format-truncation -Wno-unused-but-set-variable)
Verification: build/*.app (tar) contains assets/ + ELF; build/cmake-build-esp32s3/*.app.elf should exist.
Resolving Missing-Symbol Loader Errors
Tactility's ELF loader resolves via firmware symbol table. Pre-flight check before flashing:
# After IDF export.sh, run:
# xtensa-esp32s3-elf-nm -D Apps/MyApp/build/cmake-build-esp32s3/MyApp.app.elf | grep " U "
# Compare against firmware export macros ESP_ELFSYM_EXPORT / DEFINE_MODULE_SYMBOL via grep -R in firmware/
Known Missing Symbols & Workarounds
lv_font_montserrat_14,lv_font_montserrat_18→ Not exported. Use exported abstraction:#include "tactility/lvgl_fonts.h" lv_obj_set_style_text_font(label, lvgl_get_text_font(FONT_SIZE_SMALL), 0); // FONT_SIZE_SMALL/DEFAULT/LARGE mapped to board-specific sizeslv_obj_set_style_bg_grad_color/dir/stop→ Not exported on 0.8.0-dev (causedApplication failed to start: missing symbolblue OK dialog). Use solid0x0E0E14instead.LV_OPA_95,LV_OPA_5, etc. → Not defined. Only 0/10/20/30/40/50/60/70/80/90/100/COVER/TRANSP exist in vendored LVGL 8.x. UseCOVERor90.lv_arc_create,lv_arc_set_range/value/bg_angles,lv_arc_get_value→ Not exported on 0.7.0-dev, added in PR #496dff93cb6 Add lv_arc.h symbolsafter 0.7 release. Device modalError / Application failed to start: missing symbolwith heap still healthy (~31KB free) indicates loader failure, not OOM. Fix: arc-free horizontal slide usinglv_slider_create+ swipe vialv_indev_get_point/lv_indev_active(exported since 0.6). Seereferences/book-browser.md.lv_rolleralso not exported on 0.7/0.8.lv_anim_init,lv_anim_set_var/values/duration/exec_cb/path_cb/start,lv_anim_path_ease_out/linear→ Exported since 0.6 (lines 435-446). Safe for 0.7/0.8 fleet. Use for verse fade 220/300ms editorial transition. Verified no leak, heap stable 54KB on .129.lv_binfont_create/destroy→ Exported on 0.8.0-dev (lv_binfont_create) but VFS path handling fragile —tt_app_get_assets_pathreturns void, checkbuf[0]!=0. Prefer embedded C LVGL fonts (v5 July 12):lv_font_conv --format lvgl --lv-include lvgl.h --bpp 4 -r 32-126 -r 0x2018-0x201D→Source/fonts/*.c~40-85KB each,LV_FONT_DECLARE,(lv_font_t*)&georgia_italic_26, lives in PSRAM viaCONFIG_ELF_LOADER_LOAD_PSRAM=y. Heap stable 47539 free. Seereferences/pretty-font.md. Legacy binfont.fntalso bundled inassets/fonts/52KB but not used by default.LV_SYMBOL_STAR→ Symbols may be absent depending on LVGL symbol set. UseLV_SYMBOL_OK,LV_SYMBOL_DUMMY, or plain UTF-8.lv_obj_set_style_max_width→ Not exported on 0.7.0-dev and 0.8.0-dev (July 16 2026 —check_symbols.pyfound 1 missing,E ELF: Can't find symbol). TriggersApplication failed to start: missing symbolblue OK dialog (/api/screenshotreturns PNG 320x240). Fix: useLV_PCT(92)/LV_PCT(80)fixed percents for side gutters, no max_width. Verifiedxtensa-esp32s3-elf-nm -D89 undef → 0 missing after removal.lv_obj_set_scrollbar_mode→ Version-dependent, not reliably exported on 0.7/0.8. Avoid — use default scroll (addLV_OBJ_FLAG_SCROLLABLE) and let LVGL handle it. On QVGA intro, make scrollArea scrollable and use flex column withLV_FLEX_FLOW_COLUMN+pad_row 8.xTaskGetCurrentTaskHandle→ Not exported. Replace wait loops withtt_lvgl_unlock(); vTaskDelay(10); tt_lvgl_lock(portMAX_DELAY);lv_image_set_rotation→ Uselv_obj_set_style_transform_rotation(obj, 1800, 0)(value 1800 = 180°)cJSON,esp_websocket_client,lwip_htonsmacros → Vendor sources directly or use rawlwip_socket/lwip_connectetc., which are exported.- C++ lambdas in
.cfiles →auto make_btn = [](...){}fails compile. Keep app source as pure C (noauto, no lambdas) or rename to.cppwithextern \"C\"shims. - How to bring new widgets (arc, roller, chart, etc) → Edit
firmware/Modules/lvgl-module/source/symbols.c, addDEFINE_MODULE_SYMBOL(lv_arc_create), etc., rebuild device firmware viapython device.py es3c28p && idf.py build flash -p /dev/cu.usbmodem101, then rebuild app SDK. Device .129 on 0.8.0-dev already has arc, but .112 0.7.0-dev does not. See session July 12 teaching how to expose more LV elements.
See references/symbols.md for full list.
Asset & Offline Data Pattern
Large data (e.g., 4.8MB Zefania XML bible) must be split to keep RAM tiny. Pattern proven for BibleVerse:
- Monolith → per-book split:
assets/bible/<bnum>.bin= concatenated NUL-terminated UTF-8 verses,<bnum>.idx= headerBIBK+ version u16 + count u32 + entries{offset u32, cnum u16, vnum u16},books.jsoncatalog. - Biggest chunk Psalms ~215KB vs 4.1MB monolith → load only one book at a time via
tt_app_get_assets_path. - Hybrid fallback: try
A:assets path, else scan/sdcard/bibles/WEB/same binary format for user translations. - Always
tar tvf build/*.app | wc -lto verify 60+ bins packaged.
See references/offline-data.md for converter recipe and templates/convert_bible.py.
Persistence (Settings-like File)
Follow BookPlayer/EpubReader pattern:
char ud_path[320]; // from tt_app_get_user_data_path(app, ud_path, sizeof)
char prog_path[320];
snprintf(prog_path, sizeof prog_path, "%s/progress.txt", ud_path); // via tt_app_get_user_data_child_path(app, "progress.txt", ...)
Store bnum cnum vnum globalIdx; write atomically on verse change. Same for favorites.txt.
Immersive UI Pattern
- Single verse fullscreen centered, container
LV_OBJ_FLAG_SCROLLABLEremoved to avoid drag recalc. - Header + bottom toolbar hidden by default (
LV_OBJ_FLAG_HIDDEN), tap toggles. - Adaptive scaling:
apply_verse_scaling()measuresstrlen(verse)→ selectsFONT_SIZE_LARGE/DEFAULT/SMALL+ line_space 4-14 + pad tightened v4 2-10 + letter_spacing. Ultra-short (<40c) gets letter_spacing 1 and breathing room. Very long (350c+ Esther 8:9) gets compact small. - Editorial v4/v5 (July 12): Warmer verse
#F2F0E8flat no bubble, ref uppercaseEPHESIANS 1:4tracking 2#9A9AA8, gap 10 (was 14) for closeness like PSALMS 23:1 reference image, fade transition 220ms verse / 300ms ref vialv_anim_init/set_var/values/duration/exec_cb/path_ease_out/start(exported since 0.6, stable heap 54KB). v5 pretty font: Georgia Italic embedded C 26/22/20/18/16pt + regular 24 vialv_font_conv --format lvgl --lv-include lvgl.h→LV_FONT_DECLARE+ resolve helpersresolve_verse_font/book_fontfallback tolvgl_get_text_font(), lives in PSRAM 315KB.rodata, heap 47539 stable, vision confirms "serif italic editorial like PSALMS 23:1" (Habakkuk 1:17). Seereferences/pretty-font.md. - Center column
90%x80%centered (not 88x84) to avoid bottom ref crop on 240px screens. - Auto-advance:
lv_timer_create(timer_cb, 60000, ctx)+lv_timer_reseton manual nav. - Rate-limit label updates (store last values) to avoid audio stutter if audio plays.
- Reference:
references/immersive-ui.mdTiers tuned from real device screenshots: short verses get giant breathing room, long verses shrink + tighten.static void apply_verse_scaling(AppCtx* ctx, const char* verse) { size_t len = strlen(verse); enum LvglFontSize f; int line_sp, pad_v, pad_h, letter_sp=0; if (len < 40) { f=FONT_SIZE_LARGE; line_sp=14; pad_v=32; pad_h=36; letter_sp=1; } // ultra-short else if (len < 100) { f=FONT_SIZE_LARGE; line_sp=11; pad_v=24; pad_h=28; } // short else if (len < 200) { f=FONT_SIZE_DEFAULT; line_sp=9; pad_v=18; pad_h=22; } // medium else if (len < 350) { f=FONT_SIZE_SMALL; line_sp=6; pad_v=12; pad_h=18; } // long else { f=FONT_SIZE_SMALL; line_sp=4; pad_v=8; pad_h=14; } // epic 350+ lv_obj_set_style_text_font(ctx->lbl_verse, lvgl_get_text_font(f), 0); lv_obj_set_style_text_line_space(ctx->lbl_verse, line_sp, 0); lv_obj_set_style_text_letter_space(ctx->lbl_verse, letter_sp, 0); lv_obj_set_style_pad_top(ctx->lbl_verse, pad_v, 0); lv_obj_set_style_pad_bottom(ctx->lbl_verse, pad_v, 0); lv_obj_set_style_pad_left(ctx->lbl_verse, pad_h, 0); lv_obj_set_style_pad_right(ctx->lbl_verse, pad_h, 0); } // Call after lv_label_set_text(verse) - Reference:
references/immersive-ui.md
QVGA intro layout: left info + right small icon rail (PocketDungeon v5-v7 July 16-17)
For 320x240 / 320x198 usable (after statusbar) intro screens cramp when vertically stacked (title+cards+wide text buttons = clipped GOAL text, Loot gold, survive monsters truncated). Pattern that fixes it:
- Structure: header (title+subtitle+best compact) +
mainRowflex row (ROW,flex_grow 1, pad 4/8 column, bottom 8) = leftleftColflex_grow 1 scrollable column (pad_row 8, pad_bottom 6) + rightrightRailfixed 44px narrow column (size CONTENT height, pad_row 10, no scroll). - Left cards iteration (v6-v7): v6 had GOAL + HOW TO PLAY (2 cards, CONTROLS merged into HOW TO PLAY line
Arrows/WASD/Swipe/Quadrant) + right icons 38/30. User then asked: keep only GOAL + CONTROLS on intro, teach symbols via actual playable tutorial levels instead of text explanation. So v7 final:makeCard(leftCol, "GOAL", "Reach deepest floor. Collect gold, survive."), makeCard(leftCol, "CONTROLS", "Arrows / WASD / Swipe\nTap quadrant = move\nQ / ESC = exit")= 2 cards, airy, no HOW TO PLAY. Each cardPCT 100%width,SIZE_CONTENTheight, pad 9, row 3, bgCOLOR_PANELradius 10, labels WRAP width 100%. - Right icons: small icons on right as user requested — replace text
ENTER DUNGEONbutton with icons: play 38px accent bgCOLOR_ACCENTradiuss/3, close/exit 30px muted bg0x252836, bothlv_btn_create+ centered labelLV_SYMBOL_PLAY/LV_SYMBOL_CLOSE. No text hint below (was clipped). Saves vertical space, gives 40-50px more for cards. - Verification via
/api/screenshot:curl -s http://<ip>/api/screenshot -o /tmp/intro.pngthen vision check for clipping/padding. Screenshot shows 6.2-6.6KB PNG, header Best F3 G18, left 75% cards, right 25% icons. If bottom clipped, reduce cards to 2, shrink rail to 40px, icons 32/26. - Symbol hygiene: no
lv_obj_set_style_max_width(unexported, causesmissing symbolblue OK dialog), nolv_obj_set_scrollbar_mode— usePCT 92/80/70for gutters and default scroll viaLV_OBJ_FLAG_SCROLLABLE.
See references/intro-fullscreen.md v5-v7 and references/sdk-mismatch-missing-symbol.md for SDK 0.8.0-dev bump (board .112 OS 0.8.0-dev vs manifest 0.7.0-dev) and lv_obj_set_style_max_width removal (89 undef → 0 missing).
Tutorial level pattern: 2-row teach-by-play (PocketDungeon v7 July 17)
User wanted: instead of text explanation in intro, have 3 intro playable levels — first presents player and stairs, next bat and gold, both with 2 rows regular cols, then go current first level. Implementation:
DungeonModel.h:TUTORIAL_ROWS=2,renderRowsfield inDungeonState,floor 0,1 = tutorial,displayFloor()maps internal floor to user-visible (tutorial → 1),isTutorial(),tutorialId(),setBests,reset(seed, tutorials=true),generateTutorialFloor(tId),clearAllTiles().DungeonModel.cpp:resetsets floor 0 if tutorials else 2 (first real = 2 internally → display 1),clearAllTilesborder walls,generateTutorialFloorsetsrenderRows=TUTORIAL_ROWS, hides rows >=3 via walls, player {1,1}, Tut0: Stairs at {COLS-2,1}, Tut1: Treasure {3,1} + Stairs + Bat {5,1} hp1, no random walls,moveMonstersskipped whenisTutorial(), best update only floor>=2.DungeonRenderer.cpp: respectsstate.renderRows, hides cells y>=renderRows viaHIDDENflag, re-centers board for tutorialset_height grid.cellPx*TUTORIAL_ROWS align CENTER 0,-10, status showsTUTORIAL X/2 HP Gold, messageTUT 1: Move to > @=you/TUT 2: Kill b, take $, use >vs normalArrows/Swipe/Tap.PocketDungeon.cppintro final per user July 17: only GOAL + CONTROLS cards (2), symbols taught via play, not text.- Verification: hermes-verify-tutorial*.py checks 2 cards, no HOW TO PLAY, tutorial gen Bat+Treasure+Stairs, renderRows,
TUTORIAL_ROWS, isTutorial, build package ok, nm 0 missing. - Result: flow Intro (Goal+Controls left, icons right) → Tut1 2×9 @→> → Tut2 2×9 @+b+$→> → Real game F1 infinite (display floor). Installed to 192.168.68.112, screenshots intro_tutorial.png 6.3K.
See references/pocket-dungeon.md tutorial update and references/intro-fullscreen.md v7.
Deployment & Running
Old docs say POST :6666/app/install. Firmware 0.7.0-dev+ dashboard moved to port 80:
PUT /api/apps/installmultipartfilefield (not POST, not :6666)POST /api/apps/run?id=<appId>,GET /api/apps
tactility.py tool v3.5.1 still uses :6666 and needs Developer mode enabled on device. If port 6666 refuses, use dashboard API directly or enable dev server in Settings. Always pass esp32s3 platform arg to install unless you built all platforms.
Full recipe + discovery in references/deploy.md.
Common Pitfalls
- Hermes venv hijacks IDF Python — two flavors
- Flavor A (tactility.py):
requestsimport fails withTypeError: unsupported operand type(s) for |: 'type' and 'type'on Python 3.9 becausePYTHONPATHincludes Hermes venv'surllib3using Python 3.10+ union syntax. - Flavor B (idf.py build/flash):
Cannot import module "pydantic_core._pydantic_core"/ModuleNotFoundError— Hermes 3.11 venv'spydantic_core/*.soshadows IDF's 3.9/3.10 venv. Evenidf.py --versionfails. - Fix both: clear
PYTHONPATH=""and explicitly setIDF_PYTHON_ENV_PATH=/Users/adolforeyna/.espressif/python_env/idf5.3_py3.9_envbefore sourcing export.sh. Also:unset PYTHONPATH; unset PYTHONHOME export IDF_PYTHON_ENV_PATH=/Users/adolforeyna/.espressif/python_env/idf5.3_py3.9_env . /Users/adolforeyna/esp/esp-idf/export.sh - Stale Python version: If project was configured with
idf5.3_py3.10_envbut you now use 3.9 env →idf.py fullcleanrequired or you get "project was configured with .../idf5.3_py3.10_env/bin/python". Do fullclean then re-generatepython device.py <id>. - Verification:
$IDF_PYTHON_ENV_PATH/bin/python -c "import pydantic_core; print('ok')" env -u PYTHONPATH -u PYTHONHOME idf.py --version # ESP-IDF v5.3.2-dirty
- Flavor A (tactility.py):
- Werror=format-truncation —
snprintfwith 320B buffers flagged. Add-Wno-format-truncationviatarget_compile_options(safe, buffers intentionally oversized). - Direct font symbols — Any
&lv_font_montserrat_*will compile but fail at runtime. Always uselvgl_get_text_font(). - Asset size bloat — Bundling 4MB monolithic bin triggers OOM at runtime. Split per-book and stream.
4b. Book browser OOM — 66 buttons = 275B heap free (July 11 BibleVerse on 192.168.68.112 0.7.0-dev). Building 66×
lv_button_create+ labels + nested flex rows = 132-264 LVGL objs → heaptotal 310KB free 275B min_free 23 largest_block 23→failed to create temp directoryon install, screenshot 500. Fix: v4 clean horizontal slide ~10 objs (no top bar/Go/card, center LARGE 50% bigger directly on bg tappable, bottom row [X left][slider flex][39/66 right]), swipe viaLV_EVENT_PRESSED/PRESSINGtrackingbook_drag_start_x28px threshold, pluslv_sliderfast scrub. After fix heap10943B free largest 7168on .112,54471→54355stable on .129 0.8.0-dev (0.8 has more heap: 64KB pre). ELF itself lives in PSRAM (CONFIG_ELF_LOADER_LOAD_PSRAM=y, 5MB free), but LVGL objects are internal heap only — PSRAM doesn't help browser. Editorial v4 also adds fade transitionlv_anim_*220/300ms no leak. - Touch input swallowed by full-screen containers — GameKit bubbling pitfall (PocketDungeon July 16 v3) — Symptom: after adding
introContainer+gameContainerfull-screen coveringroot,GameKit::attachInput(root, scene)alone stopped working. Tap/swipe dead. Root cause: LVGL event bubbling — topmostLV_OBJ_FLAG_CLICKABLEcontainer capturesCLICKED/GESTUREbeforeroot. AlsorulesBoxinside introContainer swallowed taps needed forpointToQuadrant. Fix patternattachInputToCurrent():- Re-attach input to root + current active containers with
CLICKABLE|GESTURE_BUBBLE - Make inner info boxes non-clickable/non-scrollable so taps bubble to parent
- Start button calls
lv_event_stop_bubbling(e)to prevent double trigger - Handle
Touch/SwipeInputKind at game-over restart - After fix + rebuild
esp32s3, touch quadrants + swipe + keys work on 192.168.68.112. Seereferences/intro-fullscreen.mdv3 section.
- Re-attach input to root + current active containers with
- Fullscreen toolbar-less apps trap user — no exit (PocketDungeon July 16 v4) — Removing
tt_lvgl_toolbar_create_for_appgains 40-50px but loses system back button → no way to exit. Fix: three exit paths:- (1) Hardware:
InputKind::Cancel/Menu(Q/ESC → Cancel viakeyToInputmaps q/Q → Cancel,LV_KEY_ESC→ Cancel) →tt_app_stop(), include viaTactilityCpp/App.h(pullstt_app.h), save bests + SFX Cancel - (2) X button top-right in game phase (
LV_SYMBOL_CLOSE36×28 bg #2A2D40 radius 6 →onExitButtonClicked) - (3) Long-press board
LV_EVENT_LONG_PRESSEDongameContainer→ pause overlayAppPhase::Pausedwith Resume/Exit Game (overlay bg #000 OPA_70, box 180px, Resume blue accent #365CF5, Exit grey #3A3A4A, hint Q/ESC = exit / Long press = pause); any input in Paused resumes. Intro also has Exit button. Header addspauseOverlay+Paused+ 3 static callbacks. Verification: hermes-verify-exit*.py checkstt_app_stop,pauseOverlay,LONG_PRESSED,Cancel, build ok.
- (1) Hardware:
- Black bar crop + Georgia sizing + 15px lift correction (BibleVerse v6 July 12) — Verse cropped by header bar
size 100%x44 bg #15151F COVER + border 1 #232338overlapping center col. User: "black bar on top cropping text" + "reduce text size for this font" + "main text can be moved a bit up 30px" then corrected "should have been 15px". Fix: header36px(was 44),bg 0x0E0E14 TRANSP 0(was 15151F COVER), border 0, pad ver 4, centerPCT(90)x82% align CENTER 0,8initially →-22(30px up) → corrected to-7(15px up: base +8 -15). Ctrl bar also TRANSP, verse pad 4. Georgia Italic wider than Montserrat → downtier:LARGE→22(was 26),DEFAULT→18(was 20),SMALL→16, ultra<40cstill 26 viaresolve_verse_font_ultra. Pads tighter6/4/3/2/1(was 10/8/6/4/2). Vision: Hab 2:9 "No extra black bar overlapping, fully visible" + Zeph 1:3 "balanced now, optical center correct" PASS, heap 46215-47591 stable. Screenshot rate-limit 2s+ to avoidConnectionResetError 54heap spike. Gotcha:lv_timer_tincomplete in SDK —t->user_datainvalid, must uselv_timer_get_user_data(t)(exported). Seereferences/black-bar-crop.md+references/pretty-font.md. - Book browser animation + auto-show 10s (BibleVerse v6 July 12) — User: "add animation to the bible book selection. Also let's start the app with the menu showing, but make the menu UI auto hide after 10s." Impl:
AppCtx { lv_timer_t* chrome_auto_hide_timer; bool ui_visible; },chrome_auto_hide_cb(lv_timer_t* t){ AppCtx* ctx = lv_timer_get_user_data(t); if(book_browser_visible) return; set_chrome_visible(false); delete timer; },schedule_chrome_auto_hide(ctx,10000)creates timer,onShowAppset_chrome_visible(true)+ schedule 10s,toggle_uire-arms 10s when shown else deletes,toggle_book_browserdeletes auto-hide timer,onHideAppcleanup. Book browser anim:book_y_anim_cb(void* var,int32 v){ lv_obj_set_y((lv_obj_t*)var,v); }+book_fade_anim_cb→text_opa,set_book_browser_visible(true)→remove FLAG_HIDDEN,mid_wrap y 20→0 260ms ease_out+ name fadeOPA_20→COVER 220ms,book_dial_update_centeralso fades name60→255 180mson swipe/slider change. Verified .129: 8683 bytes at 0.8s after run menu visible → 6882 hidden at ~2s, auto-hide 10s configured. Seereferences/immersive-ui.md+references/book-browser.md. - Assuming
lv_font_montserrat_18exists — Not even compiled for all boards. Stick to SMALL/DEFAULT/LARGE. - Build cache stale after main.c edit —
tactility.py cleanbefore rebuild when CMake glob changes. - Install without platform arg —
tactility.py Apps/X install <ip>tries allmanifest platforms; if only esp32s3 ELF built you getELF file not built for esp32p4. Passesp32s3explicitly. - 6666 refused — Dev server disabled. Use dashboard
PUT /api/apps/installon port 80, or enable Developer mode on device Settings first. - Wrong board flashed (color vs RLCD) — Family has two boards: ES3C28P (2.8" color,
es3c28p) and waveshare-esp32-s3-rlcd (4.2" mono). Symptoms: monochrome UI = flashed RLCD firmware onto color board. Alwaysgrep CONFIG_TT_DEVICE_ID sdkconfigandls /dev/cu.usbmodem*before flash. Family color board is ES3C28P at/dev/cu.usbmodem101, 16MB flash@80MHz,partitions-16mb-with-sd.csv. - Personal remote app not yet pulled locally — User says pull it from repo (2026-07-16). PocketDungeon existed only on personal remote (git.reynafamily.com/adolforeyna/tactility_apps) commit
d976595feat: add Pocket Dungeon game prototype, not in local Apps/ after fetch of origin. Fixcd apps && git fetch personal && git merge personal/main. - Board ending in 112 shorthand — User shorthand ending on 112 means WiFi IP in local subnet. Discovery: 192.168.68.0/24 scan, 192.168.68.112 ping OK (ESP32-S3, osVersion 0.8.0-dev, MAC 14:c1:9f:d1:56:90), 192.168.68.111 also online = 2 boards active. No USB present is fine for network install via port 6666 or dashboard 80. Use arp + socket probe. Must pass esp32s3 platform arg else fails with ELF file not built for esp32.
- GameKit / SfxEngine shared libraries (2026-07-16) — New pattern for game apps.
main/CMakeLists.txtglobs../../Libraries/GameKit/Source/*.c*+SfxEngine+ INCLUDE_DIRS + REQUIRESTactilitySDK esp_driver_i2s esp_driver_gpio. GameKit provides Loop, Scene, Input (arrows/WASD+gestures+quadrant), Grid, Draw, Prefs. App entry still registerApp. 40KB .app tar contains elf/esp32s3.elf. Extended v2-v4 with AppPhase Intro/Playing/Paused, introContainer+gameContainer+pauseOverlay,attachInputToCurrent()bubbling fix, exit handling via Cancel/Menu + X + long-press. Seereferences/pocket-dungeon.md+references/intro-fullscreen.mdv3/v4.
Verification Checklist
TACTILITY_SDK_PATHset, IDFexport.shsourced, using IDF python binary fortactility.pybuild/<App>.appexists,tar tvfshows expected assetsxtensa-esp32s3-elf-nm -D *.app.elfundefined checked; 0 missing vs firmware exports (0 undef total v4 editorial = healthy)- No direct
lv_font_montserrat_*orLV_SYMBOL_STARrefs in source (greplv_font_montserrat) main/CMakeLists.txthas-Wno-format-truncationallowance- Settings path via
tt_app_get_user_data_child_path, tested load/save - If immersive UI: tap toggle, timer 60s, hidden chrome by default verified on device/sim, fade 220/300ms no leak heap stable >10KB
- If book browser: ~10 objs, [X left][slider flex][N/66 right], no top bar/Go/card, LARGE name 50% bigger tappable, swipe 28px, heap >10KB (275B = OOM, 31KB+error dialog = missing symbol)
References
references/build-env.md— full env error transcript + fixreferences/symbols.md— exported vs missing symbol list, OOM vs missing-symbol diagnosis, heap sizes275B vs 31KB vs 54KB stable, anim/binfont exportsreferences/offline-data.md— per-book binary format and converter usagereferences/deploy.md— dashboard vs dev-port install, PUT /api/apps/install, platform arg handling, discovery via browser tools, .112 0.7.0-dev vs .129 0.8.0-dev fleetreferences/pocket-dungeon.md— 2026-07-16 PocketDungeon GameKit+SfxEngine via GLOB_RECURSE, personal remote pull (git.reynafamily.com), board shorthand 112 discovery, PYTHONPATH fix, 40KB .app build + network install esp32s3references/intro-fullscreen.md— Intro state machine AppPhase + fullscreen no-toolbar, rules/controls screen, HideStatusBar flag not parsed for ELF appsreferences/sdk-mismatch-missing-symbol.md— SDK/OS version mismatch .112 0.8.0-dev vs 0.7 manifest + unexportedlv_obj_set_style_max_width/scrollbar_modecausingmissing symboldialog, QVGA cramped fix,/api/screenshotremote QAreferences/immersive-ui.md— UI pattern v4 editorial: warm #F2F0E8, uppercase tracking 2 ref #9A9AA8 gap 10, fade transition anim, binfont futurereferences/book-browser.md— book selector evolution: 66-button OOM → arc rotary (0.7 missing) → horizontal slide v3 → clean v4 no top/Go/card LARGE name + [X][slider][N/66], PSRAM vs heapreferences/black-bar-crop.md— v6 black bar 44px #15151F COVER overlapping verse crop fix, Georgia down-tier LARGE→22 DEFAULT→18 memo, screenshot rate-limitreferences/audio-apps.md— RLCD Mp3Player task stack/prio/YIELD + VoiceRecorder stereo→mono downmix + ES7210 vs ES8311 MCLK/DMA fixes (2026-07-12)scripts/verify_symbols.py— rerunnable undefined-symbol checker