Buko84: the notification text is too big on the M9. It is, and the fix was
already made once for the same reason.
28 px was chosen so the message is legible from across a room, which it is, but
on a 240x320 panel it fills the glance and crowds out the message it is supposed
to be showing. The T-Deck was moved to 20 px for exactly this and the M9 has the
same size panel; nobody widened the gate at the time.
Three things had to change, not one: the extern declaration, the 20 px Latin
fallback font's own build gate, and a LV_FONT_MONTSERRAT_20 flag on the M9 env,
which was not building that size at all. Missing any of them is a link error
rather than a small font, which is why it is worth naming.
All nine S3 envs and both ESP32-P4 targets green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two changes to the at-a-glance notification, both asked for by Kaj.
The title now carries the sender and the hop count, not just the thread. A
channel post reads "#public - Alice - 3 hops"; a direct message reads
"Alice - direct" without naming the sender twice, since a DM is already titled
with them. Zero hops says "direct" rather than "0 hops", and 0xFF is
OUT_PATH_UNKNOWN, an advert that arrived by flood, so it is omitted rather than
printed as "255 hops" -- the same trap that produced the Discovered list bug.
The point is that a count off a dark screen tells you nothing you can act on,
whereas a named contact one hop away against channel chatter from five hops out
is the reason to pick the device up at all.
Muted channels no longer light the screen. That was arguably backwards: muting
silenced the chime while the message still woke the display and printed itself
in large type, which is the louder half of the notification rather than the
quieter one. Muting is a statement about a conversation, so it now covers both.
Mentions keep their own mute bit, matching how the chime already treats them, so
a channel muted for chatter still surfaces an @-mention. If mute should instead
mean nothing at all gets through, that is a one-line change.
All nine S3 envs and both ESP32-P4 targets green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Buko84 again, and the same defect I fixed for the lock-screen picker: each swatch
set its colour for the default state only, so the theme was free to repaint the
focused one. On a d-pad board the swatch you are standing on is always focused,
so it turned white and hid the colour you were choosing.
I fixed the lock-screen picker and missed this one, which is the second instance
of the same mistake in the same file. The colour is now pinned across the
focused, pressed and checked states here too, and focus draws as a ring outside
the swatch so it can never cover the colour.
The swatches are also scaled rather than raw pixels now, the same class as #238.
All nine S3 envs and both ESP32-P4 targets green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five new user-facing strings arrived with #429/#439/#440/#441/#442/#444/#445.
All nine S3 envs and both ESP32-P4 targets green on the merged result.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Conflict with the M9 pending-focus block from #440 resolved by keeping both and
making the ordering explicit. #442 reworks the focus-hint logic so a hint
belonging to the page behind a newly opened overlay cannot suppress the
new-chat composer fallback (#436); #440's block places focus deliberately and
sets focus_set. Without a guard the reworked chain fell through to the
just-opened-chat branch and moved that focus, which is precisely what #440's
block exists to prevent, so the chain now has an explicit first arm for it.
Two things, both found by looking rather than by being told.
A Heltec V4-R8 crash dump from beta_77 decodes to a data race in the Arduino
core's Wi-Fi event list. Correcting for the unwinder mangling Xtensa return
addresses, the event task was inside _eventCallback copying a WiFiEventCbList
entry, iterating the callback vector, while loopTask was still in setup() inside
_M_realloc_insert growing that same vector through WiFi.onEvent(). The
reallocation frees the buffer the event task is walking, so it then calls
through a dangling function pointer, which is the garbage program counter in the
dump.
Neither side locks, and the vector is the core's, not ours. What is ours is the
ordering: both handlers were registered AFTER the stack was started, one of them
after WiFi.begin(), so a STA_DISCONNECTED arriving during boot association lands
in the window. A failed first association is ordinary, which is why this happens
at all. Both now register before WiFi.mode(WIFI_STA), so nothing mutates the
vector once events can flow and the race is impossible rather than unlikely.
This is a crash during setup(), so on an unlucky unit it is a bootloop, which
makes it worth more than its single report.
The moved handler drops its WIFI_DEBUG_PRINTLN tracing, which comes from a
header not in scope that early. The prints compile to nothing in a release build
and the reconnect flag they accompanied is unchanged.
Separately: the location-privacy setting shipped in beta_77 on the wrong page.
It was inside the Sensors section rather than GPS, because the insertion was
anchored to a comment belonging to the Sensors block, and that section is
described in its own code as V4-with-kit only. So on most boards it was not
reachable at all, which is exactly what two people reported: they went looking
and correctly concluded it was not there. It now sits in the GPS section, is
named "Location privacy" rather than "Position in adverts" (nobody searching for
a privacy control scans for the word "adverts"), and offers four buttons instead
of a cycling one, so every option and the current choice are visible without
touching anything. Given the failure here was discoverability, showing the most
without interaction is the point.
All nine S3 envs and both ESP32-P4 targets green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dreirund reported the device as frozen at "Download mode... reflash over USB".
It was not frozen: that is the state working as designed, waiting to be flashed
with its own UI stopped. They accepted that once told, and made the fair point
that the button should say what it will do.
So the menu entry now reads "Download mode (wait for USB flash)", and the
message shown before the UI stops says that RESET cancels, which is the piece of
information whose absence turned a working feature into a bug report. It also
stays on screen long enough to be read.
A state a user cannot leave without already knowing the way out is a bug in the
telling, even when the code is right.
All nine S3 envs and both ESP32-P4 targets green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Written now so cutting it is a build and a publish rather than a writing session.
Not tagged and not published.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#238 was a card laid out in raw pixels while the text inside it scaled, so the
contents outgrew the box. That is a class rather than one bug, so I swept the
file for containers sized in raw pixels large enough to hold text and found
eight more: the lock and unlock progress cards, the pager locking bar, the
on-screen keyboard's text mirror, and the accent-colour hex entry row.
None of these has been reported. They are latent rather than theoretical: the
same conditions that produced #238 apply, and #418 is a live report of
translated labels overflowing their buttons, so a longer translation inside a
fixed box is a real case rather than an imagined one.
SC() is identity at 100%, so this changes nothing on a board that already looks
correct, and only grows the box where the text inside it has already grown.
Also swept for the other documented recurring trap in this UI, a geometry read
taken before layout has run. Zero instances left, so that one is genuinely
closed rather than merely quiet.
All nine S3 envs and both ESP32-P4 targets green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Filed as a code trace rather than a reproduction, with the caveat that it would
rather be challenged than sat on. Challenged, and it holds: every link is still
there in current main.
sdHealthTick asserts the tile-backend pause and then returns while holding it if
the SD lifecycle is busy. Holding it across busy ticks is deliberate, so queued
tile requests cannot keep winning the boundary while recovery waits, and for
every busy reason but one that is fine.
The exception is a pending About-page SD-usage rescan. The tile worker services
that request near the top of its loop but takes the backend lease later in the
same pass, so a request armed in between can only be cleared by the very task
the pause has parked. The flag keeps sdRuntimeLifecycleBusy() true, the tick
keeps returning, the pause keeps the worker parked, and nothing else can break
it: the card is locked out until reboot. Opening Settings, About is enough to
arm it.
So for that one reason the pause is released and the worker allowed to drain.
Recovery is not abandoned, it retries on the next tick; it just stops holding
the door shut on the only task that can open it. That is the same reasoning as
the reader excluding itself from this predicate a few lines above, with a
comment saying it would otherwise deadlock, so the pattern already had a
precedent in this file.
All nine S3 envs and both ESP32-P4 targets green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both reported on the T-Display P4 by D-Melhede, and neither is P4-specific once
you look: they are hardcoded pixel values in a UI whose text scales.
#236, a Lua app's first line cut in half by the status bar: the app host set its
body offset from a hardcoded 44, commented as matching the AppPage tall bar.
That is only true where STATUSBAR_H is its 22 default. The bar is
SB_TOP_PAD + SB_ROW*2 on the round-corner phone panel and SC(22) everywhere
else, so it grows with the UI scale, and anywhere it exceeded 44 the app drew
under it. The host now asks for the height the bar actually has. The same
mistake was already fixed elsewhere in the file, with a comment saying to
measure with statusBarCurH() rather than assume; the Lua host had not caught up.
That also means this was never only a P4 bug. Any board at Large or Huge scale
had it, which includes the Tanmatsu, whose default has been Large since v29.
#238, the drawer options not fitting: the card is laid out in raw pixels while
the text inside it scales, so the contents outgrew the box. Now scaled with SC(),
which is identity at 100% and therefore changes nothing on a board that was
already correct, and the card is additionally clamped to the sheet's height so a
larger scale cannot push it off screen.
All nine S3 envs and both ESP32-P4 targets green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Twelve new user-facing strings arrived with #413. Added as placeholder rows so
the audit is clean and translators have the keys to fill in.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
s_nav_drawer_corner and s_nav_drawer_gear are declared inside #if CAP_KEYPAD_NAV
but were assigned unconditionally, in both drawer-close paths and twice while
building the grid. On a board without keypad navigation the declarations are not
there and the assignments are, so the Heltec V4 TFT and the Wio Tracker L2 did
not compile.
Same trap as twice already today. It is invisible on the board the change was
written for and only the matrix finds it, which is the argument for building the
matrix rather than the board on the desk.
All nine S3 envs and both ESP32-P4 targets green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Conflict with #407 resolved in favour of the newer gesture: #407 changed the M9
unlock from a hold to a double-press, and #409 branched before that and still
described the hold. The M9 strings keep #407's wording; #409's Wio Tracker L2
arm is taken as written, since that board's gesture is not what changed.
Conflict with #414 resolved by keeping both intents: the theme chip's icon stays
the sun in either state, which is #414's point and matches the DND chip since
#396 (the fill carries the state, so a changing glyph reads as a different
control), while taking the keypad-nav id #412 adds. #412 branched before that
decision rather than disagreeing with it.
Requested by dreirund: with no GPS fix, no Wi-Fi and no companion app there was
no way to set the clock at all. That is not cosmetic, because message timestamps
are built from it, so a device used purely offline could not hold a correct one.
Settings, Clock now takes local time as "YYYY-MM-DD HH:MM". mktime() reads it
through the configured zone, the same zone the clock is displayed in, so what you
type is what you see rather than something you convert to UTC yourself, and
tm_isdst is left at -1 so summer time is not an hour out. The field is prefilled
with the current reading, which makes a correction an edit of a digit or two
rather than typing a full stamp on a device keyboard.
Rejected values are refused the same way every other clock source is: below the
send-timestamp floor is not accepted, so a mistyped year cannot walk the ratchet
backwards.
All nine S3 envs and both ESP32-P4 targets green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#415: a Lua app was covered by the app drawer whenever a message arrived.
Reported by jadestarwatcher, reproduced by mysterywavi, both on T-Deck. Mine,
from the #393 badge fix. Apps launched from the drawer deliberately leave it
alive underneath (appTileCb says so outright), and my badge refresh rebuilt it on
every unread-count change, with openAppDrawer() ending in a move_foreground that
threw it on top of the running app.
The refresh now runs only when nothing is drawn in front of the drawer, tested by
sibling order rather than by listing the tools, so a tool added later is covered
without anyone remembering to update a list. The status bar is excluded, since it
legitimately floats above the drawer at all times and treating it as covering
would have stopped the badges refreshing at all. Leaving the signature stale is
what makes it self-healing: the check runs again each tick, so the count is right
by the time the drawer is back in front, with no need to hook every close path.
Scroll position is now preserved across the rebuild too, which my change had also
been resetting.
#410: on the M9 the accent variants appeared but could not be selected. Also mine,
from the #387 work. The picker was handled below m9HandleArrowKey, which takes
LEFT and RIGHT for the caret and returns, so the arrows never reached it: the box
was drawn and nothing could touch it. Lifted above that call, as its own function
rather than a second copy of the logic.
#399, requested by @Danie10: an option to advertise a position near you rather
than your address. Settings, GPS, cycling exact / 100 m / 250 m / 1 km.
The displacement is derived from the node identity, so it is the same offset
every time. That is the whole point rather than an implementation detail: a fresh
random offset per advert would scatter points around the true position, and
averaging a night of them would recover the centre exactly. A fixed displacement
instead looks like a node that sits somewhere else, which is what a manually set
location already looks like. It applies only to our own adverts; the map and the
GPS page keep the real fix, because the aim is to tell other people less, not to
lie to yourself.
Schema v57. The host test caught the new field the moment it was added, which is
what it is for.
All nine S3 envs and both ESP32-P4 targets green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drive the L2 I2C backlight during sleep and wake, poll its expander button reliably, and remove irreversible software power-off. Make a two-second wake-button hold lock or unlock, and keep Home and empty-map chrome clear and readable.
Signed-off-by: Michael A. Cojocari <michael.cojocari@gmail.com>
Open the selected message action menu with a normal Enter press. Replace the lock-screen long wait with a d-pad-center double press while preserving directional behavior and the controller long-press fallback.
Signed-off-by: Michael A. Cojocari <michael.cojocari@gmail.com>
Restrict the drawer settings gear to Up or Right from the top-right tile so other top-row Up presses stay put. Hide the M9 unread-message blinker while the drawer is frontmost.
Signed-off-by: Michael A. Cojocari <michael.cojocari@gmail.com>
Day/Night themes, the Seeed Wio Tracker L2, the M9 panel actually sleeping,
P4 Wi-Fi honouring its own setting, three Lua SDK fixes, and reboot notices
that are visible.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The forward declaration went in among the keypad-nav ones, which sit inside
#if CAP_KEYPAD_NAV. On every board without keypad navigation the declaration
vanished while the three calls stayed, so the Heltec V4 TFT and the Wio Tracker
L2 did not compile. Same trap the popup-registry declaration already documents.
Moved to where every touch board sees it. Caught by building the matrix rather
than the one board in front of me, which is the whole reason for building it.
All nine S3 envs and both ESP32-P4 targets green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Changing the theme restarted the device with nothing on screen, which reads as a
crash rather than as the setting working. Kaj reported it on hardware.
The cause is that showAlert() only CREATES the notice; it is painted by the next
LVGL frame, and rebootDevice() never returns to one. So announcing a reboot and
then calling it immediately shows nothing at all. My first attempt at this was
exactly that mistake, and it changed nothing on the device.
The language switch already solved it, and the comment there names the failure:
the device went dark under the user's finger, so the reboot is scheduled on a
one-shot timer and the notice gets a frame to paint. That is now a shared
rebootWithNotice() helper, and the theme switch uses it.
Screen rotation and remote mode had the identical defect. Both announced a
reboot and then rebooted immediately, so neither notice had ever been visible;
both now go through the same helper. Any future setting that reboots should.
Verified on a T-Deck: clean software reset, no panic, and the earlier crash
report did not recur. The coredump partition was erased first, so that is a real
observation rather than an absent one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three things Jade reported, the first of which is blocking an app submission.
Missing keys on the T-Deck. A Lua app there never received A, P, Q, Enter or
Backspace. The rule that withholds them exists so an app can never swallow its
own exit, which is right on a board where a reserved key IS the only way out.
On the T-Deck it is not: an app page is closed by tapping the back bar, and the
app host is not in the popup registry, so those keys closed nothing while an app
was running. They were simply dead inside every app. They are now forwarded.
The M9 keeps its reservation, because its reserved keys are sentinel bytes for
Back and Home that are never typed text and it has no touch to tap out with, and
the Pager already reserved nothing. That is why this was T-Deck-only.
The trackball click. With an app open the trackball branch forwarded motion and
explicitly discarded the button, so an app could be steered but never clicked.
It now delivers the same synthetic centre tap the touchless boards' select key
sends, on the press edge: the flag is a per-frame "held" level, so forwarding it
raw would have fired a tap every frame the button stayed down.
Repeat counts. The firmware already counts repeaters heard rebroadcasting an
outgoing flood, keyed on a fingerprint taken at transmit time; it is the refresh
glyph on a sent bubble. wada.mesh.send() now returns that fingerprint as its
second value on success, and wada.mesh.repeats(fp) reads the count back. Additive,
so an app reading only the first return is unaffected. The count grows as repeats
arrive, so it is polled rather than read once, and a direct message has none
because it is not flooded. Documented on the SDK page.
Built on all nine S3 envs and both ESP32-P4 targets.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
oumike. A firmware-wide appearance setting: Night stays the default and renders
exactly as before, Day is a low-glare light palette with its own semantic roles
for text, panels, fields, borders, controls, focus, charts, status colours and
chat surfaces. Selecting a mode restarts, so every LVGL object is rebuilt from
one coherent palette rather than half-repainted. There is a Night/Day selector
in Settings, Display, the Control Center Theme chip is now a direct toggle whose
sun/moon icon shows the active mode, and the standalone console UI has a
matching Day palette.
Main already carried the palette scaffolding for this, pinned to Night behind a
comment saying it was waiting on this branch, so the merge mostly replaces those
stubs with the real thing.
The prefs schema needed care. The branch appended theme_mode as v54, but v54 and
v55 were taken by boot_wifi_time/boot_wifi_open and loud_alerts before this
merged, so the field moves to the tail behind them and the version becomes v56,
with the trailing-field assert and the migration step moved to match. A packed
struct read back at the wrong offsets is the failure this schema's asserts exist
to prevent, so the invariants from both sides are kept rather than one replacing
the other.
That also surfaced a stale test: the v53 case asserted that v53 plus exactly two
bytes was the whole struct, which stopped being true when loud_alerts landed and
was not caught because the host test was not run then. It now counts every byte
appended since and asserts the new fields come back at their defaults. The
schema host tests pass. While there, the size comment said "about 500 bytes";
the struct is 114.
Built on all nine S3 envs and both ESP32-P4 targets.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
oumike. This is the actual merge: the previous commit claimed it and did not
contain it. The merge had refused because the working tree still held the
swatch fix, and the error was hidden by a filter on the command's output, so
only the swatch change was committed under a message describing both.
What lands here is oumike's work. Keypad navigation could skip a whole row on
the Send advert screen: candidates were scored as axis distance plus an off-axis
penalty with no notion of rows, so a wide dropdown two rows down beat the narrow
right-aligned switch directly beneath. navMoveDir now finds the nearest row band
first and only scores within it, which is direction-generic and so fixes the
same skip sideways.
Also: the focus highlight was invisible on two dropdowns created without inline
styling, since reverse-videoing to white on a stock-light-theme card is white on
white; an open dropdown list was clipped by the title bar because the existing
clamp callback was never wired to those two; and the Do Not Disturb chip now
uses the crescent moon the status bar already uses instead of a slashed bell
sitting beside the Sound chip.
navMoveDir is shared by every keypad board, so the whole matrix was rebuilt on
the real merge rather than on the earlier commit that lacked it: all nine S3
envs and both ESP32-P4 targets, all green, with both sides of the merge verified
present in the tree.
Tested on hardware by the author on T-Deck, ThinkNode M9 and T-LoRa Pager.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
oumike. Keypad navigation could skip a whole row on the Send advert screen,
because candidates were scored on axis distance plus an off-axis penalty with no
notion of rows, so a wide dropdown two rows down beat the narrow right-aligned
switch directly beneath. navMoveDir now establishes the nearest row band first
and only scores within it. That is shared navigation code, so the whole matrix
was built: all nine S3 envs and both ESP32-P4 targets, all green, and nothing
from today's work was reverted by the merge.
Also in it: the focus highlight was invisible on two dropdowns that were created
bare, since reverse-videoing to white on a stock-light-theme card is white on
white; an open dropdown list was clipped by the title bar because the existing
clamp callback was never wired to those two; and the Do Not Disturb chip now
uses the crescent moon the status bar already uses, instead of a slashed bell
sitting next to the Sound chip. Tested by the author on T-Deck, M9 and Pager.
Separately, the lock-screen colour swatches (#389, Buko84). Each swatch had its
colour set only for the default state, which left the theme free to repaint the
focused and pressed ones. On a d-pad board the swatch you are standing on is by
definition focused, so it turned white and you could not see the colour you were
about to choose, which is what Buko84 described. The colour is now pinned across
those states and focus shows as a ring outside the swatch rather than a fill
over it.
That is the picker, not the reported black lock text, which is still open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>