Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d0301b80a |
No files matched your search
@@ -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
|
||||
@@ -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
@@ -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 · 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 — 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);
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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).
|
||||
@@ -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()
|
||||
@@ -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")))
|
||||
@@ -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>"
|
||||
)
|
||||
@@ -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 ""))
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user