'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>
- New "Remote & web app" docs section covering the browser control panel
(Chats / Contacts / Discovered / Settings / Terminal), Screen mirror (VNC)
and Remote UI, with a 7-shot phone gallery of the real web app.
- Refreshed all device screenshots via the DOC_CAPTURE tour (now also
captures the Remote screen) and added the MQTT bridge settings page.
- New automated web-capture harness (scripts/doc/web-demo/): extracts the
real page from firmware, serves it with mock data, screenshots via Playwright.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Tanmatsu (ESP32-P4) now bakes FIRMWARE_VERSION="wadamesh <tag>" like the S3
release path, and the app-store publish passes the exact beta tag into the IDF
build — so the About page shows the real beta number instead of the MeshCore
default "v1.16.0-touch".
- beta_40 stable release notes: rolls up beta_36 through beta_40.
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>
Catch a comma/em-dash/over-length description locally and abort, instead of
discovering it as a failed verify_metadata check on the upstream PR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Vol+ short = top-bar dropdown toggle; long hold = sound master toggle
- Message-notification LED: flash on new message, soft green glow on unread; toggle in Display settings
- Backups use the internal FFat partition (export/scan/import) + Import button on the Backups page
- microSD browsable in the file manager (P4 SDMMC slot 0; the C6 radio's slot 1 untouched)
- Status-bar icon overlaps fixed (charging-slide + language indicator now SC-scaled with the cluster)
- Vol- screen sleep/wake/lock (screen-lock work)
- Companion link: TCP server start + BLE bond NVS persistence + app-sent channel-message mirror
All Tanmatsu changes gated #if defined(HAS_TANMATSU); T-Deck/V4 bins stay byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Standalone dev tool (pyserial) that drives the companion binary protocol over a
serial port — DEVICE_QUERY/APP_START, battery/storage, stats, telemetry with a
Cayenne-LPP decoder, and meshcomod CLI commands. No firmware coupling; dev-only.
Ported verbatim from PR #29 by @samuelcoustet.
Co-Authored-By: samuelcoustet <257299705+samuelcoustet@users.noreply.github.com>
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>
release.sh now regenerates deploy/meshamerica-catalog.json for the tag and, when
run on main, commits + pushes that one file so apps.meshamerica.com (the flasher
Cascadia Mesh uses) always points at the newest immutable bins instead of going
stale on the previous beta. Idempotent (no-op when already current); non-fatal if
the push fails; also mirrors the catalog into the VPS publish tree.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
One command to cut a source-only (no prebuilt/) tag on the monorepo for wadamesh
to pin, keeping the shallow lib clone ~12 MB instead of 1.4 GB.
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>
macOS's bundled rsync errors on --chmod=D755,F644 (invalid argument). rsync -a already preserves the source perms (644 files / 755 dirs), so the flag was redundant; drop it so deploys run on a stock Mac.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Propagate the mesh-mark animation (repeater broadcast rings + cyan signal
packets on the transport trails) to the flasher page and the standalone
wadamesh-mark.svg / wadamesh-lockup.svg. The standalone SVGs embed the CSS
inside the file so they animate even when used as <img>. badge.svg (favicon)
left static. All collapse to the static mark under prefers-reduced-motion.
Add scripts/deploy-site.sh: rsync deploy/site/ to the VPS web root
(/srv/wadamesh/site, the apex wadamesh.com origin), env-driven WADAMESH_VPS
like release.sh, with --dry-run support.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
deploy/site/: a visual install journey served at the apex. Hero + an animated
mesh backdrop, then a two-path chooser:
- Standalone -> embedded esp-web-tools flasher (board picker, 3 steps)
- With Launcher -> step-by-step + app-image downloads (/latest/*.bin)
apex vhost switched from 301->flasher to serving the landing; release.sh now also
rolls the app-only bins into /latest/ for the Launcher path. Deployed + verified
live through Cloudflare (wadamesh.com 200).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Kaj Schittecat <kaj@schittecat.com>
- deploy/flasher/: browser Web Serial installer (board picker for V4 + T-Deck),
manifests pointing at firmware.wadamesh.com/latest/*-merged.bin
- deploy/nginx/firmware: serve /latest/ + Access-Control-Allow-Origin so the
flasher fetches bins cross-origin
- deploy/nginx/flasher: flasher.wadamesh.com vhost + wadamesh.com/www 301->flasher
- release.sh: refresh the rolling /latest/ merged bins each publish
Deployed + verified live through Cloudflare. VPS IP kept out of the repo
(Cloudflare fronts the origin).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Kaj Schittecat <kaj@schittecat.com>
declare -A needs bash 4+; macOS ships bash 3.2. Replaced the env->binname map
with a portable 'env:name' string + parameter-expansion split. Verified
end-to-end: built beta_1 for both boards, rsynced to the firmware VPS, origin
serves the listing + bins.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Kaj Schittecat <kaj@schittecat.com>
The wadamesh.com distribution stack, ready to drop onto a new VPS behind
Cloudflare:
- deploy/nginx/tiles.wadamesh.com.conf — OSM tile proxy (HTTP origin, double-quoted
location regex, resolver + variable proxy_pass, OSM UA, 14d disk cache)
- deploy/nginx/firmware.wadamesh.com.conf — bins + the beta_<N> update-check listing
- scripts/release.sh — build both boards tagged, refresh the listing, rsync to the VPS
- deploy/README.md — VPS + Cloudflare DNS/SSL/cache setup, publish flow
VPS target supplied via $WADAMESH_VPS at publish time — no IP/SSH in the repo
(Cloudflare fronts the origin). Web flasher + OTA re-enable are the remaining
pre-launch pieces (noted in deploy/README.md).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Kaj Schittecat <kaj@schittecat.com>
First board lands. wadamesh builds the LVGL touch firmware as its own project,
consuming the MeshCore core via lib_deps @ git tag (ALLFATHER-BV/MeshCore
#v1.16.0-wada.0) — no vendored core in this repo. Output is byte-identical to the
in-tree meshcomod build (delta = embedded build-path strings only).
Contents: companion_radio app (MyMesh/main/DataStore), ui-touch LVGL UI,
variants/heltec_v4 board glue, boards/heltec_v4.json, lv_conf, the bundled
ed25519 lib, AsyncElegantOTA, partition table, and a flattened platformio.ini
for env heltec_v4_tft_companion_radio_usb_tcp_touch.
MeshCore-derived files (app glue) remain MIT; ui-touch is GPL — see NOTICE.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Kaj Schittecat <kaj@schittecat.com>