Compare commits

...
1 Commits
Author SHA1 Message Date
Kaj SchittecatandClaude Opus 5.5 9d0301b80a touch: beta_92, faster screens on most boards, Arabic fixes, GitHub fixes
Frame rate: full-width PSRAM draw band (6 bands a frame instead of 14 on the
T-Deck), the still aurora drawn once and copied, LVGL at -O2 with its blend and
label loops in IRAM (scripts/build/perf_lvgl.py, 1 KB more loop stack), circle
and gradient caches, newlib memcpy. Rolled out to the M9, Attaky, RAK, Wio,
CrowPanel and V4-R8; the Pager gets the band only, the V4 the caches only.
Frame benchmark tour (DOC_PERF_BENCH) for measuring it.

Fixes: hop counts read from the packed path length (#621); Arabic shaping
rows patched in LVGL (patch_lvgl_arabic.py), Arabic 101 shift layer, kaf,
symbols, text sent as standard letters (#582); info popup colour runs that
never wrap (#602); keypad nav reaches the chat header (#623); the browser
opens the changelog (#622, gen-changelog.py at every site deploy); the
Theme background colour picker no longer edits the accent; flasher entries
for P4 units with factory ESP-AT Wi-Fi chips (#612, #619).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:53:33 +02:00
18 changed files with 1020 additions and 99 deletions

No files matched your search

+2
View File
@@ -46,3 +46,5 @@ CLAUDE.md
# device's AppFS partition and is both large and device-specific.
tanmatsu/tools/appfs-src/
tanmatsu/tools/dev_appfs.bin
# Generated by scripts/build/gen-changelog.py at every site deploy
deploy/site/changelog.html
+1
View File
@@ -729,6 +729,7 @@
<div class="lede">Typing and getting around.</div>
<ul>
<li><b>Secondary layouts</b>: Cyrillic, Greek, Arabic, French, German and more</li>
<li><b>Arabic on a built-in keyboard</b>: the letters follow the Arabic 101 layout. The seven that sit on punctuation keys there are on the digits: ج د ذ ز ط ظ و on 1 to 7 and ك on 8 (Alt+Q to Alt+I on the T-LoRa Pager). Shift gives أ (H), إ (Y), آ (N), لأ لإ لآ (G, T, B), ، (K), ؛ (P), the tashkeel (Q W E R A S X, shadda on Z) and ؟ on the question mark</li>
<li><b>Accent popups</b>: long-press for accented letters</li>
<li><b>Enter sends</b> the message, and a <b>flash</b> of the screen and keys on a new message</li>
<li><b>Keyboard navigation</b>, and on the T-Deck the <b>Trackball</b> mode: Scroll, Cursor or Navigate</li>
+22 -3
View File
@@ -472,6 +472,13 @@
<p style="color:var(--mut);font-size:.8rem;margin:8px 0 0">Radio <b>LR2021</b>:</p>
<esp-web-install-button id="inst-tdp4-lr2021"></esp-web-install-button>
<a class="dl2" id="dl-tdp4-lr2021" download><span class="dl2-row"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v12M7 11l5 5 5-5M5 21h14"/></svg>Download .bin</span><span class="dlver" id="dl-tdp4-lr2021-ver"></span></a>
<p style="color:var(--mut);font-size:.8rem;margin:12px 0 0"><b>Wi-Fi never works</b> with the image above? Your unit's Wi-Fi chip still runs LilyGo's factory firmware (ESP-AT). Use the matching image below instead: it brings Wi-Fi back.</p>
<p style="color:var(--mut);font-size:.8rem;margin:6px 0 0">Radio <b>SX1262</b>, factory Wi-Fi chip:</p>
<esp-web-install-button id="inst-tdp4-at"></esp-web-install-button>
<a class="dl2" id="dl-tdp4-at" download><span class="dl2-row"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v12M7 11l5 5 5-5M5 21h14"/></svg>Download .bin</span><span class="dlver" id="dl-tdp4-at-ver"></span></a>
<p style="color:var(--mut);font-size:.8rem;margin:8px 0 0">Radio <b>LR2021</b>, factory Wi-Fi chip:</p>
<esp-web-install-button id="inst-tdp4-lr2021-at"></esp-web-install-button>
<a class="dl2" id="dl-tdp4-lr2021-at" download><span class="dl2-row"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v12M7 11l5 5 5-5M5 21h14"/></svg>Download .bin</span><span class="dlver" id="dl-tdp4-lr2021-at-ver"></span></a>
<p data-wm-betaonly style="color:var(--mut);font-size:.78rem;margin:6px 0 0">New board: beta channel only until the next stable.</p></div>
<div class="board" data-search="lilygo t-display p4 tdisplay lcd tft hi8561 esp32-p4 sx1262 lr2021 touch handheld"><h4>LilyGo T-Display P4 <span style="font-weight:600;color:var(--mut)">(LCD)</span></h4><p>ESP32-P4 &middot; TFT-LCD touch handheld. Pick the image matching the radio chip.</p>
<span class="sup part" tabindex="0"><span class="dot"></span>Partially supported<span class="pop"><b>Partially supported</b>Both LCD radio variants are compile-tested and need hardware validation. Bluetooth pairing is unavailable; use Wi-Fi or USB for the phone-app link.</span></span>
@@ -481,7 +488,14 @@
<p style="color:var(--mut);font-size:.8rem;margin:8px 0 0">Radio <b>LR2021</b>:</p>
<esp-web-install-button id="inst-tdp4-lcd-lr2021"></esp-web-install-button>
<a class="dl2" id="dl-tdp4-lcd-lr2021" download><span class="dl2-row"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v12M7 11l5 5 5-5M5 21h14"/></svg>Download .bin</span><span class="dlver" id="dl-tdp4-lcd-lr2021-ver"></span></a>
<p data-wm-betaonly style="color:var(--mut);font-size:.78rem;margin:6px 0 0">New screen variant, not yet hardware-tested &mdash; beta only. Flash with care and report back.</p></div>
<p style="color:var(--mut);font-size:.8rem;margin:12px 0 0"><b>Wi-Fi never works</b> with the image above? Your unit's Wi-Fi chip still runs LilyGo's factory firmware (ESP-AT). Use the matching image below instead: it brings Wi-Fi back.</p>
<p style="color:var(--mut);font-size:.8rem;margin:6px 0 0">Radio <b>SX1262</b>, factory Wi-Fi chip:</p>
<esp-web-install-button id="inst-tdp4-lcd-at"></esp-web-install-button>
<a class="dl2" id="dl-tdp4-lcd-at" download><span class="dl2-row"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v12M7 11l5 5 5-5M5 21h14"/></svg>Download .bin</span><span class="dlver" id="dl-tdp4-lcd-at-ver"></span></a>
<p style="color:var(--mut);font-size:.8rem;margin:8px 0 0">Radio <b>LR2021</b>, factory Wi-Fi chip:</p>
<esp-web-install-button id="inst-tdp4-lcd-lr2021-at"></esp-web-install-button>
<a class="dl2" id="dl-tdp4-lcd-lr2021-at" download><span class="dl2-row"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v12M7 11l5 5 5-5M5 21h14"/></svg>Download .bin</span><span class="dlver" id="dl-tdp4-lcd-lr2021-at-ver"></span></a>
<p data-wm-betaonly style="color:var(--mut);font-size:.78rem;margin:6px 0 0">New screen variant, not yet hardware-tested, beta only. Flash with care and report back.</p></div>
<div class="board" data-search="lilygo t-lora pager tlora esp32-s3 qwerty rotary encoder lr1121 sx1262"><h4>LilyGo T-LoRa Pager</h4><p>ESP32-S3 QWERTY + rotary encoder.</p>
<span class="sup part" tabindex="0"><span class="dot"></span>Partially supported<span class="pop"><b>Partially supported</b>New community port (thanks @codemonkeybr). The core works: LoRa mesh communication, chat, contacts, channels and the phone-app link. Pick the build that matches your radio chip.</span></span>
<p style="color:var(--mut);font-size:.8rem;margin:0">Radio <b>LR1121</b>:</p>
@@ -829,11 +843,16 @@ We use no tracking, analytics, or advertising cookies. Our CDN and security prov
[
['tdp4-lr2021', 'tdisplay-p4-lr2021', 'T-Display P4 (AMOLED, LR2021)'],
['tdp4-lcd', 'tdisplay-p4-lcd', 'T-Display P4 (LCD, SX1262)'],
['tdp4-lcd-lr2021', 'tdisplay-p4-lcd-lr2021', 'T-Display P4 (LCD, LR2021)']
['tdp4-lcd-lr2021', 'tdisplay-p4-lcd-lr2021', 'T-Display P4 (LCD, LR2021)'],
// Units whose C6 Wi-Fi chip still runs LilyGo's factory ESP-AT firmware (#612).
['tdp4-at', 'tdisplay-p4-at', 'T-Display P4 (AMOLED, SX1262, factory Wi-Fi chip)'],
['tdp4-lr2021-at', 'tdisplay-p4-lr2021-at', 'T-Display P4 (AMOLED, LR2021, factory Wi-Fi chip)'],
['tdp4-lcd-at', 'tdisplay-p4-lcd-at', 'T-Display P4 (LCD, SX1262, factory Wi-Fi chip)'],
['tdp4-lcd-lr2021-at', 'tdisplay-p4-lcd-lr2021-at', 'T-Display P4 (LCD, LR2021, factory Wi-Fi chip)']
].forEach(([id, board, label]) => {
const bin = 'https://firmware.wadamesh.com/releases/BETA/' + tag + '/wadamesh-' + board + '-merged.bin';
const btn = document.getElementById('inst-' + id);
if (btn){ const m = { name:'wadamesh ' + tag + ' \u2014 ' + label, version:tag, new_install_prompt_erase:true,
if (btn){ const m = { name:'wadamesh ' + tag + ' \u00b7 ' + label, version:tag, new_install_prompt_erase:true,
builds:[{ chipFamily:'ESP32-P4', parts:[{ path:bin, offset:0 }] }] };
btn.setAttribute('manifest', URL.createObjectURL(new Blob([JSON.stringify(m)], {type:'application/json'}))); }
const dl = document.getElementById('dl-' + id);
+13
View File
@@ -118,6 +118,19 @@
* cache also means less state to keep coherent across slot reuse. */
#define LV_IMG_CACHE_DEF_SIZE 8
// Render caches (perf pass 2026-10-09, measured on the T-Deck with the frame benchmark):
// the UI draws many rounded corners at many radii (cards, chips, buttons, tiles) and
// LVGL kept masks for only four, and every glass card's gradient was worked out again on
// each band of each frame. Same pixels, a few percent less time a frame.
#if !(defined(DOC_CAPTURE) && defined(BENCH_NO_LVCACHE)) // the frame benchmark's "before"
#define LV_CIRCLE_CACHE_SIZE 16
#define LV_GRAD_CACHE_DEF_SIZE 8192 // bytes, from the LVGL heap (PSRAM)
#endif
// LV_MEMCPY_MEMSET_STD=1 (newlib's memcpy/memset, word-wise even when the two sides are
// aligned differently) is a -D next to LV_CONF_PATH in platformio.ini, not set here: it
// changes LVGL's headers, and src/ compiles can resolve "lv_conf.h" to the stale copy
// vendored with the core, so only a flag reaches every file of a build alike.
/* Image-as-font (lv_imgfont): maps Unicode codepoints to colour images so we can
* render full-colour emoji inline with text. The emoji set is pre-baked to
* RGB565+alpha lv_img_dsc_t C-arrays (emoji_data.c) and wired as the tail of the
+38 -11
View File
@@ -17,7 +17,9 @@ framework = arduino
board = heltec_v4
monitor_speed = 115200
board_build.partitions = partitions_tft_touch.csv
extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_lvgl_anim_uaf.py, pre:scripts/build/patch_meshcore_txt_timestamp.py, merge-bin.py
extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_lvgl_anim_uaf.py, pre:scripts/build/patch_lvgl_arabic.py, pre:scripts/build/patch_meshcore_txt_timestamp.py, pre:scripts/build/perf_lvgl.py, merge-bin.py
; perf_lvgl.py is here for the V4-R8, which extends this env; the V4 itself sets no options
; (no flash left for -O2, no internal RAM for IRAM), so its LVGL builds exactly as before.
build_flags =
; 1.17 core: ESP32Board.cpp includes <target.h> -> MicroNMEALocationProvider.h (header-only
@@ -149,6 +151,7 @@ build_flags =
-D ENABLE_ADVERT_ON_BOOT=0
-D LV_CONF_PATH=lv_conf.h
-D LV_CONF_INCLUDE_SIMPLE=1
-D LV_MEMCPY_MEMSET_STD=1 ; newlib memcpy/memset in LVGL (see include/lv_conf.h)
; Smoother scroll: lower deceleration than LVGL's default (10) so flicks glide
; longer instead of stopping abruptly. lv_conf leaves this unset, so this -D wins.
-D LV_INDEV_DEF_SCROLL_THROW=7
@@ -245,6 +248,8 @@ lib_deps =
[env:heltec_v4_r8_tft_companion_radio_usb_tcp_touch]
extends = env:heltec_v4_tft_companion_radio_usb_tcp_touch
board = heltec_v4_r8
custom_lvgl_o2 = yes ; LVGL at -O2 (scripts/build/perf_lvgl.py), like the T-Deck
custom_lvgl_iram = lv_draw_sw_blend.c, lv_draw_label.c ; their hot loops in IRAM, paid by the band in PSRAM
; Own filter (not inherited): the R8's DISPLAY_CLASS is OUR LGFXDisplay, so the UIColor
; palette must be a project object (UIColorPalette.cpp) -- otherwise the linker pulls the
; core's same-named LGFXDisplay.o from the archive for UIColor and every method collides.
@@ -344,7 +349,9 @@ framework = arduino
board = attaky_mesh_series
monitor_speed = 115200
board_build.partitions = partitions_tft_touch.csv
extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_lvgl_anim_uaf.py, pre:scripts/build/patch_meshcore_txt_timestamp.py, merge-bin.py
extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_lvgl_anim_uaf.py, pre:scripts/build/patch_lvgl_arabic.py, pre:scripts/build/patch_meshcore_txt_timestamp.py, pre:scripts/build/perf_lvgl.py, merge-bin.py
custom_lvgl_o2 = yes ; LVGL at -O2 (scripts/build/perf_lvgl.py), like the T-Deck
custom_lvgl_iram = lv_draw_sw_blend.c, lv_draw_label.c ; their hot loops in IRAM, paid by the band in PSRAM
build_flags =
; 1.17 core: ESP32Board.cpp includes <target.h> -> MicroNMEALocationProvider.h (header-only
@@ -444,6 +451,7 @@ build_flags =
-D ENABLE_ADVERT_ON_BOOT=0
-D LV_CONF_PATH=lv_conf.h
-D LV_CONF_INCLUDE_SIMPLE=1
-D LV_MEMCPY_MEMSET_STD=1 ; newlib memcpy/memset in LVGL (see include/lv_conf.h)
-D LV_INDEV_DEF_SCROLL_THROW=7
-D USER_SETUP_LOADED=1
-D ST7789_DRIVER=1
@@ -530,7 +538,9 @@ framework = arduino
board = t-deck
monitor_speed = 115200
board_build.partitions = variants/lilygo_tdeck/partitions_tdeck_touch.csv
extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_lvgl_anim_uaf.py, pre:scripts/build/patch_meshcore_txt_timestamp.py, merge-bin.py
extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_lvgl_anim_uaf.py, pre:scripts/build/patch_lvgl_arabic.py, pre:scripts/build/patch_meshcore_txt_timestamp.py, pre:scripts/build/perf_lvgl.py, merge-bin.py
custom_lvgl_o2 = yes ; LVGL at -O2 (scripts/build/perf_lvgl.py): ~5% less time a frame, ~39 KB flash
custom_lvgl_iram = lv_draw_sw_blend.c, lv_draw_label.c ; their hot loops in IRAM: ~8% less a frame, ~6 KB internal RAM
build_flags =
; 1.17 core: ESP32Board.cpp includes <target.h> -> MicroNMEALocationProvider.h (header-only
@@ -642,6 +652,7 @@ build_flags =
-D PIN_TB_RIGHT=1
-D LV_CONF_PATH=lv_conf.h
-D LV_CONF_INCLUDE_SIMPLE=1
-D LV_MEMCPY_MEMSET_STD=1 ; newlib memcpy/memset in LVGL (see include/lv_conf.h)
; Smoother scroll: lower deceleration than LVGL's default (10) so flicks glide
; longer instead of stopping abruptly. lv_conf leaves this unset, so this -D wins.
-D LV_INDEV_DEF_SCROLL_THROW=7
@@ -716,7 +727,7 @@ board_build.flash_size = 16MB
board_upload.flash_size = 16MB
board_upload.maximum_size = 16777216
board_build.partitions = variants/lilygo_tdeck/partitions_tdeck_touch.csv
extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_lvgl_anim_uaf.py, pre:scripts/build/patch_meshcore_txt_timestamp.py, merge-bin.py
extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_lvgl_anim_uaf.py, pre:scripts/build/patch_lvgl_arabic.py, pre:scripts/build/patch_meshcore_txt_timestamp.py, merge-bin.py
build_unflags =
-std=gnu++11
@@ -819,6 +830,7 @@ build_flags =
-D ENABLE_ADVERT_ON_BOOT=0
-D LV_CONF_PATH=lv_conf_tdeck_pro.h
-D LV_CONF_INCLUDE_SIMPLE=1
-D LV_MEMCPY_MEMSET_STD=1 ; newlib memcpy/memset in LVGL (see include/lv_conf.h)
-D MAX_CONTACTS=2000
-D MAX_GROUP_CHANNELS=40
-D ADMIN_PASSWORD='"password"'
@@ -952,7 +964,7 @@ board_build.flash_size = 16MB
board_upload.flash_size = 16MB
board_upload.maximum_size = 16777216
board_build.partitions = variants/lilygo_tdeck/partitions_tdeck_touch.csv
extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_lvgl_anim_uaf.py, pre:scripts/build/patch_meshcore_txt_timestamp.py, merge-bin.py
extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_lvgl_anim_uaf.py, pre:scripts/build/patch_lvgl_arabic.py, pre:scripts/build/patch_meshcore_txt_timestamp.py, merge-bin.py
build_unflags =
-std=gnu++11
@@ -1079,6 +1091,7 @@ build_flags =
-D ENABLE_ADVERT_ON_BOOT=0
-D LV_CONF_PATH=lv_conf_tdeck_pro.h
-D LV_CONF_INCLUDE_SIMPLE=1
-D LV_MEMCPY_MEMSET_STD=1 ; newlib memcpy/memset in LVGL (see include/lv_conf.h)
-D MAX_CONTACTS=2000
-D MAX_GROUP_CHANNELS=40
-D ADMIN_PASSWORD='"password"'
@@ -1145,7 +1158,9 @@ board_build.flash_size = 16MB
board_upload.flash_size = 16MB
board_upload.maximum_size = 16777216
board_build.partitions = partitions_tft_touch.csv
extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_lvgl_anim_uaf.py, pre:scripts/build/patch_lgfx_dmadesc.py, merge-bin.py
extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_lvgl_anim_uaf.py, pre:scripts/build/patch_lvgl_arabic.py, pre:scripts/build/patch_lgfx_dmadesc.py, pre:scripts/build/perf_lvgl.py, merge-bin.py
custom_lvgl_o2 = yes ; LVGL at -O2 (scripts/build/perf_lvgl.py), like the T-Deck
custom_lvgl_iram = lv_draw_sw_blend.c, lv_draw_label.c ; their hot loops in IRAM, paid by the band in PSRAM
build_unflags =
-std=gnu++11
@@ -1248,6 +1263,7 @@ build_flags =
-D ENABLE_ADVERT_ON_BOOT=0
-D LV_CONF_PATH=lv_conf.h
-D LV_CONF_INCLUDE_SIMPLE=1
-D LV_MEMCPY_MEMSET_STD=1 ; newlib memcpy/memset in LVGL (see include/lv_conf.h)
-D MAX_CONTACTS=2000
-D MAX_GROUP_CHANNELS=40
-D ADMIN_PASSWORD='"password"'
@@ -1307,7 +1323,9 @@ board_build.flash_size = 16MB
board_upload.flash_size = 16MB
board_upload.maximum_size = 16777216
board_build.partitions = partitions_tft_touch.csv
extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_lvgl_anim_uaf.py, pre:scripts/build/patch_lgfx_dmadesc.py, merge-bin.py
extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_lvgl_anim_uaf.py, pre:scripts/build/patch_lvgl_arabic.py, pre:scripts/build/patch_lgfx_dmadesc.py, pre:scripts/build/perf_lvgl.py, merge-bin.py
custom_lvgl_o2 = yes ; LVGL at -O2 (scripts/build/perf_lvgl.py), like the T-Deck
custom_lvgl_iram = lv_draw_sw_blend.c, lv_draw_label.c ; their hot loops in IRAM, paid by the band in PSRAM
build_unflags =
-std=gnu++11
@@ -1400,6 +1418,7 @@ build_flags =
-D ENABLE_ADVERT_ON_BOOT=0
-D LV_CONF_PATH=lv_conf.h
-D LV_CONF_INCLUDE_SIMPLE=1
-D LV_MEMCPY_MEMSET_STD=1 ; newlib memcpy/memset in LVGL (see include/lv_conf.h)
-D MAX_CONTACTS=2000
-D MAX_GROUP_CHANNELS=40
-D ADMIN_PASSWORD='"password"'
@@ -1451,7 +1470,7 @@ framework = arduino
board = lilygo-t-lora-pager
monitor_speed = 115200
board_build.partitions = variants/lilygo_tlora_pager/partitions_tlora_pager_touch.csv
extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_lvgl_anim_uaf.py, pre:scripts/build/patch_meshcore_txt_timestamp.py, pre:scripts/build/patch_nimble_freertos_timer.py, merge-bin.py
extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_lvgl_anim_uaf.py, pre:scripts/build/patch_lvgl_arabic.py, pre:scripts/build/patch_meshcore_txt_timestamp.py, pre:scripts/build/patch_nimble_freertos_timer.py, merge-bin.py
build_flags =
; 1.17 core: ESP32Board.cpp includes <target.h> -> MicroNMEALocationProvider.h (header-only
@@ -1566,6 +1585,7 @@ build_flags =
-D ENABLE_ADVERT_ON_BOOT=0
-D LV_CONF_PATH=lv_conf.h
-D LV_CONF_INCLUDE_SIMPLE=1
-D LV_MEMCPY_MEMSET_STD=1 ; newlib memcpy/memset in LVGL (see include/lv_conf.h)
; Smoother scroll: lower deceleration than LVGL's default (10) so flicks glide
; longer instead of stopping abruptly. lv_conf leaves this unset, so this -D wins.
-D LV_INDEV_DEF_SCROLL_THROW=7
@@ -1683,7 +1703,7 @@ framework = arduino
board = lilygo-t-lora-pager
monitor_speed = 115200
board_build.partitions = variants/lilygo_tlora_pager/partitions_tlora_pager_touch.csv
extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_lvgl_anim_uaf.py, pre:scripts/build/patch_meshcore_txt_timestamp.py, pre:scripts/build/patch_nimble_freertos_timer.py, merge-bin.py
extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_lvgl_anim_uaf.py, pre:scripts/build/patch_lvgl_arabic.py, pre:scripts/build/patch_meshcore_txt_timestamp.py, pre:scripts/build/patch_nimble_freertos_timer.py, merge-bin.py
build_flags =
; 1.17 core: ESP32Board.cpp includes <target.h> -> MicroNMEALocationProvider.h (header-only
@@ -1810,6 +1830,7 @@ build_flags =
-D ENABLE_ADVERT_ON_BOOT=0
-D LV_CONF_PATH=lv_conf.h
-D LV_CONF_INCLUDE_SIMPLE=1
-D LV_MEMCPY_MEMSET_STD=1 ; newlib memcpy/memset in LVGL (see include/lv_conf.h)
; Smoother scroll: lower deceleration than LVGL's default (10) so flicks glide
; longer instead of stopping abruptly. lv_conf leaves this unset, so this -D wins.
-D LV_INDEV_DEF_SCROLL_THROW=7
@@ -1904,7 +1925,9 @@ board_build.partitions = variants/thinknode_m9/partitions_m9_touch.csv
; DriveDiosInSleepMode (0x012A, FW 0x0308+) with CMD_PERR -> begin() = -706.
; The pre-script makes RadioLib's config() skip it on old FW (idempotent,
; per-env libdeps copy). See scripts/build/patch_radiolib_lr11x0.py.
extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_lvgl_anim_uaf.py, pre:scripts/build/patch_meshcore_txt_timestamp.py, pre:scripts/build/patch_radiolib_lr11x0.py, pre:scripts/build/patch_meshcore_rv3028_define.py, merge-bin.py
extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_lvgl_anim_uaf.py, pre:scripts/build/patch_lvgl_arabic.py, pre:scripts/build/patch_meshcore_txt_timestamp.py, pre:scripts/build/patch_radiolib_lr11x0.py, pre:scripts/build/patch_meshcore_rv3028_define.py, pre:scripts/build/perf_lvgl.py, merge-bin.py
custom_lvgl_o2 = yes ; LVGL at -O2 (scripts/build/perf_lvgl.py), like the T-Deck
custom_lvgl_iram = lv_draw_sw_blend.c, lv_draw_label.c ; their hot loops in IRAM, paid by the band in PSRAM
build_unflags =
-std=gnu++11
@@ -2105,6 +2128,7 @@ build_flags =
-D ENABLE_ADVERT_ON_BOOT=0
-D LV_CONF_PATH=lv_conf_m9.h
-D LV_CONF_INCLUDE_SIMPLE=1
-D LV_MEMCPY_MEMSET_STD=1 ; newlib memcpy/memset in LVGL (see include/lv_conf.h)
-D MAX_CONTACTS=2000
-D MAX_GROUP_CHANNELS=40
-D DISPLAY_CLASS=ST7789LCDDisplay
@@ -2171,7 +2195,9 @@ board = rak_tap_v2
monitor_speed = 115200
board_build.flash_mode = dio ; RAK3312 requires DIO (board JSON defaults to qio)
board_build.partitions = variants/rak_tap_v2/partitions_rak_tap_v2_touch.csv
extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_lvgl_anim_uaf.py, pre:scripts/build/patch_meshcore_txt_timestamp.py, merge-bin.py
extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_lvgl_anim_uaf.py, pre:scripts/build/patch_lvgl_arabic.py, pre:scripts/build/patch_meshcore_txt_timestamp.py, pre:scripts/build/perf_lvgl.py, merge-bin.py
custom_lvgl_o2 = yes ; LVGL at -O2 (scripts/build/perf_lvgl.py), like the T-Deck
custom_lvgl_iram = lv_draw_sw_blend.c, lv_draw_label.c ; their hot loops in IRAM, paid by the band in PSRAM
; Round-1b USB: MODE=1 HW CDC (single COM7). No 1200bps touch — use DTR/RTS reset.
build_unflags =
@@ -2281,6 +2307,7 @@ build_flags =
-D ENABLE_ADVERT_ON_BOOT=0
-D LV_CONF_PATH=lv_conf.h
-D LV_CONF_INCLUDE_SIMPLE=1
-D LV_MEMCPY_MEMSET_STD=1 ; newlib memcpy/memset in LVGL (see include/lv_conf.h)
-D LV_INDEV_DEF_SCROLL_THROW=7
-D USER_SETUP_LOADED=1
-D ST7789_DRIVER=1
+20
View File
@@ -0,0 +1,20 @@
# beta_92 - faster screens on most boards, Arabic fixed up, and a round of GitHub fixes. TEST BUILD.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# Order: what this build is, speed, then the fixes.
Test build, on top of beta_91. The stable build stays beta_85.
# --- Faster screens ---
The screens draw faster. On a T-Deck with the still aurora, Home redraws in 85 ms instead of 126, the app drawer scrolls at 69 ms a frame instead of 102 and a chat list at 49 instead of 70: a third to a half more frames a second, with the same pixels.
How: the T-Deck drew every frame in 14 slices, each a walk through the whole screen, and now draws it in 6; the still aurora is drawn once and copied instead of worked out again every frame; and the drawing code is compiled for speed, with its busiest loops in the chip's fast memory.
The same treatment reaches the ThinkNode M9, the Attaky, the RAK WisMesh Tap V2, the Wio Tracker L2, the CrowPanel 3.5 and the Heltec V4-R8, and the T-LoRa Pager draws in fewer slices. Those were not tested on their own hardware yet: please report how scrolling feels there, and anything that looks off.
The Heltec V4 gets the smaller part (rounded corners and gradients are cached now); it has no room left for the rest.
# --- Fixes ---
Hop counts above 64 are gone (#621, reported by jesshampshire). With 2- and 3-byte path hashes the hop count shares its byte with the hash size, and a few places read the whole byte: the Discovered list, the "auto-delete above N hops" setting (it could throw away nodes one hop away) and the checks for direct neighbours.
Arabic, thanks to jcyrio and a native-speaking tester (#582): letters join properly now (آ, ئ, ة and the Persian digits were wrong in the graphics library), Shift gives the Arabic 101 shift layer (أ إ آ, لأ لإ لآ, ، ؛ and the tashkeel), ك is on 8, ! $ @ type themselves again and ? types ؟. Text leaves the device as standard Arabic letters, so a #channel name gives the same channel as on a phone.
The message info popup keeps its colours when a long repeater name wraps (#602). NewtsLament found the cause.
On the keypad boards (ThinkNode M9, Tanmatsu, T-LoRa Pager) a chat's settings, with Region & scope, can be reached again: move up to the conversation header and press Enter (#623, reported by bobbythefett).
The web browser opens the wadamesh changelog when there is no home page on the SD card and Wi-Fi is up (#622, an idea from jesshampshire). It is also on the site: wadamesh.com/changelog.html.
Settings > Theme > Background > Colour changed the accent colour instead; it sets the background now.
T-Display P4 with Wi-Fi that never connects: the flasher now offers "factory Wi-Fi chip" images, for units whose Wi-Fi chip still runs LilyGo's original firmware (#612; jalevdev spotted the missing choice in #619).
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""Write deploy/site/changelog.html: every build's notes from release-notes/, newest first.
One page that both a browser and the firmware's text reader show well (the reader opens
it when there is no home page on the SD card, #622): plain HTML, no scripts, the notes as
lists. The notes files are the same ones release.sh turns into version.json, so the page
says what each build said. Lines starting with '#' are section comments there and are
left out here too. scripts/deploy-site.sh runs this before every upload.
Usage: python3 scripts/build/gen-changelog.py [out.html]
"""
import html
import os
import re
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
NOTES = os.path.join(ROOT, "release-notes")
OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(ROOT, "deploy", "site", "changelog.html")
def builds():
found = []
for name in os.listdir(NOTES):
m = re.fullmatch(r"beta_(\d+)\.txt", name)
if m:
found.append((int(m.group(1)), name))
for num, name in sorted(found, reverse=True):
with open(os.path.join(NOTES, name), encoding="utf-8") as f:
lines = [l.strip() for l in f]
title = ""
if lines and lines[0].startswith("#"):
# "# beta_90 - colour comes back: ... TEST BUILD." -> the part after the dash
head = lines[0].lstrip("# ").strip()
title = re.sub(r"^beta_\d+\s*[-:]\s*", "", head)
title = re.sub(r"\s*(TEST BUILD|STABLE)\.?\s*$", "", title).strip()
title = title.replace(" \u2014 ", ", ").replace("\u2014", ", ")
# The early notes were written with em dashes; the site uses none, so the
# page shows those clauses with a comma (the notes files keep their text).
notes = [l.replace(" \u2014 ", ", ").replace("\u2014", ", ")
for l in lines if l and not l.startswith("#")]
if notes:
yield "beta_%d" % num, title, notes
def main():
parts = []
for tag, title, notes in builds():
parts.append('<section id="%s">' % tag)
parts.append("<h2>%s</h2>" % html.escape(tag))
if title:
parts.append('<p class="t">%s</p>' % html.escape(title))
parts.append("<ul>")
parts.extend("<li>%s</li>" % html.escape(n) for n in notes)
parts.append("</ul></section>")
page = """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WADAMESH Changelog</title>
<meta name="description" content="What changed in every WADAMESH build, newest first.">
<link rel="icon" href="wadamesh-badge.svg">
<style>
:root{--bg:#ffffff;--bg2:#f4f6f2;--ink:#15181e;--mut:#5d6770;--line:#e5e8e2;--teal:#15b6a6;--teal-ink:#0e8d80;color-scheme:light}
@media (prefers-color-scheme:dark){:root{--bg:#111416;--bg2:#181c1f;--ink:#e8ecee;--mut:#9aa4ab;--line:#262b2f;--teal-ink:#3fd0c0;color-scheme:dark}}
body{margin:0;font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Inter,sans-serif;background:var(--bg);color:var(--ink)}
main{max-width:46rem;margin:0 auto;padding:28px 18px 60px}
a{color:var(--teal-ink)}
h1{font-size:1.5rem;letter-spacing:.06em;margin:0 0 4px}
h1 .t{color:var(--teal)}
.lede{color:var(--mut);margin:0 0 26px}
section{border-top:1px solid var(--line);padding:18px 0 6px}
h2{font:700 1rem/1.3 "JetBrains Mono",ui-monospace,monospace;margin:0;color:var(--teal-ink)}
p.t{margin:4px 0 0;font-weight:600}
ul{margin:10px 0 0;padding-left:1.2rem}
li{margin:0 0 8px}
</style>
</head>
<body>
<main>
<h1>WADA<span class="t">MESH</span> changelog</h1>
<p class="lede">What changed in every build, newest first. Install any of them at <a href="https://wadamesh.com/">wadamesh.com</a>.</p>
%s
</main>
</body>
</html>
""" % "\n".join(parts)
with open(OUT, "w", encoding="utf-8") as f:
f.write(page)
print("changelog: %d builds -> %s" % (sum(1 for p in parts if p.startswith("<h2>")), OUT))
if __name__ == "__main__":
main()
+5
View File
@@ -0,0 +1,5 @@
// Force-included into the LVGL sources named by custom_lvgl_iram (scripts/build/perf_lvgl.py):
// the functions LVGL marks LV_ATTRIBUTE_FAST_MEM there are placed in IRAM, like IRAM_ATTR.
// A header rather than a -D, so the attribute's brackets and quotes never meet a shell.
#pragma once
#define LV_ATTRIBUTE_FAST_MEM __attribute__((section(".iram1.lvgl")))
+211
View File
@@ -0,0 +1,211 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# LVGL 8.4 shapes Arabic through the ap_chars_map[] table in src/misc/lv_txt_ap.c, and
# a few of its rows are wrong (LVGL fixed most of them only in v9; #582, reported by a
# native speaker on the T-LoRa Pager). A row is {offset from U+0622, final form, then the
# initial, medial and isolated forms as offsets from the final one, {joins the previous
# letter, joins the next}}. Lookup also matches every presentation form a row produces,
# so one bad row can swallow other characters:
#
# - آ (U+0622) had no row: it never joined the letter before it (بآ), and لآ never
# became its ligature (U+FEF5), because the lam-alef check finds the alef by its row.
# Its forms are final U+FE82 and isolated U+FE81, like the other alef rows.
# - ئ (U+0626) said it does not join the next letter, so شيئا, رئيس, هيئة broke apart
# after it (LVGL 9 #6430).
# - ة (U+0629) had the initial and medial offsets of a joining letter, which land on
# ت (U+FE95, U+FE96). It never joins the next letter, so it gets isolated and final
# forms only, as the other non-joining rows do (LVGL 9 still has the medial one wrong).
# - ۰ (U+06F0) had +1/+2/-1 forms: lookup then took ۱ and ۲ for forms of ۰, and all
# three were drawn as U+06EF (LVGL 9 #6430, #10718).
# - The alef row (U+0627) was labelled آ; only the comment changes.
#
# Idempotent and fail-closed, like patch_lvgl_anim_uaf.py, and installed the same way: a
# PlatformIO pre-script with a pre-link check, and --patch-file for the IDF builds
# (Tanmatsu, T-Display P4), which vendor LVGL through their fetch-deps.sh.
import os
import sys
MARKER = "wadamesh-lvgl-arabic-patch"
EDITS = (
(
" /*{Key Offset, End, Beginning, Middle, Isolated, {conjunction}}*/\n"
" {1, 0xFE84, -1, 0, -1, {1, 0}}, // أ\n",
" /*{Key Offset, End, Beginning, Middle, Isolated, {conjunction}}*/\n"
" {0, 0xFE82, -1, 0, -1, {1, 0}}, // آ (wadamesh-lvgl-arabic-patch: LVGL 8 had no row)\n"
" {1, 0xFE84, -1, 0, -1, {1, 0}}, // أ\n",
),
(
" {4, 0xFE8A, 1, 2, -1, {1, 0}}, // ئ\n",
" {4, 0xFE8A, 1, 2, -1, {1, 1}}, // ئ (joins the next letter)\n",
),
(
" {5, 0xFE8E, -1, 0, -1, {1, 0}}, // آ\n",
" {5, 0xFE8E, -1, 0, -1, {1, 0}}, // ا\n",
),
(
" {7, 0xFE94, 1, 2, -1, {1, 0}}, // ة\n",
" {7, 0xFE94, -1, 0, -1, {1, 0}}, // ة (never joins the next letter)\n",
),
(
" {206, 0x06F0, 1, 2, -1, {0, 0}}, // ۰\n",
" {206, 0x06F0, 0, 0, 0, {0, 0}}, // ۰ (its own form only)\n",
),
)
REQUIRED = ("const ap_chars_map_t ap_chars_map[] = {", " if(ch_code == 0x0622) {")
def patch_source(source):
if any(source.count(line) != 1 for line in REQUIRED):
raise RuntimeError("lv_txt_ap.c does not match LVGL 8.4 (LVGL version drift?)")
olds = [source.count(old) for old, _ in EDITS]
news = [source.count(new) for _, new in EDITS]
marker = source.count(MARKER)
if all(n == 1 for n in news) and all(o == 0 for o in olds) and marker == 1:
return source, False
if not (all(o == 1 for o in olds) and all(n == 0 for n in news) and marker == 0):
raise RuntimeError("lv_txt_ap.c ap_chars_map does not match LVGL 8.4 (LVGL version drift?)")
patched = source
for old, new in EDITS:
patched = patched.replace(old, new, 1)
if (any(patched.count(new) != 1 for _, new in EDITS)
or any(patched.count(old) != 0 for old, _ in EDITS)
or patched.count(MARKER) != 1):
raise RuntimeError("lv_txt_ap.c patch verification failed")
return patched, True
def patch_file(path):
with open(path, encoding="utf-8") as source_file:
source = source_file.read()
patched, changed = patch_source(source)
if changed:
with open(path, "w", encoding="utf-8") as source_file:
source_file.write(patched)
return changed
def verify_source(source):
_, changed = patch_source(source)
if changed:
raise RuntimeError("lv_txt_ap.c still has LVGL 8's Arabic shaping rows")
def verify_file(path):
with open(path, encoding="utf-8") as source_file:
verify_source(source_file.read())
def self_test():
fixture = (REQUIRED[0] + "\n" + "".join(old for old, _ in EDITS)
+ " LV_AP_END_CHARS_LIST\n};\n" + REQUIRED[1] + "\n")
patched, changed = patch_source(fixture)
assert changed
assert MARKER in patched
assert "{0, 0xFE82, -1, 0, -1, {1, 0}}" in patched
assert "{4, 0xFE8A, 1, 2, -1, {1, 1}}" in patched
assert "{7, 0xFE94, -1, 0, -1, {1, 0}}" in patched
assert "{206, 0x06F0, 0, 0, 0, {0, 0}}" in patched
same, changed = patch_source(patched)
assert not changed
assert same == patched
verify_source(patched)
try:
verify_source(fixture)
except RuntimeError:
pass
else:
raise AssertionError("verification must reject an unpatched source")
for invalid in ("static void nothing(void) {}\n",
fixture.replace(EDITS[1][0], ""),
patched + EDITS[0][0],
"/* " + MARKER + " */\n" + fixture):
try:
patch_source(invalid)
except RuntimeError:
pass
else:
raise AssertionError("drift or a half-patched source must fail closed")
print("patch_lvgl_arabic self-test passed")
def install(platformio_env):
path = os.path.join(
platformio_env.subst("$PROJECT_LIBDEPS_DIR"),
platformio_env.subst("$PIOENV"),
"lvgl",
"src",
"misc",
"lv_txt_ap.c",
)
def apply_or_error():
if not os.path.isfile(path):
return "LVGL patch target is missing: %s" % path
try:
changed = patch_file(path)
except (OSError, RuntimeError) as error:
return str(error)
print("[patch_lvgl_arabic] %s" % ("patched lv_txt_ap.c" if changed else "already patched"))
return None
if os.path.isfile(path):
error = apply_or_error()
if error is not None:
print("[patch_lvgl_arabic] ERROR: %s" % error)
platformio_env.Exit(1)
else:
print("[patch_lvgl_arabic] LVGL not fetched yet - patch deferred to pre-link check")
def verify_patched(target, source, env):
del target, source
if os.path.isfile(path):
try:
verify_file(path)
return 0
except (OSError, RuntimeError):
pass
error = apply_or_error()
if error is not None:
print("[patch_lvgl_arabic] ERROR: %s" % error)
return 1
print("[patch_lvgl_arabic] ERROR: LVGL was patched after compilation")
print("[patch_lvgl_arabic] ERROR: re-run `pio run` to compile the fix")
return 1
platformio_env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", verify_patched)
try:
Import("env") # noqa: F821 - provided by PlatformIO/SCons
except NameError:
env = None
if env is not None:
install(env)
if env is None and __name__ == "__main__":
if len(sys.argv) == 2 and sys.argv[1] == "--self-test":
self_test()
elif len(sys.argv) == 3 and sys.argv[1] == "--patch-file":
try:
changed = patch_file(sys.argv[2])
print("patch_lvgl_arabic: %s" % ("patched" if changed else "already patched"))
except (OSError, RuntimeError) as error:
print("patch_lvgl_arabic: ERROR: %s" % error, file=sys.stderr)
raise SystemExit(1)
elif len(sys.argv) == 3 and sys.argv[1] == "--verify-file":
try:
verify_file(sys.argv[2])
print("patch_lvgl_arabic: verified")
except (OSError, RuntimeError) as error:
print("patch_lvgl_arabic: ERROR: %s" % error, file=sys.stderr)
raise SystemExit(1)
else:
raise SystemExit(
"usage: patch_lvgl_arabic.py --self-test | --patch-file <lv_txt_ap.c> | --verify-file <lv_txt_ap.c>"
)
+53
View File
@@ -0,0 +1,53 @@
# PlatformIO pre-script: how LVGL itself is compiled, per environment.
#
# custom_lvgl_o2 = yes
# LVGL's sources at -O2 instead of the framework's -Os. The software renderer is
# where a frame's time goes (walking the object tree, styles, glyphs, filling),
# and -Os gives its loops up for size. Everything else keeps -Os, so the flash
# cost stays with the code that earns it. -O2 goes a little deeper into the loop
# task's stack, so such builds also get WADA_LVGL_O2, for which src/main.cpp
# gives that task 1 KB more.
#
# custom_lvgl_iram = lv_draw_sw_blend.c, lv_draw_label.c, ...
# The functions LVGL marks LV_ATTRIBUTE_FAST_MEM in these source files run from
# IRAM instead of through the S3's 16 KB instruction cache, which the rest of
# the frame then has to itself. Costs their size in internal RAM, so only the
# files that carry a frame (the pixel blend, labels, masks), not every marked
# function (lines, arcs, shadows, images barely run in this UI).
#
# Perf benchmark builds override both from PLATFORMIO_BUILD_FLAGS: -DPERF_LVGL_O2 or
# -DBENCH_NO_O2, and -DBENCH_LVGL_IRAM=a.c+b.c (or =none).
import os
import re
Import("env")
flags_env = os.environ.get("PLATFORMIO_BUILD_FLAGS", "")
o2 = str(env.GetProjectOption("custom_lvgl_o2", "")).strip().lower() in ("1", "yes", "true", "on")
if "-DPERF_LVGL_O2" in flags_env:
o2 = True
if "-DBENCH_NO_O2" in flags_env:
o2 = False
iram = [f.strip() for f in str(env.GetProjectOption("custom_lvgl_iram", "")).split(",") if f.strip()]
m = re.search(r"-DBENCH_LVGL_IRAM=(\S+)", flags_env)
if m:
iram = [] if m.group(1) == "none" else m.group(1).split("+")
IRAM_H = os.path.join(env.subst("$PROJECT_DIR"), "scripts", "build", "lvgl_iram.h")
if o2 or iram:
if o2:
env.Append(CPPDEFINES=["WADA_LVGL_O2"]) # src/main.cpp gives the loop task more stack
def lvgl_object(env, node):
flags = list(env["CCFLAGS"])
if o2:
flags = [f for f in flags if f not in ("-Os", "-O1", "-O3")] + ["-O2"]
if node.name in iram:
flags += ["-include", IRAM_H]
return env.Object(node, CCFLAGS=flags)
env.AddBuildMiddleware(lvgl_object, "*lvgl/src/*")
print("perf: LVGL %s%s" % ("at -O2" if o2 else "at -Os",
(", IRAM: " + " ".join(iram)) if iram else ""))
+4
View File
@@ -24,6 +24,10 @@ DEST_PATH="${WADAMESH_SITE_PATH:-/srv/wadamesh/site}"
DRY=""
[ "${1:-}" = "--dry-run" ] && DRY="--dry-run"
# The changelog page (what the firmware's text browser opens when there is no home page on
# the SD card, #622) is generated from release-notes/ so it always matches the builds.
python3 "$ROOT/scripts/build/gen-changelog.py"
# Mirror deploy/site/ -> web root. --delete prunes files no longer in the repo;
# everything under deploy/site/ is static (html, svg, json manifests).
# Note: -a preserves source perms (644 files / 755 dirs) — no --chmod, since the
+8
View File
@@ -23,6 +23,14 @@
#include "ui-touch/device_caps.h" // CAP_BLE_KEYBOARD: keyboard mode keeps the phone link paused from boot
#endif
#if defined(WADA_LVGL_O2)
// LVGL compiled at -O2 (scripts/build/perf_lvgl.py) goes 160 bytes deeper into the
// loop task's stack at its deepest (measured on the T-Deck: 2108 -> 1948 bytes left of
// the framework's 8 KB). 1 KB more leaves more room than before; the draw band that
// moved to PSRAM freed ten times that much internal RAM.
SET_LOOP_TASK_STACK_SIZE(9 * 1024);
#endif
// Believe it or not, this std C function is busted on some platforms!
static uint32_t _atoi(const char* sp) {
uint32_t n = 0;
+26 -3
View File
@@ -177,6 +177,15 @@ static const char* const kb_ar_lower[] = {
"ئ","ء","ؤ","ر","ى","ة","و","ز","ظ","ذ","د","\n",
LV_SYMBOL_UP, "1#", " ", LV_SYMBOL_BACKSPACE, LV_SYMBOL_OK, NULL
};
/* Shift: what the letter rows cannot hold (#582). Tashkeel and tatweel, the hamza
* forms with the lam-alef pairs and Arabic punctuation, then the third letter row again
* so typing goes on without switching back. */
static const char* const kb_ar_upper[] = {
"\u064B","\u064C","\u064D","\u064E","\u064F","\u0650","\u0651","\u0652","\u0640","\u061B","\u060C","\n",
"\u0623","\u0625","\u0622","\u0644\u0623","\u0644\u0625","\u0644\u0622","\u061F","!","\u00AB","\u00BB",".","\n",
"ئ","ء","ؤ","ر","ى","ة","و","ز","ظ","ذ","د","\n",
LV_SYMBOL_UP, "1#", " ", LV_SYMBOL_BACKSPACE, LV_SYMBOL_OK, NULL
};
static const lv_btnmatrix_ctrl_t kb_ar_lower_ctrl[] = {
0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,
@@ -603,7 +612,7 @@ static const OsKeyboardLayout k_os_layouts[] = {
kb_el_upper, kb_el_upper_ctrl },
{ KeyboardLayoutId::AR, "AR",
kb_ar_lower, kb_ar_lower_ctrl,
kb_ar_lower, kb_ar_lower_ctrl }, // unicameral: same map for both
kb_ar_upper, kb_ar_lower_ctrl }, // Shift: tashkeel, hamza forms, punctuation
{ KeyboardLayoutId::FR, "FR",
kb_fr_lower, kb_fr_lower_ctrl,
kb_fr_upper, kb_fr_upper_ctrl },
@@ -801,8 +810,20 @@ static const char* hw_ar_lower[26] = {
"ع", "ر", "ص", "ء", "غ", "ئ"
};
static const char* hw_ar_digits[10] = {
nullptr, "ج", "د", "ذ", "ز", "ط", "ظ", "و", nullptr, nullptr
nullptr, "ج", "د", "ذ", "ز", "ط", "ظ", "و", "ك", nullptr
};
/* Shift: the standard Arabic (101) shift layer, so أ إ آ, the lam-alef pairs, the
* tashkeel and Arabic punctuation are reachable (#582). Shadda lives on ` there, a key
* these keyboards do not have, so it takes Shift+Z (~ on the 101). The fonts have no
* curly quotes, so U and M give a plain apostrophe. ك (on ; there) rides digit 8. */
static const char* hw_ar_upper[26] = {
"\u0650", "\u0644\u0622", "}", "]", "\u064F", "[", "\u0644\u0623", "\u0623", "\u00F7", "\u0640",
"\u060C", "/", "'", "\u0622", "\u00D7", "\u061B", "\u064E", "\u064C", "\u064D", "\u0644\u0625",
"'", "{", "\u064B", "\u0652", "\u0625", "\u0651"
};
/* No shifted digits: the symbol fallback in keyboardLayoutMapHwKey() reads this table
* for ! @ # $ % ..., and sharing hw_ar_digits made those type ج د ذ ز (#582). */
static const char* hw_ar_digits_shift[10] = { nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr };
/* French AZERTY mapping for the T-Deck's US-QWERTY physical keyboard.
* We only remap the alpha keys so punctuation/digits remain predictable on the
@@ -1014,7 +1035,7 @@ static const HwPhoneticMap k_hw_maps[KEYBOARD_LAYOUT_COUNT] = {
/* UK */ { hw_uk_lower, hw_uk_upper, hw_uk_digits, hw_uk_digits_shift },
/* SR */ { hw_sr_lower, hw_sr_upper, hw_sr_digits, hw_sr_digits_shift },
/* EL */ { hw_el_lower, hw_el_upper, hw_el_digits, hw_el_digits_shift },
/* AR */ { hw_ar_lower, hw_ar_lower, hw_ar_digits, hw_ar_digits }, // unicameral
/* AR */ { hw_ar_lower, hw_ar_upper, hw_ar_digits, hw_ar_digits_shift },
/* FR */ { hw_fr_lower, hw_fr_upper, hw_fr_digits, hw_fr_digits_shift },
/* NL */ { hw_nl_lower, hw_nl_upper, hw_nl_digits, hw_nl_digits_shift },
/* DE */ { hw_de_lower, hw_de_upper, hw_de_digits, hw_de_digits_shift },
@@ -1036,6 +1057,8 @@ const char* keyboardLayoutMapHwKey(KeyboardLayoutId id, int key, bool shifted) {
int d = key - '0';
return shifted ? m.digits_shift[d] : m.digits[d];
}
/* Arabic writes its own question mark (Shift+/ on the Arabic 101 layout). */
if (id == KeyboardLayoutId::AR && key == '?') return "\u061F";
/* A keyboard that sends the shifted symbol ('!' for Shift+1, etc.) instead
* of the digit+shift flag: map the US-QWERTY top-row symbol to its digit
* index and reuse the layout's shifted-digit glyph. */
+513 -82
View File
@@ -3048,6 +3048,66 @@ static void discoveredFlushIfDue(unsigned long now) {
// Synchronous flush for the deliberate reboot / power-off / download-mode paths
// (mirrors the chat-history flush) so a manual reboot never drops recent finds.
static void discoveredFlushNow() { if (s_disc_dirty) saveDiscovered(); }
// A path-length byte is packed: the hop count in its low 6 bits, the path hash size
// above (MeshCore's 2- and 3-byte path hashes). Never read the raw byte as hops: a
// node one hop away over 2-byte hashes is 0x41, which showed as "65 hops", fell to
// the hop filter and was never "direct" (#621). OUT_PATH_UNKNOWN (0xFF) gives 63,
// so it still counts as far and never as direct.
static inline uint8_t pathHops(uint8_t path_len) { return (uint8_t)(path_len & 0x3F); }
// Arabic typed on the device sits in its text field as presentation forms: LVGL shapes
// a label's text in place (U+FE80.., the lam-alef ligatures U+FEF5..), and that is what
// lv_textarea_get_text() returns. Sent or saved like that, other clients get characters
// they cannot search or compare, and a #channel name hashes to another key than the same
// name typed on a phone (#582). Map them back to the standard letters; everything else is
// copied as it is. `out` always ends in a NUL and is cut on a whole character.
static size_t arabicUnshape(const char* in, char* out, size_t cap) {
// U+FE80..U+FEF4 hold each letter's forms in order: the letter, and how many forms.
static const struct { uint16_t base; uint8_t n; } kForms[] = {
{0x0621,1},{0x0622,2},{0x0623,2},{0x0624,2},{0x0625,2},{0x0626,4},{0x0627,2},{0x0628,4},
{0x0629,2},{0x062A,4},{0x062B,4},{0x062C,4},{0x062D,4},{0x062E,4},{0x062F,2},{0x0630,2},
{0x0631,2},{0x0632,2},{0x0633,4},{0x0634,4},{0x0635,4},{0x0636,4},{0x0637,4},{0x0638,4},
{0x0639,4},{0x063A,4},{0x0641,4},{0x0642,4},{0x0643,4},{0x0644,4},{0x0645,4},{0x0646,4},
{0x0647,4},{0x0648,2},{0x0649,2},{0x064A,4},
};
// The Persian letters LVGL shapes into Presentation Forms-A.
static const struct { uint16_t first; uint8_t n; uint16_t base; } kFormsA[] = {
{0xFB56,4,0x067E},{0xFB7A,4,0x0686},{0xFB8A,2,0x0698},
{0xFB8E,4,0x06A9},{0xFB92,4,0x06AF},{0xFBFC,4,0x06CC},
};
static const uint16_t kLamAlef[4] = { 0x0622, 0x0623, 0x0625, 0x0627 }; // U+FEF5.. in pairs
if (!out || !cap) return 0;
size_t o = 0;
auto put2 = [&](uint32_t cp) { // every letter here is U+0600..U+07FF: two bytes
out[o++] = (char)(0xC0 | (cp >> 6));
out[o++] = (char)(0x80 | (cp & 0x3F));
};
const unsigned char* p = reinterpret_cast<const unsigned char*>(in ? in : "");
while (*p) {
uint32_t cp = *p;
size_t len = 1;
if (cp >= 0xF0 && p[1] && p[2] && p[3]) len = 4;
else if (cp >= 0xE0 && p[1] && p[2]) { len = 3; cp = ((cp & 0x0F) << 12) | ((p[1] & 0x3F) << 6) | (p[2] & 0x3F); }
else if (cp >= 0xC0 && p[1]) len = 2;
uint32_t base = 0, base2 = 0;
if (len == 3 && cp >= 0xFE80 && cp <= 0xFEF4) {
uint32_t off = cp - 0xFE80;
for (const auto& f : kForms) { if (off < f.n) { base = f.base; break; } off -= f.n; }
} else if (len == 3 && cp >= 0xFEF5 && cp <= 0xFEFC) {
base = 0x0644; base2 = kLamAlef[(cp - 0xFEF5) >> 1];
} else if (len == 3 && cp >= 0xFB56 && cp <= 0xFBFF) {
for (const auto& f : kFormsA) if (cp >= f.first && cp < (uint32_t)(f.first + f.n)) { base = f.base; break; }
}
const size_t need = base ? (base2 ? 4 : 2) : len;
if (o + need >= cap) break;
if (base) { put2(base); if (base2) put2(base2); }
else { memcpy(out + o, p, len); o += len; }
p += len;
}
out[o] = '\0';
return o;
}
// Remove discovered entries heard via more hops than the configured limit (the
// "auto-delete above N hops" setting; 0 = off). Returns true if anything went.
static bool discoveredSweepHops() {
@@ -3057,7 +3117,7 @@ static bool discoveredSweepHops() {
if (maxhops == 0) return false;
bool changed = false;
for (int i = 0; i < DISCOVERED_MAX; ++i)
if (s_discovered[i].used && s_discovered[i].path_len > maxhops) {
if (s_discovered[i].used && pathHops(s_discovered[i].path_len) > maxhops) {
s_discovered[i].used = false;
changed = true;
}
@@ -3095,7 +3155,11 @@ static bool g_cap_touch_hw_started = false;
// build). 24 × 240 × 2 B = 11.5 KB. Flush time is bound by SPI clock
// (80 MHz), not memcpy speed, so the throughput delta is small —
// LVGL just calls flush_cb more often.
#if defined(DOC_CAPTURE) && defined(BENCH_BUF_LINES)
static constexpr int LV_DRAW_BUF_LINES = BENCH_BUF_LINES; // perf benchmark builds only
#else
static constexpr int LV_DRAW_BUF_LINES = 24;
#endif
static lv_color_t* g_draw_buffer = nullptr;
// Operating clock of the LAST successful SD.begin() on SPI-SD boards (0 = none). Recorded at
// every mount/remount site (main.cpp boot adoption + the two UITask mounts) because SD.begin's
@@ -4756,7 +4820,7 @@ static void webMirrorTick() {
#endif
#if defined(DOC_CAPTURE)
static uint32_t s_dbg_flush_us = 0, s_dbg_flush_px = 0; // capture builds: display flush cost
static uint32_t s_dbg_flush_us = 0, s_dbg_flush_px = 0, s_dbg_flush_n = 0; // capture builds: display flush cost
#endif
static void lvglFlushImpl(lv_disp_drv_t* disp_drv, const lv_area_t* area, lv_color_t* color_p);
static void lvglFlush(lv_disp_drv_t* disp_drv, const lv_area_t* area, lv_color_t* color_p) {
@@ -4765,6 +4829,7 @@ static void lvglFlush(lv_disp_drv_t* disp_drv, const lv_area_t* area, lv_color_t
lvglFlushImpl(disp_drv, area, color_p);
s_dbg_flush_us += micros() - t;
s_dbg_flush_px += (uint32_t)lv_area_get_size(area);
++s_dbg_flush_n;
#else
lvglFlushImpl(disp_drv, area, color_p);
#endif
@@ -6395,6 +6460,10 @@ static void navAddStatusBarActions() {
};
add(g_statusbar.inbox_mark); add(g_statusbar.inbox_add); add(g_statusbar.inbox_qr);
add(g_statusbar.chan_gear);
// The redesign hides the gear behind the conversation header (avatar + title), which
// opens the same chat settings. Without it a keypad had no way left to a channel's
// region scope or a chat's options (#623).
add(g_statusbar.chat_head);
// On a settings detail / tool page (Monitor, Spectrum) the bar's left_label is the
// "‹ Title" Back affordance — make it reachable so nav can go Back from a double-topbar page.
if (s_settings_open_cat >= 0 || s_apppage_title) add(g_statusbar.left_label);
@@ -6454,6 +6523,7 @@ static void navMaybeRebuild() {
// they could stay unreachable until the next unrelated content change.
if (g_statusbar.inbox_add && !lv_obj_has_flag(g_statusbar.inbox_add, LV_OBJ_FLAG_HIDDEN)) sig ^= 0x5BD1E995u;
if (g_statusbar.chan_gear && !lv_obj_has_flag(g_statusbar.chan_gear, LV_OBJ_FLAG_HIDDEN)) sig ^= 0x1B873593u;
if (g_statusbar.chat_head && !lv_obj_has_flag(g_statusbar.chat_head, LV_OBJ_FLAG_HIDDEN)) sig ^= 0x2545F491u;
if (!s_nav_dirty && scr == s_nav_prev_scr && sig == s_nav_prev_sig) return;
s_nav_dirty = false; s_nav_prev_scr = scr; s_nav_prev_sig = sig;
lv_obj_t* keep = lv_group_get_focused(s_nav_group); // preserve focus across a same-page rebuild (toggle/click)
@@ -13273,9 +13343,9 @@ static void openDiscoveredModalCb(lv_event_t* e) {
// and a count needs its plural or every multi-hop row reads as broken.
char hop_buf[16];
if (e_disc.path_len == OUT_PATH_UNKNOWN) snprintf(hop_buf, sizeof(hop_buf), "%s", TR("path unknown"));
else if (e_disc.path_len == 0) snprintf(hop_buf, sizeof(hop_buf), "%s", TR("direct"));
else snprintf(hop_buf, sizeof(hop_buf), "%u %s", (unsigned)e_disc.path_len,
e_disc.path_len == 1 ? TR("hop") : TR("hops"));
else if (pathHops(e_disc.path_len) == 0) snprintf(hop_buf, sizeof(hop_buf), "%s", TR("direct"));
else snprintf(hop_buf, sizeof(hop_buf), "%u %s", (unsigned)pathHops(e_disc.path_len),
pathHops(e_disc.path_len) == 1 ? TR("hop") : TR("hops"));
snprintf(meta_buf, sizeof(meta_buf), "%s · %s · %s…", type_label, hop_buf, keyhex);
lv_label_set_text(meta, meta_buf);
lv_obj_set_style_text_color(meta, lv_color_hex(COLOR_SUB), LV_PART_MAIN);
@@ -21317,8 +21387,7 @@ static void createPrivateChannelSubmitCb(lv_event_t* e) {
kbMirrorSyncToReal();
if (!s_addch_name_ta || !s_addch_secret_ta) return;
char name[32];
strncpy(name, lv_textarea_get_text(s_addch_name_ta), sizeof(name) - 1);
name[sizeof(name) - 1] = '\0';
arabicUnshape(lv_textarea_get_text(s_addch_name_ta), name, sizeof(name)); // #582
// Trim trailing whitespace and default to "Private" if empty.
for (int i = (int)strlen(name) - 1; i >= 0 && (name[i] == ' ' || name[i] == '\t'); --i) name[i] = '\0';
if (name[0] == '\0') strncpy(name, "Private", sizeof(name));
@@ -21599,8 +21668,7 @@ static void joinPrivateChannelSubmitCb(lv_event_t* e) {
if (!s_addch_secret_ta) return;
char jname[32] = "Joined"; // default if the user leaves Name blank
if (s_addch_name_ta) {
strncpy(jname, lv_textarea_get_text(s_addch_name_ta), sizeof(jname) - 1);
jname[sizeof(jname) - 1] = '\0';
arabicUnshape(lv_textarea_get_text(s_addch_name_ta), jname, sizeof(jname)); // #582
for (int i = (int)strlen(jname) - 1; i >= 0 && (jname[i] == ' ' || jname[i] == '\t'); --i) jname[i] = '\0';
if (jname[0] == '\0') strncpy(jname, "Joined", sizeof(jname));
}
@@ -21715,7 +21783,10 @@ static void joinHashtagChannelSubmitCb(lv_event_t* e) {
if (!s_addch_hashtag_ta) return;
// Normalize: strip leading '#'s, drop whitespace, ASCII-lowercase. Matches
// Meshcomod-client/apps/web-client/src/main.ts:deriveHashtagChannelSecret.
const char* raw = lv_textarea_get_text(s_addch_hashtag_ta);
// Standard Arabic letters first: the key is a hash of the name, and a phone types
// the letters, not the presentation forms the field holds (#582).
char raw[80];
arabicUnshape(lv_textarea_get_text(s_addch_hashtag_ta), raw, sizeof(raw));
char norm[40]; int nn = 0;
const char* p = raw;
while (*p == '#') ++p;
@@ -23158,11 +23229,8 @@ static void termDoSend(bool is_channel, const uint8_t* pub, int16_t chan_slot,
static uint8_t s_term_attempt = 4;
const char* sender = the_mesh.getNodePrefs()->node_name;
if (!sender || !sender[0]) sender = "me";
size_t tlen = strlen(text);
if (tlen > MAX_TEXT_LEN) tlen = MAX_TEXT_LEN;
char body[MAX_TEXT_LEN + 1];
memcpy(body, text, tlen);
body[tlen] = '\0';
arabicUnshape(text, body, sizeof(body)); // standard Arabic letters (#582), whole characters
uint32_t ts = the_mesh.getRTCClock()->getCurrentTimeUnique();
if (ts <= s_last_term_tx_ts) ts = s_last_term_tx_ts + 1;
@@ -23687,7 +23755,7 @@ static void webPushContacts() {
i, t, (c.type == ADV_TYPE_REPEATER) ? 1 : 0, (c.type == ADV_TYPE_ROOM) ? 1 : 0,
webSnapContains(fav, nfav, c.id.pub_key) ? 1 : 0,
webSnapContains(ign, nign, c.id.pub_key) ? 1 : 0,
(c.out_path_len == 0) ? 1 : 0, (long)c.gps_lat, (long)c.gps_lon,
(pathHops(c.out_path_len) == 0) ? 1 : 0, (long)c.gps_lat, (long)c.gps_lon,
(unsigned)c.last_advert_timestamp);
jsonEsc(p, e, c.name);
p += snprintf(p, e - p, "\"}");
@@ -26648,6 +26716,9 @@ static void openSignalInfoPopup() {
#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION)
static const char kReaderDefaultUrl[] = "wadamesh.com";
static const char kReaderHomeUrl[] = "sd:/home.htm";
// Online with no SD home page: what changed in each build, newest first (#622). The site
// generates it from release-notes/ (scripts/build/gen-changelog.py) at every deploy.
static const char kReaderChangelogUrl[] = "https://wadamesh.com/changelog.html";
static lv_obj_t* s_reader_root = nullptr;
static lv_obj_t* s_reader_url_ta = nullptr;
static lv_obj_t* s_reader_go = nullptr;
@@ -27295,6 +27366,41 @@ static char (*s_disc_row_name)[26] = nullptr; // PSRAM (see discoverRowsRead
static int s_disc_row_n = 0;
static void recolorEscape(char* dst, size_t cap, const char* src); // defined with the chat helpers
// Append `text` at body[o] tinted `rgb`, one recolor run per word, and return the new
// length. LVGL 8 starts every line with the recolor state cleared, so a run that wraps
// leaves its closing '#' to open a bogus command on the next line, which swallows text,
// colours and newlines (#602, reported by NewtsLament). A run holding no break character
// that fits the label can never wrap: the break characters (" -_/" in include/lv_conf.h,
// plus the LVGL defaults in case a stale config is in force) go between the runs, untinted,
// and a word wider than the label, or holding a '#', is written plain (escaped).
static int recolorWords(char* body, int o, int cap, uint32_t rgb, const char* text,
const lv_font_t* font, lv_coord_t max_w) {
static const char kBreaks[] = " -_/,.;:";
for (const char* p = text; *p;) {
if (strchr(kBreaks, *p)) {
if (o + 1 >= cap) break;
body[o++] = *p++;
continue;
}
const char* w = p;
while (*p && !strchr(kBreaks, *p)) ++p;
const int wl = (int)(p - w);
const bool hash = memchr(w, '#', (size_t)wl) != nullptr;
if (!hash && lv_txt_get_width(w, (uint32_t)wl, font, 0, LV_TEXT_FLAG_NONE) <= max_w) {
if (o + wl + 9 >= cap) break; // "#RRGGBB " + word + "#", whole or not at all
o += snprintf(body + o, (size_t)(cap - o), "#%06X %.*s#", (unsigned)(rgb & 0xFFFFFF), wl, w);
} else {
for (int i = 0; i < wl; ++i) {
if (o + 2 >= cap) break;
body[o++] = w[i];
if (w[i] == '#') body[o++] = '#';
}
}
}
if (o >= cap) o = cap - 1;
body[o] = '\0';
return o;
}
// Rebuild the feed label from the engine's _discover[] table, strongest-signal first.
// Discover's per-row snapshots are ~2.3 KB that only matter while the page is
@@ -27348,7 +27454,7 @@ static void discoverBuildFeed() {
char ago[10];
if (age < 60) snprintf(ago, sizeof ago, "%us", (unsigned)age);
else snprintf(ago, sizeof ago, "%um", (unsigned)(age / 60));
const char* direct = (h.path_len == 0) ? " #53C06B direct#" : "";
const char* direct = (pathHops(h.path_len) == 0) ? " #53C06B direct#" : "";
// Distance (#239): the DISCOVER_RESP wire format carries no position, but a
// node we've also heard an advert from has lat/lon on its contact entry —
// pair that with our own fix. Empty badge = one of the fixes is unknown.
@@ -30251,7 +30357,7 @@ static bool ctPassesFilter(const ContactInfo& c, bool is_fav, int fav_count, con
case 2u: if(is_rep) return false; break; // peers
case 3u: if(fav_count>0 && !is_fav) return false; break; // favorites
case 4u: if(!(c.gps_lat!=0 || c.gps_lon!=0)) return false; break; // has location
case 5u: if(c.out_path_len != 0) return false; break; // 0-hop / direct neighbors only
case 5u: if (pathHops(c.out_path_len) != 0) return false; break; // 0-hop / direct neighbors only
default: break; // 0 = all
}
if(needle && needle[0]){
@@ -34406,7 +34512,7 @@ static void renderMapMarkers() {
ContactInfo c;
if (!the_mesh.getContactByIdx(i, c)) continue;
if (c.gps_lat == 0 && c.gps_lon == 0) continue;
if (s_map_direct_only && c.out_path_len != 0) continue;
if (s_map_direct_only && pathHops(c.out_path_len) != 0) continue;
double mwx, mwy;
latLonToWorldPx((double)c.gps_lat / 1.0e6, (double)c.gps_lon / 1.0e6,
s_map_zoom, &mwx, &mwy);
@@ -34468,7 +34574,7 @@ static void renderMapMarkers() {
ContactInfo c;
if (!the_mesh.getContactByIdx(i, c)) continue;
if (c.gps_lat == 0 && c.gps_lon == 0) continue;
if (s_map_direct_only && c.out_path_len != 0) continue;
if (s_map_direct_only && pathHops(c.out_path_len) != 0) continue;
const double lat = (double)c.gps_lat / 1.0e6;
const double lon = (double)c.gps_lon / 1.0e6;
@@ -39210,7 +39316,7 @@ static void openMessageInfoPopup(int msg_idx) {
blen += snprintf(body + blen, sizeof(body) - blen, "\nRoute");
int off = 0;
for (uint8_t h = 0; h < cnt && off + (int)hsz <= m.in_path_n; ++h, off += hsz) {
if (blen >= (int)sizeof(body) - 72) break; // hard guard: never overflow body[]
if (blen >= (int)sizeof(body) - 128) break; // hard guard: never overflow body[]
// Always lead with the path-hash prefix, then the name when we know it.
// The hash is what the node is actually keyed by on the wire, so it is
// the part that stays true when a repeater is named misleadingly or two
@@ -39220,12 +39326,14 @@ static void openMessageInfoPopup(int msg_idx) {
for (uint8_t b = 0; b < hsz && b < 4; ++b)
snprintf(hashstr + b * 2, sizeof(hashstr) - b * 2, "%02X", m.in_path[off + b]);
char nm[33]; // ContactInfo.name is 32B; hold the full name (emoji eat 4B each)
blen += snprintf(body + blen, sizeof(body) - blen, "\n %u. ", (unsigned)(h + 1));
if (the_mesh.uiHopName(&m.in_path[off], hsz, nm, sizeof(nm))) {
blen += snprintf(body + blen, sizeof(body) - blen, "\n %u. #%06X %s %s#",
(unsigned)(h + 1), (unsigned)nodeSigColorHex(nm), hashstr, nm);
char nm_c[33], words[48];
copyUtf8ReplacingMissingGlyphs(nullptr, nm_c, sizeof nm_c, nm);
snprintf(words, sizeof words, "%s %s", hashstr, nm_c);
blen = recolorWords(body, blen, (int)sizeof(body), nodeSigColorHex(nm), words, &g_font_12, card_w - 28);
} else {
blen += snprintf(body + blen, sizeof(body) - blen, "\n %u. #%06X %s#",
(unsigned)(h + 1), (unsigned)nodeSigColorHex(hashstr), hashstr);
blen = recolorWords(body, blen, (int)sizeof(body), nodeSigColorHex(hashstr), hashstr, &g_font_12, card_w - 28);
}
}
}
@@ -39255,7 +39363,7 @@ static void openMessageInfoPopup(int msg_idx) {
// Bounded (<= ECHO_MAX_HOPS) + hard buffer guard + read-only lookup -> no risk.
uint8_t rhc = the_mesh.uiRepeatHopCount(m.sent_fp);
for (uint8_t r = 0; r < rhc; r++) {
if (blen >= (int)sizeof(body) - 96) break; // escaped name (<=64B) + markers must fit whole
if (blen >= (int)sizeof(body) - 128) break; // the name's runs + markers must fit whole
uint8_t hh[4];
uint8_t hsz = the_mesh.uiRepeatHop(m.sent_fp, r, hh, sizeof(hh));
if (hsz == 0) continue;
@@ -39263,17 +39371,15 @@ static void openMessageInfoPopup(int msg_idx) {
for (uint8_t b = 0; b < hsz && b < 4; ++b)
snprintf(hashstr + b * 2, sizeof(hashstr) - b * 2, "%02X", hh[b]);
char nm[33]; // ContactInfo.name is 32B; hold the full name (emoji eat 4B each)
blen += snprintf(body + blen, sizeof(body) - blen, "\n ");
if (the_mesh.uiHopName(hh, hsz, nm, sizeof(nm))) {
// #223 class: this name sits inside a '#RRGGBB …#' run — a literal '#' or a
// broken UTF-8 tail in it desyncs every colour block after this line.
char nm_c[33], nm_esc[66];
// #223 class: a broken UTF-8 tail in a name desyncs every colour block after
// it; recolorWords() leaves a word with a '#' untinted (and escaped).
char nm_c[33];
copyUtf8ReplacingMissingGlyphs(nullptr, nm_c, sizeof nm_c, nm);
recolorEscape(nm_esc, sizeof nm_esc, nm_c);
blen += snprintf(body + blen, sizeof(body) - blen, "\n #%06X %s#",
(unsigned)nodeSigColorHex(nm), nm_esc); // name resolved -> no redundant hash
blen = recolorWords(body, blen, (int)sizeof(body), nodeSigColorHex(nm), nm_c, &g_font_12, card_w - 28); // name resolved -> no redundant hash
} else
blen += snprintf(body + blen, sizeof(body) - blen, "\n #%06X %s#",
(unsigned)nodeSigColorHex(hashstr), hashstr);
blen = recolorWords(body, blen, (int)sizeof(body), nodeSigColorHex(hashstr), hashstr, &g_font_12, card_w - 28);
}
}
if (blen >= (int)sizeof(body)) blen = sizeof(body) - 1;
@@ -43419,14 +43525,15 @@ static bool pagerChatComposerNav(bool up) {
return false;
}
if (!up) {
// Send (→, the cluster's right edge) + NEXT: hop to the top-bar items
// (the channel-settings gear). DM chats collect no bar items, so settle
// on the newest bubble instead of the default wrap-to-top.
if (g_statusbar.chan_gear && lv_obj_is_valid(g_statusbar.chan_gear) &&
!lv_obj_has_flag(g_statusbar.chan_gear, LV_OBJ_FLAG_HIDDEN)) {
s_nav_show = true;
lv_group_focus_obj(g_statusbar.chan_gear);
return true;
// Send (→, the cluster's right edge) + NEXT: hop to the top-bar item that opens
// the chat settings: the gear where it shows, else the conversation header.
// Without either, settle on the newest bubble instead of the default wrap-to-top.
for (lv_obj_t* bar : { g_statusbar.chan_gear, g_statusbar.chat_head }) {
if (bar && lv_obj_is_valid(bar) && !lv_obj_has_flag(bar, LV_OBJ_FLAG_HIDDEN)) {
s_nav_show = true;
lv_group_focus_obj(bar);
return true;
}
}
if (lv_obj_t* b = pagerChatBottomBubble(cp)) { s_nav_show = true; lv_group_focus_obj(b); return true; }
return false;
@@ -45040,13 +45147,17 @@ static bool s_bg_live = false; // the hook paints the background: the sky, or
#if CAP_BG_IMAGE
static lv_color_t* s_bg_img = nullptr; // the picture behind every screen, fitted and dimmed (bgImageLoad)
static int s_bg_w = 0, s_bg_h = 0;
static bool s_bg_sky = false; // s_bg_img is the still sky drawn once (auroraBoot), not a picture
#endif
static bool auroraFill(lv_draw_ctx_t* ctx, const lv_draw_rect_dsc_t* dsc, const lv_area_t* coords) {
lv_area_t clip;
if (!_lv_area_intersect(&clip, coords, ctx->clip_area)) return true;
#if CAP_BG_IMAGE
const lv_color_t* pic = s_bg_img;
if (pic && (clip.x2 >= s_bg_w || clip.y2 >= s_bg_h)) return false; // the screen changed size: plain fill
if (pic && (clip.x2 >= s_bg_w || clip.y2 >= s_bg_h)) { // the screen changed size
if (!s_bg_sky) return false; // a picture: plain fill
pic = nullptr; // the sky: worked out, as without the copy
}
#else
const lv_color_t* pic = nullptr;
#endif
@@ -45376,6 +45487,23 @@ static void auroraBoot() {
if (!s_aur_grid) return;
memcpy(s_aur_shown, s_aur_grid, (size_t)s_aur_gw * s_aur_gh * 3 * sizeof(uint16_t));
s_aur_on = true;
#if CAP_BG_IMAGE
// A still sky never changes: drawn once into PSRAM, each frame after copies its
// rows, several times cheaper than working every pixel out again (and the very
// same pixels). The picture path does the copying; boards with the picture option
// have the PSRAM for it.
#if defined(DOC_CAPTURE) && defined(BENCH_NO_SKY_CACHE)
if (false) { // perf benchmark builds only: the sky worked out every frame
#else
if (s_bg_kind == TOUCH_BG_AURORA && heap_caps_get_total_size(MALLOC_CAP_SPIRAM) >= (4u << 20)) {
#endif
lv_color_t* sky = (lv_color_t*)psAlloc((size_t)s_aur_w * s_aur_h * sizeof(lv_color_t));
if (sky) {
for (int y = 0; y < s_aur_h; ++y) auroraRow(sky + (size_t)y * s_aur_w, 0, y, s_aur_w);
s_bg_img = sky; s_bg_w = s_aur_w; s_bg_h = s_aur_h; s_bg_sky = true;
}
}
#endif
}
s_bg_live = true;
s_aur_rect = c->draw_rect; c->draw_rect = auroraDrawRect;
@@ -55352,7 +55480,7 @@ static bool hwNearest(uint8_t key4[4], int8_t& snr_q4, int8_t& rssi, uint32_t& m
const int dn = the_mesh.discoverCount();
for (int i = 0; i < dn; ++i) {
MyMesh::DiscoverHit h;
if (the_mesh.discoverGet((uint8_t)i, h) && h.path_len == 0) consider(h.pubkey, h.our_snr_q4, h.our_rssi, h.last_ms);
if (the_mesh.discoverGet((uint8_t)i, h) && pathHops(h.path_len) == 0) consider(h.pubkey, h.our_snr_q4, h.our_rssi, h.last_ms);
}
return have;
}
@@ -57751,7 +57879,7 @@ static void hwRadarNodes() {
s_hw_radar_geo = true;
} else {
n.bearing = (int16_t)((c.id.pub_key[0] | (c.id.pub_key[1] << 8)) % 360);
const int hops = c.out_path_len == 0xFF ? 3 : LV_MIN(3, (int)c.out_path_len);
const int hops = c.out_path_len == 0xFF ? 3 : LV_MIN(3, (int)pathHops(c.out_path_len));
n.dist = (int16_t)(300 + hops * 220);
}
if (n.dist > 1000) n.dist = 1000;
@@ -58919,16 +59047,20 @@ static void refreshStatusLabels() {
if (s_reader_ok) { // …with a page
readerRenderBody();
readerSetAddrExpanded(false); // collapse the address bar -> fullscreen reading
} else if (s_reader_home_missing) { // no optional SD home page: retain the normal URL-entry UX
} else if (s_reader_home_missing) { // no optional SD home page
s_reader_home_missing = false;
s_reader_hist_n = 0; s_reader_hist_pos = -1;
s_reader_url[0] = 0;
if (s_reader_text) s_reader_text[0] = 0;
s_reader_nlinks = 0;
if (s_reader_url_ta) lv_textarea_set_text(s_reader_url_ta, kReaderDefaultUrl);
s_reader_pristine = true;
readerRenderBody();
readerSetAddrExpanded(true);
if (WiFi.status() == WL_CONNECTED) {
readerStart(kReaderChangelogUrl); // online: the changelog (#622)
} else { // offline: the normal URL-entry UX
if (s_reader_url_ta) lv_textarea_set_text(s_reader_url_ta, kReaderDefaultUrl);
s_reader_pristine = true;
readerRenderBody();
readerSetAddrExpanded(true);
}
} else { // …with an error — SHOW it (don't sit on "Loading…")
readerShowMessage(s_reader_msg[0] ? s_reader_msg : "Couldn't load page", 0xE0A0A0);
readerSetAddrExpanded(true); // expand so the address bar + retry are reachable
@@ -60245,7 +60377,11 @@ static void accentResetCb(lv_event_t* e) {
if (lv_event_get_code(e) == LV_EVENT_CLICKED) accentSetSelection(0x15B6A6u, true);
}
static void openAccentPicker() {
// accentPickerClose() clears s_picker_for_bg, which the Theme page's background
// colour sets just before calling this: keep it, or that picker edits the accent.
const bool for_bg = s_picker_for_bg;
accentPickerClose();
s_picker_for_bg = for_bg;
const lv_coord_t sw = lv_disp_get_hor_res(nullptr);
const lv_coord_t sh = lv_disp_get_ver_res(nullptr);
s_accent_picker = lv_obj_create(lv_layer_top());
@@ -61646,13 +61782,13 @@ static void regionsScanTimerCb(lv_timer_t*) {
for (int i = 0; i < contact_count; ++i) {
ContactInfo contact;
if (the_mesh.getContactByIdx((uint32_t)i, contact) &&
contact.type == ADV_TYPE_REPEATER && contact.out_path_len == 0)
contact.type == ADV_TYPE_REPEATER && pathHops(contact.out_path_len) == 0)
add_repeater(contact.id.pub_key);
}
const uint8_t count = the_mesh.discoverCount();
for (uint8_t i = 0; i < count && s_region_scan_count < REGION_SCAN_MAX_REPEATERS; ++i) {
MyMesh::DiscoverHit hit;
if (!the_mesh.discoverGet(i, hit) || hit.node_type != ADV_TYPE_REPEATER || hit.path_len != 0)
if (!the_mesh.discoverGet(i, hit) || hit.node_type != ADV_TYPE_REPEATER || pathHops(hit.path_len) != 0)
continue;
add_repeater(hit.pubkey);
}
@@ -63192,7 +63328,7 @@ void UITask::discoveredContact(const ContactInfo& contact, bool is_new, uint8_t
// (0 = off). Drop the advert and remove any existing entry for this node.
{
const uint8_t maxhops = touchPrefsGetDiscoveredMaxHops();
if (maxhops > 0 && path_len > maxhops) {
if (maxhops > 0 && pathHops(path_len) > maxhops) {
for (int i = 0; i < DISCOVERED_MAX; ++i)
if (s_discovered[i].used &&
memcmp(s_discovered[i].ci.id.pub_key, contact.id.pub_key, PUB_KEY_SIZE) == 0)
@@ -65632,6 +65768,9 @@ void luaHostRequestSendPerm(const char* app_id, const char* app_name) {
bool luaHostMeshSendDM(const char* to_name, const char* text, bool* was_room) {
if (was_room) *was_room = false;
if (!to_name || !*to_name || !text || !*text) return false;
char unshaped[MAX_TEXT_LEN + 1]; // an app's text field shapes Arabic too (#582)
arabicUnshape(text, unshaped, sizeof(unshaped));
text = unshaped;
ContactInfo* by = nullptr;
const uint32_t n = the_mesh.getNumContacts();
for (uint32_t i = 0; i < n; ++i) {
@@ -65692,6 +65831,9 @@ void luaHostRequestDmSendPerm(const char* app_id, const char* app_name) {
bool luaHostMeshSendChannel(const char* chan_name, const char* text) {
if (!chan_name || !*chan_name || !text || !*text) return false;
char unshaped[MAX_TEXT_LEN + 1]; // an app's text field shapes Arabic too (#582)
arabicUnshape(text, unshaped, sizeof(unshaped));
text = unshaped;
for (int i = 0; i < MAX_GROUP_CHANNELS; i++) {
ChannelDetails cd;
if (!the_mesh.getChannel(i, cd) || !cd.name[0]) continue;
@@ -65980,7 +66122,7 @@ int luaHostDiscoverAt(int idx, char* pk_hex, size_t pk_cap, char* name, size_t n
// The reverse link: how well THEY heard US. This is the half a wardriving
// survey cannot get any other way, and it is why a probe beats listening.
*their_snr = (float)h.their_snr_q4 / 4.0f;
*hops = h.path_len;
*hops = pathHops(h.path_len);
const uint32_t now = millis();
*first_ms_ago = now - h.first_ms;
*last_ms_ago = now - h.last_ms;
@@ -68047,11 +68189,10 @@ bool UITask::sendComposerToActiveThread(const char* override_text) {
const char* text = (override_text && override_text[0]) ? override_text : _compose_buf;
if (!text[0]) return false;
const char* sender = (_node_prefs && _node_prefs->node_name[0]) ? _node_prefs->node_name : "me";
size_t tlen = strlen(text);
if (tlen > MAX_TEXT_LEN) tlen = MAX_TEXT_LEN;
// Standard Arabic letters, not the field's presentation forms (#582), cut to
// MAX_TEXT_LEN on a whole character.
char truncated[MAX_TEXT_LEN + 1];
memcpy(truncated, text, tlen);
truncated[tlen] = '\0';
arabicUnshape(text, truncated, sizeof(truncated));
if (_active_thread_is_channel) {
const char* thread_name = _ui_threads[_active_thread_idx].name;
@@ -68844,34 +68985,55 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
g_draw_buffer = (lv_color_t*)heap_caps_malloc(buf_bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!g_draw_buffer) g_draw_buffer = (lv_color_t*)malloc(buf_bytes);
#else
#if defined(HAS_RAK_TAP_V2) || defined(HAS_WIO_TRACKER_L2)
const int draw_band_w = 320;
#elif defined(HAS_CROWPANEL_35)
const int draw_band_w = 480;
// The draw band: what LVGL renders between two flushes. Every band is a whole walk of
// the object tree (~5 ms on the T-Deck), so a frame wants few of them: 12800 px
// (25.6 KB) is 40 rows of a 320-px screen, 6 bands a frame where a 240x24 band took
// 14. Bigger thrashes the S3's 32 KB data cache. That band lives in PSRAM: these
// displays are fed by the CPU (Adafruit's FIFO writes, LovyanGFX copying into its own
// DMA buffers, the Pager's pushColors), so it needs no DMA-capable RAM, and the
// internal RAM it used to take pays for the faster LVGL build (perf_lvgl.py). Measured
// on the T-Deck with the frame benchmark (2026-10-09), the same pixels. The V4 keeps
// its internal 240x24 band (2 MB of quad PSRAM is too little and too slow), and so does
// e-paper, which waits on the panel, not on drawing.
bool band_psram = false;
#if defined(DOC_CAPTURE) && defined(BENCH_BAND_W)
const int draw_band_w = BENCH_BAND_W; // perf benchmark builds only
g_draw_buf_px = draw_band_w * LV_DRAW_BUF_LINES;
#if defined(BENCH_BUF_PSRAM)
band_psram = true;
#endif
#elif (defined(HAS_TDECK_GT911) && !defined(HAS_TDECK_PRO)) || defined(HAS_THINKNODE_M9) || \
defined(ATTAKY_MESH_SERIES) || defined(HAS_RAK_TAP_V2) || defined(HAS_WIO_TRACKER_L2) || \
defined(HAS_CROWPANEL_35) || defined(TLORA_PAGER)
#if defined(HAS_CROWPANEL_35) || defined(TLORA_PAGER)
const int draw_band_w = 480;
#else
const int draw_band_w = 320;
#endif
g_draw_buf_px = 12800;
band_psram = true;
#elif defined(HELTEC_LORA_V4_R8)
// The R8's async flush byte-swaps each band into a 6144-px internal DMA buffer
// (LGFX_SWAP_BUF_PX) and the band has to fit it, so it stays 240x24; in PSRAM all the
// same, which gives its internal RAM to the faster LVGL build.
const int draw_band_w = 240;
g_draw_buf_px = draw_band_w * LV_DRAW_BUF_LINES;
band_psram = true;
#else
const int draw_band_w = 240;
#endif
const size_t buf_bytes = sizeof(lv_color_t) * draw_band_w * LV_DRAW_BUF_LINES;
// Internal DMA-capable DRAM — this is the hot loop's read source during
// SPI flush. The T-Pager is deliberately the exception: ST7796LCDDisplay
// uses synchronous pushColors (not DMA), while BLE needs this contiguous
// internal block later when a client connects and negotiates security.
#if defined(TLORA_PAGER)
g_draw_buffer = (lv_color_t*)heap_caps_malloc(
buf_bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!g_draw_buffer) {
g_draw_buffer = (lv_color_t*)heap_caps_malloc(
buf_bytes, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
const size_t buf_bytes = sizeof(lv_color_t) * (size_t)g_draw_buf_px;
// Everywhere else internal DMA-capable DRAM, the flush's read source. Either way the
// other kind is the fallback.
if (band_psram) {
g_draw_buffer = (lv_color_t*)heap_caps_malloc(buf_bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!g_draw_buffer)
g_draw_buffer = (lv_color_t*)heap_caps_malloc(buf_bytes, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
} else {
g_draw_buffer = (lv_color_t*)heap_caps_malloc(buf_bytes, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
if (!g_draw_buffer)
g_draw_buffer = (lv_color_t*)heap_caps_malloc(buf_bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
}
#else
g_draw_buffer = (lv_color_t*)heap_caps_malloc(
buf_bytes, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
if (!g_draw_buffer) {
g_draw_buffer = (lv_color_t*)heap_caps_malloc(
buf_bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
}
#endif
if (!g_draw_buffer) g_draw_buffer = (lv_color_t*)malloc(buf_bytes);
// Last-ditch under severe DRAM pressure — e.g. a unit whose PSRAM didn't
// init (some T-Deck clones are QSPI, not the expected OPI), so even SPIRAM
@@ -70035,8 +70197,8 @@ void UITask::setGpsPower(bool on) {
bool UITask::setNodeName(const char* s) {
if (!_node_prefs) return false;
if (!s) s = "";
strncpy(_node_prefs->node_name, s, sizeof(_node_prefs->node_name) - 1);
_node_prefs->node_name[sizeof(_node_prefs->node_name) - 1] = '\0';
// Adverts carry the name: standard Arabic letters, not presentation forms (#582).
arabicUnshape(s, _node_prefs->node_name, sizeof(_node_prefs->node_name));
// Report the real write result — the "Name saved" toast used to show even
// when the storage write silently failed.
return the_mesh.savePrefs();
@@ -75939,7 +76101,276 @@ static void docHomeAudit() {
}
#endif
#if defined(DOC_PERF_BENCH)
// Where a frame goes: CPU cycles inside the draw context's calls (rects, glyphs, images,
// the rest), wrapped around whatever is installed (the sky hooks or LVGL's own), and
// inside unpacking the compressed SemiBold glyphs. What is left is LVGL walking the
// object tree, styles and layout.
static uint64_t s_pf_rect = 0, s_pf_letter = 0, s_pf_img = 0, s_pf_other = 0, s_pf_unpack = 0;
static void (*s_pf_rect_fn)(lv_draw_ctx_t*, const lv_draw_rect_dsc_t*, const lv_area_t*) = nullptr;
static void (*s_pf_letter_fn)(lv_draw_ctx_t*, const lv_draw_label_dsc_t*, const lv_point_t*, uint32_t) = nullptr;
static void (*s_pf_img_fn)(lv_draw_ctx_t*, const lv_draw_img_dsc_t*, const lv_area_t*, const uint8_t*, lv_img_cf_t) = nullptr;
static void (*s_pf_line_fn)(lv_draw_ctx_t*, const lv_draw_line_dsc_t*, const lv_point_t*, const lv_point_t*) = nullptr;
static void (*s_pf_arc_fn)(lv_draw_ctx_t*, const lv_draw_arc_dsc_t*, const lv_point_t*, uint16_t, uint16_t, uint16_t) = nullptr;
static void (*s_pf_blend_fn)(lv_draw_ctx_t*, lv_draw_layer_ctx_t*, const lv_draw_img_dsc_t*) = nullptr;
static const uint8_t* (*s_pf_glyph_fn)(const lv_font_t*, uint32_t) = nullptr;
static void pfRect(lv_draw_ctx_t* c, const lv_draw_rect_dsc_t* d, const lv_area_t* a) {
const uint32_t t = ESP.getCycleCount(); s_pf_rect_fn(c, d, a); s_pf_rect += ESP.getCycleCount() - t;
}
static void pfLetter(lv_draw_ctx_t* c, const lv_draw_label_dsc_t* d, const lv_point_t* p, uint32_t l) {
const uint32_t t = ESP.getCycleCount(); s_pf_letter_fn(c, d, p, l); s_pf_letter += ESP.getCycleCount() - t;
}
static void pfImg(lv_draw_ctx_t* c, const lv_draw_img_dsc_t* d, const lv_area_t* a, const uint8_t* m, lv_img_cf_t cf) {
const uint32_t t = ESP.getCycleCount(); s_pf_img_fn(c, d, a, m, cf); s_pf_img += ESP.getCycleCount() - t;
}
static void pfLine(lv_draw_ctx_t* c, const lv_draw_line_dsc_t* d, const lv_point_t* a, const lv_point_t* b) {
const uint32_t t = ESP.getCycleCount(); s_pf_line_fn(c, d, a, b); s_pf_other += ESP.getCycleCount() - t;
}
static void pfArc(lv_draw_ctx_t* c, const lv_draw_arc_dsc_t* d, const lv_point_t* p, uint16_t r, uint16_t a0, uint16_t a1) {
const uint32_t t = ESP.getCycleCount(); s_pf_arc_fn(c, d, p, r, a0, a1); s_pf_other += ESP.getCycleCount() - t;
}
static void pfBlend(lv_draw_ctx_t* c, lv_draw_layer_ctx_t* l, const lv_draw_img_dsc_t* d) {
const uint32_t t = ESP.getCycleCount(); s_pf_blend_fn(c, l, d); s_pf_other += ESP.getCycleCount() - t;
}
static const uint8_t* pfGlyph(const lv_font_t* f, uint32_t letter) {
const uint32_t t = ESP.getCycleCount();
const uint8_t* r = s_pf_glyph_fn(f, letter);
s_pf_unpack += ESP.getCycleCount() - t;
return r;
}
static void pfInstall() {
lv_disp_t* d = lv_disp_get_default();
lv_draw_ctx_t* c = d && d->driver ? d->driver->draw_ctx : nullptr;
if (!c || s_pf_rect_fn) return;
s_pf_rect_fn = c->draw_rect; c->draw_rect = pfRect;
s_pf_letter_fn = c->draw_letter; c->draw_letter = pfLetter;
s_pf_img_fn = c->draw_img_decoded; c->draw_img_decoded = pfImg;
if (c->draw_line) { s_pf_line_fn = c->draw_line; c->draw_line = pfLine; }
if (c->draw_arc) { s_pf_arc_fn = c->draw_arc; c->draw_arc = pfArc; }
if (c->layer_blend) { s_pf_blend_fn = c->layer_blend; c->layer_blend = pfBlend; }
lv_font_t* semi[3] = { &g_font_semi_12, &g_font_semi_14, &g_font_semi_16 };
for (lv_font_t* f : semi) {
if (!s_pf_glyph_fn) s_pf_glyph_fn = f->get_glyph_bitmap;
if (f->get_glyph_bitmap == s_pf_glyph_fn) f->get_glyph_bitmap = pfGlyph;
}
}
static void pfReset() { s_pf_rect = s_pf_letter = s_pf_img = s_pf_other = s_pf_unpack = 0; }
static unsigned pfMs10(uint64_t cyc, int frames) { // tenths of a ms a frame
return (unsigned)(cyc * 10u / ((uint64_t)ESP.getCpuFreqMHz() * 1000u) / (uint64_t)frames);
}
// Frame cost of the screens people scroll, measured the same way every run: whole
// redraws, then scroll frames (12 px a frame, back and forth). Per screen: ms a frame,
// how much of it went to the display (flush), how many bands a frame took, and the
// drawing split (ms a frame).
static void benchReport(const char* what, uint32_t t0, int frames, uint32_t f0, uint32_t n0) {
const uint32_t ms = millis() - t0, fl = (s_dbg_flush_us - f0) / 1000u, n = s_dbg_flush_n - n0;
Serial.printf("DBG: BENCH %-20s %3u.%u ms/frame flush %3u.%u bands %u.%u\n", what,
(unsigned)(ms * 10 / frames / 10), (unsigned)(ms * 10 / frames % 10),
(unsigned)(fl * 10 / frames / 10), (unsigned)(fl * 10 / frames % 10),
(unsigned)(n * 10 / frames / 10), (unsigned)(n * 10 / frames % 10));
const unsigned r = pfMs10(s_pf_rect, frames), l = pfMs10(s_pf_letter, frames), u = pfMs10(s_pf_unpack, frames);
const unsigned i = pfMs10(s_pf_img, frames), o = pfMs10(s_pf_other, frames);
Serial.printf("DBG: BENCH %-20s rects %u.%u glyphs %u.%u (unpack %u.%u) images %u.%u other %u.%u\n", "",
r / 10, r % 10, l / 10, l % 10, u / 10, u % 10, i / 10, i % 10, o / 10, o % 10);
}
static void benchFull(const char* what, int frames) {
docSettle(4);
const uint32_t f0 = s_dbg_flush_us, n0 = s_dbg_flush_n, t0 = millis();
pfReset();
for (int k = 0; k < frames; ++k) { lv_obj_invalidate(lv_scr_act()); lv_refr_now(nullptr); }
benchReport(what, t0, frames, f0, n0);
}
static void benchScroll(const char* what, lv_obj_t* o, int frames) {
if (!o || !lv_obj_is_valid(o)) { Serial.printf("DBG: BENCH %-20s (not on screen)\n", what); return; }
docSettle(4);
lv_obj_update_layout(o);
if (lv_obj_get_scroll_bottom(o) < 30) lv_obj_scroll_to_y(o, 0, LV_ANIM_OFF);
const uint32_t f0 = s_dbg_flush_us, n0 = s_dbg_flush_n, t0 = millis();
pfReset();
for (int k = 0; k < frames; ++k) {
lv_obj_scroll_by(o, 0, (k / 10) % 2 ? 12 : -12, LV_ANIM_OFF);
lv_refr_now(nullptr);
}
benchReport(what, t0, frames, f0, n0);
lv_obj_scroll_to_y(o, 0, LV_ANIM_OFF);
lv_refr_now(nullptr);
}
static void docPerfBench() {
Serial.printf("DBG: BENCH heap internal %u free, %u largest; psram %u free\n",
(unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT),
(unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA),
(unsigned)heap_caps_get_free_size(MALLOC_CAP_SPIRAM));
Serial.printf("DBG: BENCH background %d, sky %s, band %u px, memcpy std %d\n", (int)s_bg_kind,
#if CAP_BG_IMAGE
s_bg_img ? "copied" : "drawn",
#else
"drawn",
#endif
(unsigned)g_draw_buf_px, (int)LV_MEMCPY_MEMSET_STD);
pfInstall();
navGoToMainTab(HOME_TAB_INDEX); setHomeDrawer(false); docSettle(10);
benchFull("home redraw", 10);
setHomeDrawer(true); docSettle(10);
benchScroll("drawer scroll", s_appdrawer_root, 20);
setHomeDrawer(false); docSettle(8);
navGoToMainTab(CHAT_INBOX_TAB_INDEX); docSettle(10);
benchFull("chats redraw", 10);
benchScroll("chats scroll", g_lv.dm.list_cont, 20);
{
const int chi = auditFindThread(true);
if (chi >= 0) {
openThreadDetailByIdx(chi, true); docSettle(16);
LvChatPanel* cp = navOpenChatPanel();
benchScroll("conversation scroll", cp ? cp->msgs : nullptr, 20);
if (cp) closeChatPanel(cp);
docSettle(8);
}
}
navGoToMainTab(CONTACTS_TAB_INDEX); docSettle(12);
benchScroll("contacts scroll", g_lv.contacts_list, 20);
navGoToMainTab(SETTINGS_TAB_INDEX); docSettle(10);
benchScroll("settings tab scroll", s_settings_landing, 20);
openSettingsCategory(CAT_THEME); docSettle(14);
benchScroll("theme page scroll", s_tp.page, 20);
closeSettingsCategory(); docSettle(8);
navGoToMainTab(HOME_TAB_INDEX); docSettle(8);
Serial.printf("DBG: BENCH loop stack low-water %u bytes\n",
(unsigned)(uxTaskGetStackHighWaterMark(nullptr) * sizeof(StackType_t)));
Serial.println("DBG: BENCH done");
}
#endif
#if defined(DOC_FIX_VERIFY)
// Capture builds only: print and photograph what the GitHub fixes of 2026-10-09 do on the
// device (#582 Arabic, #602 info colours, #623 keypad chat settings, background colour).
static void docVerifyHex(const char* tag, const char* t) {
char line[400]; int n = snprintf(line, sizeof line, "DBG: VERIFY %s:", tag);
const unsigned char* p = reinterpret_cast<const unsigned char*>(t);
while (*p && n < (int)sizeof(line) - 8) {
uint32_t cp = *p; int len = 1;
if (cp >= 0xF0) { cp = ((cp & 7) << 18) | ((p[1] & 63) << 12) | ((p[2] & 63) << 6) | (p[3] & 63); len = 4; }
else if (cp >= 0xE0) { cp = ((cp & 15) << 12) | ((p[1] & 63) << 6) | (p[2] & 63); len = 3; }
else if (cp >= 0xC0) { cp = ((cp & 31) << 6) | (p[1] & 63); len = 2; }
n += snprintf(line + n, sizeof(line) - n, " %04X", (unsigned)cp);
p += len;
}
Serial.println(line);
}
static void docFixVerify() {
navGoToMainTab(HOME_TAB_INDEX); docSettle(8);
// 1. Arabic keys (#582): the Shift layer, kaf, the symbols that used to type letters.
{
const char keys[] = "hHnNyYbBgGtTkKpP8?!@$xXzZ";
for (const char* k = keys; *k; ++k) {
const bool sh = (*k >= 'A' && *k <= 'Z');
const char* m = keyboardLayoutMapHwKey(KeyboardLayoutId::AR, *k, sh);
char tag[12]; snprintf(tag, sizeof tag, "key '%c'", *k);
docVerifyHex(tag, m ? m : "(passes through)");
}
}
// 2. Arabic shaping and the way back (#582): type into a field, read it, unshape it.
{
static const char* const kWords[] = {
"الآن", // الآن
"شيئا", // شيئا
"رئيس", // رئيس
"مدرسة بآ", // مدرسة بآ
"۰۱۲", // ۰۱۲
};
lv_obj_t* box = lv_obj_create(lv_layer_top());
lv_obj_set_size(box, lv_disp_get_hor_res(nullptr) - 20, 200);
lv_obj_align(box, LV_ALIGN_CENTER, 0, 10);
lv_obj_set_style_bg_color(box, lv_color_hex(0x101418), LV_PART_MAIN);
lv_obj_set_style_bg_opa(box, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_flex_flow(box, LV_FLEX_FLOW_COLUMN);
for (const char* w : kWords) {
lv_obj_t* ta = lv_textarea_create(box);
lv_textarea_set_one_line(ta, true);
lv_obj_set_width(ta, lv_pct(100));
lv_obj_set_style_text_font(ta, &g_font_16, LV_PART_MAIN);
lv_textarea_add_text(ta, w);
char un[96];
arabicUnshape(lv_textarea_get_text(ta), un, sizeof un);
docVerifyHex("typed ", w);
docVerifyHex("field ", lv_textarea_get_text(ta));
docVerifyHex("unshape", un);
Serial.printf("DBG: VERIFY round trip %s\n", strcmp(un, w) == 0 ? "OK" : "DIFFERS");
}
docSettle(6);
auditShot("verify_arabic");
lv_obj_del(box);
docSettle(4);
}
// 3. Info popup colours (#602): long names wrap, every run stays whole.
{
char body[700]; int bl = snprintf(body, sizeof body, "Route");
const char* names[] = { "A3 NL-GEE-RP02-Kluis-Omni-Repeater", "7F ON3MB TestRepeater Brussels Noord",
"C1 #hash in a name", "E2 AVeryLongRepeaterNameWithoutAnySeparatorsAtAll" };
for (int i = 0; i < 4; ++i) {
bl += snprintf(body + bl, sizeof(body) - bl, "\n %d. ", i + 1);
bl = recolorWords(body, bl, (int)sizeof body, nodeSigColorHex(names[i]), names[i], &g_font_12, 192);
}
Serial.printf("DBG: VERIFY info body: %s\n", body);
lv_obj_t* card = lv_obj_create(lv_layer_top());
lv_obj_set_size(card, 212, 200);
lv_obj_center(card);
lv_obj_set_style_bg_color(card, lv_color_hex(0x101418), LV_PART_MAIN);
lv_obj_t* l = lv_label_create(card);
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
lv_label_set_recolor(l, true);
lv_obj_set_width(l, 192);
lv_obj_set_style_text_font(l, &g_font_12, LV_PART_MAIN);
lv_obj_set_style_text_color(l, lv_color_hex(COLOR_TEXT), LV_PART_MAIN);
lv_label_set_text(l, body);
docSettle(6);
auditShot("verify_info_colours");
lv_obj_del(card);
docSettle(4);
}
// 4. Theme > Background > Colour opens the background picker, not the accent one.
{
openSettingsCategory(CAT_THEME); docSettle(10);
s_picker_for_bg = true; // what themeBgColorCb does before opening the picker
openAccentPicker(); docSettle(8);
Serial.printf("DBG: VERIFY bg picker: for_bg=%d\n", (int)s_picker_for_bg);
auditShot("verify_bg_picker");
accentPickerClose(); docSettle(4);
closeSettingsCategory(); docSettle(6);
}
#if CAP_KEYPAD_NAV
// 5. Keypad nav reaches the chat settings through the conversation header (#623).
{
const int chi = auditFindThread(true);
if (chi >= 0) {
openThreadDetailByIdx(chi, true); docSettle(14);
const bool was = s_kbd_nav;
s_kbd_nav = true; s_nav_dirty = true;
navMaybeRebuild(); docSettle(4);
bool found = false;
for (int i = 0; i < s_nav_count && i < kNavMax; ++i) if (s_nav_objs[i] == g_statusbar.chat_head) found = true;
Serial.printf("DBG: VERIFY keypad nav: chat_head %s (%d stops, head %s)\n", found ? "reachable" : "MISSING",
s_nav_count, g_statusbar.chat_head && !lv_obj_has_flag(g_statusbar.chat_head, LV_OBJ_FLAG_HIDDEN) ? "shown" : "hidden");
if (found) { s_nav_show = true; lv_group_focus_obj(g_statusbar.chat_head); docSettle(6); auditShot("verify_chat_head_focus"); }
s_kbd_nav = was; s_nav_dirty = true;
if (LvChatPanel* cp = navOpenChatPanel()) closeChatPanel(cp);
docSettle(8);
} else Serial.println("DBG: VERIFY keypad nav: no channel to open");
}
#endif
navGoToMainTab(HOME_TAB_INDEX); docSettle(6);
Serial.println("DBG: VERIFY done");
}
#endif
static void docAuditFocus() {
#if defined(DOC_FIX_VERIFY)
docFixVerify();
return;
#endif
#if defined(DOC_PERF_BENCH)
docPerfBench();
return;
#endif
#if defined(DOC_HOME_AUDIT)
docHomeAudit();
return;
+2
View File
@@ -14,6 +14,8 @@ python3 "$(cd "$(dirname "$0")/.." && pwd)/scripts/build/pre_gen_baked.py"
# anim_timer use-after-free fix (#428) still needs it. Idempotent; fails on drift.
python3 "$(cd "$(dirname "$0")/.." && pwd)/scripts/build/patch_lvgl_anim_uaf.py" \
--patch-file "$(cd "$(dirname "$0")" && pwd)/components/lvgl/upstream/src/misc/lv_anim.c"
python3 "$(cd "$(dirname "$0")/.." && pwd)/scripts/build/patch_lvgl_arabic.py" \
--patch-file "$(cd "$(dirname "$0")" && pwd)/components/lvgl/upstream/src/misc/lv_txt_ap.c"
cd "$(dirname "$0")"
export IDF_TOOLS_PATH="$PWD/esp-idf-tools"
# VS Code may launch this wrapper from PlatformIO's virtualenv. Pin the
+2
View File
@@ -29,6 +29,8 @@ mkdir -p components/lvgl
cp -R "$LIBDEPS/lvgl" components/lvgl/upstream
python3 ../scripts/build/patch_lvgl_anim_uaf.py \
--patch-file components/lvgl/upstream/src/misc/lv_anim.c
python3 ../scripts/build/patch_lvgl_arabic.py \
--patch-file components/lvgl/upstream/src/misc/lv_txt_ap.c
echo "vendored: lvgl ($(find components/lvgl/upstream/src -name '*.c' | wc -l | tr -d ' ') c)"
# --- Arduino libraries: ALL go into ONE 'ardlibs' component so inter-library #includes
+2
View File
@@ -60,6 +60,8 @@ python3 "$(cd "$(dirname "$0")/.." && pwd)/scripts/build/pre_gen_baked.py"
# anim_timer use-after-free fix (#428) still needs it. Idempotent; fails on drift.
python3 "$(cd "$(dirname "$0")/.." && pwd)/scripts/build/patch_lvgl_anim_uaf.py" \
--patch-file "$(cd "$(dirname "$0")" && pwd)/components/lvgl/upstream/src/misc/lv_anim.c"
python3 "$(cd "$(dirname "$0")/.." && pwd)/scripts/build/patch_lvgl_arabic.py" \
--patch-file "$(cd "$(dirname "$0")" && pwd)/components/lvgl/upstream/src/misc/lv_txt_ap.c"
cd "$PROJECT_DIR"
export IDF_TOOLS_PATH="$PWD/esp-idf-tools"
# VS Code may launch this wrapper from PlatformIO's virtualenv. Pin the
+2
View File
@@ -31,6 +31,8 @@ mkdir -p components/lvgl
cp -R "$LIBDEPS/lvgl" components/lvgl/upstream
python3 ../scripts/build/patch_lvgl_anim_uaf.py \
--patch-file components/lvgl/upstream/src/misc/lv_anim.c
python3 ../scripts/build/patch_lvgl_arabic.py \
--patch-file components/lvgl/upstream/src/misc/lv_txt_ap.c
echo "vendored: lvgl ($(find components/lvgl/upstream/src -name '*.c' | wc -l | tr -d ' ') c)"
# --- RadioLib: the P4 drives a RAW SX1262 through the core's RadioLib path (unlike the