The first real report read "works, about a day, 57 min ago" and the two halves
looked like they disagreed. They answer different questions: "about a day" is
how long the tester ran the build, "57 min ago" is when they sent the report.
Now it says "ran it for about a day, reported 57 min ago".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things Kaj hit on a T-Deck within a minute of the first flash.
Taps that missed a checkbox went through the card to whatever sat behind the
popup. The card had LV_OBJ_FLAG_CLICKABLE cleared, copied from the version
picker, and a non-clickable object is invisible to hit-testing, so the tap
carried on to the page underneath. The card is clickable again, and tap-outside
now compares the point against the card's own area rather than assuming nothing
inside it can reach the backdrop. Events do not bubble in LVGL 8 unless asked,
so the clear was never needed for its stated purpose.
The card is also explicitly scrollable and capped to the panel height. Eleven
rows do not fit 240 px, and a card taller than its backdrop puts its own
controls outside the area that catches taps.
"Report this build" reads like reporting a fault with it, which is the opposite
of what the button is for. It is "Send a test report" now, "Send another test
report" once you have, and the form is titled "Test report: <tag>". Renamed in
the docs, the site, the issue template and the tracking-issue text too, so the
instruction people are given matches the button they are looking for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four things the merges needed.
The POI menu from #549 was not in the popup registry. Every modal in this file
registers its closer there, and one that does not cannot be dismissed by Back or
by the screen-lock teardown: it stays on screen while the page behind it
changes. That is the same failure #553 describes for Known Regions, which #550
fixes in the same slice.
Czech had no POI string. It was added after #549 was opened, so thirteen
language files got the row and the fourteenth did not.
Auto keyboard backlight never switched off when the screen timeout is Never.
#550 changed Auto to follow the screen timeout instead of a fixed three-second
window, which is right, except that Never then means lit forever. Auto is the
default mode, so a device left on a desk would burn its keyboard backlight until
the battery gave out, where the old window at least ended. The Never case is
bounded to a minute; mode On is still there for a permanently lit keyboard.
Compact chat rows have vertical padding on the T-Deck Pro and Max (#563).
Compact mode leans on the alternating row tint to show where a message ends and
e-paper has no tint, so messages ran together, worst when one wrapped.
Not fixed here: #562, the Pro terminal drawing black on black. The console
already special-cases e-ink, the Pro's palette is white paper and black ink,
LovyanGFX treats equal fore and back as transparent rather than a filled block,
and consoleBegin gets the display before anything renders. The remaining suspect
is the pre-LVGL boot screen, which fills black on purpose because "our pre-LVGL
screens are always dark" and inks the whole panel on e-paper. That needs the
device to confirm, and guessing at an e-paper fix is how a regression ships.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A promote spans every test build published since the last stable, and reports
keep arriving on a build after the next one has been cut, so showing only the
newest beta hid exactly the evidence the decision needs. The page now carries a
chip per build above stable and switches between them.
They never merge. A report vouches for the build it was sent from and for
nothing cut after it; averaging them would let somebody sign off code they
never ran.
The verdict got stricter, because testing it showed it was hollow. "Is anybody
running this board" was answered by the opt-in install count, which most people
will leave off, so a single green board could declare a whole release ready.
A board now also counts as out there once anybody has ever reported on it, on
any build: reports are not opt-in, and somebody who reported on beta_85 still
owns that board when beta_86 lands. So silence on a board we have heard from
before blocks the promote, and silence on a board nobody has ever reported on
does not.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The matrix was only reachable from a corner button and a line buried in the
channel explainer. It now has a card in "Docs, tools and help", next to the
others, which is where people actually look.
The tracking issue is dropped: the matrix lives on the site and no write
credential for it exists anywhere. The publishing code stays in place and
dormant, so it is one line in /etc/wadamesh-reports.env if that changes, and
the service and deploy script now say publishes:false is the intended state
rather than a missing piece.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nginx and the service were both adding Access-Control-Allow-Origin to the
matrix response. Two of them is not a stronger permission: the CORS check
fails on a duplicate and the browser rejects the response, so
wadamesh.com/beta could not read firmware.wadamesh.com at all. Only the
service sets it now, verified from the live origin.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Settings, About gains "Report this build": works or a problem, five tick
boxes for what you actually exercised, how long you have run it, an optional
line. It posts to the report service and lands on wadamesh.com/beta, which is
what the next promote is decided on. Ticking matters: "it works" from somebody
who watched it boot and "it works" from somebody who messaged on it all week
are not the same claim, and a board only goes green on two of the second kind.
"Report a bug" draws a QR for a prefilled GitHub issue form. The issue is filed
from the reporter's phone under their own account, so no write token has to
exist in a public firmware image and the reporter gets the replies.
"Count this device" is off by default. It lets the update check say which board
and version it is running, so a board with no reports can be told apart from a
board nobody owns. Those need opposite responses and nothing could tell them
apart before.
Mechanics: the POST rides the existing core-0 tile/update worker and its
WiFiClient/HTTPClient, like the version check, because a second pair on that
~8 KB stack overflows it. The device id is a salted SHA-256 of the public key
truncated to 64 bits, so a second report replaces the first without saying who
sent it. Sending is two taps and the second lists every field that leaves.
Prefs v64 appends report_ping + report_done_n at the tail, which the schema's
trailing static_assert now checks.
Also here: scripts/build/matrix-check.sh prints what testers reported for a tag,
and release.sh runs it before a --promote. It never blocks; a board nobody owns
can never go green. With no reports at all it now says so rather than reporting
a clean bill of health, which is the habit this is meant to replace.
Built on all eleven S3 envs and the T-Display P4.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A beta goes stable today because it has been out a while and nobody shouted.
With 13 board images, most of them in hands we never hear from, "nobody
shouted" mostly means nobody with that board was listening. This makes the
evidence explicit: testers report per board per tag from the device, and the
promote decision reads off a grid.
The device cannot talk to GitHub itself. TLS does not fit (mbedTLS wants ~30 KB
of heap for a handshake and ~5 KB survives Wi-Fi association on the 2 MB
boards), and a token in a GPL image is a token everybody has. So the split is:
* Structured test reports go to a small service on the firmware VPS over the
plain HTTP the device already speaks, and it holds the only write
credential. SQLite is the database; the per-tag tracking issue is rendered
from it and rewritten in place, so GitHub can be down or rebuilt at will.
* Bug reports do not come through here at all. The device draws a QR that
opens .github/ISSUE_TEMPLATE/bug.yml prefilled with board, version and
diagnostics, and the reporter files it under their own GitHub account:
right attribution, GitHub's own spam controls, no token.
Green needs two separate devices whose owners ran the build for a day or more.
A board nobody runs cannot go green and does not block a promote; a board
somebody runs and has not vouched for does. That rule is the whole point: it
distinguishes untested from unowned, which "nobody shouted" cannot.
wadamesh.com/beta.html renders the same JSON for the promote decision.
Two things found while deploying, both repo-vs-box drift that would have bitten
somebody eventually:
* The repo's copy of the firmware vhost was missing /apps/ and /bringup/,
which had been added on the box and never committed. Deploying it would
have 404'd the Lua app and language store for every device. The repo copy
is now taken from the box, and the deploy script refuses to push a vhost
that has fewer locations than the live one.
* The repo's tile-transcode unit named a venv interpreter that does not exist
on the box (python3.14 there has no ensurepip, so venv cannot bootstrap pip
at all). The live unit uses the system python; the repo now says so too.
The firmware side (the report page, the QR, the opt-in ping) is not in this
commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The front page still advertised 11 languages and listed the original
built-in set; the docs said twelve in two places and thirteen in a third.
langs.json carries 14 downloadable languages, so 15 with English.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review of #531 turned up four things that had to be fixed before release:
- An unusable region name no longer discards the radio settings. The save
returned early, so frequency, bandwidth, SF, CR and TX power were thrown
away, and the rule rejects names with capitals or a "$private" scope, so
anyone carrying an older region could not change their radio at all. On the
pager that path is the silent blur save, so it failed with nothing on screen.
Now only the region is skipped, and it says so.
- Restored useChainedFont() on the telemetry Show button: without the fallback
chain that label is boxes in Russian, Ukrainian, Bulgarian, Serbian, Greek.
- Region scanning installs unknown repeaters as transient contacts so their
encrypted replies can be matched. They were never removed, so each scan left
up to 16 nameless entries in the contact table (and in Contacts). They are
dropped when the scan ends or the page closes.
- One radio request per scan tick. The loop ran through all 16 repeaters in a
single 200 ms tick, which is that many key derivations back to back.
The V4-R8 also did not build (its src/ files see a vendored lv_conf.h, so the
new text-size fonts were missing) and then did not fit at 101.3%. It builds at
85.6% now: the board no longer carries the compiled-in translations it
inherited from the V4, which it never needed, since with 8 MB of PSRAM its
store works like any other 8 MB board.
T-Deck Max (#545) joins the release matrix: build list, flasher manifest,
DEVICES.md and the site's board list. Board names in the flasher lost their
em dashes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
M9 Back and Bluetooth-keyboard Esc stop at the locked app-drawer root, M9
focus fixes, optional M5Stack CardKB on the V4 and V4-R8, V4-R8 touch and
text-size accessibility, Wio L2 SD retry, high-contrast day and night themes
(#544), channel region discovery (#541) and the Czech translation
contributed in #540 by brebtatv.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A built-in USB Files app (T-Deck and Heltec V4 for now) lets the page at
files.wadamesh.com browse, upload, download, rename and delete files on the
SD card, internal storage and the map tiles over Web Serial. File level, not
USB mass storage: the device keeps its filesystems mounted, the radio keeps
running, and the firmware enforces what may change. Live data (identity,
contacts, settings, history; /meshcomod on the card) is download-only.
- UsbFilesProtocol.h: framed messages (E7 5A, CRC32), a resyncing parser,
path checks and the access policy; host tests in test/.
- UsbFilesSession: one request at a time (both Arduino USB serial drivers
drop bytes when their RX queue is full), RX queue sized per session, SD
data through a DMA-capable bounce buffer, FatFs listings with sizes, a
beacon so the page never talks first, SD Scan's malware verdict per file.
- The companion link and MyMesh's console step off USB while it runs.
- Website: labelled side buttons, a USB Files button and card; the
files.wadamesh.com page, vhost and deploy script.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An infected M9 card turned up with 373 findings. Every list row is a focus
stop on a keyboard board, and the firmware's key navigation collects at most
160 of them per screen, so in 1.0 the Remove all and Scan again buttons below
the list could not be reached on an M9 at all.
1.1 puts the buttons above the list, first in focus order, on the results
and failed-removal screens, and shows at most 60 rows plus an "and N more"
row (the title carries the full count). App-only: no firmware change.
Harness: a 373-finding scenario, and a check on every list screen that the
buttons come first and the focus stops stay under 160. It fails on 1.0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Some ThinkNode M9 cards shipped with a dormant Windows worm (Elecrow
security advisory, September 2026). An infected card seen since carries
autorun.inf in the root, launching xlfqf.pif on open, explore and autoplay
with random-junk comment lines in between: the Sality autorun pattern.
- SD Scan store app (deploy/apps/sdscan/1.0, requires "sd", not seeded):
walks the card a small page per tick, lists what it finds and why, and
removes it after a confirmation screen with Cancel first. It says on
every screen that it only removes files it recognises and that
formatting the card is the safe fix. On older firmware it still finds
threats by name but cannot remove them.
- Firmware: wada.sd.check(path) and wada.sd.remove(path), plus paging for
wada.sd.list(path, start, max) and caps().sd_clean. What counts as a
threat lives in SdThreat.h: autorun.inf, Windows program, script and
shortcut extensions, or a real MZ+PE header under any name. remove()
classifies again in firmware and refuses anything else, so no app can
use it to delete tiles, backups or chat history. It clears read-only,
hidden and system first, because FAT refuses to delete a read-only file.
- A warning when a card with Windows malware in its top folder is mounted,
at boot or on insert, offering SD Scan (or the Store).
- Tests: test/test_sd_threat.cpp, and SD Scan harness scenarios including
the real infected card's root. Removal checked on a T-Deck.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Web interface: unlock a manually locked screen (#506). The device publishes
its lock state over the mirror socket and the page shows an Unlock button
while it is set. The lock screen absorbs taps by design and only a held
trackball or BOOT press unlocks on the device, so a browser had no way back
from a screen it had locked itself.
- Console mode: hold the panel for three seconds to leave it (#507). The
banner and `help` both say so. Console mode also applies the "Older keyboard
protocol" setting now: that is applied in the graphical startup path, which
console mode returns before, so the console ran on protocol detection alone.
On a T-Deck that needs the older protocol every keystroke there is garbage,
which is why `ui` could not be typed and the reporter had to side-load a
second firmware to get the device back.
- Console mode: a touch wakes a dark panel again on the touch-only boards,
where the keyboard and button wake paths sit below the console branch's
return. The waking press is swallowed so it cannot also type.
- Paste into a key field lifts the key out of the surrounding text (#526): a
32 hex digit channel secret or a 64 hex digit public key, spaced keys
included. Before, the field filled with prose and the length cap cut the key
off.
- ThinkNode M9: a waiting firmware update is visible (#443). The red "!" over
the bottom-bar gear is built in the #else of that board's block, so the M9
had no update signal at all; it gets a third slot in its own notice row,
steady amber rather than blinking. The Settings tile in the app drawer
carries an "!" on every board.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bluetooth serves either the phone app or a keyboard (Settings > Bluetooth).
Pairing lists keyboards in pairing mode and pairs with or without a code;
the paired keyboard reconnects by itself.
Keys are read the way phones and computers read them: the keyboard's Report
Map says which report carries the keys and how, so media keys and touchpads
are left alone, and the boot protocol is only the fallback. Key positions are
translated with the chosen layout (US, UK, German, French, Belgian), with
AltGr and dead keys.
While a keyboard is connected:
- text goes into the focused field, and touchscreen-only boards keep the
on-screen keyboard down (a second tap on a field brings it up anyway)
- the arrows, Tab, Enter and Page Up/Down drive the focus highlight
- Esc is the back button; a "Back key" setting picks another key for
keyboards whose Esc key types a character
- tab hotkeys and their hints over the tab bar are switched on
- Command tapped alone opens the emoji picker in a chat
- the status bar shows a keyboard instead of the Bluetooth glyph
The Bluetooth page is now a flex column with the choice on top, and the
settings page refit keeps it scrollable when its content grows.
Settings: prefs v61 (mode, layout, paired keyboard) and v62 (Back key).
Tests: test/test_hid_report_map.cpp, test_touch_prefs_schema.cpp.
Not on the P4 boards (Tanmatsu, T-Display P4).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It was listed as fully supported, but it is community-maintained and not
tested on every release, and every build before beta_80 fetched the wrong file
when updating itself. The badge now says so, in the same form as the V4-R8
card, and points owners on older builds at the website to update.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The "System Information" key fix (6bb98a1) changed the flat packs in
deploy/apps/lang/, but devices never download those: they fetch the immutable
copy at apps/lang/<ver>/<code>.lang, keyed on the version in langs.json. With
everything still at v21, the store kept serving the old key from lang/21/ and
no device re-downloaded anything, so the fix reached only the Heltec V4, which
bakes the table into its firmware.
Bumped "# ver:" and the catalog to 22 in all 13 languages and snapshotted the
packs into lang/22/, the documented three-step publish. The generator ignores
the version line, so the baked table and the published beta_80 firmware are
unaffected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds LilyGo_TDeck_Pro_companion_radio_touch to release.sh (as
wadamesh-tdeck-pro), a flasher manifest labelled "(experimental)", and a
website card following the V4-R8 pattern: experimental in the heading,
"Partially supported", beta only.
The card says plainly what is known: contributed through #469, all the main
hardware works, but it has been tested on a single v1.1 unit, v1.0 units are
the least proven, and the e-paper display redraws rather than updating
instantly.
The site is not deployed with this commit: the card's install button points at
manifest-tdeck-pro.json, which only exists once a beta containing the board is
published.
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>
Consolidated development PR from @oumike (with @aigarslv and @tmetz1987),
taken up to 703afb9: everything except the final merge that brought in the
MQTT Observer (#518, 618ffd0). That feature is held back for a separate
decision and lives on its own branch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The MeshCore founders' givealittle fundraiser has closed (reported on
Discord by jason000008), so the banner was sending visitors to a page that no
longer takes donations. Removed the markup, its styles and its dismiss script
together.
The corner button rails need no change: .fab-rail already falls back to
top:16px, which is exactly what the banner script set once it was dismissed,
so everyone now sees the layout returning visitors already had.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1.1 logs a survey to CSV and notes in its own docs that there is no on-device
viewer for it, so checking coverage meant pulling the card and opening a laptop.
1.2 adds a second screen that reads the run's own log back and plots it on the
firmware's basemap via wada.map, with each stop coloured by whichever direction
of the link you ask about. Colour cycles through how well we heard them, how well
they heard us, and the worst imbalance.
Design notes a reviewer may want up front.
- A sweep writes one row per responder against a single GPS fix, so a corner
where ten nodes answered is ten rows sharing one coordinate. The loader
aggregates by STOP, not by row, keeping the best link in each direction and the
largest-magnitude single-reply asymmetry at that stop. Drawing a raw row would
have coloured the marker by whichever node answered first, which is arrival
order and none of the three metrics the button offers.
- The log is read in 4 KB windows, one per tick, and finishes only on a read that
yields nothing. A callback gets 100,000 VM opcodes and parsing a whole log in
one would exceed it; and since queued rows keep draining while the screen is
open, treating a short read as end-of-file silently dropped everything appended
mid-walk.
- Stops are capped. Past the cap the reservoir halves in place and widens its
stride, and it carries the newest stop across each halving and appends the
final position when the walk ends, so the drawn route ends where the run did.
Simulated: a 5,000-stop run keeps 315 stops spread 79/78/79/79 across the four
quarters of the route and ends on stop 5,000. A prefix cap would have drawn the
first 400 and nothing after.
- Drawing is strided to at most ~150 stops. Each marker and segment is an LVGL
object created synchronously and the host puts no ceiling on an app's view
(k_map_markers_max belongs to the Map tab), so the bound has to come from the
app, on a loop the radio shares.
- The map view is closed before any ui.clear(), routed through one helper so the
order cannot be broken by a later edit. See the note in drop_map: MapUd is the
only userdata type without a generation field, so clearing with a view open
strands the one-view counter and leaves a stale pointer. Reported separately.
- ui.canvas argchecks 1..480 on both dimensions where map.view allows 1..800, so
the bare-plot fallback asked for 792 px on an 800 px board and would have
raised inside the very branch that exists to keep the app standing. Clamped,
and the call is guarded.
- Probing pauses while the review screen is open, per the store's guidance that a
survey should probe on a cadence its user chose. Entering banks the in-flight
probe only if it is still inside its harvest window, and otherwise drops it, on
the same terms the tick path uses; both call one shared predicate so they
cannot drift apart. Either way the discovery set is cleared so nothing crosses
the pause.
- Zoom and centre are fitted in Web Mercator units, matching latLonToWorldPx:
latitude costs 1/cos(lat) times as many pixels as longitude, and the visual
centre is the inverse of the mean projected y rather than the mean latitude.
Both matter on a long north-south run. A run straddling the antimeridian is
fitted on latitude alone, with the centre longitude unwrapped onto 0..360
before averaging so it lands near the line rather than on the far side of the
world, and the card says it happened.
- Layout reserves every fixed element and gives the plot the remainder, because
on_open is handed the body height rather than the panel height, and a
half-height plot pushed the control row off the bottom. The two-line label
reservations were checked against the shipped glyph tables at the narrowest
label width, which is what shortened two of the strings.
- The manifest gains "icon": "map". 1.1 shipped without one, so the drawer tile
falls back to the generic app symbol; an app cannot ship artwork, and of the
sixteen glyph names map is the one that describes what 1.2 now does. Drop it if
you would rather the tile stayed as it is.
TWO FIXES HERE BELONG TO 1.1, NOT TO THE REVIEW SCREEN. Both corrupt the file the
new screen reads, which is why they are here, but say the word and I will pull
them out and send 1.2 as the viewer alone.
- Ticks pause while the display sleeps; millis() does not. On waking, the harvest
deadline is long past, the radio still holds replies heard at the old position,
and sys.gps() reports the new one, so those replies were logged in the wrong
place. Pocket the device, walk 400 m, wake it. Sweeps dropped this way are now
counted on the status line, alongside a sweep discarded because the survey was
stopped mid-listen - two causes, one counter, and the label says dropped rather
than naming either.
- Renaming a run wrote the previous run's queued rows into the new file, and
wrote_header being process-local appended a second header mid-file whenever an
existing log was reopened. The on-device reader skips those lines; a
spreadsheet does not.
TESTED ON: nothing. I have a T-Deck Plus but have not run this, so treat every
runtime claim above as read from the firmware source rather than observed. Some
specifics are worth naming rather than hiding. The body-height figure and the
capability, argcheck and projection details above come from reading
src/ui-touch/, not from the SDK page, which is out of date in at least two places
(it documents an input event type the firmware never emits, which is why this
version has no on_input, and it omits both argcheck ranges). The riskiest
unverified behaviour is whether a map view can be created again after being
closed on a screen switch; that is guarded by pcall with a canvas fallback. Label
wrapping at the narrowest width is calculated, not seen.
Written with AI assistance, then reviewed by nine independent passes and two
cross-vendor panels. Every finding that survived verification is fixed. The one
that mattered most was the arrival-order colour above, which the first panel
missed and the second caught.
ONE QUESTION, not decided here. 1.2 is 34,159 bytes against 1.1's 8,477, and
wardrive already requires sdk_ext, so boards that cannot run it still pay for it
in the baked catalog. "seed": false looks right to me, but the README calls that
your knob, so the row is left exactly as it was.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: tmetz1987 <tylerzmetzger@gmail.com>
A release with twenty-odd notes pushed the whole install flow off the screen, so
the box that is meant to be a summary had become the page.
It is a native <details> now, collapsed on load, reading "See what's new in
beta_78 - 24 changes". The count matters: a bare "click here" asks people to
gamble a tap, while a number lets them decide whether they care. Native rather
than scripted so it opens with the keyboard, works without JavaScript, and does
not need a click handler; the JS only fills the count and forces it closed on
each channel switch, so flipping stable/beta cannot leave a stale panel open.
The default disclosure triangle is hidden and replaced with one that rotates,
because that marker renders differently in every browser.
Markup verified as balanced and the element confirmed to carry no "open"
attribute, which is what makes it start collapsed. I could not render it in a
browser to check visually: the harness refuses to navigate to localhost, and a
file:// page is served as a static snapshot with no JavaScript, so the count and
the collapse behaviour are unverified on screen.
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>
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>
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>
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.
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>
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>
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>
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>