Files
Adolfo Reyna e68909d64d
Main / Build (BookPlayer) (push) Has been cancelled
Main / Build (Brainfuck) (push) Has been cancelled
Main / Build (Breakout) (push) Has been cancelled
Main / Build (Calculator) (push) Has been cancelled
Main / Build (Diceware) (push) Has been cancelled
Main / Build (EpubReader) (push) Has been cancelled
Main / Build (GPIO) (push) Has been cancelled
Main / Build (GraphicsDemo) (push) Has been cancelled
Main / Build (HelloWorld) (push) Has been cancelled
Main / Build (M5UnitTest) (push) Has been cancelled
Main / Build (Magic8Ball) (push) Has been cancelled
Main / Build (MediaKeys) (push) Has been cancelled
Main / Build (MystifyDemo) (push) Has been cancelled
Main / Build (SerialConsole) (push) Has been cancelled
Main / Build (Snake) (push) Has been cancelled
Main / Build (TamaTac) (push) Has been cancelled
Main / Build (TodoList) (push) Has been cancelled
Main / Build (TwoEleven) (push) Has been cancelled
Main / Bundle (push) Has been cancelled
chore: move app skill in-repo + AGENTS.md
- 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
2026-07-17 09:51:20 -04:00

28 KiB
Raw Permalink Blame History

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
hermes
tags related_skills
tactility
esp32
elf-loader
lvgl
esp-idf
embedded
systematic-debugging

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/MyApp via tactility.py ... build esp32s3 --local-sdk
  • Fixing ELF: Can't find symbol on 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::App subclass, different workflow)
  • Board support (devicetree, display drivers) — see tactility-board-support project 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_18Not 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 sizes
    
  • lv_obj_set_style_bg_grad_color/dir/stopNot exported on 0.8.0-dev (caused Application failed to start: missing symbol blue OK dialog). Use solid 0x0E0E14 instead.
  • 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. Use COVER or 90.
  • lv_arc_create, lv_arc_set_range/value/bg_angles, lv_arc_get_valueNot exported on 0.7.0-dev, added in PR #496 dff93cb6 Add lv_arc.h symbols after 0.7 release. Device modal Error / Application failed to start: missing symbol with heap still healthy (~31KB free) indicates loader failure, not OOM. Fix: arc-free horizontal slide using lv_slider_create + swipe via lv_indev_get_point / lv_indev_active (exported since 0.6). See references/book-browser.md. lv_roller also 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/linearExported 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/destroyExported on 0.8.0-dev (lv_binfont_create) but VFS path handling fragile — tt_app_get_assets_path returns void, check buf[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-0x201DSource/fonts/*.c ~40-85KB each, LV_FONT_DECLARE, (lv_font_t*)&georgia_italic_26, lives in PSRAM via CONFIG_ELF_LOADER_LOAD_PSRAM=y. Heap stable 47539 free. See references/pretty-font.md. Legacy binfont .fnt also bundled in assets/fonts/ 52KB but not used by default.
  • LV_SYMBOL_STAR → Symbols may be absent depending on LVGL symbol set. Use LV_SYMBOL_OK, LV_SYMBOL_DUMMY, or plain UTF-8.
  • lv_obj_set_style_max_widthNot exported on 0.7.0-dev and 0.8.0-dev (July 16 2026 — check_symbols.py found 1 missing, E ELF: Can't find symbol). Triggers Application failed to start: missing symbol blue OK dialog (/api/screenshot returns PNG 320x240). Fix: use LV_PCT(92) / LV_PCT(80) fixed percents for side gutters, no max_width. Verified xtensa-esp32s3-elf-nm -D 89 undef → 0 missing after removal.
  • lv_obj_set_scrollbar_mode → Version-dependent, not reliably exported on 0.7/0.8. Avoid — use default scroll (add LV_OBJ_FLAG_SCROLLABLE) and let LVGL handle it. On QVGA intro, make scrollArea scrollable and use flex column with LV_FLEX_FLOW_COLUMN + pad_row 8.
  • xTaskGetCurrentTaskHandle → Not exported. Replace wait loops with tt_lvgl_unlock(); vTaskDelay(10); tt_lvgl_lock(portMAX_DELAY);
  • lv_image_set_rotation → Use lv_obj_set_style_transform_rotation(obj, 1800, 0) (value 1800 = 180°)
  • cJSON, esp_websocket_client, lwip_htons macros → Vendor sources directly or use raw lwip_socket/lwip_connect etc., which are exported.
  • C++ lambdas in .c files → auto make_btn = [](...){} fails compile. Keep app source as pure C (no auto, no lambdas) or rename to .cpp with extern \"C\" shims.
  • How to bring new widgets (arc, roller, chart, etc) → Edit firmware/Modules/lvgl-module/source/symbols.c, add DEFINE_MODULE_SYMBOL(lv_arc_create), etc., rebuild device firmware via python 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 = header BIBK + version u16 + count u32 + entries {offset u32, cnum u16, vnum u16}, books.json catalog.
  • 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 -l to 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_SCROLLABLE removed to avoid drag recalc.
  • Header + bottom toolbar hidden by default (LV_OBJ_FLAG_HIDDEN), tap toggles.
  • Adaptive scaling: apply_verse_scaling() measures strlen(verse) → selects FONT_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 #F2F0E8 flat no bubble, ref uppercase EPHESIANS 1:4 tracking 2 #9A9AA8, gap 10 (was 14) for closeness like PSALMS 23:1 reference image, fade transition 220ms verse / 300ms ref via lv_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 via lv_font_conv --format lvgl --lv-include lvgl.hLV_FONT_DECLARE + resolve helpers resolve_verse_font/book_font fallback to lvgl_get_text_font(), lives in PSRAM 315KB .rodata, heap 47539 stable, vision confirms "serif italic editorial like PSALMS 23:1" (Habakkuk 1:17). See references/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_reset on manual nav.
  • Rate-limit label updates (store last values) to avoid audio stutter if audio plays.
  • Reference: references/immersive-ui.md
    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)
    
    Tiers tuned from real device screenshots: short verses get giant breathing room, long verses shrink + tighten.
  • 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) + mainRow flex row (ROW, flex_grow 1, pad 4/8 column, bottom 8) = left leftCol flex_grow 1 scrollable column (pad_row 8, pad_bottom 6) + right rightRail fixed 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 card PCT 100% width, SIZE_CONTENT height, pad 9, row 3, bg COLOR_PANEL radius 10, labels WRAP width 100%.
  • Right icons: small icons on right as user requested — replace text ENTER DUNGEON button with icons: play 38px accent bg COLOR_ACCENT radius s/3, close/exit 30px muted bg 0x252836, both lv_btn_create + centered label LV_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.png then 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, causes missing symbol blue OK dialog), no lv_obj_set_scrollbar_mode — use PCT 92/80/70 for gutters and default scroll via LV_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, renderRows field in DungeonState, 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: reset sets floor 0 if tutorials else 2 (first real = 2 internally → display 1), clearAllTiles border walls, generateTutorialFloor sets renderRows=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, moveMonsters skipped when isTutorial(), best update only floor>=2.
  • DungeonRenderer.cpp: respects state.renderRows, hides cells y>=renderRows via HIDDEN flag, re-centers board for tutorial set_height grid.cellPx*TUTORIAL_ROWS align CENTER 0,-10, status shows TUTORIAL X/2 HP Gold, message TUT 1: Move to > @=you / TUT 2: Kill b, take $, use > vs normal Arrows/Swipe/Tap.
  • PocketDungeon.cpp intro 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/install multipart file field (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

  1. Hermes venv hijacks IDF Python — two flavors
    • Flavor A (tactility.py): requests import fails with TypeError: unsupported operand type(s) for |: 'type' and 'type' on Python 3.9 because PYTHONPATH includes Hermes venv's urllib3 using Python 3.10+ union syntax.
    • Flavor B (idf.py build/flash): Cannot import module "pydantic_core._pydantic_core" / ModuleNotFoundError — Hermes 3.11 venv's pydantic_core/*.so shadows IDF's 3.9/3.10 venv. Even idf.py --version fails.
    • Fix both: clear PYTHONPATH="" and explicitly set IDF_PYTHON_ENV_PATH=/Users/adolforeyna/.espressif/python_env/idf5.3_py3.9_env before 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_env but you now use 3.9 env → idf.py fullclean required or you get "project was configured with .../idf5.3_py3.10_env/bin/python". Do fullclean then re-generate python 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
      
  2. Werror=format-truncationsnprintf with 320B buffers flagged. Add -Wno-format-truncation via target_compile_options (safe, buffers intentionally oversized).
  3. Direct font symbols — Any &lv_font_montserrat_* will compile but fail at runtime. Always use lvgl_get_text_font().
  4. 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 → heap total 310KB free 275B min_free 23 largest_block 23failed to create temp directory on 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 via LV_EVENT_PRESSED/PRESSING tracking book_drag_start_x 28px threshold, plus lv_slider fast scrub. After fix heap 10943B free largest 7168 on .112, 54471→54355 stable 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 transition lv_anim_* 220/300ms no leak.
  5. Touch input swallowed by full-screen containers — GameKit bubbling pitfall (PocketDungeon July 16 v3) — Symptom: after adding introContainer + gameContainer full-screen covering root, GameKit::attachInput(root, scene) alone stopped working. Tap/swipe dead. Root cause: LVGL event bubbling — topmost LV_OBJ_FLAG_CLICKABLE container captures CLICKED/GESTURE before root. Also rulesBox inside introContainer swallowed taps needed for pointToQuadrant. Fix pattern attachInputToCurrent():
    • 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/Swipe InputKind at game-over restart
    • After fix + rebuild esp32s3, touch quadrants + swipe + keys work on 192.168.68.112. See references/intro-fullscreen.md v3 section.
  6. Fullscreen toolbar-less apps trap user — no exit (PocketDungeon July 16 v4) — Removing tt_lvgl_toolbar_create_for_app gains 40-50px but loses system back button → no way to exit. Fix: three exit paths:
    • (1) Hardware: InputKind::Cancel/Menu (Q/ESC → Cancel via keyToInput maps q/Q → Cancel, LV_KEY_ESC → Cancel) → tt_app_stop(), include via TactilityCpp/App.h (pulls tt_app.h), save bests + SFX Cancel
    • (2) X button top-right in game phase (LV_SYMBOL_CLOSE 36×28 bg #2A2D40 radius 6 → onExitButtonClicked)
    • (3) Long-press board LV_EVENT_LONG_PRESSED on gameContainer → pause overlay AppPhase::Paused with 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 adds pauseOverlay + Paused + 3 static callbacks. Verification: hermes-verify-exit*.py checks tt_app_stop, pauseOverlay, LONG_PRESSED, Cancel, build ok.
  7. 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 #232338 overlapping 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: header 36px (was 44), bg 0x0E0E14 TRANSP 0 (was 15151F COVER), border 0, pad ver 4, center PCT(90)x82% align CENTER 0,8 initially → -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 <40c still 26 via resolve_verse_font_ultra. Pads tighter 6/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 avoid ConnectionResetError 54 heap spike. Gotcha: lv_timer_t incomplete in SDK — t->user_data invalid, must use lv_timer_get_user_data(t) (exported). See references/black-bar-crop.md + references/pretty-font.md.
  8. 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, onShowApp set_chrome_visible(true) + schedule 10s, toggle_ui re-arms 10s when shown else deletes, toggle_book_browser deletes auto-hide timer, onHideApp cleanup. Book browser anim: book_y_anim_cb(void* var,int32 v){ lv_obj_set_y((lv_obj_t*)var,v); } + book_fade_anim_cbtext_opa, set_book_browser_visible(true)remove FLAG_HIDDEN, mid_wrap y 20→0 260ms ease_out + name fade OPA_20→COVER 220ms, book_dial_update_center also fades name 60→255 180ms on swipe/slider change. Verified .129: 8683 bytes at 0.8s after run menu visible → 6882 hidden at ~2s, auto-hide 10s configured. See references/immersive-ui.md + references/book-browser.md.
  9. Assuming lv_font_montserrat_18 exists — Not even compiled for all boards. Stick to SMALL/DEFAULT/LARGE.
  10. Build cache stale after main.c edittactility.py clean before rebuild when CMake glob changes.
  11. Install without platform argtactility.py Apps/X install <ip> tries all manifest platforms; if only esp32s3 ELF built you get ELF file not built for esp32p4. Pass esp32s3 explicitly.
  12. 6666 refused — Dev server disabled. Use dashboard PUT /api/apps/install on port 80, or enable Developer mode on device Settings first.
  13. 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. Always grep CONFIG_TT_DEVICE_ID sdkconfig and ls /dev/cu.usbmodem* before flash. Family color board is ES3C28P at /dev/cu.usbmodem101, 16MB flash@80MHz, partitions-16mb-with-sd.csv.
  14. 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 d976595 feat: add Pocket Dungeon game prototype, not in local Apps/ after fetch of origin. Fix cd apps && git fetch personal && git merge personal/main.
  15. 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.
  16. GameKit / SfxEngine shared libraries (2026-07-16) — New pattern for game apps. main/CMakeLists.txt globs ../../Libraries/GameKit/Source/*.c* + SfxEngine + INCLUDE_DIRS + REQUIRES TactilitySDK 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. See references/pocket-dungeon.md + references/intro-fullscreen.md v3/v4.

Verification Checklist

  • TACTILITY_SDK_PATH set, IDF export.sh sourced, using IDF python binary for tactility.py
  • build/<App>.app exists, tar tvf shows expected assets
  • xtensa-esp32s3-elf-nm -D *.app.elf undefined checked; 0 missing vs firmware exports (0 undef total v4 editorial = healthy)
  • No direct lv_font_montserrat_* or LV_SYMBOL_STAR refs in source (grep lv_font_montserrat)
  • main/CMakeLists.txt has -Wno-format-truncation allowance
  • 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 + fix
  • references/symbols.md — exported vs missing symbol list, OOM vs missing-symbol diagnosis, heap sizes 275B vs 31KB vs 54KB stable, anim/binfont exports
  • references/offline-data.md — per-book binary format and converter usage
  • references/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 fleet
  • references/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 esp32s3
  • references/intro-fullscreen.md — Intro state machine AppPhase + fullscreen no-toolbar, rules/controls screen, HideStatusBar flag not parsed for ELF apps
  • references/sdk-mismatch-missing-symbol.md — SDK/OS version mismatch .112 0.8.0-dev vs 0.7 manifest + unexported lv_obj_set_style_max_width / scrollbar_mode causing missing symbol dialog, QVGA cramped fix, /api/screenshot remote QA
  • references/immersive-ui.md — UI pattern v4 editorial: warm #F2F0E8, uppercase tracking 2 ref #9A9AA8 gap 10, fade transition anim, binfont future
  • references/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 heap
  • references/black-bar-crop.md — v6 black bar 44px #15151F COVER overlapping verse crop fix, Georgia down-tier LARGE→22 DEFAULT→18 memo, screenshot rate-limit
  • references/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