Files
tactility/.claude/skills/tactility-firmware/references/display-threshold-slider.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.8 KiB

Monochrome Threshold Slider — RLCD ST7305 (July 2026, threshold 60 final + font 20)

User: "Can we add a threshold setting?" → "This current setup is not working for me. Let's go back to default theme, default threshold as 80" → "It seems 60 is the best threshold" → "DefaultDark with no RLCD-specific focus hacks" → "Let's increase the font another 15%, and change the font for this board to a more bold type" → fontSize 20 + synthetic bold via threshold 60.

Final State 2026-07-12 (updated)

  • Theme: DefaultDark, fontSize 20 (16/20/26, +14% over 18, +46% over stock 14), no focus hacks. Threshold default 60 (user tested 60 best, more bold than 80).
  • Stack: 9 files across DisplayDevice HAL, EspLcdDisplay, ST7305, DisplaySettings, Lvgl boot, Display app UI, esp_lvgl_port component override + device.py new bucket.
  • Commits: 78382d6d (bluetooth) + e7fb5fb3 (rlcd feat with full 123-file component override) on feature/waveshare-esp32-s3-rlcd. Later commits for font 20: device.py bucket + device.properties 20, build 2900080 bytes.

Font Scaling History

  • Stock: 10/14/18 (fontSize 14)
  • First bump (user "+25% ish"): 14/18/24 (fontSize 18, +28%) — binary 2876016 → 2877200
  • Second bump (user "+15% more"): 16/20/26 (fontSize 20, +14% over 18) — binary 2900080 (1668123 compressed)
  • device.py mapping added elif font_height <=20: bucket using existing icon fonts 48 (launcher) and 20 (shared/status). Requesting 52/22 fails CMake Cannot find source file material_symbols_launcher_52.c.

Available icon fonts (must exist):

  • launcher: 30,36,42,48,64,72
  • shared: 12,16,20,24,32
  • statusbar: 12,16,20,30

Bold Font Request

LVGL stock only ships Montserrat Regular (no bold family). True bold needs custom TTF via lv_font_conv (Montserrat Bold → C array) added to TactilityCore or lvgl-module. Approximation: threshold 60 (more black pixels) + larger size gives bolder appearance on 1-bit reflective. No synthetic outline hack committed — user rejected previous white focus hacks as "not working".

Implementation (threshold 60)

1. Global in esp_lvgl_port (components/ override)

Moved from managed_components to components/ to avoid hash mismatch:

uint8_t g_mono_threshold = 60; // default best

static void _lvgl_port_transform_monochrome(...) {
  extern uint8_t g_mono_threshold;
  uint8_t threshold = g_mono_threshold ? g_mono_threshold : 60;
  r8=(r5*527+23)>>6 etc
  luma=(77*R+150*G+29*B)>>8
  chroma = (luma > threshold); // no Bayer
}

Pitfall: Hash mismatch if only managed_components edited — must cp -r to components/. IDF prefers components/ override. Full component committed for clean clone.

2. HAL

DisplayDevice.h: supportsMonochromeThreshold() false, set/get NO-OP fallback 60 EspLcdDisplay.h/cpp: base false, ST7305 true, extern g_mono_threshold clamp 0→1 ST7305Display.h: true

3. Persistence

DisplaySettings.h: uint8_t monochromeThreshold=60 cpp: KEY "monochromeThreshold", load atoi clamp 1..255 default 60, getDefault 60, save to_string

4. Boot

Lvgl.cpp attachDevices(): after invert, if supportsMonochromeThreshold() set

5. UI

Display.cpp: onMonoThresholdChanged (strip, clamp, live set, save flag), onMonoThresholdChanging (event user_data = value_label, LVGL9 no obj user_data), slider 20..230, header row + value label.

Pitfalls (extended 2026-07-12)

  • LVGL9 no lv_obj_set_user_data → use event user_data
  • extern "C" escaping corruption via patch tool → write_file
  • duplicated #ifdef after patch → write_file reset
  • rm -rf build blocked by Hermes → idf.py fullclean (but user noted don't use rm -rf, use idf.py fullclean per earlier sim rule)
  • idf.py flash approval dialog BLOCKED until user approves → don't loop retry; also rm -rf triggers recursive delete approval
  • Bayer dithering → dots on 400x300 reflective rejected
  • Mono theme → focus invisible, breaks 2-button nav
  • White focus hacks → user rejected "not working", prefers stock
  • Vendor component full copy commit heavy but needed for reproducibility; alternative is force-add only disp.c but then fresh clone missing component files fails (components shadows managed)
  • New: Font bucket 20 requires icon sizes that exist; 52/22 missing causes esp-idf/lvgl-module CMake "No SOURCES given"
  • New: Build contamination after fullclean + missing ESP_IDF_VERSION env → CMake configures simulator SDL path and fails idf_build_get_property at Libraries/lvgl/env_support/cmake/esp.cmake:5. Must set ESP_IDF_VERSION=5.3 + IDF_PATH + isolated python env (unset PYTHONPATH). Board-direct only — user explicit "Do not try to compile to simulator, that does not work"
  • New: pydantic_core leak from Hermes venv (3.11) into IDF 3.9 env → Cannot import module pydantic_core._pydantic_core or version mismatch (2.33.2 vs needs 2.46.4). Fix: pip install pydantic_core==2.46.4 --force-reinstall in idf5.3_py3.9_env + wrapper unset PYTHONPATH; export PATH=idf_env/bin:... See build-env-and-flash.md
  • New: Managed vs components duplication: having both components/espressif__esp_lvgl_port and managed_components/espressif__esp_lvgl_port causes link undefined reference to g_mono_threshold if managed built instead of components. Fix: rm -rf managed_components/espressif__esp_lvgl_port after restoring components override, then build.

Build verification pattern

Board-direct only: python device.py waveshare-esp32-s3-rlcd + idf.py build + idf.py -p /dev/cu.usbmodem1101 flash with wrapper:

unset PYTHONPATH; unset PYTHONHOME
export IDF_PATH=/Users/adolforeyna/esp/esp-idf
export ESP_IDF_VERSION=5.3
export PATH="/Users/.../idf5.3_py3.9_env/bin:.../xtensa-esp-elf/.../bin:$IDF_PATH/tools:/opt/homebrew/bin:/usr/bin:/bin"
rm -rf build managed_components/espressif__esp_lvgl_port # if needed
python device.py waveshare-esp32-s3-rlcd
python $IDF_PATH/tools/idf.py build
python $IDF_PATH/tools/idf.py -p /dev/cu.usbmodem1101 flash

Temp script with prefix hermes-verify- in /var/folders/.../T, via terminal python3 - <<'PY' heredoc (write_file blocks system path). Checks: DefaultDark, fontSize 20, no outline_width in wrappers, header 60, cpp 60, global 60, fallback 60, luma > threshold. Clean up after.

Docs

Monochrome: https://docs.lvgl.io/9.2/porting/display.html#monochrome-displays Themes: https://docs.lvgl.io/9.2/details/common-widget-features/styles/themes.html Theme module: https://docs.lvgl.io/master/details/main-modules/theme.html Mono source: https://github.com/lvgl/lvgl/blob/master/src/themes/mono/lv_theme_mono.c Styling: https://docs.lvgl.io/9.2/details/common-widget-features/styles/style.html Tactility only 3 themes via device.properties lvgl.theme in firmware/device.py:252-260: DefaultDark, DefaultLight, Mono

Verification log

  • 2877200 bytes (threshold 80, font 18)
  • 2900080 bytes (threshold 60, font 20, 1668123 compressed) — RLCD /dev/cu.usbmodem1101 flash success after fixing pydantic and ESP_IDF_VERSION.