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>
Four small UI fixes, all reported on the ThinkNode M9 build.
Keypad nav could skip a row. navMoveDir ranked candidates by
"primary + 8 * cross" (gap along the pressed axis, plus a heavy penalty
for being off-axis), with no notion of rows. On Send advert, DOWN from
the Flood button landed on the "Flood every" dropdown two rows below it:
the dropdown is wide enough to overlap Flood's x-range, so its cross
penalty was 0, while the "On boot" switch directly beneath is narrow and
right-aligned, so its horizontal gap out-weighed a whole extra row of
distance. navMoveDir now walks the candidates twice — the first pass
takes the nearest element's rectangle as the row (or column) band, the
second discards anything lying entirely past that band before applying
the existing score. A press can no longer jump over an intervening
focusable, while alignment still decides within a row. Split the shared
geometry into navDirCandidate() and navDirMetrics() so both passes use it.
The focus highlight was invisible on the advert dropdowns. LVGL's stock
theme here is the light one (nothing calls lv_theme_default_init, and
lv_conf.h leaves LV_THEME_DEFAULT_DARK at its default), so a dropdown
with no explicit styling renders as a near-white card — and navFocusCb's
highlight reverse-videos the focused widget to COLOR_TEXT, painting white
on white. The settings pages never hit this because they set COLOR_PANEL
inline on every dropdown they build; the two on Send advert were created
bare.
An open dropdown list was clipped by the title bar. lv_dropdown's
constructor parents its list to lv_obj_get_screen(), which for a page on
lv_layer_top is that layer, so the list sat below the status bar in the
child order and got covered where they overlapped. clampDropdownListCb
already exists for exactly this ("wire onto every settings dropdown") and
was simply never wired here.
Both of those are now handled by one styleDropdown() helper — panel
colours and the 12px font on the button and its list, an accent-filled
selected row (LVGL draws it in LV_PART_SELECTED with the CHECKED state,
which the stock theme fills with its own blue primary rather than the UI
accent), and the clamp — applied to the two advert dropdowns. The
settings pages keep their inline styling for now.
Do Not Disturb gets its usual crescent moon. The control-center chip used
a bell that gained a slash when DND was on, which reads as "notification
sound" and collided with the Sound chip beside it; the status bar already
used the moon. The chip's own fill (solid accent on, faint off) carries
the state, so the glyph no longer changes with it. No new font asset:
sleepicons_font already carries U+F186 and is spliced into the g_font_16
fallback chain that ccToggle renders its icon in.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jrote1 is fitting a battery-backed DS3231 to the T-Deck's I2C bus so the device
keeps time across a full power-off without needing GPS or Wi-Fi. The RTC adapter
already had the hard parts, refusing an untrustworthy read and revalidating every
write, so this teaches it a third chip rather than adding a second path.
Three things differ from the soldered PCF parts and all three are handled by
normalising into the existing decoder rather than duplicating its rules:
- It lives at 0x68 rather than 0x51. That is also what makes it safe to add: it
cannot be mistaken for either chip an existing board carries.
- Day-of-week and day-of-month are swapped in its calendar block.
- It reports "my time is not trustworthy" as OSF in a status register instead of
in bit 7 of seconds, so that bit is folded into the one the decoder already
refuses on. A module whose backup cell went flat is therefore rejected the
same way a powered-off M9 is, rather than having its frozen calendar believed.
0x68 is a busy address, so identification requires a positive DS3231 trait
rather than an ACK: its temperature register pair, whose low byte is a quarter
degree in the top two bits and zero elsewhere, with a high byte in a range a
working chip could be at. A module that has never been set by us can also arrive
in 12-hour mode, which is converted rather than decoded as if it were 24-hour.
Transparent when no module is fitted, which is nearly every T-Deck: reads come
from the software clock, writes stop there, and the probe logs that nothing
answered. CAP_HARDWARE_RTC stays 0 for the board, since this is an aftermarket
addition rather than something a T-Deck has, so the cold-boot network time sync
stays on offer for everyone else.
Unverified on hardware: neither of us has the module wired yet. The host tests
for the shared decoder still pass.
Built on all nine S3 envs and both ESP32-P4 targets.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The M9's screen was never really going off (#390, Buko84, who spotted the lock
screen still faintly visible in bright light). Backlight off is not screen off:
the ST7789 keeps refreshing the same static image behind a dark backlight, which
is exactly what retains an image into the glass. The panel-sleep command the
T-Deck and V4 use was a no-op stub on this board and the M9's backlight path
never called it anyway. Both halves are fixed. The M9 takes the display driver's
default constructor branch, on the global SPI instance it shares with the radio
and with no PIN_TFT_SCL/SDA defined, so the command goes over that same bus
object inside a transaction rather than a second one on pins this board does not
declare.
The Discovered list showed impossible hop counts (#394, jesshampshire). 0xFF is
OUT_PATH_UNKNOWN, meaning an advert arrived by flood with no known path, and it
was being printed as a number: "255 hop". It now says the path is unknown, says
"direct" for zero hops, and pluralises, so a two-hop node no longer reads as
"2 hop".
Keyboard backlight controls are hidden on the older T-Decks (#382,
jesshampshire), which have no controllable backlight, so those controls did
nothing while still looking like settings. The "Older keyboard protocol" switch
added in beta_74 already declares which keyboard is fitted, so it gates these
too rather than asking the same question twice. The other half of that request,
swallowing the backlight key combo so it does not type a stray character, needs
to know which character actually appears; asked on the issue.
Also removed a duplicated pair of declarations in the prefs header.
Built on all nine S3 envs and both ESP32-P4 targets.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Buko84 asked whether the M9's notification sounds can be made louder. There is
no volume setting because there is nothing to turn: the buzzer on these boards
is a bare piezo on a GPIO driven by tone(), a fixed-duty square wave, so
amplitude is simply whatever the part does at that pitch.
Frequency is the lever that does exist. A piezo is far louder near its mechanical
resonance, typically around 4 kHz, than at the 1 to 2.6 kHz the chime uses, so
"Loud alerts" in Settings, Sound plays the same three-note shape shifted into
that band. Same chime, so it still reads as the same alert rather than a new one,
and toggling it plays it so the difference is audible while the switch is still
under your finger.
Off by default and opt-in, because the exact resonant peak varies by part and
this has not been measured on an M9. Schema v55, field appended at the tail.
Built on all nine S3 envs and both ESP32-P4 targets.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nordic letters on the M9 (#387, leon699). The letters were never missing: ø is
in the 'o' accent set and å and æ are in the 'a' set. The picker that reaches
them was switched off on this board because it is tap-to-pick and the M9 has no
touch, and the key-driven path built for the Pager was gated to the Pager. The
M9 does have a d-pad, and its mention box already uses it, so the accent box now
works the same way: left and right walk the variants while the box is up, Enter
takes one, Back or Backspace dismisses. There is no arming step, because the box
only exists for the moment after typing a base letter, so the arrows go back to
the caret as soon as it closes. The Accent popups setting is now shown on the M9
as well, since it finally controls something there.
Found while doing it: the key-selection cap was 8 while the 'a' set holds 9
variants, æ having been appended for #237 without bumping it. So æ specifically
was unreachable by key selection on the Pager too, which is the exact letter
class this issue is about.
The app drawer's message count was stale after unlocking (#393, Buko84). The
badges are a snapshot taken when the grid is built and nothing refreshed them,
so a message arriving while the drawer was open stayed invisible until you
navigated away and back, which is precisely what the reporter found. The refresh
tick now rebuilds on an actual change of the counts, not on every tick.
Also a guard, prompted by chasing a wrong theory: the whole prefs struct is
persisted as one blob, and the file-backed store rejects any value over 2048
bytes silently, so crossing that ceiling would stop every preference saving with
nothing in the log. It is about 500 bytes today and grows most releases, so the
ceiling is now a build error instead of a future support thread.
Built on all nine S3 envs and both ESP32-P4 targets.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wi-Fi came back on after a power cycle on the T-Display P4 while every switch
in the UI read off, and the radio really was on: it downloaded a map tile
(#373, wb6zsu, with photographs of both states). The C6 runs its own AT
firmware with auto-connect enabled, so it rejoins the last access point by
itself at power-on, before the firmware ever asks it to. The loop that enforces
the radio pref only acts on a change, and on the first pass it recorded the
state instead of applying it, so "off" never became a transition and the join
the C6 made on its own was never torn down. That is also why toggling Wi-Fi on
and then off fixed it: that made a transition. The state is now applied on the
first pass, once the AT link is actually up. The Tanmatsu carried the same
first-pass logic and got the same fix.
The map's own location marker is a bare white glyph and vanishes on light
basemaps (#366, 100monkeys). The contact markers a few lines below it already
carry a dark border for exactly this reason and self never got one, so it now
gets the same: a dark chip behind the glyph, which reads on any basemap without
spending a colour that already means something else.
Wi-Fi passwords can be revealed while typing (#381, jesshampshire). Entering a
PSK blind on a device keyboard is a real failure mode: the reporter needed about
ten attempts to join their own network. The toggle sits on the password label's
line so it costs no vertical space, and it switches the keyboard mirror too, or
the characters stay masked in the one field being looked at.
The composer shows a character count (#350, jrote1). The 160-character cap was
silent: typing just stops, which reads as a broken keyboard. It appears only in
the last quarter and marks the limit when reached, so an ordinary short message
carries no readout. Counted in codepoints to match how the cap is enforced;
by bytes an emoji would read as four characters.
Built on all nine S3 envs and both ESP32-P4 targets.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The PR added the board and its env, which makes it build. It does not make it
ship. Three lists decide that and none of them knew about it: release.sh ENVS
(what gets built and named into a release), gen-flasher-meta.py BOARDS (which
manifests the web flasher gets) and the flasher page itself, which lists boards
by hand.
Without these the board compiles cleanly forever and never appears in a release
or on the flasher, which looks like nothing is wrong.
Named wadamesh-wio-tracker-l2, its own Seeed Studio section on the flasher,
marked beta-channel-only like the Attaky was on arrival, since stable is still
beta_65 and this board has never been in a stable build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
oumike. ESP32-S3, 16 MB flash, 8 MB octal PSRAM, quad-SPI 320x240 panel via
LovyanGFX, GT911 touch, SX1262 at 22 dBm, GPS, microSD on SD_MMC. New board
file, new variant, one new env. Seeed start selling it next week and the
maintainers have the hardware; we do not.
Merged on their hardware bring-up rather than ours: display, touch, GPS,
microSD, battery and buttons are confirmed on-device by the author. What this
side can prove is that it does not cost anyone else anything, so the whole
matrix was built: all nine S3 envs including the new one, plus both ESP32-P4
targets.
The PR also carries accumulated touch-UI work from the author's development
line, which he flagged himself and offered to split out. Merged as-is because
the three-way merge came through with zero conflicts and nothing from today was
reverted: the prefs schema is still v54, and kb_force_legacy, boot_wifi_open,
the P4 gpsEnsureBigRxRing fallback and the chat-store migration logging are all
still present. Verified explicitly rather than assumed, since the diff against
main showed 1706 deletions purely because the branch predates today's merges.
Two bring-up findings worth keeping: Arduino's Serial1.setPins() takes (rx, tx),
so PIN_GPS_TX is the pin we receive on -- the same naming trap the V4-R8 and
Pager envs already carry, and it left this board's GPS listening on a silent
pin. And the panel is fixed landscape (CAP_ROTATABLE 0), so the keyboard's
rotate arrows are not built there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
oumike's #384closes#374: MeshCore seeds _txt_last_ts from an unrestricted
random uint32 and sendMessage() then raises the real RTC timestamp above it, so
outbound direct messages carried future dates on most boots. Patched in the
vendored core, fail-closed and idempotent.
#386closes#383: hardware RTC for the Pager's PCF85063A and the M9's PCF8563
with validation on both sides, time retained through a full power-off, and an
opt-in cold-boot Wi-Fi time sync. Prefs schema to v54, appended at the tail with
the assert updated.
cvhviz's #385 makes the M9's battery saver reachable at all (the machinery was
wired but the only enable switch was compiled for two other boards), fixes
GPS-off leakage, and enlarges the GPS UART ring mid-session.
Two build problems, neither visible to the authors:
- #384 wires its core patch into the IDF builds as a fail-closed VERIFY, and
the vendored copy in this checkout predated it. Applying the same patch step
fixes it; the error already said to run fetch-deps.sh, which is the right
design.
- #385 calls gpsEnsureBigRxRing() from UITask.cpp and MyMesh.cpp, which every
target compiles, while the definition is in src/main.cpp, which the two
ESP32-P4 targets never compile. Both P4 builds failed to link. A no-op
definition for those boards lives in UITask.cpp now, which is the standing
rule for anything shared between the UI and the Arduino main. A no-op is
honest here: neither P4 drives GPS through Serial1.
Built on all eight S3 envs and both ESP32-P4 targets.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
byrdo1308's Tanmatsu sits permanently on "migration pending, Last save FAIL",
and his boot capture contains NO migration lines at all. That is the useful
fact: the migration is not failing verification, it is bailing before anything
loggable happens.
There were two silent early returns and no success line either, so from outside
"never attempted", "gave up immediately" and "ran and failed" were
indistinguishable. Now each says which:
starting
no scratch buffer (allocation failed)
read-back verify FAILED across N segment(s); old file kept, will retry
cannot create the commit marker; old file kept
OK, N segment(s), M bytes
His other answer narrows it further and is worth recording: contacts and
Discovered DO persist across reboots on that device, so FFat works and this is
specific to the chat store's migration rather than the filesystem.
No behaviour change, only diagnostics. His next boot log should name the step.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three battery fixes plus the record in M9_PORT.md:
- The touchSleep battery saver was unreachable on the M9: the settings
switch and its callback were compiled only for the T-Deck and V4-R8,
and the pref defaults OFF with no other setter. Both #ifs now include
HAS_THINKNODE_M9 (the M9 is in batteryIsCharging's #else branch, so
the "USB powered" gate can never wrongly block there).
- WadaNmeaLocationProvider parked GPS_RESET asserted while the GPS was
stopped, per the core's convention. On the M9 that polarity is
inverted through R46 into NPN Q16, so "asserted" sourced base current
the entire time the GPS was OFF. Park the line LOW instead — the
zero-current level for both circuit styles; every start path still
pulses reset() explicitly, so the clean-reset guarantee holds.
- A mid-session GPS enable ran the whole session on the stock 256 B
Serial1 RX ring (the 4 KB TTFF ring is boot-gated on the persisted
pref, and setRxBufferSize is a no-op on a running UART) — and with
the saver newly reachable, one 50 ms park overflows that ring at the
M9's 115200 baud. gpsEnsureBigRxRing() cycles the UART to 4 KB from
both "gps on" funnels (toggleGPS, CMD_SET_CUSTOM_VAR) right after a
successful start, so a refused enable never spends the DRAM.
Also corrects both stale "keyboard S2 on the switched rail" comments
(schematic truth: always-on 3V3 via R3 — it decides the deep-sleep
drain budget). All 8 envs compile; on-device VERIFY list in M9_PORT.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MYDmpc53KpdmTLKzUNsX2s
Read the caveat before taking this one: it does NOT fix a live bug. The
sweep was suspected of causing repeated ~1.7 s ui:lvgl stalls that appear
only while the Spectrum page is open. Instrumenting it disproved that -
measured on M9 hardware the sweep costs 5181-5904 us/bin, so the constant's
own "~5.5 ms/bin -> ~44 ms UI block" estimate was right all along and eight
bins really is on budget.
That measurement is the "re-verify with a micros() log on hardware" the
standby(0x01) comment has been asking for; the verified figure is now
recorded at the constant so nobody re-derives it, along with a pointer that
the real cost is in the REDRAW path (spectrumPushWaterfall /
spectrumDrawTrace - ~1.8 s of LVGL compute against only ~190 ms of panel
flush), not the sweep.
What remains is a cheap guard: bound the tick by wall clock rather than a
fixed bin count, so a slower radio or a busier bus can never turn it into a
multi-second block. It is clamped so it can only ever REDUCE bins per tick,
never raise them above SPEC_CHUNK - on hardware it simply settles at 7-8,
i.e. today's behaviour, on every board. Hysteresis stops it flapping 7<->8,
since 8 x ~5.5 ms sits inside the jitter band of the budget.
Drop this commit if you would rather not carry the guard; nothing else
depends on it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Christopher Van Hoose <cvhvisuals@icloud.com>
The one that matters most is #354: tileCacheRemove() was written for the
firmware's own LittleFS cache but resolves through s_tile_fs, which is
re-pointed at the SD card root on every board that caches tiles there. So it
was calling SD.remove() on the user's own offline map pack, permanently, for
every queued tile whose .jpg happened to be absent. Gated to the internal
partition now.
The rest: remount through the full ladder after an in-device format (#359),
live labels no longer overlap the rows beneath (#357), hidden and inert widgets
stay out of the d-pad focus group (#358), keypress work is attributed in the
stall ring (#360), and GPS acquisition is measured from when it started rather
than when you looked (#365).
Built on all eight S3 envs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
s_ign_names_migrated was set before the work, so a failed re-open for write
(the same NVS failure ignWriteAll returns false for) latched a fold that never
happened. From then on ign_nm2 does not exist and ign_nm is never consulted
again, so ignNamesReadAll returns 0: every name block silently stops firing for
the rest of the boot. Worse, the next Block tap writes a fresh ign_nm2 holding
only that one entry, and the next boot's migration sees ign_nm2 present, skips,
and the old list is orphaned for good.
Latch on success instead, and remove ign_nm only after the new key actually
holds the list -- keeping it costs nothing, since nothing but this function
reads it and this function reads it at the correct old stride.
The "nothing to do" case still latches on the first call, so the steady-state
cost the latch exists for is unchanged.
Widening UiSegMsg from 233 to 240 bytes is safe to READ in both directions --
the header is self-describing and readHistoryRec prefix-reads or skips the
surplus. Appending is not. uiSegAppendRecords writes sizeof(UiSegMsg) into
whatever file the table points at, and after an upgrade that file is the
still-non-full active segment whose header says 233:
record 100: reader reads at 23316, record starts at 23316 (off by 0)
record 101: reader reads at 23549, record starts at 23556 (off by 7)
record 102: reader reads at 23782, record starts at 23796 (off by 14)
Every record after the first appended one is read at the wrong stride. The
damage is silent and it escalates: garbage bubbles, and a `seq` read out of a
long name's tail bytes lands far in the future, which makes the monotonic
filter drop every later segment and leaves _ui_seq_next poisoned. The next
compaction writes the result back for good.
Only a non-full segment can ever receive an append, so flag exactly those for
the existing rewrite_open repair when their on-disk width is not ours. Step 0
of segBuildJob runs that before any append and lays down a fresh header at the
current width; a full old-width segment is left alone, since it reads correctly
from its own header. The synchronous drain was never exposed -- it turns every
append into a whole-segment rewrite already.
The mirror case is documented on UiSegMsg rather than fixed: an older build
reading this record is fine, but an older build APPENDING to it writes 233-byte
records into a 240-byte-header file. That write belongs to the old firmware and
no change here can prevent it -- a version bump would only turn it into an
outright rejection.
MeshCore names run to 31 characters (ContactInfo::name[32]), but
UIMessage::sender held 24, so every longer name was cut on the way in.
d304d3f made the cut visible instead of letting it push the author into
the message body; this makes it stop happening. MAX_SENDER_NAME goes to
31 — exactly the wire width, so nothing is lost.
Truncation could never fix three of these. "Ack" and "Mention" insert
@[<sender>] into the composer, and textMentionsMe needs the whole name
followed by ']', so a mention of a long-named node never reached them.
Block-by-name compares the stored name against the sender on the RX
path; for ROOM posts those two came from different widths already — the
check sees the untruncated 31-char author while the stored entry came
from the 24-char field — so a room author with a long name has simply
been unblockable. Info and the chat-list preview showed a cut name.
The obvious change — widening UiHistoryMsg::sender — would have been
quietly destructive, so the on-disk width is frozen at 25 instead and
the overflow is APPENDED to UiSegMsg, after seq. sender sits between
thread and text, so widening it in place shifts text for every record
already on disk, and nothing would have caught that: the segment store
is what actually holds messages now, and uiSegOpenValidated checks only
magic/version/rec_size — it never consults k_ui_history_version or
k_ui_history_min_version, which guard the legacy files alone. An old
segment would have passed validation, prefix-read into the new offsets,
lost the first 8 characters of every message body and read seq as 0 (so
loadMsgsFromSegments renumbers the lot), and the next compaction would
have written that back permanently.
Appending at the tail is the evolution path UiSegHeader already
documents, and it is correct in both directions: an old 233-byte record
zero-fills the new field and yields its original short name, and older
firmware reading a new 240-byte record copies the first 233 bytes and
ignores the tail. That second half is why k_ui_seg_version STAYS 1 —
bumping it would make older firmware reject the file outright and
quarantine a history it could have read. So there is no migration, no
version bump, and no wiped chat history. static_asserts now pin the
offsets an already-written record depends on.
TOUCH_IGNORED_NAME_LEN has to move in lockstep (28 -> 32) or blocking
breaks: a slot narrower than a sender stores a cut name the full-width
check can never match, and the block sits in Settings doing nothing.
Its NVS blob has no header — the entry count is its length divided by
the slot width — so re-slotting in place would mangle every entry after
the first and the next write would make that stick. The key is renamed
ign_nm -> ign_nm2 and the old blob folded in once at first read, which
is unambiguous in a way that sniffing the blob length is not (28 and 32
share multiples at 224 and 448, both inside the 16-entry cap). The
invariant the header only stated in prose is a static_assert now.
Split/rejoin is a host-tested header next to ChannelSenderSplit, with
the field-width bounds spelled out: a corrupt record can leave the name
field unterminated, and the message body is what follows it on disk.
Signed-off-by: Ruben Laban <ruben@tun0.nl>