pisti87 read v18 and called it chaos. He was right, and it was mine.
Two defects in the extractor I added yesterday:
- It joined every string literal in a call argument, so a ternary became one
key. `cut ? "Paste (move)" : "Paste (copy)"` shipped as the single key
"Paste (move)Paste (copy)", and with it "Unblock Block",
"Unfav Favorite", "Other networksNetworks", "Batteryactivityon" and
"Stop sharing loc Share my loc". None of those strings exist anywhere in
the firmware. Literals are now grouped only when genuinely adjacent, which
is what the compiler concatenates, so both branches become their own key.
- It scanned raw source, comments included. That is how "Geblokkeerde
gebruikers" -- Dutch, appearing only inside a comment about how a long
translation degrades -- became a KEY in the Hungarian file. Comments are
stripped now, string literals preserved.
202 invented rows removed across the thirteen files. Only rows that were both
unknown to the extractor AND still untranslated were touched, so no
translator's work could be lost either way.
Also his: the curly quotes in the Hungarian credits render as boxes because no
bundled font carries U+201E/U+201D. Five values de-curled. And five strings he
found raw are wrapped: the Wi-Fi rescan and hidden-network rows, the update
check, and the downgrade prompt. The two Wi-Fi rows build their label at
runtime, since TR() returns a pointer and cannot join a glyph literal at
compile time.
Languages go to v19. v18 is deleted rather than left behind: it was only ever
correct for about a day and everything in it is superseded.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cvhviz. The M9's magnetometer was documented on the board and nothing had ever
talked to it. The driver is written from the datasheet's register map rather
than SensorLib, whose setOutputDataRate() writes the ODR into the OSR bits, and
the axis orientation is measured on hardware at four headings rather than
inherited from a declaration Meshtastic marks unverified and never uses. The
+-32 G range looks absurd for a 0.5 G planet until you measure the board's own
hard-iron bias at about 7x Earth's field.
Also carries several fixes found while testing on hardware: every Lua app opened
on a white page on keypad-nav boards (the focus highlight harvested the app body
as a target and reverse-video filled the page), a use-after-free in the Lua net
worker when an app closed mid-request, an unfreed http_get buffer, canvas pixel
buffers GC'd while LVGL still drew from them, one RTC I2C read per contact, and
map re-open costing 2.5 s on every visit.
Three changes on merge:
- The map tile-keep gate read `total && total < 4 MB`, so a board reporting
zero PSRAM -- the most constrained case there is -- landed on the roomy side
of the test and kept its tiles. Dropped the non-zero guard.
- gpscompass is 55 KB of Lua, more than every other app combined, and it wants
a magnetometer the seeded boards do not have. The author deliberately left
it out of lua_builtin.h; that intent now lives in the catalog as
"seed": false rather than in whether someone remembers to regenerate, since
the generator runs from a pre-build hook as of this branch.
- consoleModeToggleCb was defined inside a !HAS_TANMATSU region while the
Settings row that binds it compiles on every board, so the Tanmatsu link
broke. Moved it out. The console boot path is gated on CAP_CONSOLE alone, so
the switch now does what it says there too.
Built on all seven S3 envs plus both ESP32-P4 targets.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pisti87 reported a long list of text that stays English whatever the language,
and said the strings were in his language file and still did not appear (#257).
Both halves are true, and the reason is the audit.
The extractor only ever recognised TR("literal"). Three very common shapes were
therefore invisible:
mk_row_btn("Reload tiles in view", cb) // helper TR()s its parameter
for (auto& r : rows) TR(r.label) // literal lives in a local table
TR(contactsSortOptName(m)) // helper returns one of several
All three translate correctly at runtime, so the source looks properly wrapped.
But the literal at the call site was never emitted as a key, so it never entered
a .lang file, so no translator could ever supply it -- and adding it by hand
did nothing, because the audit's key list is what the files are checked against.
That is 51 strings across the map options sheet, the sort sheets, the contacts
filters and the home launcher.
The audit now understands all three, plus tr("...") in the Lua apps, and the
newly visible keys are in all thirteen files as placeholders so translators can
see them. 1017 keys, up from 966.
Four strings were genuinely raw and are now wrapped: the reader's idle status,
the Discover empty feed, the crash-report export button and Paste (move/copy).
Lua apps had no way to translate anything at all, so every built-in was hard
English regardless of the device language. wada.sys.tr() gives them the same
table the interface uses; airtime 1.4 is the first to use it, with the
`sys.tr or identity` fallback so it still runs on older firmware.
Two more instances of the drift this issue is really about:
- gen-lua-builtin.py read out/firmware/apps/, which nothing writes -- the
deploy rsyncs deploy/apps/ straight to the VPS. So the mirror was stale and
the two apps added in beta_68 were never baked in: boards that cannot reach
the Store shipped without them. It reads the canonical directory now, and
regenerates from the same pre-build hook as the language table.
- Baking a row whose translation equals its key does nothing, since TR()
returns the key on a miss. Skipping them takes the header from 1.11 MB to
939 KB and gives the V4 back 16 KB of flash, which matters at 89%.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The map About/credits sheet was 820 bytes of raw English built with snprintf and
no TR() anywhere in it, so it stayed English in every language (#257). It is now
three keys: the two attribution headers (the OpenTopoMap variant is credited
separately because its style is CC-BY-SA) and the body, kept whole rather than
split per paragraph so translators get prose instead of fragments. The buffer
grows 820 -> 2048 because Hungarian runs about 1.5x English here and the
Cyrillic and Greek files are two bytes a letter.
Hungarian text from pisti87 (#257). Two edits to what he posted, both flagged on
the issue: the hard line breaks he inserted at the English wrap points are gone,
because the label wraps itself and a fixed break lands mid-sentence on any other
panel width; and the header reads "Terkep adatok" rather than "Map adatok",
which looked like a copy-paste artifact given the rest is fully translated. The
OpenTopoMap variant is derived from his own wording and is his to correct.
Also raw, from the same report: the Discovered auto-add hint and the four type
words it interpolates. The hint buffer goes to 240 bytes and the type list to
128, since the translated plurals are longer than "chats, repeaters".
The reason none of that would have shipped: gen-lang-builtin.py exists so the
baked-in table and the .lang files the store serves cannot drift, and its
docstring promises a pre-build step that runs it. Nothing ran it. Editing a
.lang and building produced an image carrying the OLD translations, silently.
It is now a real pre: hook on all seven PlatformIO envs and a line in both IDF
build scripts, regenerating only when a .lang is newer than the header.
deploy-apps.sh grew the matching check for the other half of that path: a
catalog version that disagrees with the file's own "# ver:" publishes
translations to a version no device asks for.
All thirteen languages snapshot to v17 -- the merged region and SD work added
keys to every file, not just Hungarian.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pisti87 asked whether the obsolete entries in the language files will be removed
(#262). Measuring it first was the right move, because the tool that would have
driven that cleanup was wrong.
source_keys() only ever matched TR("literal"). Strings reached indirectly, as in
TR(kSettingsCats[c].label), were invisible to it, so every settings-category name
looked unreferenced: About, Backups, Language, MQTT bridge, App permissions and
the rest. Anyone pruning the .lang files on that output would have deleted live
translations for some of the most visible labels in the UI.
The audit now also pulls literals out of any table whose name appears inside a
TR(...) subscript. Deliberately greedy: over-collecting keeps a translation alive,
under-collecting deletes one.
That immediately found a real gap it had been hiding. "App permissions", the
settings category added with the permissions page, was missing from all 13
languages and the old audit reported everything as covered. Added, builtin
regenerated, published as language v15.
Also adds --obsolete, the reverse check pisti87 actually needs: rows in each .lang
file that no TR() key matches. It reports and never deletes, and says plainly that
these are candidates to check rather than a delete list, for the reason above.
Numbers now: 950 keys reachable, hu.lang carries 1044 rows, 94 unreferenced. The
earlier figure was 119, so 25 of those were the false positives just fixed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Second 7-dimension adversarially-verified audit over the 08-19 tree.
Headliners: SUB_MAP over a display-only Lua app orphaned the app page
(key-only soft-lock); Back-ladder z-order redesign (CC/power always-
frontmost, confirm-modal deference, Lua key-forward suppressed while a
confirm is up — the send-permission dialog was unanswerable); map pan
flag could go stale across tab jumps/popups; null-close progress rows
now genuinely block the registry dismiss (shared fix); terminal RX
mirror, fullscreen title, wallpaper caption, storage-error guidance
widened to M9; accent/@-mention pickers suppressed (dead chrome on a
touchless board); kb-backlight cache only latches ACKed duties (0xFF
sentinel == duty 255 skipped the first write every boot); deep sleep
actually drops the rails now (display refcount, LEDC pin re-route,
RTC/digital holds) and powers off radio+GPS; TCXO fallback no longer
codifies the disproven 0.0f; RadioLib old-FW patch fail-closes at
link; ENV_SKIP_GPS_DETECT + CORE_DEBUG_LEVEL=0 added to the env.
Full round log in M9_PORT.md 'Audit pass 2 (2026-08-20)'. All four
touch envs (M9, T-Deck, V4-R8, pager) compile clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pisti87 found "Settings → Quick replies" drawing a tofu box, in English as well
as Hungarian. It is not a language bug and it is not the beta_62 fallback-chain
fix falling short — the glyph simply was not in any font we ship.
gen-touch-fonts.sh asked $noto_sans for the symbol set, and Noto Sans does not
contain U+2190-2193 or U+2260/2264/2265 (verified directly against the release
the script pins). lv_font_conv omits a glyph its source font lacks rather than
failing, so those seven characters silently never made it into extras_font_*,
in every language, since the fonts were first generated this way.
Scope is much wider than the one line reported: 137 uses of → in the touch UI
plus arrows in all 13 .lang files. pisti87 happened to open one of them.
Fixed by cutting the seven from Montserrat, which has all of them, is already
the primary UI face and already the first --font in this script — so no new
dependency, no new licence line, and the arrow matches the text beside it. They
had to be REMOVED from the Noto Sans symbol list as well: with the codepoint
claimed by a later font that cannot supply it, the Montserrat pass produced
nothing and the regenerated files came back byte-identical apart from a comment.
Noto Sans Symbols 2 was the obvious candidate and does NOT have them either —
it errors outright when asked, which is how that was ruled out.
Not visually confirmed: I have no way to see the glyph render from here. The
regenerated fonts grew ~390 lines per size and lv_font_conv accepted the request
(it hard-errors when the source font has none of the symbols, as Symbols 2 did),
so the glyphs are in. Worth a look on a device before the beta ships.
Reported by pisti87.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pisti87's T-Deck rebooted every time he logged into a repeater, but only in
Hungarian. The login clock-skew warning does:
snprintf(msg, n, TR("Device clock differs from \"%s\" by %lu min%s"),
name, minutes, suffix);
and the Hungarian row reordered the conversions to "%lu ... %s ... %s". Varargs
are positional, so snprintf read the name POINTER as an unsigned long and then
took the minute count -- the integer 3 -- as a char* and dereferenced address 3.
Instant panic, every login, Hungarian only. English fit the declared order, so
it never showed there.
Fixing the four bad Hungarian rows is not sufficient: TR() returns a format
string and translations come from .lang files that users download or hand-write,
so any file can crash any device. TR() now compares the ordered conversion
signatures of key and translation and falls back to the English key on a
mismatch -- the key IS the call site's format string, so it is always correct.
The scan runs only for keys containing '%', which is a small minority.
Also fixed the four rows (three were Hungarian-only crashes or dropped values),
bumped hu to v11, and taught audit-lang.py to fail the build on a mismatch so a
future translation PR cannot reintroduce this. Unit-checked that the audit
detects the original bad row and accepts the repaired one.
Reported by pisti87.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The firmware asked for these keys and no .lang file carried them, so they
rendered English in all 13 languages with nothing to show a translator that
they were missing. Mostly strings that shipped with a feature whose PR did not
touch the language files: the SD arbitration and migration warnings, the
Wi-Fi/BLE coexistence messages, the keyboard-navigation vocabulary
(Up/Down/Left/Right/Select/Scroll) and section names (General, Clock & time,
Sensors, Screen, Home, Messages, Keys, Compact, Snake).
They go in with an EMPTY translation, which is the format's own way of saying
'not translated yet': the runtime loader requires a non-empty value
(`if (*p && tab[1])`) and gen-lang-builtin.py requires `if k and v`, so both
skip these rows and the English falls through exactly as before. Confirmed:
i18n_builtin.h regenerates byte-identical. No `# ver:` bump for the same
reason — device behaviour is unchanged, so there is nothing to re-download.
Also fixes the audit's own row parser to match the loader: a line is a comment
only when it has NO tab. Testing for a leading '#' hid every key starting with
LVGL recolor markup ("#7A7F87 Wardrive: …#"), the exact class the loader has a
comment warning about.
Every language is now at 0 missing keys.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing checked this. The .lang files are hand-maintained and i18n_builtin.h is
generated FROM them, so drift is invisible in both directions: a row whose
English no longer appears in any TR() call is translated for nothing, and a
TR() key with no row renders English in every language forever.
Matching mirrors TR() in i18n.cpp, which is the part a grep gets wrong —
adjacent C literals concatenate, LV_SYMBOL_* icon prefixes are stripped before
lookup, non-ASCII is often written as \x escapes, and menu/tab labels reach
TR() through a table field rather than as a literal (resolved by harvesting the
table's initialiser).
Current state: 910 TR() keys; every language carries ~57 dead rows and is
missing ~53 keys (hu: 77 dead, 52 missing).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
'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>
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>
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>
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>
- New Discover app: active NODE_DISCOVER sweep -> signal-ranked nearby list, tap-to-add-contact, GPS wardriving log (SD CSV) + signal-coloured coverage overlay on the map. Board-agnostic.
- T-Display P4 TFT-LCD (HI8561) variant support (WADA_P4_LCD build; AMOLED bin untouched).
- Wire the Attaky Core board (#158, @attakygit) into the release matrix + web flasher.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The V4-R8 uses an ESP32-S3R8 (8 MB OCTAL PSRAM) which claims GPIO33-37, so
Heltec moved every control signal off them and the Expansion Kit V2 rebuilt the
display/touch/SD block onto a hardware-SPI bus:
Vext 36->40 (active-LOW) . GPS_EN 34->42 . LED 35->46 . ADC_Ctrl removed
TFT (LovyanGFX HW-SPI): MOSI=15 SCK=16 MISO=45 CS=47 DC=48 RST=21 BL=44
touch (CHSC6x, shares board I2C 17/18) . micro-SD (shared TFT SPI, CS=3)
The build defines HELTEC_LORA_V4_TFT too, so it reuses all the V4 touch-UI code;
HELTEC_LORA_V4_R8 gates only the deltas. 8 MB enables the on-device web browser
(CAP_WEB_BROWSER) and the SD card (CAP_SD). Also wires the board into the release
+ web-flasher pipeline (release.sh, gen-flasher-meta.py, the flasher picker).
UNTESTED — no V4-R8 hardware yet. Pin sources: Meshtastic heltec_v4_r8 variant,
Heltec V4-R8 pinmap, and the Expansion_board_V2.03 schematic. All three touch
envs build clean; V4/T-Deck unregressed. On-device TODO: CHSC6x shared-Wire on
17/18, SD_CS=3, and the V2 PAM8904 buzzer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New "Web" app: on-device HTTPS text browser (strip HTML to readable text,
tappable links, back/fwd/refresh, no proxy). Gated to 8 MB boards via
CAP_WEB_BROWSER; hidden on the 2 MB V4 (can't do the TLS handshake).
- Tappable URLs in chat -> Open in web (8 MB) / Create QR (all boards).
- Drawn vector globe icon for the Web tile.
- Fix: French AZERTY on-screen keyboard lost its spacebar/OK/kbd-switch row
(a stray "" mid-map terminated the LVGL button map early). Restored.
- V4 web fetch resilience (192->96->48->24 KB buffer fallback).
- Home signal box: "tap for details" cue back at the top.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Persistence (Leon P / MrWeavis, beta_38):
- Deleting a message or clearing a chat now persists synchronously, so it
survives an immediate hardware reset instead of "coming back" (the tombstone
used to ride the 30 s lazy SD flush, which a reset beats).
- The history file is now written compactly — only the used records,
oldest-first, instead of all ~5000 ring slots every time. A typical chat
writes ~48 KB instead of ~1.2 MB, so the synchronous delete/clear/reboot
flush no longer hitches the UI (and normal message flushes are lighter too).
Old history files still load (the reader derives its slot count from size).
Emoji picker:
- Baked the 14 glyphs that were listed but never in the font (umbrella, coffee,
pizza, cake, rocket, camera, phone, laptop, calendar, antenna, e-mail, house,
car, watch); they rendered as the notdef box.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both new S3 boards are installable from the browser now: release.sh
builds and ships their bins per channel, gen-flasher-meta emits their
esp-web-tools manifests, and the site gets install cards for both.
The cards are pinned to the beta feed (with a note) until the first
stable promote that includes their bins; the wmSetChannel comment says
how to unpin then. beta_38's beta feed and GitHub release carry the
new bins retroactively.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New board: ThinkNode_M9_companion_radio_touch env, variants/thinknode_m9/
(board glue, LR1110 target, keyboard driver, partitions, M9_PORT.md pin
reference), boards/thinknode_m9.json, device_caps HAS_THINKNODE_M9 block,
HAS_M9_KEYBOARD input path in the touch UI (d-pad focus nav, tab jump keys,
QWERTY typing).
Based on a contributed bring-up patch by ded (SalishMesh), who also remote-
tested every build on a preproduction unit. Hard-won fixes on top of the
patch, each verified on hardware:
- LR1110 radio init -706: preprod chips run transceiver FW 0x0303, which
rejects DriveDiosInSleepMode (0x012A, added in FW 0x0308) that RadioLib
7.x sends unconditionally during begin(). New build-time patch
scripts/build/patch_radiolib_lr11x0.py tolerates it on old FW;
radio_init() prints the chip's device/FW/errors either way.
- TCXO: DIO3 at 3.3 V (the patch's active-oscillator/tcxo=0 theory gave
-707 on hardware).
- Display: rotation 1 (not 3), plus the missing landscape s_ui_rotation
override so LVGL renders 320x240 instead of portrait-into-landscape.
- Keyboard: the controller is a separate ESP32-S2 I2C slave (0x6C) with
addressed registers - reads select register 0x01 first (bare reads
return the HW-version register forever). Boot probe logs the
controller's HW/FW; backlight duty register wired.
- USB pad release: the keyboard bus lives on GPIO20/21 = the S3's native
USB D-/D+ pads, owned from reset by the ROM USB-Serial-JTAG (D+ pullup
on SDA). M9Board::begin() releases them before any bus init.
- GPIO35-37 are octal-PSRAM pins on the S3R8 - never drive them (the
patch's "SD CS = 36" misread package pin 36 = GPIO48; SD is on the
shared SPI bus, support returns with PIN_SD_CS=48 later).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New: stable/test update channels with an opt-in "Get test builds (beta)" toggle on
OTA devices (switches both the update check and what you install); experimental
opt-in MQTT bridge (consent-gated, payload encryption, direct messages off by
default); Portugal (Narrow) region preset (#74); saved-contacts counter in the
Contacts overflow menu (#72).
Fixed: the signal probe is now strictly zero-hop so a repeater can't re-flood it;
the contact list sorts before capping so the most relevant contacts show, with a
"+N more — search to narrow" footer (#73); the blocked-users list opened from
Contacts shows the two-line title bar with a back button; the Contacts overflow
popup fits the screen again.
Also lands the two-channel release tooling (scripts/release.sh stable/beta modes +
gen-flasher-meta channel arg), the docs site, and the MQTT reference decryptor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fix LVGL scroll-momentum use-after-free reboot when a scrollable overlay
(emoji / quick-reply / add-channel / channel sheets, Wi-Fi scan, pickers,
app drawer) is freed mid-throw: new closeScrollOverlay() helper
(lv_indev_wait_release + lv_obj_del_async). Root-caused from field beta_18
coredumps.
- Radio & Mesh settings: airtime / duty-cycle readout + "Answer telemetry
requests" toggle, grouped into RADIO / MESH / SIGNAL sections.
- Accent popup dismissed on Enter-to-send (T-Deck physical keyboard).
- Heltec V4: Expansion-Kit Home env line + chart flow below the stats line
so they fit the 240x320 screen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Emoji: bake 4 more Noto colour glyphs (soccer, american football, moai,
transgender flag) and add them to the picker. The trans flag is a ZWJ sequence
the single-codepoint imgfont lookup can't render directly, so its combined Noto
image is keyed on the lead codepoint (U+1F3F3) with the trailing symbol mapped to
the zero-width glyph — the whole sequence renders as the one flag. Only the 4 new
glyphs are baked + spliced (scripts/build/add-emoji.py); the existing 252 stay
byte-identical (no Noto-main art drift).
Chats header: group the three actions at the LEFT edge in the order + (add) /
✓ (mark all read) / QR (share), and draw a real baked QR glyph (qr_icon.h, from
scripts/build/gen-qr-icon.py, recoloured to the text colour) instead of the old
LV_SYMBOL_IMAGE 'picture' stand-in.
Contacts sort: every sort option now toggles ascending/descending — re-tapping the
active row in the Sort sheet flips it and shows a ▲/▼ arrow. New 'Nearest first'
(distance) sort orders by distance from our GPS and excludes contacts that have
shared no location.
Discovered list: gains the same sort sheet (Recently heard / Name / Nearest, each
asc/desc) via a sort button at the top of the list; distance likewise excludes
no-location entries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The web-flasher manifest referenced https://firmware.wadamesh.com/latest/<board>-merged.bin
— a stable filename cached 4h (Cache-Control max-age=14400) and overwritten in place each
release. version.json (max-age=300) advertised the new tag within minutes, but the latest/
bin could keep serving the PREVIOUS beta's bytes per-POP for up to 4h, so a user flashing
right after a release could install the old version (reported: flashed beta_11, device showed
beta_10). Point the manifest at the immutable releases/TOUCH/<tag>/ bin instead — that URL is
never overwritten so its cache is always correct, and the 5-min manifest TTL propagates the
new path fast.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The esp-web-tools manifest carried a frozen version:beta_1 while the rolling
/latest/ bins moved on, so the web flasher always said beta_1. Fix:
- scripts/build/gen-flasher-meta.py emits version.json (tag + notes) + per-release
manifests (version=tag) into /latest/; release.sh + the CI workflow run it, so
the metadata rolls with every release.
- the site points the install buttons at the VPS manifests and renders a live
'What's new in <tag>' block from version.json.
- drop the frozen static site manifests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>