SD Scan for the ThinkNode M9 SD-card worm, the firmware calls behind it,
and the startup warning.
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>
Bluetooth keyboards on every S3 board, PR #523 from oumike, and the web
unlock / console escape / paste key / M9 update notice fixes.
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>
- Pager: the SD card mounts again after the card was used on a computer
(#522): one SD-rail power cycle and retry, and power-off unmounts and powers
the card down
- At a glance: emoji render instead of squares (#521)
- Transfer page: upload an offline OSM tile folder to /tiles on the SD card
(#515), with strict path checks and resumable, atomic writes
- T-Deck Pro: touch release debounce, hardware-timed chat-row holds, and a
typed space no longer starts the screen lock
- Heltec V4 with the original Expansion Kit: the IO button goes back (tap)
or home (hold), larger keys with a magnified preview, and larger status-bar
targets (#525)
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>
Pressing down past the last message page-scrolls the chat list with an
LVGL animation. In a long chat the list uses compressed scroll
coordinates, and chatVirtRemap1To1Scroll re-anchored the position on
every LV_EVENT_SCROLL with lv_obj_scroll_to_y(LV_ANIM_OFF). That deletes
the running animation from inside its own step. LVGL 8.4 reads the
animation again after the step, and when the step was also its last
(the UI loop was busy for longer than the animation) it finished the
freed animation and freed it a second time. By the next round that
memory belonged to something else, and anim_timer called a garbage
get_value_cb: the jump to 0x00020000 in the beta_79 and beta_80 M9 dumps,
both at the same call site.
- chatVirtRemap1To1Scroll follows a scroll that an animation drives and
leaves the re-anchor to chatVirtOnScrollEnd. The page scroll now also
completes on long chats; the early re-anchor stopped it after a frame.
- scripts/build/patch_lvgl_anim_uaf.py skips LVGL's completion check when
the animation list changed during the step, as LVGL 9 does. Applied to
every touch env as a pre-script and to the vendored P4 copy
(fetch-deps.sh, build.sh). Idempotent, fails closed on source drift.
- test/lvgl_anim_uaf/run.sh reproduces it on the host under
AddressSanitizer: stock LVGL with the old handler reports the
use-after-free in anim_timer, and either fix alone runs clean.
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>
The per-board OTA_BIN_NAME chain ended in a bare #else naming the Heltec V4
TFT, so any board nobody had added to it silently downloaded the V4 image.
Update.end() only checks that an image is valid for an ESP32-S3, which the V4
build is, so nothing stopped it being flashed.
The Seeed Wio Tracker L2 shipped exactly like that. Its build defines only
HAS_WIO_TRACKER_L2, which the chain never tested, and the released beta_79
image carries "wadamesh-heltec-v4-tft" as its update name: an on-device update
on a Wio L2 installs Heltec V4 firmware onto Wio hardware and needs a USB
reflash to recover. The T-Deck Pro (HAS_TDECK_PRO) was in the same position
and would have done the same the moment it was published.
Every board is now listed explicitly, the V4 TFT included (the V4-R8 also
defines HELTEC_LORA_V4_TFT, and its branch already comes first), and anything
unlisted is an #error. Forgetting this table is now a build break instead of
a field brick.
Verified on all twelve OTA-capable targets: each compiles, and each binary
asks for exactly the artifact release.sh publishes for it (checked
mechanically: 10 PlatformIO envs plus both T-Display P4 SKUs, no mismatches).
Units already on beta_77 to beta_79 still have the wrong name compiled in, so
Wio L2 owners must update from the website over USB rather than on the device
until they are on a build with this fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#458 renamed TR("System info") to TR("System Information") and updated the
translation table to match, so the title stayed translated. But
src/ui-touch/i18n_builtin.h is GENERATED ("DO NOT EDIT") by
scripts/build/gen-lang-builtin.py from deploy/apps/lang/*.lang, and the rename
never reached those packs. Every regeneration since then -- the PlatformIO pre-hook
and the IDF build.sh both run it -- quietly wrote the old key back, so the source
asked for "System Information" while all thirteen tables answered "System info",
and the page title fell back to English in every language.
That is also why #517 appeared to revert the rename: its copy of the header was a
freshly regenerated one, faithfully reproducing the stale packs.
Rekeyed the thirteen source packs, then regenerated. The generated table changed by
exactly one key per language and nothing else, which confirms it was otherwise in
step with the packs, and the fix now survives the build's own regeneration, which is
the thing that used to undo it. The corrected packs reach devices that download a
language once the app store is republished with the next beta.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>