The worker's 8 KB internal stack was only ever claimed lazily, the first
time the map or store asked for it — by which point the Wi-Fi driver has
taken the internal heap. Its own comment always said 'allocated at
UITask::begin() time' and explained why (grab it before Wi-Fi associates,
then it stays ours); that call site never existed. Measured on the V4:
68 KB free / 59 KB largest block at end of boot, versus 664 bytes free and
a 4-byte largest block once Wi-Fi was up — so tiles, the update check and
the app catalog were all dead there, not just the store.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every gutter in the store was a constant tuned on the T-Deck: a 100 px
button reserve and a 150 px name column leave a 240 px V4 / M9 panel about
90 px of text, so app titles and language names dot-truncated to nothing.
The action buttons, their gutters and the language row's name column now
scale below 260 px (62/12 and 108 instead of 78/22 and 150), and the tab
labels dot-truncate inside their cell instead of overflowing it — a
translated 'Languages' does not fit a 72 px cell in every language.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
'Remove channel' was built by mk_full(), a local row helper my scanner did
not know about. Listing the functions that take UI text has the same blind
spot as grepping for the strings themselves: the one helper nobody wrote
down keeps its labels English forever.
So the scanner gained a --wide mode that inverts the question — every prose
literal in the file is suspect unless it is consumed by a known NON-UI sink
(logging, strcmp, paths, Lua glue, format-only args). That surfaced the
whole family of local builders: mk_full, mk_btn, mk_label, mk_ta, mk_switch,
setupHeader, setupBtn, fmActionBtn, ccToggle, make_launcher,
setAddChannelError. --fix now wraps prose in ANY argument of those helpers
rather than a hardcoded index, so their differing signatures don't matter.
103 calls wrapped: the channel sheet (Remove channel, Delete chat), every
contact action (Open chat, Ping, Message, Telemetry, Trace SNR, Admin,
Range test, Sightline, Block, Favorite, Reset path), the Profile/Radio
settings rows, auto-add and experimental switches, the add/join channel
menu and its errors, file-manager actions, control-center chips, the
launcher tiles and the whole first-boot wizard. 40 new keys translated in
all 13 languages; units and protocol tokens (MHz, SF, CR, TX, AF, BT, GPS,
DND, Wi-Fi) map to themselves. Files at ver 7, both tools green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
My earlier sweeps searched for strings I could think of, which is why you
kept finding more. scripts/build/i18n-scan-unwrapped.py now parses every
call that takes user text — LVGL setters (label/checkbox/dropdown/roller/
placeholder/tab/table/msgbox), showAlert, showConfirm, appPageBegin,
createSettingsModal and the settings row helpers — splits the argument list
and reports any human-readable literal not wrapped in TR(). It found 55;
49 got wrapped (--fix does it mechanically), 6 skipped as brand/technical
(WADAMESH, CPU, PSK, OK, and two unit readouts). It now reports zero.
The 33 newly reachable keys are translated in all 13 languages: the Flood/
Zero-hop scope buttons, Share QR, Search contacts, Play / Set as
notification sound / Set as wallpaper, Probe now, On map, Install update,
Save update bin to SD, Choose .wav from files, Power, Clear history, the
Bluetooth/MQTT/Logs/System info sheet titles, both dropdown option lists,
the size preset list, Import, the URL bar, and Snake's New game.
Two format bugs behind the last stragglers: keys whose text begins with
LVGL recolor markup ('#7A7F87 Wardrive: ...#') were dropped as comment
lines by BOTH the device loader and the audit — a line is a comment only
when it has no tab, fixed in both. And several rows had been written with
literal '\xE2\x80\xA6' text where the file needs the real character; the
loader only unescapes \n \t \\, so those keys never matched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three real bugs behind the endless 'Download failed', found by putting a
diagnostic on the device instead of guessing:
1. HTTP read quit early. The loop treated a gap in the stream as
end-of-data, so a 49 KB file came back as 16 KB (n=16492 measured) and,
worse, that truncated buffer was returned as success. It now reads to
Content-Length, gives stalls a 4 s progress grace, and FAILS on a short
read instead of handing back half a file.
2. SD writes from PSRAM died after the first 4 KB (wr=4096, measured twice).
Writes now go through a small internal-RAM bounce buffer, which is what
every proven-good SD writer in this firmware already does. Full 49344
bytes written and installed, confirmed on the bench.
3. A truncated write still stamped the new '# ver:' (it is the file's FIRST
line) while the appended rows were lost — so the tab claimed v4, offered
no Update, and showed English. Downloads are atomic now: temp file,
verify on-card size, then rename over the target. A failure leaves the
old file and its old version untouched.
Plus: if a /lang write fails, an identical control write to /apps proves
whether the card is healthy, and if so /lang is rebuilt and the install
retried — self-healing the FAT damage that mid-write resets can leave.
Language changes now show 'Restarting to apply the language...' and reboot
on a timer, so the notice actually paints. showAlert() followed immediately
by rebootDevice() never rendered a frame — the device just went dark under
your finger. Applies to Update, Use, and the built-in picker alike.
The boot self-heal no longer toasts its failures (that was the 'download
failed' that greeted you when merely opening the store). Files at ver 5.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The update download was actually succeeding: one 48 KB f.write on FAT can
return a short count without error, so the file landed while the code
reported failure — which also suppressed the reboot-to-apply (Kaj saw the
new version only after re-entering the tab and switching languages by
hand). The write is chunked with a real completion check now, and
s_langdl_ok is volatile.
The audit's TR() extractor required the argument to start with a quote, so
every TR(LV_SYMBOL_X " text") call — all the sheet rows wrapped in the
previous commit — was invisible to it, and 'full coverage' was reported
while 16 keys had no rows anywhere: the chat/channel sheet actions (Mark
as read, Share secret, Region & scope, Chat icon, Delete history, Log in
again, Reset path), contacts overflow (Search, Auto-add settings, Blocked
list), URL menu (Open in web, Create QR), power menu (Power off, Download
mode), plus '(device behind)' and 'Copy internal data to SD'. The regex
now accepts the macro prefix and strips glyphs exactly like runtime TR().
All 16 translated in all 13 languages; files at ver 4, audit green at 731
keys per file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each .lang carries a '# ver:' header (missing = v1) and langs.json carries
the catalog ver. An installed file whose ver differs from the catalog shows
a blue Update on its row (sub line shows 'code.lang vN'); updating the
ACTIVE language reboots on success to apply the fresh overlay, updating an
inactive one just refreshes the row. Downloads go to the immutable
/apps/lang/<ver>/<code>.lang path so the day-long edge cache can never
serve a stale file after a translation bump — same lesson as the flasher's
mutable-latest cache bite. The boot self-heal resolves the ver from the
catalog on the worker (flat-path fallback) and never reboots on its own.
Canonical files bumped to ver 2 (the full-coverage sweep).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 727-row / 13-column kI18n table no longer compiles into the firmware.
TR() now consults only the loaded .lang overlay (bsearch) and falls back to
English. deploy/apps/lang/*.lang is the canonical translation source —
translators edit those files and PR them; scripts/build/i18n-audit.py
(replacing the retired generator) verifies every TR() key in the source is
covered by every language file, byte-accurate against C escapes. Reclaims
~400 KB of flash on every board: T-Deck 85.1->75.3%, V4 81.4->71.6%,
Pager 87.7->77.8%.
The sweep: 94 keys had no translations at all — everything Remote/web
access, the P4 antenna dialogs, history-limit texts, DND, keyboard-light
toasts, GPS/clock states and all the new Store/Language strings. All are
translated in all 13 languages now; the audit reports full coverage (707
keys per file).
Migration: a built-in language picked before this claims its file code at
boot; if the file is missing the download self-heals in the background
(that boot runs English, the next applies it) and retries every boot until
it lands. Wi-Fi-less devices stay English until they get one chance to
fetch — the honest cost of un-baking 400 KB.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
appPageBeginSlim: same '< title' + tap-to-close contract as appPageBegin,
but the status bar stays ONE line — no double-height glass row. The store
uses it; its tab bar was sitting under the glass row before. The tall
treatment is forced per-tick off s_apppage_title, so slim is a real flag
in that decision, not a one-shot.
The 'N apps / M installed' strip and its refresh button are gone — the
catalog auto-fetches on open, so the strip only cost a row.
Settings > Language now opens the Lua Store on the Languages tab (the old
picker page stays for non-Lua boards and the docs tour). The tab itself
follows the files-are-languages model: Installed = English + downloaded
.lang files (plus a read-only row for a legacy built-in pick so the active
choice stays visible), Available = the catalog. The 13 baked-in columns no
longer appear as choices — they only back partial files as the fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Lua Store is now a segmented three-tab page. Apps keeps the catalog and
sideloaded scripts; Built-in holds the show/hide switches; Languages is new.
Opening is instant: the card re-listing that ran synchronously on open now
runs on the net worker (request/done flags like the catalog), and the app
cache pre-warms at boot. Card text no longer runs under the action button —
title and description both stop at the button column.
Languages as files: every translation column of the i18n table exports to a
.lang file (scripts/build/gen-lang-files.py -> apps/lang/<code>.lang on the
firmware host + langs.json catalog). The device downloads them from the
Languages tab into <data>/lang/, and at boot the active file loads into
PSRAM and overlays TR() lookups via binary search — the file wins, then the
built-in ui_lang column, then English, so a partial or hand-edited file
degrades gracefully. The file's '# base:' header names that fallback column.
Users can drop their own .lang files on the card (or edit a downloaded one)
for languages the firmware never shipped; switching reboots to apply, same
as the Settings picker (which now also clears the file overlay). Prefs v48
appends the active file code, trailing per the schema law.
This is stage one of un-baking translations from the image: the mechanism,
catalog and files ship now while the built-in table stays as the fallback;
the flash win lands when the table columns are dropped after field proof.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
openAppDrawer() called luaStoreScanInstalled(), which lists <root>/apps and
reads every manifest — SD I/O on the UI thread, on every single open, and
again each time the Store closed over the drawer. That is the lag. The scan
is cached now and only forced where the on-disk set can actually have
changed: opening the Store (picks up sideloaded files), installing, removing
and the refresh button. A failed scan stays invalid so it retries.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two reasons it still showed. The settings landing is built ONCE at boot, so
the build-time skip never re-ran after a toggle; each category card is now
remembered and shown/hidden live (flex closes the gap), applied from the
Store switch, the long-press hide and the build itself. And the 'hidden by
default' only fired for blobs older than v46 — anyone already on v46 (every
current tester) kept app_hide=0, so a v47 one-time flip sets the MQTT bit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Long-pressing a hideable built-in tile (Spectrum, Discover, VNC, Remote,
Web, Terminal, Files, Signal, Mentions) now offers Hide with the same
confirm sheet installed Lua apps use for Remove; hiding sets the same bit
the Lua Store's built-in switches toggle, so the two views agree. Core
tiles (Chats/Contacts/Map/Settings/...) stay unhideable.
Store cards: name and version share the title line and the description
gets its own full-width wrapped line — it was previously squeezed beside
the version badge into an unreadable sliver.
The MQTT bridge joins the hideable list (it is a Settings section, so the
bit gates that section) and ships hidden by default on new installs and on
the v46 migration: it is experimental and forwards message content to a
broker, so it should be opt-in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Catalog rows are cards now (icon chip with the app initial, name, version
badge that turns accent when installed, wrapped description, rounded action
button tinted by action: teal Get, blue Update, red Remove). Section
headers are small-caps accent. A header strip shows 'N apps / M installed'
with a refresh control that re-fetches the catalog on demand — before this
the list was only ever fetched once per session. Loading and unavailable
states are proper cards explaining what to do instead of one dim line, and
sideloaded apps get their own 'Your own apps' section.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Kaj confirmed the 1.3 ports stand on their own, so the built-in versions
are gone, not gated: ~320 lines of monitor page + helpers removed from
UITask.cpp, ChannelUtil.cpp/.h deleted, and every reference with them —
drawer tiles, APPACT_MONITOR/APPACT_AIRTIME actions, dispatch cases, the
popup-registry row, anyPopupOpen and tab-lock checks, the Store's built-in
hide toggles, and their doc-capture shot (Lua apps stay out of the tour;
the tour itself gets revisited separately).
Also new in the catalog: Nodes (contacts by last-heard with freshness
colours) and Breakout (canvas + per-frame tick + drag-to-move paddle).
Five apps now. V4 reclaims ~4 KB, T-Deck ~6 KB.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The row anchored the name to the top and the description to the bottom of
a fixed 52px box, so on the T-Deck's line heights they overlapped. Both are
now positioned from measured line heights with the row sized to fit, and
the title is width-capped clear of the action button.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two API gaps the ports exposed: wada.ui.scroll(true) makes the app body
scroll vertically (games leave it off so swipes still steer), and
wada.ui.text_h(size) returns the actual font line height — the overlapping
labels came from stacking rows on guessed heights.
Monitor 1.3 lays every row out from measured heights, scrolls, and shows
the full 16-entry heard ring. Airtime 1.3 replaces tap-to-reset with a
Reset button top-right (a tap was too easy to trigger by accident).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
wada.ui.chart now uses the same recipe as the built-in Monitor/Airtime
charts (panel fill, 1px dark border, radius 6, dim grid, no point dots,
2px series) and gains chart:axis(ticks, gutter) for LVGL's Y value labels
— which draw to the LEFT of the chart, so the apps inset it by the gutter.
Labels gain label:width(px) to wrap instead of running off the screen.
Monitor 1.2: dBm axis, 72-point history, wrapped metrics/feed rows.
Airtime 1.2: % axis, and the chart height is now whatever is left after
the detail block is reserved — the text that fell off the bottom was the
chart claiming a fixed height on short screens.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the primitive the ports were missing: wada.ui.chart (two shifting
series, line or bar, settable range) plus rx_events/rx_dropped/tx_pkts/
freq/bw/sf/duty_pct on wada.mesh.stats.
RF Monitor 1.1: live RSSI + noise-floor chart, peak with tap-to-reset,
link-margin grade, RX/min, radio params, colour-coded heard feed, narrow
and wide layouts. Airtime 1.1: live + average utilization bars, peak,
rx/tx air split, duty ceiling, tx budget, packet counters, tap to restart
the window. Native versions stay compiled in until these are field-proven.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Store trusted a directory re-listing taken immediately after writing the
files; on FAT that can lag, leaving the row reading 'Get' until reopen. The
install result now updates the installed list directly (rescan still runs).
The RF Monitor / Airtime ports lacked the native tools' depth on device, so
they ship as 'RX Log (example)' / 'Air Bars (example)' — wada.mesh demos for
app authors. The native versions remain the real tools; full-parity ports are
future work, not a day-one swap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
wada.mesh: read-only contacts/rx_log/stats/self via thin UITask bridges
(no mesh types cross into the host TU). wada.net.http_get: async on the
existing net worker, http-only, 32 KB cap, one in flight per app, callback
delivered on the UI thread under the guarded pcall. RF Monitor and Airtime
rewritten as Lua catalog apps (live on the VPS with snake); their native
versions deliberately stay compiled in for beta_59 until the ports are
field-proven. lib/lua joined both IDF component builds (P4 verified on
RISC-V). Catalog source tracked at deploy/apps/.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop myapp.lua into /meshcomod/apps/ on the SD (any reader, the Files
app, USB) and it appears in the drawer named after the file (ver 'dev').
Manifests stay the store's install format; the second scan pass only
picks up ids no manifest claimed. macOS ._* AppleDouble junk skipped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The built-in Snake tile and embedded copy are gone where the host exists,
install it from the catalog like any app (non-Lua boards keep the native
game). Installed app tiles gain a long-press remove with a confirm sheet;
the post-long-press release click is swallowed so the app does not launch
under it. Store 'Remove' and the tile path share one removal helper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Store (new drawer tile) lists the VPS catalog (apps.json on
firmware.wadamesh.com, fetched on the net worker like the version check),
installs/updates/removes Lua apps (two files under <root>/apps/, .sav kept
across reinstalls), shows sideloaded apps, and hosts show/hide switches
for the built-in drawer tiles (app_hide bitmask, prefs v46 appended under
the schema's trailing assert). Installed apps appear as drawer tiles and
launch through the host. Catalog + snake 1.0 staged under out/firmware/apps
and live on the VPS (nginx /apps locations added).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LuaAppHost: one sandboxed app at a time as an AppPage overlay, every
callback under a 100k-instruction budget + traceback pcall (an app error
is a toast and a clean close, never a reset — proven live when the first
Snake build errored: 4 contained retries, zero leak). Bindings: wada.ui
canvas/label/button + theme colors, hardware-swipe/trackball/touch input,
timer, sys (millis/toast/board/random), per-app KV store under the data
root. Snake rewritten in Lua as the embedded reference app; the drawer
launches it via the host and prefers a user-editable /apps/snake.lua.
CAP_LUA_APPS defaults on for all touch boards. T-Deck: 85.1% flash
(host layer = 13 KB over the VM), app opens at 19 KB PSRAM.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Vendored 5.4.7 core+stdlib under lib/lua (mains excluded), LUA_32BITS
baked into luaconf.h so the VM and embedders share one ABI. LuaHost spike
behind WADA_LUA_SPIKE: PSRAM-only allocator with a hard 256KB cap,
io/os-free sandbox, instruction-budget hook. Measured on the T-Deck:
flash +96KB, state+libs 7.5KB/2ms, hostile while-true contained in 30ms,
zero leak. Inert (byte-identical build) when the flag is off.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
backupInternalFs() still returned FFat on the T-Display P4, but the hot
store moved to LittleFS('storage') at beta_54 and FFat is never mounted
since - every profile export and backup failed at open with 'can't open
file'. The list and restore paths share the helper, so all three heal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The PR inserted LANG_HU at slot 1 (shifting every stored language choice)
and dropped LANG_RO from the enum while the table still carries all 14
columns, so it did not compile as-is. Romanian is restored to the enum in
the table's column order, a one-time v45 remap keeps every existing user's
language selection, one row had its Hungarian cell pasted twice, and the
enum gained an append-only warning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per user feedback on beta_57: the retry default is the user's choice to
make. Applies to the v45 migration line, the fresh-install default, and
the pre-UI mesh state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The default lock wallpaper had MESHCOMOD baked into the image pixels
(both the rendered RGB565 embed and the SPIFFS-seeded JPEG) - the beta_56
branding fix only changed the animation text drawn over it. Regenerated
both assets with the WADAMESH wordmark and brand-teal padlock; the JPEG's
size change makes the SPIFFS seeder replace the old copy on next boot.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
beta_57 inserted retry_echo MID-struct (between rx_queue and web_mirror)
instead of at the blob tail. The stored cfg is a raw memcpy, so on upgrade
every later u8 shifted one byte: remote_mode inherited remote_landscape's
force-flipped 1 (v37) - every upgraded device booted into remote mode -
map_tile_debug inherited hist_sync_after's 2 (debug overlay on), and
hist_sync_after fell to 0 (blocking chat-flush fallback disabled).
v45: retry_echo moved to the true tail, a one-time repair resets the
shifted run on v44 blobs (originals unrecoverable - beta_57 re-flushed
over them), and auto-retry is opt-in now (default OFF) per user feedback.
Struct gains a loud append-only warning. Layout verified with a host-side
byte-level test of all three upgrade paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Beta 57 inserted retry_echo into the middle of the packed TouchCfg blob, shifting the remote-mode and later fields during migration. Define a v45 layout with retry_echo appended at the true tail and recover ambiguous v44 suffix bytes to safe defaults while preserving the stable prefix. Add a host regression test for v43, broken v44, and v45 blobs.
Signed-off-by: Pixel Perfect <me@pixp.cc>
The Format button (and its busy overlay) is compiled for T-Deck, M9 and
V4-R8, and Copy/Paste for every Files-app board, but both deferred workers
in UITask::loop were gated T-Deck-only. Elsewhere the tap set a pending
counter nothing consumed: the notice stayed up forever and the format
latch blocked SD remounting until reboot. Widen the format worker to the
button's gate (via the per-board sdSharedSPI accessor), ungate paste.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A repeater name with an emoji (multi-byte UTF-8 cut at a byte cap) or a
literal '#' desynced the LVGL recolor runs in the Discover feed, visually
killing every row after it; same class in the message-info repeat list.
Names are now UTF-8-boundary-cleaned and '#'-escaped at both sites, and
copyUtf8ReplacingMissingGlyphs no longer emits a split glyph at its cap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>