Commit Graph
32 Commits
Author SHA1 Message Date
Kaj SchittecatandClaude Opus 5 6868911f59 touch: SD Scan, find and remove Windows malware on the SD card
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>
2026-09-18 13:33:30 +02:00
Kaj SchittecatandClaude Opus 5 ce37110515 touch: web unlock, a way out of console mode, paste keys, M9 update notice
- 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>
2026-09-17 20:43:06 +02:00
Kaj Schittecat 02734a4642 Merge PR #523 from oumike: SD recovery, offline map upload, glance emoji, T-Deck Pro and V4 kit input
- 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)
2026-09-17 19:55:51 +02:00
Kaj SchittecatandClaude Opus 5 793a673c64 touch: Bluetooth LE keyboards on every S3 board
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>
2026-09-17 19:52:21 +02:00
Michael A. Cojocari e83bb7582e feat: upload offline maps to SD 2026-09-16 20:53:54 -04:00
Kaj SchittecatandClaude Opus 5 2f6984443d touch: fix the chat page-scroll panic on long chats (#428, #475)
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>
2026-09-16 14:43:14 +02:00
Michael A. Cojocari fdde0dd0cc Merge remote-tracking branch 'refs/remotes/pr/497' into integration/all-open-prs-20260913 2026-09-13 18:58:22 -04:00
Michael A. Cojocari a6fdbfcf36 Merge remote-tracking branch 'refs/remotes/pr/469' into integration/all-open-prs-20260913 2026-09-13 18:56:57 -04:00
Michael A. Cojocari 2a481ef64c Add M9 lock-home-to-drawer option 2026-09-10 20:04:57 -04:00
Kaj SchittecatandClaude Opus 5 503b2fe1f2 touch: sound previews obey the master switch (#464), exact telemetry position (v59)
#464 (PD0RCM, T-Deck Plus): "I can hear the sample sound and the volume change,
but not incoming messages." That board has the hardware, and previews and
arrivals go through the same playback function on it, so the audio path was
never the problem.

Every preview on the Sound page played UNCONDITIONALLY: the sample, the volume
steps, and each per-type toggle. An arriving message checks the master Sound
switch and Do Not Disturb. So with the master off the page chimes at you the
entire time you configure it while nothing ever sounds on arrival, which reads
exactly like a broken notification path and is almost certainly what he hit.
A preview is now silent precisely when a real notification would be, and says
which of the two silenced it rather than just going quiet. The master switch
keeps its own confirmation chime, because that fires exactly when you turn it
ON, which is the one case where a sound is the right answer.

Telemetry position (v59, honza_87628, via Discord): the advert displacement of
#399 was extended to telemetry answers because a privacy setting a telemetry
request walks straight around is a hole rather than a feature. He then made the
fair counter-argument that an answer is not a broadcast: it is encrypted to one
contact who already holds the permission, and it only goes out because they
asked. Losing the ability to give trusted contacts a real fix is a real loss.

So make it a choice instead of an assumption. New tail field telem_loc_exact,
OFF by default, so the private behaviour is what you get unless you say
otherwise, and the broadcast advert stays displaced either way. The switch sits
directly under "Share my location when asked", where the decision belongs.

Schema 58 -> 59. The host test enumerates every appended field and caught the
change, as designed; all three of its migration assertions are updated and it
passes.

Reported-by: PD0RCM, honza_87628
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 11:37:54 +02:00
Michael A. Cojocari f3136ea951 Merge branch 'main' into feat/tdeck-pro-target
# Conflicts:
#	deploy/apps/lang/bg.lang
#	deploy/apps/lang/de.lang
#	deploy/apps/lang/el.lang
#	deploy/apps/lang/es.lang
#	deploy/apps/lang/fr.lang
#	deploy/apps/lang/hu.lang
#	deploy/apps/lang/it.lang
#	deploy/apps/lang/nl.lang
#	deploy/apps/lang/pt-br.lang
#	deploy/apps/lang/ro.lang
#	deploy/apps/lang/ru.lang
#	deploy/apps/lang/sr.lang
#	deploy/apps/lang/uk.lang
#	src/ui-touch/UITask.cpp
2026-09-08 15:41:45 -04:00
Michael A. Cojocari 0fafb09044 Expand browser file transfer management (#448) 2026-09-07 14:46:56 -04:00
Michael A. Cojocari 28fb57ef4c Fix Attaky usability issues (#423) 2026-09-06 16:55:41 -04:00
Kaj Schittecat 6db04d3e1d Merge PR #413 (oumike): authenticated browser file transfer 2026-09-06 11:11:57 +02:00
Kaj SchittecatandClaude Opus 5 ee15fcb335 Fix two beta_76 regressions of mine, and advertise a position without your address
#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>
2026-09-06 09:57:25 +02:00
Michael A. Cojocari 9f25f963d6 Add authenticated browser file transfer 2026-09-05 14:32:08 -04:00
Michael A. Cojocari 3749df2907 Work 2026-09-05 12:23:54 -04:00
Kaj SchittecatandClaude Opus 5 2a8b62c6a7 Merge PR #375: Day and Night themes (#296)
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>
2026-09-04 09:11:54 +02:00
Michael A. Cojocari 092528a8fb Fix RTC retention and cold-boot time sync (#383) 2026-09-02 21:37:35 -04:00
Ruben Laban 7eada9b8c3 touch: store the whole sender name, not the first 24 characters
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>
2026-09-01 08:10:57 +02:00
Ruben Laban 9dd3d14152 touch: a colon in an unprefixed post is not an author
The split accepted any "X: " prefix once the length test came out, so a post
that carries no author but does contain ": " — "the repeater at Ouderkerk is
back up: full quieting again" — got read as an author of 36 characters. That
invents a sender AND hides the start of the body.

The old `slen <= MAX_SENDER_NAME` test used to catch this as a side effect;
dropping it for long names dropped the plausibility check with it. Restore the
check at the WIRE width (31 — MeshCore keeps every name in a char[32]) rather
than the destination width, so a real 31-char name still splits and truncates
while a 32-plus "prefix" is left in the body where it belongs.

A static_assert pins kMaxWireName >= MAX_SENDER_NAME, so widening the field can
never leave the split rejecting names the field holds fine.
2026-09-01 08:09:41 +02:00
Ruben Laban 78ca05f5bd touch: a long name no longer pushes the author into the message body
A channel post arrives on the wire as "SenderName: body" — the author is
in the text, not a separate field. Both split sites gave up when the name
was longer than MAX_SENDER_NAME (24), and giving up meant the sender fell
back to from_name, which for a channel is the CHANNEL name. The bubble
then read

  #channel      date
  nick: message

instead of the author over their own message. MeshCore names run to 31
chars (ContactInfo::name[32]), so this hit every post from a node with a
25+ char name — the "occasionally" in the report.

Truncate the label instead of rejecting the split, backing off to a UTF-8
character boundary so a multi-byte name is never cut mid-sequence (the
missing-glyph sanitiser would render the tail as '*'), and mark the cut
with "..." so a shortened name does not read as a different node.

Widening MAX_SENDER_NAME is the real cure but not an option here: sender[]
sits in the MIDDLE of the persisted UiHistoryMsg record, and this file
format only tolerates fields APPENDED to the end — moving `text` would
force k_ui_history_min_version up and discard everyone's chat history.

The two copies of the split are now one host-tested header, matching how
Utf8Text/ReaderContent are factored.

Signed-off-by: Ruben Laban <ruben@tun0.nl>
2026-09-01 07:53:14 +02:00
Michael A. Cojocari 952075a22e Day/Night 2026-08-31 09:37:45 -04:00
Kaj Schittecat 65ea09ea89 Merge PR #316: WAV/MP3 playback for Lua apps
oumike. Closes #315 (pisti87's request).

Two conflicts, both resolved by keeping BOTH sides rather than choosing:

  - sdRuntimeLifecycleBusy() gained an audio-playback source here and a web
    reader source in #317. They are independent consumers of the same card and
    both have to gate the mount lifecycle. The reader's self-exclusion is kept:
    it calls this from its own task while holding the card and would otherwise
    deadlock against itself.
  - The Lua harness caps table needed sd_list from #312 as well as the audio
    flags, and the test order needed the wardrive suite from #324 as well as
    audio_api.

Built on all eight S3 envs and both ESP32-P4 targets.
2026-08-27 10:49:59 +02:00
Kaj Schittecat 9fb65f91c4 Merge PR #320 2026-08-27 10:28:55 +02:00
Kaj Schittecat 0987f1385f Merge PR #317 2026-08-27 10:28:15 +02:00
Michael A. Cojocari 417a52caa1 Fix Wardrive UTF-8 text handling
Signed-off-by: Michael A. Cojocari <michael.cojocari@gmail.com>
2026-08-25 16:12:24 -04:00
Michael A. Cojocari 5c029e7526 Latch keyboard symbol modifiers
Signed-off-by: Michael A. Cojocari <michael.cojocari@gmail.com>
2026-08-24 15:09:53 -04:00
Michael A. Cojocari 9d7d1f2df7 Add SD home page to web reader
Signed-off-by: Michael A. Cojocari <michael.cojocari@gmail.com>
2026-08-23 21:40:52 -04:00
Michael A. Cojocari acdcc6e7fc Work 2026-08-23 20:32:26 -04:00
Pixel Perfect 65f25334c4 fix(prefs): repair beta 57 config migration
Beta 57 inserted retry_echo into the middle of the packed TouchCfg blob, shifting the remote-mode and later fields during migration. Define a v45 layout with retry_echo appended at the true tail and recover ambiguous v44 suffix bytes to safe defaults while preserving the stable prefix. Add a host regression test for v43, broken v44, and v45 blobs.

Signed-off-by: Pixel Perfect <me@pixp.cc>
2026-08-03 19:01:35 -07:00
mikecarper 04d5f33575 Retry messages until an echo is heard 2026-08-01 17:13:03 -07:00