The per-board OTA_BIN_NAME chain ended in a bare #else naming the Heltec V4
TFT, so any board nobody had added to it silently downloaded the V4 image.
Update.end() only checks that an image is valid for an ESP32-S3, which the V4
build is, so nothing stopped it being flashed.
The Seeed Wio Tracker L2 shipped exactly like that. Its build defines only
HAS_WIO_TRACKER_L2, which the chain never tested, and the released beta_79
image carries "wadamesh-heltec-v4-tft" as its update name: an on-device update
on a Wio L2 installs Heltec V4 firmware onto Wio hardware and needs a USB
reflash to recover. The T-Deck Pro (HAS_TDECK_PRO) was in the same position
and would have done the same the moment it was published.
Every board is now listed explicitly, the V4 TFT included (the V4-R8 also
defines HELTEC_LORA_V4_TFT, and its branch already comes first), and anything
unlisted is an #error. Forgetting this table is now a build break instead of
a field brick.
Verified on all twelve OTA-capable targets: each compiles, and each binary
asks for exactly the artifact release.sh publishes for it (checked
mechanically: 10 PlatformIO envs plus both T-Display P4 SKUs, no mismatches).
Units already on beta_77 to beta_79 still have the wrong name compiled in, so
Wio L2 owners must update from the website over USB rather than on the device
until they are on a build with this fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#458 renamed TR("System info") to TR("System Information") and updated the
translation table to match, so the title stayed translated. But
src/ui-touch/i18n_builtin.h is GENERATED ("DO NOT EDIT") by
scripts/build/gen-lang-builtin.py from deploy/apps/lang/*.lang, and the rename
never reached those packs. Every regeneration since then -- the PlatformIO pre-hook
and the IDF build.sh both run it -- quietly wrote the old key back, so the source
asked for "System Information" while all thirteen tables answered "System info",
and the page title fell back to English in every language.
That is also why #517 appeared to revert the rename: its copy of the header was a
freshly regenerated one, faithfully reproducing the stale packs.
Rekeyed the thirteen source packs, then regenerated. The generated table changed by
exactly one key per language and nothing else, which confirms it was otherwise in
step with the packs, and the fix now survives the build's own regeneration, which is
the thing that used to undo it. The corrected packs reach devices that download a
language once the app store is republished with the next beta.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same QWERTY deck as English (no letters removed, so loanwords/names still type normally) with the three most common extra letters - s-caron, c-caron, z-caron - promoted directly onto the on-screen deck, matching how Romanian's a-breve/s-cedilla/t-cedilla were added. The remaining eight - a-macron, e-macron, g-cedilla, i-macron, k-cedilla, l-cedilla, n-cedilla, u-macron - reach the physical T-Deck keyboard via the number row (same trick as Romanian's digit row) and the touch keyboard's existing accent popups on their base letter.
Also fixes kbLayoutCode()'s k_codes[] table, which was missing an "RO" entry (so the on-screen language-cycle button mislabeled Romanian as "EN"); LV is appended alongside the fix.
All 22 upper/lower Latvian glyphs sit in Latin Extended-A (U+0100-017F), already covered by the bundled extras_* fallback fonts, so no font changes are needed.
Two Discord reports a day apart on different boards: museifu696 (M9, beta_78)
had no charging symbol at all, and thesupergeek (V4-R8) found charge state not
being detected after comparing against a T-Deck.
For the M9 this was structural. batteryIsCharging lived inside
#if defined(HAS_TDECK_GT911) || defined(HELTEC_LORA_V4_R8)
whose #else returned a flat false, so on the M9 -- and the Pager, RAK Tap, Wio
L2, Attaky, P4 and Tanmatsu -- no voltage could ever produce a charging icon.
That split was a real, evidence-based decision, but it was about the Heltec V4
alone: its noisier ADC path made the EMA drag the average down so the threshold
never tripped (the note above the block explains it). The #else then answered
the question for every OTHER board too, purely by falling through, and nobody
had evaluated those.
So share only the verdict, not the sampler. The EMA stays gated exactly as
before, which means battery PERCENTAGE on the direct-read boards is untouched
and the V4's original complaint cannot recur: the direct read is what it keeps,
and its plugged-in reading already sat high.
Sharing is sound rather than assumed: the same mv feeds batteryPercentFromMv(),
so a board reporting the USB rail instead of the battery would already show a
permanent 100% and someone would have said so.
Kept deliberately PURE. batteryMvSmoothed()'s charge_flip calls this twice with
different arguments to compare the old and new verdicts, so a latching Schmitt
trigger would corrupt itself; flap is bounded by the 20 s publish hold and the
50 mV margin instead. The threshold against each board's uncalibrated ADC still
wants on-device confirmation, as the V4-R8 note already says.
Two follow-ons: kBattChargingMv (4250) is removed, dead and misleading since the
test actually uses batteryFullMv() + 50; and the idle-power-save comment that
reasoned from "the M9 is in batteryIsCharging's #else branch, so the USB-powered
gate can never block" is corrected, because it now can, which is the point of
that gate.
Reported-by: museifu696, thesupergeek
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kaj's T-Deck coredump: abort() on core 1, decoding to luaG_callerror ->
abort() reached from luaAppPress(). luaG_callerror is "attempt to call a ...
value", and the abort is Lua's DEFAULT panic function, because lua_atpanic was
never installed anywhere in this file. So any error reaching Lua with no
protected frame resets the device.
The callback itself was never the problem: guardedCall wraps it in pcall with a
traceback handler and turns a fault into a toast plus a clean close, which is
the host's whole contract. The hole is everything that runs BEFORE that pcall
is armed:
- pushCallback() used lua_getfield, which honours __index. An app table
carrying a metatable whose __index is not callable raises exactly
luaG_callerror, unprotected. It is now a raw lookup, which is also the
correct semantics: callbacks are plain fields on the table the chunk
returned, and no shipped app uses setmetatable.
- the argument marshalling in sendInput/sendKey pushes a table and several
values before the pcall exists, and those raise on stack growth or OOM.
lua_checkstack appeared ZERO times in this file; pushCallback now reserves
the room up front, and checkstack reports failure rather than raising.
lua_atpanic is installed as well. It cannot prevent the abort -- Lua aborts once
a panic function returns and there is no safe frame to unwind to -- but the
error text now reaches the serial log and a toast instead of vanishing, which is
the difference between a reproducible report and a mystery reset.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>