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>
#464 (PD0RCM, T-Deck Plus): "I can hear the sample sound and the volume change,
but not incoming messages." That board has the hardware, and previews and
arrivals go through the same playback function on it, so the audio path was
never the problem.
Every preview on the Sound page played UNCONDITIONALLY: the sample, the volume
steps, and each per-type toggle. An arriving message checks the master Sound
switch and Do Not Disturb. So with the master off the page chimes at you the
entire time you configure it while nothing ever sounds on arrival, which reads
exactly like a broken notification path and is almost certainly what he hit.
A preview is now silent precisely when a real notification would be, and says
which of the two silenced it rather than just going quiet. The master switch
keeps its own confirmation chime, because that fires exactly when you turn it
ON, which is the one case where a sound is the right answer.
Telemetry position (v59, honza_87628, via Discord): the advert displacement of
#399 was extended to telemetry answers because a privacy setting a telemetry
request walks straight around is a hole rather than a feature. He then made the
fair counter-argument that an answer is not a broadcast: it is encrypted to one
contact who already holds the permission, and it only goes out because they
asked. Losing the ability to give trusted contacts a real fix is a real loss.
So make it a choice instead of an assumption. New tail field telem_loc_exact,
OFF by default, so the private behaviour is what you get unless you say
otherwise, and the broadcast advert stays displaced either way. The switch sits
directly under "Share my location when asked", where the decision belongs.
Schema 58 -> 59. The host test enumerates every appended field and caught the
change, as designed; all three of its migration assertions are updated and it
passes.
Reported-by: PD0RCM, honza_87628
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jesshampshire asked for the Discover pane to present like the Chats and
Contacts panes. It was the only one of the three without a leading icon, so a
repeater and an ordinary peer looked identical until you read the meta line
under the name.
Same glyph set and the same 22 px column the Contacts rows use: antenna for a
repeater, loop for a room, person for everything else, rendered in g_font_16 so
the FontAwesome PUA codepoints resolve through its fallback chain. The name and
meta labels shift right by that column and lose the width to it.
The type stays spelled out in the meta line as well: the icon cannot tell a
sensor or chat node from a plain peer, so dropping the word to buy the width
back would lose information on exactly the rows where it matters. On the narrow
boards that line ellipsises as it already did.
Requested-by: jesshampshire
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#467 (Buko84): leaving a page stashes the focused row so the cursor can go back
where it was, and it stashed it as a raw pointer. Reading a channel's unread
messages clears its badge, which rebuilds every thread row -- so on the way out
that pointer is either dead or, worse, recycled by the allocator onto a
DIFFERENT row, which is how the cursor lands on an unrelated channel. With no
unread messages nothing rebuilds and it works, which is exactly the split he
identified.
Identify the row by the slot it occupied instead. A list rebuilt in place keeps
its order, so the slot is reliable where the address is not; the pointer is only
consulted when the number of targets actually changed. The restore also scrolls
that row back into view now: the rest of the rebuild deliberately runs with
focus-scroll suppressed, which is right for the transient focus during
recollect, but it left the restored cursor off-screen whenever the rebuild had
also reset the list's scroll position.
#465 (jrote1): his follow-up screenshot shows the gate passing (178406 cycles,
last wake timer) but only 15% asleep. 178406 parks at the 50 ms throttle is
~2.5 h, which against 15% puts uptime near 16.5 h -- so every park runs its full
length and the parks are not the problem. The device simply was not ELIGIBLE
for the other 85%, and nothing reported which of the six gate conditions was
holding it shut.
So charge the wall time between checks to whichever condition was blocking at
that moment, and surface the biggest one ("held by mesh busy 62%") on the
Battery page and the live Lock-settings diag. gatePasses() becomes gateBlocker()
returning the condition rather than a bool. Gaps over 5 s are dropped rather
than blamed on a condition, and the clock is stamped AFTER a park so a completed
park is never mis-attributed to the next blocker.
Also: device_caps.h computed CAP_SOUND from HAS_UI_SOUND, which UITask.cpp does
not define until ~1400 lines AFTER that header is included -- so CAP_SOUND was 0
on every board. Nothing consumed it, so it was silent rather than broken, but
any future `#if CAP_SOUND` would have compiled notification sound out
everywhere. The board list moves into device_caps.h (every macro involved is a
-D on the command line, so all of them are genuinely visible there) and
HAS_UI_SOUND now derives from CAP_SOUND. Verified per board: on for T-Deck,
V4-TFT, V4-R8, M9, Pager and P4; off for RAK Tap, Wio L2 and Attaky, which is
the pre-existing behaviour, unchanged.
Reported-by: Buko84, jrote1
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Piguyraspberry reported the P4 lock screen never appearing and its settings
page rendering blank. Both are the same thing: the board declares
CAP_LOCK_SCREEN but nothing behind it was ever wired for it.
- nothing can REACH the lock. The control-center Lock button is compiled
only for HAS_TDECK_GT911; the other triggers are the Tanmatsu volume key
and the Pager's own hold handler.
- nothing can LEAVE it. Every unlock path is a physical-control branch
(T-Deck trackball hold, Tanmatsu Vol-, M9 d-pad, Pager Backspace, Wio
wake button) and the P4 matches none. The on-screen hint even falls
through to "hold the trackball to unlock" on a board with no trackball.
- the DSEC_LOCK settings body is gated on HAS_TDECK_GT911 || HAS_THINKNODE_M9
rather than on the capability, which is why the page came up empty.
The Heltec V4-R8 sits in exactly this position and is deliberately 0, with a
comment saying enabling it is a feature rather than a cap flip. Same call
here. The settings card is already skipped under !CAP_LOCK_SCREEN, so this
removes the empty page rather than papering over it.
#451 stays open as the tracking issue for actually building it: a touch
reveal + hold-to-unlock path, a P4 hint string, and capability gates on the
settings section.
Reported-by: Piguyraspberry
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Known regions (#449, DenRalle on Tanmatsu): s_regions_modal was in no popup
registry at all. Without a row the Back ladder never peels it, so the page
stays on screen, and the open-popup COUNT stays zero, so whatever you open
next opens underneath it. Both halves of the report ("screen remains
visible", "pages open only in the background"), one missing line.
Idle power-save (#450, jrote1): the Battery page in the report shows the
gate blocked on "Wi-Fi on" with Wi-Fi disabled and cycles stuck at 0.
tsWifiOff() ANDed the wifiConfigGetRadioEnabled() pref with the real radio
state, and that pref DEFAULTS TO ENABLED when its NVS key was never written
-- so anyone who had not explicitly toggled Wi-Fi in this firmware read as
"Wi-Fi on" permanently. The gate is asking whether the modem draws power,
and WiFi.getMode() == WIFI_OFF answers exactly that on its own.
The other half of the same bug is in main.cpp: the power-down ran only if
wifi_started, but a scan or the version check can bring STA up without
going through that state machine, and then disabling Wi-Fi skipped
mode(WIFI_OFF) and left the modem running. getMode() != WIFI_OFF proves the
driver is inited, which is what the guard actually wanted to test, so it
closes the leak without reintroducing the low-heap esp_wifi_init crash the
original comment guards against. The block is one-shot behind
wifiConfigConsumeApplyRequest(), not per-loop.
Follow-ups from reviewing the merged PRs:
- #457 moved the Lua app page to the slim bar, which exposed that bar_h was
measured BEFORE appPageBegin* installs the chrome -- so the body was sized
against the OUTGOING page's bar. Install first, then measure (#236 again,
one step earlier).
- #453 makes /meshcore-*.json downloadable, and those carry "private_key"
plus every channel secret. The transfer is opt-in, LAN-only and rate
limited (3 tries, 30 s lockout), so the surface is sound -- but it is
plain HTTP, so say so on the page and flag backup rows in the list.
Reported-by: DenRalle, jrote1
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>