Commit Graph
594 Commits
Author SHA1 Message Date
Kaj SchittecatandClaude Opus 4.8 dbff43dbd7 beta_59 prep: language files v8, the SDK docs page, and device-status corrections
The audit found three keys the Store work added or renamed since v7 -- Store,
the out-of-memory message and the Languages hint -- untranslated in all 13
languages. Filled, using each language's own word for the Use button so the
hint matches the control it points at, and bumped the files to v8. Every
language is back to full coverage at 856 keys.

New deploy/site/sdk.html: the wada.* reference. The beta ships a public API and
had no user-facing documentation for it, so nobody outside the repo could write
an app. Covers the lifecycle, all seven tables, the app format, side-loading and
how to publish, plus the two constraints that surprise people -- the mesh is
read-only in v1 and http_get is plain HTTP because TLS does not fit in the heap
left after Wi-Fi associates.

Site device list: the Attaky Core and the T-Display P4 are fully supported now
(Kaj's call), so their not-hardware-verified caveats are gone. Dropped the "one
of the two primary development boards" wording, which stopped being true once
the P4 joined the test loop. The terms section named only the T-Deck and Heltec
V4 as flashable when the flasher offers ten boards; it now points at the list on
the page instead of going stale again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
beta_59
2026-08-05 00:37:22 +02:00
Kaj SchittecatandClaude Opus 4.8 9b8daea0df store: fit the Store to any panel, show downloads running, and link on P4/Tanmatsu
Three things, all "the Store only ever ran on a 320 px T-Deck".

LINK FIX (supersedes 7e5eac4, which treated the symptom). LuaAppHost.cpp
forward-declared `class WiFiClient;`. That declares a BRAND NEW incomplete
class, which is only correct where WiFiClient really is a class of that name --
it is a #define to C6Client on the T-Display P4 and a `using WiFiClient =
NetworkClient` alias under arduino-esp32 3.x on the Tanmatsu. So the file
emitted luaNetWorkerService under a signature the caller never referenced and
BOTH P4 targets failed to link, with three different mangled names, while all 8
S3 envs built clean -- which is why it stayed hidden until the first P4 build.
The TU now refuses to name the type at all: UITask owns the networking and
passes the objects as opaque handles through luaStoreHttpGetOpaque, casting
them back on its side. This kills the whole class of bug rather than adding a
third per-board alias.

LAYOUT. Cards measure their text instead of assuming "title + two description
lines" -- lv_txt_get_size on both title and description, height floored so it
can never be shorter than the icon chip or the action button. The title wraps
instead of truncating and the description sits below its ACTUAL height, which
is what was overlapping. Measuring beats reading geometry back, which returns 0
before layout. This fixes every board for translations longer than the English.
LV_LABEL_LONG_DOT only ellipsizes when the HEIGHT is bounded; with just a width
it silently wraps, which hit the card title, the language rows, the sideloaded
rows and the tab labels. Tab cells are now sized in proportion to their text,
not even thirds: an even third on the P4 (284 px logical, scaled fonts) is
~90 px and "Languages" does not fit that, while "Apps" left half its cell
empty. The bar height follows the font, floored at 30 so S3 stays identical.

FEEDBACK. The pressed button's caption becomes a spinner for the whole
download, not a 1.2 s toast, so a slow link no longer reads as a dead tap. It
is repainted from the in-flight id after a rebuild, since a card scan finishing
mid-download would otherwise restore a plain Get button. The caption is hidden
rather than deleted -- this runs inside the button's own event.

Drawer tile is "Store", short enough for every panel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-05 00:11:43 +02:00
Kaj SchittecatandClaude Opus 4.8 7e5eac4675 lua: mirror the P4's WiFiClient->C6Client alias so the host links
The T-Display P4 failed to link with an undefined reference to
luaNetWorkerService(C6Client&, HTTPClient&).

On that board the C6 runs ESP-AT, so UITask.cpp does #define WiFiClient
C6Client to route sockets through AT-over-SDIO. LuaAppHost.cpp forward-declares
WiFiClient without the alias, so it emitted the definition as
_Z19luaNetWorkerServiceR10WiFiClientR10HTTPClient while the caller referenced
the C6Client& overload -- the symbol was present, just under the wrong type.
Confirmed by reading both object files.

Mirroring the alias fixes it. This TU only forwards the reference to
luaStoreHttpGet, so an incomplete type is sufficient. Every S3 board built
fine throughout, which is why it went unnoticed until the first P4 build of
the Lua work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 23:18:35 +02:00
Kaj SchittecatandClaude Opus 4.8 372abb0479 touch: fix "SD write failed" — SD writes must not DMA out of PSRAM
Three T-Decks report "Save update bin to SD" dying with "SD write failed",
usually at 1% and never past 7%, while map tiles and settings save fine.

f.write() passes the buffer down to SD/SPI, which DMAs from it. PSRAM is not
reliably usable as a DMA source, so the write commits fewer bytes than asked
and the transfer fails at an offset that moves around with the network chunk
sizes. sdFwWorkerRun allocated its copy buffer MALLOC_CAP_SPIRAM with the
comment "PSRAM preferred", which is exactly backwards for this.

The three-way comparison makes it clear: the tile cache copies through a 1 KB
*stack* buffer and works, the .lang installer used PSRAM and failed the same
way (fixed earlier via langWriteAll's internal bounce buffer), and this used
PSRAM and fails. It has been broken since the feature landed in beta_36 --
it never worked, which matches nobody reporting it as a regression.

Buffers moved to MALLOC_CAP_DMA in all three remaining places that write to a
card: the firmware download, fmCopyFile (the Files app copy, any file to SD)
and crashDumpExport (coredump to SD, broken in the same way).

Also in the download loop: retry a short write before declaring failure, and
yield to IDLE each chunk. A ~3 MB download over a fast link never yielded, so
it could starve the task watchdog -- the trap the tile fetcher already
documents.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 22:52:24 +02:00
Kaj SchittecatandClaude Opus 4.8 e0663eb9ee touch: drop the V4 memory/store diagnostics + fix a swallowed hex escape
The RAM investigation and the language-download debugging left Serial probes
across three files: the BOOTMEM/MESHMEM/MEMPROBE macros and their 48 call
sites, plus the [STORE]/[APPCAT]/[LANGDL]/[LANGSEL] traces. They have served
their purpose and were flooding the companion USB-CDC, so they all go. Two
locals in the .lang installer (wr, wr3) existed only to feed a trace and go
with it; the control-write repair path that actually rebuilds /lang stays.

Separately, the map tile-cache help text read "256\xC3\x97256": the 256 after
the multiplication sign is all hex digits, so the compiler folded it into the
escape and emitted one out-of-range byte instead of "256x256". Breaking the
literal after the escape terminates it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 22:43:49 +02:00
Kaj SchittecatandClaude Opus 4.8 f3d238c47a store: on baked-in boards it becomes a picker, not a shop (V4)
Per Kaj: keep the Store page (Settings > Language opens it) but drop what a
board without the Store cannot do.

  - Store tile restored everywhere; the page is the language picker.
  - No Apps tab where CAP_BUILTIN_LUA_APPS: the tab bar is Built-in +
    Languages, sized for two, and opening never lands on the absent tab.
  - Long-pressing a seeded app no longer offers Remove: there is no file to
    delete, and the built-in would just reappear on the next scan.
  - Languages tab lists all 13 translations with Use where CAP_BUILTIN_LANGS
    (they ship in the image), skipping the installed-files and
    available-to-download sections and the /lang hint that does not apply.

T-Deck unchanged at 75.5%; V4 85.7%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 22:24:49 +02:00
Kaj SchittecatandClaude Opus 4.8 96b1f8fba5 lua: fix sizeof-on-pointer capping every list at zero; drop the Store tile on V4
The PSRAM move earlier tonight turned s_lua_cat/s_lua_inst/s_langcat/s_langinst
into pointers, but four size expressions still read
'sizeof(s_lua_inst)/sizeof(s_lua_inst[0])' — 4/60 = 0 on a pointer. Every list
was silently capped at zero entries, so installed apps stopped listing on ALL
boards and the seeded built-ins could never be added. Replaced with named
constants (kLuaCatMax/kLuaInstMax/kLangCatMax/kLangInstMax), which the
allocator now uses too, so the cap and the allocation can no longer disagree.

Also: the Store tile is gone on boards that ship their apps in the image
(CAP_BUILTIN_LUA_APPS) — a deliberate product decision rather than a broken
button, per Kaj: the V4 is not a Store board.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 22:05:52 +02:00
Kaj SchittecatandClaude Opus 4.8 de9d9f252d lua: ship the catalog's apps inside the image for boards without a dependable Store
Kaj's idea, and better than restoring the deleted native pages: the Lua host
already had the mechanism. luaAppLaunchFile() is file-first with an embedded
fallback — it just never had anything seeded into it.

gen-lua-builtin.py generates src/ui-touch/lua_builtin.h from out/firmware/apps/,
the same .lua the Store serves, as C++ raw strings (Snake, RF Monitor, Airtime;
10 KB). luaStoreScanInstalled() now wraps the filesystem scan and seeds any
built-in the scan did not find — after it, so a downloaded copy keeps its own
entry and version, and seeding still happens when there is no filesystem at all.
Tiles, the drawer and the Store's Installed list all work through the existing
paths; launching passes the embedded source, and a downloaded file still wins.

CAP_BUILTIN_LUA_APPS is ON for the V4 only. Flash: V4 85.5% -> 85.8%,
T-Deck 75.5% and Pager 78.1% unchanged. No RAM cost.

So the V4 now has the apps and all 13 languages out of the box; the only thing
it loses without a working Store is browsing for new ones.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 21:53:43 +02:00
Kaj SchittecatandClaude Opus 4.8 efe77a67e3 i18n: bake the .lang files into boards that cannot rely on the Store (V4)
gen-lang-builtin.py generates src/ui-touch/i18n_builtin.h from the SAME
deploy/apps/lang/*.lang files the Store serves, so the downloaded and the
compiled-in translations can never drift — translators still edit one place.
TR() consults the loaded file overlay first, then the baked table, then
English, so a downloaded language still wins on boards that can fetch one.

CAP_BUILTIN_LANGS is ON for the V4 (its net worker is fragile at ~95%
internal RAM, so the Store is not dependable there and it would otherwise be
stuck on English) and OFF elsewhere. Cost is FLASH only, no RAM:
  V4     71.7% -> 85.5%
  T-Deck 75.5% (unchanged, gate off)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 21:47:19 +02:00
Kaj SchittecatandClaude Opus 4.8 461b9c47e5 v4 ram: prove TCP/WS/MQTT cost nothing — the 36.4 KB is esp_wifi_init alone
Measured on the V4, bracketing each step of the post-mesh init block:
  wifiConfigBegin()                              172 bytes
  serial_interface.begin(Serial, TCP, WS)          0 bytes
  WiFi.mode()/WiFi.begin() -> esp_wifi_init   36,432 bytes

So gating the TCP and WebSocket servers (or MQTT, which already returns
early when disabled) frees nothing: listening sockets are cheap and the
cost only arrives per connected client. Wi-Fi is ~100 KB of the 289 KB
heap (36 KB init + ~64 KB associate), it is DMA memory that cannot live in
PSRAM, and it is not divisible by feature.

Conclusion for the V4: no application-level lever of size remains. The
options are restoring the native apps/languages so the board does not need
the network for them, rebuilding the Arduino libs with a custom sdkconfig
(Wi-Fi buffer counts + IRAM shadow), or accepting 95%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 21:37:35 +02:00
Kaj SchittecatandClaude Opus 4.8 b58c4ef11c v4 ram: per-tab attribution — no single tab dominates, cumulative +17.4 KB
Measured per builder: tabview+tab bar 8.5 KB, settings 4.5, chat detail 4.4,
map 2.9, contacts 2.7, home 1.2, chats 0.2. So deferred tab construction is
worth ~14.5 KB but needs four separate lazy builders, not one.

Cumulative effect of this session at the same boot point:
  boot int free   87444 -> 104816
  post-UI-build   60692 ->  78056   (+17.4 KB)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 20:42:43 +02:00
Kaj SchittecatandClaude Opus 4.8 7a7126635c v4 ram: PSRAM malloc threshold 1024 -> 256 (+5.4 KB total, no T-Deck regression)
Measured on the V4: post-spiffs 167496 -> 171316, pre-iface 121432 ->
126836. Verified on the T-Deck (8 MB PSRAM, the working reference) that
pushing ordinary allocations to PSRAM breaks nothing: boots, chats and the
store all fine, so the threshold is safe for every board.

Session total on the V4: ~12 KB internal reclaimed (6.7 KB statics + 5.4 KB
threshold).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 20:32:14 +02:00
Kaj SchittecatandClaude Opus 4.8 2185b5f519 v4 ram: lower the PSRAM malloc threshold to 1 KB (+2.8 KB internal, measured)
The SDK ships CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=4096, so every allocation
under 4 KB is forced into internal DRAM even with PSRAM free.
heap_caps_malloc_extmem_enable(1024) changes that at runtime; anything that
needs internal/DMA memory asks by capability and is unaffected.

Measured on the V4: post-spiffs 167496 -> 170292 free. Real but modest,
because most internal use is NOT generic malloc — Wi-Fi DMA (capability
tagged), 171 KB of static .bss/.data/IRAM-shadow, and task stacks.

Session total on the V4: ~9.5 KB internal reclaimed (6.7 KB statics to
PSRAM + 2.8 KB here), plus the worker-stack fix that made the store, map
tiles and the update check work there at all.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 20:18:27 +02:00
Kaj SchittecatandClaude Opus 4.8 4387e0d64b v4 ram: measure the boot budget end to end (probes + findings)
Attribution on the V4, per step, measured on hardware:
  SPIFFS mount            4.4 KB
  storage selection       7.5 KB
  store.begin()             0
  the_mesh.begin()         52 BYTES  (contacts load is 369 KB, all PSRAM)
  post-mesh init block   38.6 KB  <- Wi-Fi/TCP/MQTT/companion interface
  UI construction          38 KB
  Wi-Fi associate        ~55 KB

So MAX_CONTACTS and the mesh core are both exonerated: contacts already live
in PSRAM and MyMesh::begin() itself costs 52 bytes. The weight is Wi-Fi plus
the interface block that follows the mesh, and the UI.

Also checked and already optimal: LVGL's heap and the LVGL draw buffer are
on PSRAM, CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y, and Arduino defaults
useStaticBuffers to false (static TX 0 / static RX 4 / dynamic 32), so the
obvious Wi-Fi buffer lever is already pulled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 20:07:57 +02:00
Kaj SchittecatandClaude Opus 4.8 66475b776a v4 ram: move the store + Discover page tables from .bss to PSRAM (-6.7 KB DRAM)
Measured on the V4: 5.8 KB free internal heap at steady state, 3.5 KB
largest block, while 843 KB of PSRAM sat idle. These four tables (the Lua
catalog, installed apps, the language catalog and its installed list) plus
Discover's per-row key/name snapshots only matter while their page is open,
yet lived permanently in .bss. They are pointers into PSRAM now, allocated
on first use with a malloc fallback; indexing is unchanged.

.dram0.bss 74168 -> 67448 bytes, verified in the ELF.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 19:41:43 +02:00
Kaj SchittecatandClaude Opus 4.8 fbcd524a65 v4: reserve the net worker's stack in begin() + boot memory probes
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>
2026-08-04 19:17:43 +02:00
Kaj SchittecatandClaude Opus 4.8 edb5b1dbb5 store: size the Lua Store for 240 px portrait panels, not just the 320 px T-Deck
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>
2026-08-04 18:20:10 +02:00
Kaj SchittecatandClaude Opus 4.8 cac3a44074 i18n: scan by literal, not by function name — 103 more wrapped incl. Remove channel (v7)
'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>
2026-08-04 17:58:50 +02:00
Kaj SchittecatandClaude Opus 4.8 15415fd0f5 i18n: enumerate every user-visible literal instead of grepping for them (49 wrapped, v6)
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>
2026-08-04 17:46:00 +02:00
Kaj SchittecatandClaude Opus 4.8 7a034d303a lang: fix the download for real (PSRAM->SD + short reads), and say why before restarting
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>
2026-08-04 17:28:06 +02:00
Kaj SchittecatandClaude Opus 4.8 535471613d lang: chunked SD write fixes the false 'Download failed', audit sees glyph-prefixed TR, 16 more keys (v4)
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>
2026-08-04 16:48:50 +02:00
Kaj SchittecatandClaude Opus 4.8 6a5738946f i18n: wrap the bare-literal sheets and buttons — months-old translations finally render
The channel/chat long-press sheets (Mark as read, Share secret, Region &
scope, Chat icon, Delete history, Log in again, Reset path, Blocked users),
the contacts overflow menu, the URL menu, the power menu rows, five confirm
buttons and four page labels were built from glyph-concatenated string
literals that never passed through TR() — so their translations, which have
existed in the table (now the .lang files) for months, never rendered. All
wrapped now; TR() strips the glyph prefix and rebuilds the label, which is
exactly the machinery it grew for. Only five keys were genuinely new
(Install, Map, Save report, Set, Stop) — translated in all 13 files, bumped
to ver 3 (immutable /apps/lang/3/), audit fully green.

Also fixes the audit appender's substring presence check: '© OpenStreetMap'
contains 'Map', which silently skipped the new Map row in every file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 16:41:59 +02:00
Kaj SchittecatandClaude Opus 4.8 7ec5e92414 lang: language files are versioned like apps — Update button, immutable URLs
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>
2026-08-04 16:28:42 +02:00
Kaj SchittecatandClaude Opus 4.8 911d665be5 i18n: the baked-in translation table is gone — languages are files, full sweep included
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>
2026-08-04 16:21:18 +02:00
Kaj SchittecatandClaude Opus 4.8 8c0ebb89b7 store: one-line top bar, no header strip, and Settings > Language IS the Languages tab
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>
2026-08-04 15:54:51 +02:00
Kaj SchittecatandClaude Opus 4.8 b99a5b7a62 docs: LUA_APPS.md status — store v2 tabs + language files
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 15:41:19 +02:00
Kaj SchittecatandClaude Opus 4.8 f3f421398c store: three tabs (Apps / Built-in / Languages) + languages as downloadable files
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>
2026-08-04 15:40:35 +02:00
Kaj SchittecatandClaude Opus 4.8 005e3bc89c lua: stop scanning the SD card every time the app drawer opens
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>
2026-08-04 15:00:28 +02:00
Kaj SchittecatandClaude Opus 4.8 ba508396fd lua: hiding MQTT actually hides it — apply live, and reach existing installs (v47)
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>
2026-08-04 14:50:22 +02:00
Kaj SchittecatandClaude Opus 4.8 c717217563 lua: long-press hides built-in tiles, readable store cards, MQTT hideable and hidden by default
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>
2026-08-04 14:37:53 +02:00
Kaj SchittecatandClaude Opus 4.8 68f66556d0 lua: Lua Store looks like a store — cards with icon chips, header counts, refresh, real empty states
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>
2026-08-04 14:28:29 +02:00
Kaj SchittecatandClaude Opus 4.8 2791a1aa12 lua: drop the two invented apps — the catalog is conversions of existing apps only
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 14:01:01 +02:00
Kaj SchittecatandClaude Opus 4.8 c2161b4650 lua: delete the native RF Monitor + Airtime — the Lua Store versions replace them
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>
2026-08-04 13:57:28 +02:00
Kaj SchittecatandClaude Opus 4.8 e7343871bb lua: 'Lua Store' everywhere + catalog rows stack title above description
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>
2026-08-04 13:48:18 +02:00
Kaj SchittecatandClaude Opus 4.8 ea58ff3d9a lua: scrollable app pages + real line heights; Monitor scrolls, Airtime gets a Reset button
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>
2026-08-04 13:45:53 +02:00
Kaj SchittecatandClaude Opus 4.8 8f174c63b7 lua: chart matches the built-in styling + Y-axis labels; app layouts stop overflowing
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>
2026-08-04 13:41:19 +02:00
Kaj SchittecatandClaude Opus 4.8 fd4659f4ce lua: wada.ui.chart + full radio stats, and RF Monitor/Airtime rebuilt to parity (1.1)
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>
2026-08-04 13:34:06 +02:00
Kaj SchittecatandClaude Opus 4.8 47ec8a2293 lua: install button flips to Remove without reopening + the two ports are labelled SDK examples
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>
2026-08-04 13:29:10 +02:00
Kaj SchittecatandClaude Opus 4.8 14d5b06560 lua: Phase 3 — wada.mesh + wada.net, RF Monitor + Airtime as catalog apps, IDF wiring
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>
2026-08-04 13:18:12 +02:00
Kaj SchittecatandClaude Opus 4.8 f7a550685c lua: SD adoption migrates /apps too (mkdir the FAT parent so the copies succeed)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 13:07:24 +02:00
Kaj SchittecatandClaude Opus 4.8 1a1fc0f260 lua: bare .lua files on the card are apps — no manifest needed
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>
2026-08-04 13:03:14 +02:00
Kaj SchittecatandClaude Opus 4.8 7e6ff56a3f lua: rebuild the app drawer when the Store closes over it (fresh installs appear immediately)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 12:59:30 +02:00
Kaj SchittecatandClaude Opus 4.8 2a60138e16 lua: Snake is store-only on Lua boards + long-press-to-remove on installed tiles
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>
2026-08-04 12:57:30 +02:00
Kaj SchittecatandClaude Opus 4.8 ed22bd071c lua: store page fixes — layer_top stacking (drawer covered it) + pre-layout width read
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 12:50:38 +02:00
Kaj SchittecatandClaude Opus 4.8 6b29714f60 lua: Phase 2 — App Store page, catalog installs, drawer integration, hide toggles
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>
2026-08-04 12:39:35 +02:00
Kaj SchittecatandClaude Opus 4.8 322e4dd6f2 lua: Phase 1 — app host + wada.* v1 + Snake in Lua (hardware-verified)
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>
2026-08-04 12:23:11 +02:00
Kaj SchittecatandClaude Opus 4.8 f2067848a7 lua: Phase 0 — vendor Lua 5.4.7 + PSRAM-capped sandbox spike (GO: +96KB flash, 7.5KB state)
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>
2026-08-04 12:00:49 +02:00
Kaj SchittecatandClaude Opus 4.8 46b81c2f71 docs: Lua apps ships as one beta (beta_59), phases become build order
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 11:52:56 +02:00
Kaj SchittecatandClaude Opus 4.8 379ddfeff6 docs: Lua apps plan + tracker (runtime, wada.* API, store, conversion tiers, phases)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 11:35:25 +02:00
Kaj SchittecatandClaude Opus 4.8 3708eb1811 touch: beta_58 — beta_57 upgrade-bug repair (#229/#230), retry opt-in, Hungarian (#227), lock wallpaper (#228), P4 backups (#231)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
beta_58
2026-08-04 11:00:45 +02:00