Compare commits

...
270 Commits
Author SHA1 Message Date
Kaj SchittecatandClaude Opus 5.5 5833e00d25 touch: beta_97 release notes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:18:07 +02:00
Kaj SchittecatandClaude Opus 5.5 18279edac9 release: the CrowPanel Advanced 10.1 (P4) ships in betas and the web flasher
- p4-release.sh builds and verifies the three revisions (V1.2, V1.1, V1.0)
  from crowpanel_p4_101/ in the same loop as the T-Display P4 images, with
  the same OTA-name and tag checks.
- Self-update is on (CAP_OTA 1): each revision updates from its own
  wadamesh-crowpanel-p4-101-v1x.bin, so a V1.0 never takes a V1.2 image.
- gen-flasher-meta writes a manifest per revision; wadamesh.com gets a
  CrowPanel Advanced 10.1 card with a V1.2 / V1.1 / V1.0 picker, and the
  standalone flasher one card per revision. Both point at the beta feed,
  so nothing shows until the next beta publishes the images.
- README, DEVICES.md and the site's board count (15).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:02:37 +02:00
Kaj Schittecat cff8bd4eea Merge crowpanel-p4-101: experimental Elecrow CrowPanel Advanced 10.1" ESP32-P4 port 2026-10-17 20:56:50 +02:00
Kaj SchittecatandClaude Opus 5.5 328434cbda touch: repeater status and telemetry requests go direct first (#683)
The Request button (and Ping) always reset the path, flooded a blank guest
LOGIN, and sent the STATUS/TELEMETRY REQ from inside the LOGIN-OK callback,
while the repeater's flooded login reply was still being repeated around us.
Bench against a repeater next to a T-Deck, 8 tries each: login-first got the
login answer 7 times but telemetry back only 2 times; a direct request like
the MeshCore app's got 8 of 8, about 1 s each (#683 reports under 5% from the
device, over 90% from the app on the same T-Deck).

Now the REQ goes out directly first on the stored path (flood when there is
none). Only when no reply comes within 6 s (12 s when it flooded) does the UI
fall back to the guest login, and after LOGIN-OK the REQ waits 2 s instead of
firing from the callback. Same bench: 7 of 8 answered (one through the
fallback, 17 s), and the login route alone went from 2 of 8 to 6 of 8. A reply
cancels the pending fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 20:54:39 +02:00
Kaj SchittecatandClaude Opus 5.5 e37258018a crowpanel_p4_101: experimental Elecrow CrowPanel Advanced 10.1" ESP32-P4 port
Standalone ESP-IDF target (crowpanel_p4_101/) modelled on tdisplay_p4/, sharing its
ESP-IDF, components and esp-hosted 2.12.3 backend. Untested on hardware.

- variants/crowpanel_p4_101: EK79007 MIPI-DSI 1024x600 display (Elecrow timings,
  LEDC backlight on GPIO31), GT911 touch on I2C0 45/46 (RST40/INT42), SX1262 on the
  J9/J11 header with a real DIO1 IRQ, LDO3/LDO4 bring-up, C6_EN reset callback on
  GPIO32, full-system-reset restart shim.
- Board revision is a build option: WADA_CP_REV=1.0|1.1|1.2 (default 1.2). V1.2 moves
  LoRa DIO1/NRST from GPIO53/54 to 27/28; V1.0 has the C6 SDIO data lines reversed
  (run 1-bit like Elecrow's V1.0 lesson). hostedSetPins() overrides Arduino's
  generic esp32p4 SDIO pin defaults, which only match V1.0.
- LVGL at native 1024x600 with the large-screen layouts; fixed landscape.
- Shared src/: one CAP_* block in device_caps.h plus HAS_CROWPANEL_P4_101 at the P4
  platform sites (storage law, SD_MMC, hosted BLE, no RTC_CNTL, no MQTT, fonts) and
  the CrowPanel no-unlock-control sites. navOpenChatPanel/navGoToMainTab get
  fallbacks for boards without CAP_KEYPAD_NAV.
- build.sh merge writes a 0x0 merged image per revision.

T-Display P4 still builds from the same tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 20:22:13 +02:00
Kaj SchittecatandClaude Opus 5.5 ed896b3404 touch: history never drops records at the segment cap; host test for the scheduler
fcb392a merged two neighbouring segments at the 24-file cap and, when no pair
fitted in one file, retired the oldest segment. A host simulation showed that
fallback does fire for heavy users (many quiet chats next to a few busy ones):
up to 626 old messages dropped over 60 reboots on 3 of 8 seeds. Now 24 is a
soft cap: at 24 pending compactions run first, then the best merge, and only
when no pair fits may a new segment open, up to a hard cap of 48. Below 41
files some adjacent pair of the 5000 ring records always fits, so the hard
cap never stops a save and nothing is dropped; files only pass 24 in that
heavy case, so older builds still read every file for almost everyone.

test/history_segments/run.sh lifts the scheduler out of UITask.cpp verbatim
and runs it against a simulated card: mixed, many-quiet, one-busy and
reboot-heavy workloads plus power cuts between a merged write and the
absorbed file's removal. 60k messages per run, every live record must
survive every reboot with no duplicates. beta_95's code loses ~48k of 60k in
the same run; this passes every mode (max 29 files).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 17:59:29 +02:00
Kaj SchittecatandClaude Opus 5.5 0351cf4410 apps: Store installs no longer leave a cut-off app behind
luaStoreDownloadWorker wrote each downloaded file to the card with one
f.write() straight from its PSRAM buffer, and an SD write from PSRAM can come
back short. Blackjack (48 KB, the first downloaded app past ~32 KB) landed cut
off near 32 KB: the install reported a network error, and the half .lua stayed
behind as an installed app that failed with "syntax error near <eof>"
(Discord). Files now go through the internal bounce-buffer writer into a .tmp,
are size-checked on the card and only then replace the real name, so a failed
install leaves nothing and a failed update keeps the old version.

wada.fs.write and the Home widget snapshot wrote from PSRAM the same way (Lua
strings live in the app's PSRAM heap); both go through a 2 KB internal buffer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 17:32:41 +02:00
Kaj SchittecatandClaude Opus 5.5 94baf4deac touch: beta_96 - release notes, language store v27, Blackjack on the App Store page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:22:25 +02:00
Kaj SchittecatandClaude Opus 5.5 d900bb83aa fonts: bidi and zero-width marks stay invisible with the '*' fallback
#679 ends every font chain in a '*' placeholder. LVGL itself only gives
<0x20, F8FF and 200C zero width, so LRM/RLM (common in Arabic and Hebrew
messages), ZWSP, ZWJ, word joiner, the embeddings, isolates and the BOM would
each have drawn a '*'. They now resolve to an empty glyph with no advance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:22:25 +02:00
Kaj Schittecat e43c06b09a Merge PR #681: Blackjack 1.0 in the Store 2026-10-17 15:17:16 +02:00
Kaj Schittecat 39cbbea6b7 Merge PR #679: one '*' placeholder for missing glyphs, hide skin tones (#673) 2026-10-17 15:17:16 +02:00
Kaj Schittecat 6a17f2a836 Merge PR #676: lock screen PIN, the keyboard's Sym/Alt layer stays live (#675) 2026-10-17 15:17:16 +02:00
Kaj Schittecat d89185367d Merge PR #630: Touch UI design calls (P4 one-row bar, chat clock, Device page, Bluetooth at setup)
# Conflicts:
#	src/ui-touch/UITask.cpp
2026-10-17 15:17:16 +02:00
Kaj Schittecat 1e72667b97 Merge PR #680: Web page Up/Down scroll the text on keypad boards (#677) 2026-10-17 15:16:56 +02:00
Kaj SchittecatandClaude Opus 5.5 aa5de751a5 touch: chats list ticks, M9 header layout code, contact popup titles, lock wallpaper veil
- #670: the chats list kept one tick after a DM was delivered. Its change
  signature never included the delivery state, so the rebuild was skipped;
  a delivery now bumps a generation it mixes in (and marks threads dirty).
- #672: on the M9 the keyboard layout code (EN, BG) sat on top of the GPS
  glyph in the chat header. It now has its own slot left of the clock, the
  GPS glyph moves left of it and the title leaves room for both.
- #668: with Large Text a long node name wrapped in the contact popup and its
  second line ran under the buttons. The title gets a fixed one- or two-line
  box (two when the name needs it) and the card uses most of a 320 px screen
  (up to 300 wide). The scroll check also counts the nickname line now.
- #652: a custom lock wallpaper was veiled with the theme background at
  150/255, milky on Day and cloudy on Night. It gets a lighter black veil and
  light text whatever the theme, like a phone lock screen.
- [HIST] boot line: segments and records loaded, for field reports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:15:24 +02:00
Kaj SchittecatandClaude Opus 5.5 9eda9b0f26 apps: Store lists up to 40 apps, canvases fit the big screens, button:bg
- The catalog parse stopped at 16 rows and the catalog has 16, so a 17th app
  (Blackjack, #681) was silently never listed; installed apps had the same
  cap. Both are 40 now, and the catalog buffer grows from 8 KB to 24 KB so
  the JSON is not cut off. PSRAM only.
- wada.ui.canvas raised a Lua error above 480 px, which killed apps that size
  a canvas from on_open(w, h) on the T-Display P4 (body ~575 tall), Tanmatsu
  and CrowPanel 5 (800 wide) (Discord). Sides are now clamped to the screen's
  long side instead.
- button:bg(fill [, border]) colours a button (or boxes a label), so apps no
  longer fake coloured buttons from panels that keys cannot focus (Discord).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:15:18 +02:00
Kaj SchittecatandClaude Opus 5.5 5198813f99 usb files: Lua apps can be dropped into the apps folder
/meshcomod/apps on the card and /apps internally now take top-level .lua and
.json files, so an app can be side-loaded without pulling the card (Discord).
Everything else there stays read-only: the folder itself, subfolders (<id>.d
data), app save files (.sav, .wgt), hidden names and 8.3 aliases. A change
from the page invalidates the cached app listing, so the drawer and the Store
pick the new app up the next time they open. Host tests cover the policy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:54:47 +02:00
Kaj SchittecatandClaude Opus 5.5 fcb392afd0 touch: chat history kept saving at the 24-segment cap; open chats under a locked screen stay unread
History: at the segment table cap a new segment waits for the oldest to age
out of the RAM ring, but after a reboot the ring only holds what was loaded
(per-chat caps thin segments without emptying them), so nothing was ever
evicted and every new message stayed RAM-only until the next reboot lost it
(Discord, "messages are going away after Oct 13th"). segBuildJob now merges
the adjacent pair with the fewest live records into the older file (the
absorbed file is unlinked after the merged one lands; the loader's
monotonic-seq filter covers a crash in between), and retires the oldest
segment when no pair fits. A flush with records still pending no longer
reports "everything durable": it logs [HIST] flush stuck and retries.

Read state: refreshChatDetail marked the open chat read on every refresh, so
messages arriving under a dark or locked screen were cleared at once and the
lock screen's unread cards stayed empty (Discord). Only mark read while the
chat is really in view; wake, unlock and the glance closing re-run the check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:54:42 +02:00
Kaj SchittecatandClaude Opus 5.5 8daf59bdd7 doc: DOC_APPS_AUDIT capture tour for every Store app
Launches each catalog app (kAuditStore, generated by gen-radio-audit-apps.py
into the gitignored doc_radio_apps.h) and captures store_<id> for the
website's App Store page. Music gets two bench tracks and a play tap first.
Capture builds only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:54:31 +02:00
Kaj SchittecatandClaude Opus 5.5 2986354f3c site: Join the community (Discord) button next to Install
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 10:38:17 +02:00
Kaj SchittecatandClaude Opus 5.5 8109bbf43a site: App Store page listing every Store app, with the SDK and how to submit
apps.html is generated from deploy/apps/apps.json (the catalog the devices
read) plus the site-only write-ups in deploy/site/apps-info.json, by
scripts/build/gen-apps-page.py, which deploy-site.sh now runs on every
deploy. Linked from the home page (Apps button + resource card), the SDK
guide and the sitemap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 09:17:59 +02:00
halflive-743 bc32153c69 apps: add Blackjack 1.0 to the Store catalog 2026-10-16 22:06:13 -06:00
Michael A. CojocariandCopilot 7f29bccf76 docs: M9 Web page Up/Down reading (#677)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-16 21:02:59 -04:00
Michael A. CojocariandCopilot 3470c308d9 Web page: Up/Down scroll the text on keypad boards (#677)
Links were the page body's only focus targets, so spatial nav jumped from
a link to the next one however far down, or straight to the toolbar, and
the text in between could not be read on the M9 (or with a d-pad).

Up/Down on the Web page now move to the next link already on screen, else
scroll the body a page (handing focus to a link coming on screen when the
focused one scrolls off); only at the end of the page does focus leave for
the address row or toolbar. navMoveDir reports when the page took the press
so the M9 and Attaky "focus did not move" fallbacks don't scroll twice.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-16 20:23:34 -04:00
Michael A. CojocariandCopilot 77f6a04543 fix(ui): one '*' placeholder for missing glyphs, hide skin tones (#673)
Missing characters drew as '*' in labels that went through
copyUtf8ReplacingMissingGlyphs() and as LVGL's empty box everywhere else.
Every text chain (g_font_12/14/16 incl. the Tanmatsu accent slots, the
Pager map font, the glance body) now ends in a font that draws the base
Montserrat '*' for any codepoint nothing else has. It is flagged
is_placeholder, which LVGL still draws, so Constellation's name filter
keeps dropping undrawable characters.

Skin-tone modifiers U+1F3FB-1F3FF have no baked image; map them to the
invisible zero glyph (as U+FE0F is), so a toned emoji shows its base.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-16 19:52:47 -04:00
Michael A. CojocariandClaude Opus 5.5 9aa590f0c1 fix(touch): the keyboard's Sym/Alt layer stays live while a PIN is typed on the lock screen (#675)
Locked, the T-Deck loop switched the keyboard modifiers off, and the PIN
screen opens over the lock screen, still locked, so a PIN was typed with
Sym off. Sym+mic ('0') sent nothing, and Sym+W..O sent the letter under
the digit, which a PIN accepts, so the PIN silently came out wrong.
Setting the PIN in Settings worked because the device isn't locked there.

Now the modifiers stay on while the device is locked, lit and has a PIN
(the first key typed on the lock screen opens the PIN screen and counts).
The Pager-style keyboards (Pager, T-Deck Pro, Max) had the same rule for
a tapped Alt, cleared every tick while locked; the same exception
applies there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Us6CjkLFga9GkFJmmr3rrq
2026-10-15 14:55:28 -04:00
Michael A. CojocariandCopilot ea4c791270 i18n: regenerate the V4 table for the Device page strings
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-15 14:15:52 -04:00
Michael A. CojocariandCopilot 896c8e7030 Settings: the group is 'Display & lock', so 'Hardware' only names the Device page's card
docs.html follows: the group's name, Theme under it, and Run setup again
and Reboot on the Device page rather than General.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-15 14:15:25 -04:00
Michael A. CojocariandCopilot b9ff27de8c site: Settings > About is now Settings > Device
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-15 14:15:25 -04:00
Michael A. CojocariandCopilot e83c483a0f i18n: rows for the Settings > Device and keyboard protocol strings
Hardware, the setup wizard Bluetooth hint, Radio:/Keyboard: and the four
keyboard protocol states in all 14 languages, taken from #631 so they
ship with the code that uses them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-15 14:15:25 -04:00
Michael A. CojocariandCopilot a913b1331e Settings: one 'Device' page; keyboard protocol switch on Keyboard page
The settings group that held Display/Theme/Lock is now 'Hardware', so
'Device' only names the page that was About. Run setup again and Reboot
move from General to an untitled card on that page. The older keyboard
protocol switch is also on the Keyboard page instead of a note pointing
elsewhere.

Refs #416

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-15 14:15:14 -04:00
Michael A. CojocariandCopilot ec5e08038b P4 Bluetooth: the status-bar glyph shows when Bluetooth runs; setup applies only a flipped switch
- The round one-row bar hid the Bluetooth glyph outright, but the esp-hosted
  P4 build does run Bluetooth on the C6. The glyph now follows
  hasBleCapability() && isBleEnabled() as on every other board (so never on
  the ESP-AT build), with a slot in statusBarPackRound between Wi-Fi and GPS.
- The one-row repack signature includes the Bluetooth and GPS glyphs, so
  either appearing re-packs the row.
- Setup's Bluetooth switch shows Bluetooth as it is when step 3 opens, and
  Finish applies it only if the user flipped it. A switch left alone no
  longer tries to start a Bluetooth that could not start at boot, which
  showed the P4 its 'not enough memory' alert on first boot (#612).
- tdisplay_p4/main/main.cpp: drop the stale 'keep BLE gated OFF' comment.

Refs #416

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-15 14:15:14 -04:00
Michael A. CojocariandClaude Opus 5.5 6f2783c8c9 P4 landscape Settings: two real columns, Device row full width
The wide landing wrapped groups two to a row, so each row was as tall as
its taller group: Device (one row) sat beside Mesh with a gap under it,
Connections beside the Device group, and System alone at the bottom.

In two-column mode the untitled group holding the Device row now spans
both columns at the top, and the remaining groups stack in two column
containers, split in order at the point that makes the taller column
shortest. Value-slot width is tracked per row since the spanning row is
wider.

Unchanged.
(from 7d6875a)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XERzMfBu6rw4behJzs9Hmw
2026-10-15 14:15:14 -04:00
Michael A. CojocariandCopilot 43c80a1b0f fix(touch): chat header keeps the clock and battery at the bar's normal height
The taller chat bar centred them on its full height, so opening a chat
dropped them below where they sit on every other screen. The round P4 packs
them on the one-row bar's line; the double-bar boards lift them by half the
extra height (the Pager's bar does not grow, so it is unchanged).

Unchanged.
(from 5f9f5ee)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-15 14:15:14 -04:00
Michael A. CojocariandCopilot d4a8a09310 P4: one-row status bar
The round-cornered T-Display P4 drops its two-row bar for one row: the
title on the left, then the clock beside the icons on the right (battery,
signal, Wi-Fi, GPS, the DND moon), every glyph's ink on one bottom line.
No Bluetooth glyph (wadamesh does not drive the P4's Bluetooth) and no
battery %. The stacked chat header only shows while a chat is open.

Split out of 4844a9b (Touch UI follow-ups 4) and rebased onto beta_92,
whose GPS glyph joins the packed row.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-15 14:15:14 -04:00
Michael A. CojocariandClaude Opus 5.5 3e8bf7c51d fix(touch): first-boot setup keeps Bluetooth on and shows a switch for it (#416)
Since beb85bd a fresh install forced Bluetooth off and persisted it when
the setup wizard opened, and nothing on screen said so: the device was
invisible to the MeshCore app until the user found the control-center
toggle. A format + clean install hits exactly that path, which fits the
T-Deck Plus report (stock MeshCore, always advertising, paired fine).

Setup no longer forces Bluetooth off; it stays as it booted (on by
default, as with stock MeshCore). Step 3 "Wi-Fi & Bluetooth" gets a
Bluetooth switch, on for a first boot and showing the live state on
"Run setup again". Finish applies only a change, via the same
enableBle()/disableBle() path as the control center, and shows the usual
failure text if the heap guard refuses a cold start. Skip leaves
Bluetooth on.

The step's paragraph is shorter, so the switch row still fits above the
buttons on 240 px-tall panels. The new string is translated in all 14
languages; i18n_builtin.h regenerated.

(cherry picked from commit 700355a23c465faa2df3b2ec5d46f06dbb8d7873)

Translations are in the i18n PR.
(from 3e2b8b5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zE15egJdCZ9nRBZmg1YUq
2026-10-15 14:15:14 -04:00
Michael A. CojocariandClaude Opus 5.5 b4c6c20900 feat(tanmatsu): report the LoRa chip, so Device names a radio on every build (#596)
Every PlatformIO touch build and both T-Display P4 radio builds define
RADIO_CLASS, which the Device page already reads. The Tanmatsu has none:
its SX1262/SX1268 sits behind the ESP32-C6 and is reached through
TanmatsuLoraRadio. That bridge now asks the coprocessor once at init
(lora_get_status, before RX polling starts) and exposes chipName(), and
deviceRadioName() uses it, falling back to "SX126x" if the C6 did not
answer.

(cherry picked from commit 94056e7eb7baf136d0f17ffa04e7e48cbe9ac712)

Unchanged.
(from 53c1cd9)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zE15egJdCZ9nRBZmg1YUq
2026-10-15 14:15:14 -04:00
Michael A. CojocariandClaude Opus 5.5 07c92b2af8 feat(touch): Device shows the radio; info pages scroll by keypad, close with X (#596)
- The Device page lists the radio this build drives ("Radio: SX1262"),
  from RADIO_CLASS with the "Custom" prefix dropped. The Device card's
  second line shows it too on boards without the T-Deck keyboard, which
  is what tells the T-LoRa Pager's SX1262 and LR1121 builds apart.
- System Information and Memory detail close with a compact X instead
  of a "Close" button.
- Their text now scrolls from the keyboard: the X is the only focus
  stop, so Up/Down and the Pager's wheel found nothing to move to and
  the text was unreachable off-touch. A shared navInfoModalScroll()
  scrolls the body instead, from navMoveDir (trackball / d-pad / arrow
  keys) and the Pager encoder; the M9's own special case now uses it.
  Focus stays on the X at either edge.

"Radio:" translated in all 14 languages; i18n_builtin.h regenerated.

(cherry picked from commit 4b99579b4e58aa78d165204502c14263559c563e)

Translations are in the i18n PR.
(from e56c6ea)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zE15egJdCZ9nRBZmg1YUq
2026-10-15 14:15:14 -04:00
Michael A. CojocariandClaude Opus 5.5 671213771b feat(touch): Device section at the top of Settings, About merged in (#596)
Settings opens with a Device card that names the hardware on a second
line ("LilyGo T-Deck · newer keyboard"), so a wrongly detected or set
variant is visible before anyone opens a page. It replaces About, which
moves from the bottom of the list; its content is unchanged and now sits
under a new hardware block:

- Model, and on the T-Deck the keyboard the protocol probe settled on,
  or "set manually" when the override is on (new
  tdeckKeyboardDetectedMode() getter).
- The T-Deck "Older keyboard protocol" switch, moved here from the
  Keyboard page, which keeps a one-line pointer to it. Flipping it
  updates the card and the page in place.

The update-available dot moves to the Device card, and the "Diagnostics
are on the About page" hint now points at Device.

New strings are translated in all 14 languages; the moved switch's label
and note were English-only in 12 and are now translated too. The old
About rows stay for firmware that still asks for them. i18n_builtin.h
regenerated. The site docs now say Settings > Device.

Not included: a T-Deck Pro touchscreen override (V1.0 and V1.1 are
separate builds, so it cannot change at runtime; the card shows which
build is installed) and an inaccurate-RTC flag (new clock behaviour,
better as its own ticket).

Fixes #596

(cherry picked from commit 726905ea607dea496f5e74e90e7b13068552007e)

Rebased onto beta_92's card-based Settings: the hardware readout and the switch are a Hardware card on the Device page. Its translations are in the i18n PR.
(from ddcff4e)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zE15egJdCZ9nRBZmg1YUq
2026-10-15 14:15:14 -04:00
Kaj SchittecatandClaude Opus 5.5 b85eef6fec touch: beta_95, MeshCore 1.18 via meshcomod v1.18.0.1
About (Build) and the boot log name the meshcomod release the core is:
"on meshcomod v1.18.0.1". release.sh reads it from the pinned core's own
examples/companion_radio/MyMesh.h, keeps the in-tree defaults in step and checks
every staged image carries it. Language store v26 (this build's new texts),
release notes for beta_95.

Bench (T-Deck, 1.18): device and self info, 404-contact sync, the companion CLI
(ver, board, get name/radio/tx/freq, unknown command), 4 packets received and a
zero-hop advert sent in 90 s, correct device time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 18:07:34 +02:00
Kaj SchittecatandClaude Opus 5.5 ef0b27f1e0 core: MeshCore 1.18 (core-v1.18.0 = meshcomod 8aba438f), V4 language table deflated
Pins move from core-v1.17.4 to core-v1.18.0, cut from the main meshcomod
companion-v1.18.0.1 was released from. The companion changes are ported from
meshcomod's rebase into wadamesh's own MyMesh:

- onCLICommandRecv: a contact's remote CLI command runs when the user granted
  that contact remote CLI, and lands in the chat as a CLI message otherwise.
- CMD_RUN_CLI_COMMAND (66) / RESP_CODE_CLI_REPLY (29) and the local companion CLI
  (name, pin, Wi-Fi, radio, af/dutycycle, rxgain, tx, rxdelay, path.hash.mode,
  multi.acks, board, ver), also reachable from the serial CLI. Settings without a
  prefs field answer "unsupported". NodePrefs layout unchanged.
- TXT_TYPE_CLI_COMMAND is accepted for sending; sendCommandData (and our queue-
  preserving wrapper) carry the text type.
- Channel data is delivered to the app (onChannelDataRecv), unsolicited responses
  (pushed telemetry) reach it as PUSH_CODE_BINARY_RESPONSE, and raw-data sends
  decode multi-byte-hash paths.

Clock: 1.18 seeds an unsynced ESP32 clock at 1 Mar 2026, so ClockFloorRTC's
MIN_VALID_EPOCH follows it (a set equal to the seed must stay invalid).
Pager LR1121: the AGC reset re-applies the boosted-gain setting (new core arg).
IDF builds exclude the new CommonRadioPrefs.cpp (needs <target.h>/radio_driver).

The 2 MB Heltec V4 went to 100.0% with the CLI. Its built-in language table is now
one raw-deflated blob per language (895 KB -> 312 KB), inflated into PSRAM by the
S3 ROM on first use (~10 ms, ~75 KB for the active language): 91.7% flash. Checked
on a T-Deck forced onto the table (nl, hu, el) and by the post-link key check.

Builds: all 15 S3 touch envs, Tanmatsu (3,237,024 B), T-Display P4 SX1262 + LR2021.
T-Deck boots and runs on 1.18.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 17:50:32 +02:00
Kaj SchittecatandClaude Opus 5.5 d3a9cf1453 touch: radio tools redesign, Music app, Store update badge, triage fixes
Spectrum: stat tiles (peak, floor, our channel, busy), plot and waterfall drawn
at screen size (the lv_chart and zoomed waterfall were the page's render cost),
dark-to-yellow waterfall, peak hold, our-channel marker, Band/Channel and
Live/1 min/10 min chips. RF Monitor 1.4 (tiles, filled chart, typed feed, packet
detail card; the old type table did not match MeshCore payload types) and
Airtime 1.6 (big figure, tiles, duty line, rx/tx split).

Lua SDK: ui.panel, bold labels and a size-24 figure font, colors.warn/border,
app.on_audio (fires with the screen off), read-only wada.sd.list and sd: audio on
the SD_MMC boards (P4, Tanmatsu, Wio L2), caps panel/audio_events/epaper, a
"music" icon and an "audio" Store requirement. button:set() no longer writes
label fields into the button object (heap corruption, every earlier firmware).
The SDK page's input table described events the firmware never sent.

Music 1.0 (Store app): plays MP3/WAV from the card's Music folder, shuffle,
repeat, keeps going through the list with the screen off. Verified on the
T-Display P4 end to end.

Store: the update check also fetches the app catalog and badges the Store tile
with the number of installed apps that have a newer version. Baked Lua apps are
raw-deflated and inflated by the S3 ROM (14.6 KB back on the V4); the V4 language
table keeps only texts of apps it can run.

Fixes: the SD-card notice no longer fires when the card mounts fine (#667); the
Control Center clock keeps time while open (#666); M9 keyboard brightness (#664);
the battery widget's number no longer sits on a ring too small for it (Discord);
M9 shortcuts doc follows #669's composer order. New texts in all 14 languages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 17:07:11 +02:00
Kaj Schittecat 3b0af92044 Merge pull request #665 from Ender436: T-Deck Max audio (chimes, custom sounds and Lua audio through the ES8311) 2026-10-15 15:28:35 +02:00
Kaj Schittecat 833fac8943 Merge pull request #669 from oumike: the composer puts the text box next to Quick Reply, Emoji (and the M9's #) beside Send 2026-10-15 15:17:45 +02:00
Michael A. CojocariandClaude Opus 5.5 d3202fe7fe feat(touch): the composer puts the text box next to Quick Reply, with Emoji (and the M9's #) beside Send
Non-touch M9: Quick Reply | text | Emoji | # | Send. Touch boards: Quick
Reply | text | Emoji | Send. Emoji and # align off Send's slot so they
follow the text box's new width on a rotation; the P4's landscape row
matches. The M9's arrow keys walk the new order: RIGHT from the end of
the text goes to Emoji, then #, then Send; LEFT walks back # → Emoji →
text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Us6CjkLFga9GkFJmmr3rrq
2026-10-15 09:06:21 -04:00
Ender436 3166e4a654 t-deck max audio fix
Notification chimes, custom WAV notification sounds and Lua wada.audio
playback through the ES8311 codec + NS4150B amp, mirroring LilyGo's
examples/ES8311/playWAV bring-up. I2S DOUT is GPIO40, not the header
name's 17 (measured in GlomarGadaffi/tdeck-max-phone#34); the XL9555
amp enable (line 6) is bracketed around playback and the shared-speaker
mux (line 10) held to the ES8311. Reuses the pager's Es8311Codec driver
and enables CAP_SOUND/CAP_SOUND_FILES/CAP_AUDIO_STREAM for the Max.

Signed-off-by: Ender436 <68835715+Ender436@users.noreply.github.com>
2026-10-14 11:37:08 -06:00
Kaj SchittecatandClaude Opus 5.5 75ec6759dd test: the M9 keyboard test follows #628's one-read-per-poll driver and checks its 70 ms repeat filter
#662's test queued three keys and read them all after one poll; #628 (for #625,
keys typing twice) reads one key per poll and drops the same key again within
70 ms. The test now polls per key and also checks that a repeat inside 70 ms is
dropped and one after it is a new press. Host tests: 18 pass, 2 skips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:34:12 +02:00
Kaj SchittecatandClaude Opus 5.5 179577ef10 beta_94 notes: CrowPanel 5.0, hashtag links, #628's fixes, the Radio widget line
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:33:33 +02:00
Kaj SchittecatandClaude Opus 5.5 0692775ba5 Merge follow-ups: the confirm X stays on touch boards (#628), link bubbles skip a full-screen layout (#580)
#628 dropped the close X from actions-only confirms on every CAP_KEYPAD_NAV board,
which includes the V4, the T-Deck and the touch-only boards that take a Bluetooth
keyboard. Now only a board without touch drops it; touch boards keep main's X,
skipped by keypad focus.

#580's chatCreateLinkLabel called lv_obj_update_layout on the message label to read
its height, laying out the whole screen for every link bubble as it scrolled in.
The caller already measured the wrapped text; it passes that height in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:17:57 +02:00
Kaj Schittecat 507511fab6 Merge PR #628 (oumike): Touch UI follow-ups 5a: quick fixes (#603, #605, #614, #617, #620, #625, #627, M9, Tanmatsu, Wio)
# Conflicts:
#	src/ui-touch/UITask.cpp
2026-10-14 10:17:19 +02:00
Kaj Schittecat ea4913f117 Merge PR #580 (TerryTel93): feat: add support for hashtag channels links with prefill and open fu… 2026-10-14 10:16:49 +02:00
Kaj Schittecat 806ecb0f71 Merge PR #655 (oumike): Add the Elecrow CrowPanel Advance 5.0 (#651) 2026-10-14 10:16:48 +02:00
Kaj SchittecatandClaude Opus 5.5 9f633d9d6d touch(home): no bottom bar on the Radio widget; a widget's bar never sits on its text
The Radio widget drew its airtime as a 5 px bar pinned to the bottom edge, which
on the usual widget height landed on the detail line (SF / BW) and looked like a
stray purple line through the text. Gone from Radio; and hwCardBuild now leaves
the bar out wherever it would not fit under the text (the Lua app widget's bar).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:15:55 +02:00
Michael A. CojocariandCopilot 31747c5477 Chat composer: name the text box's chrome height once
chatComposerChromeH() is the row padding plus the top row, used for the
text box height, the auto-grow and the one-line radius. A board-specific
inset (the P4's bottom lift in #611) then changes one helper instead of
four expressions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-13 09:37:44 -04:00
Michael A. CojocariandCopilot 3b58f58433 Review fixes for #628
- M9 shortcuts guide: the function row is Back, Ctrl and Triangle (no Mic).
- The X-last action lists, the bubble-only chat cursor and the lifted
  message-menu X are keypad behaviour, so they follow CAP_KEYPAD_NAV
  (Pager, Tanmatsu, T-Deck, ...) rather than HAS_THINKNODE_M9.
- Actions-only confirms (#603) drop the X on keypad boards only; touch-only
  boards keep it.
- M9 map buttons: focus keeps the accent fill but no local outline, so the
  shared keypad cursor is not outranked.
- M9 chat header: compare the bar's style height, not its stale coords.
- Contacts bulk delete ends with onThreadsChanged() so web clients refetch.
- The contacts selection buffer (~12 KB at MAX_CONTACTS) is allocated when
  select mode opens and freed when it closes, not at boot.
- Boot time sync: disconnect before the saved-network scan, so a first
  attempt still mid-handshake cannot fail it with ESP_ERR_WIFI_STATE.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-13 09:31:48 -04:00
Michael A. CojocariandClaude Opus 5.5 a5996faa41 M9: the chat header's clock, battery and GPS centre vertically
They sat on the title's line. They now centre on the header's two lines,
level with the back arrow and the avatar, so the whole header is centred
vertically. The step in from the rounded corner stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
2026-10-13 09:31:48 -04:00
Michael A. CojocariandClaude Opus 5.5 32c01f0682 M9: one key press enters one character (#625)
The keyboard controller latches a single key. When one was pending,
m9KeyboardPoll read the register up to three times back to back, a
"drain" meant to catch a second key, but with one slot a second read
can only return nothing or the same key again if the controller has not
cleared it yet. Keys came out twice, often and on any key.

- One read per poll, as the Launcher and Meshtastic M9 ports do.
- The same key reported again within 70 ms of the last accepted one is
  dropped as the same press. That covers a stale latch read on the next
  poll and the matrix bouncing on a slow, deliberate press, which the
  report found worse. Nobody presses one key twice that fast on purpose.
  The guard runs from the last accepted key, so bounces cannot stretch it.

Fixes #625

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
2026-10-13 09:31:48 -04:00
Michael A. CojocariandClaude Opus 5.5 7f025ffbf5 M9: the conversation header fits its text and lines up (#627)
The M9's UI sizes grow the fonts but not the geometry, so the header's
title and subtitle, centred in a bar of fixed height, clipped the
subtitle ("3 nodes heard today"). The back arrow and avatar sat low, the
clock and battery sat mid-bar while the GPS glyph kept the top row, and
the message list started several px below the header, leaving a strip of
bare background.

- m9ChatHeadH() gives the header's height from its fonts: the two lines
  from 3 px down, 2 px under them. The bar takes that height in a
  conversation, and makeChatDetail starts the list's content at the same
  line. The list is virtualised and places its rows against that padding
  once, so it is set when the list is built.
- The back arrow and avatar centre on the two lines.
- The clock, battery and GPS glyph share the title's line, with the GPS
  just left of the clock. The group steps in from the panel's rounded
  corner, which clipped the battery, and the title stays short of it.
- Leaving the conversation puts the bar's height, the battery and the
  GPS glyph back.

Other boards are unchanged.

Fixes #627

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
2026-10-13 09:31:38 -04:00
Michael A. CojocariandClaude Opus 5.5 14589f2c6e Status bar: the smoked-glass shade is not shifted with the icons
In a tall bar, updateGlobalStatusBar shifts each child of the bar up half
a row to put the system icons on the top row. It skipped the full-bar
backdrops (fade, dim) but not the shade drawn over a background picture,
so the shade sat half a row too high. The bottom of the bar showed the
picture unshaded, a strip over the top of the content below it. Measured
on the M9's conversation header: bar y0-38, shade y-11-27.

The shade is skipped like the other backdrops. All boards with a
background picture and a tall bar (conversations, settings pages).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
2026-10-13 09:31:38 -04:00
Michael A. CojocariandCopilot b247fa3ace Wio Tracker L2: Clock settings and the cold-boot time sync
The Wio Tracker L2 has no battery-backed clock, so it gets the same
opt-in saved-Wi-Fi time sync at power-on as the T-Deck
(CAP_BOOT_TIME_SYNC / BootTimeSync).

Split out of 4844a9b (Touch UI follow-ups 4).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-13 09:31:38 -04:00
Michael A. CojocariandCopilot 9d7da2354d M9 chat: # and emoji move left of the message input
The composer reads QR | # | emoji | text | Send. With the caret at the
start of the input, LEFT walks to emoji, then #; RIGHT walks back.

Split out of 4844a9b (Touch UI follow-ups 4).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-13 09:31:38 -04:00
Michael A. CojocariandClaude Opus 5.5 10e0f84b35 Channel region scope on phone-app channel sends too (#614)
CMD_SEND_CHANNEL_TXT_MSG called sendGroupMessage directly, so a post from
the phone app on a scoped channel went out on the default scope. It now
pushes the channel's scope for the send and pops it afterwards, as the
on-device composer, web remote, terminal and Lua sends do. Touch builds
only, where per-channel scopes are stored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
2026-10-13 09:31:38 -04:00
Michael A. CojocariandClaude Opus 5.5 94a7c13ef5 M9: name the 0x88 key by its triangle; it no longer opens chat options
The key is printed with a triangle, not a microphone, so the docs and the
code now call it the triangle key (M9_KEY_TRIANGLE). In an open chat it
used to open the thread's options; keypad navigation reaches the
conversation header for that now (#623), so the key keeps only its other
job, the focused item's hold action. M9_PORT.md no longer calls it
unbound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
2026-10-13 09:31:38 -04:00
Michael A. Cojocari b63b4f3904 M9 and Tanmatsu feedback from #609
- M9: double-press Home opens the launcher settings (what Home opens, icon size)
- M9: "Mark all as read" is the first item in the chats menu
- M9: MIC key opens the chat options in a chat, else the focused item's hold action
- M9: stronger focus on the map overlay buttons
- M9: the at-a-glance card shows even when the message-flash wake lit the screen first
- Tanmatsu: screensaver preview ignores input for its first 1.2 s so it no longer ends at once

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit f479382c9b12070cffae530e3b4789d689c441c5)
2026-10-13 09:31:38 -04:00
Michael A. Cojocari 1700366f3f M9: white tab bar while the map loads, double-press Home for Settings, bubble-only chat cursor
- Map: the transparent tab bar shows white icons from opening the Map tab until
  its first tile render finishes, then returns to its map colours.
- Home: a second press within 450 ms opens Settings. The keyboard controller
  sends no key-up or long-press code for Home, so a hold can't be detected.
- Chat: the keypad cursor surrounds the message bubble only, not the info
  line under it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit 9523475232aa1a73e2c7c3395888407dfd4020c8)
2026-10-13 09:31:38 -04:00
Michael A. Cojocari 7f4d3fd0b6 M9: action menus focus first row (X last); lift message-menu X clear of buttons
The ⋯ / thread action list creates its close X after the rows on the M9, so
keypad focus opens on the first action and reaches the X last. The message
action menu's X moves up into the card padding so the first button row no
longer clips its focus cursor. Other boards unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit 8fdb23ce5f5dbf58514304ab3be7a5dd04c4cdd8)
2026-10-13 09:31:37 -04:00
Michael A. Cojocari 2964ff02e3 fix(time): cold-boot Wi-Fi time sync gets past a slow first join (#620)
The first attempt on the active network brings the radio up from cold and
searches every channel for the AP, inside the same 4 s as every other
attempt. A T-Deck that joins in about 7 s by hand never made it, and the
scan retry then skipped that network as already tried, so a home with one
saved network always ended "no association".

The first attempt now gets 8 s. The scan retry keeps the active network
and joins each candidate on the channel and BSSID the scan found it on,
which skips the all-channel search. The whole session is capped at 16 s
(was 12 s) so the retry and SNTP still fit.

Fixes #620

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
(cherry picked from commit 477e0163ccf64d65706737a9558f1ef9ab439539)
2026-10-13 09:31:37 -04:00
Michael A. Cojocari 9bbe0a468b Chat: compact rows fit emoji; composer keeps the newest message in view and its text inside the box (#617)
- Compact rows that carry a colour emoji get its overhang above the text
  line as extra top padding, measured into the row height so the
  virtualizer agrees (chatCompactEmojiLift).
- When the composer grows while the list is at the bottom, the list is
  scrolled back to the newest message instead of being covered.
- The composer's corner radius stays at its one-line size, so a four-line
  box no longer curves through the start and end of its lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkSqsocfpbhCD2AtpskvQo
(cherry picked from commit 867db9917e682ac24f2542ecd77224fcf27f232a)
2026-10-13 09:31:37 -04:00
Michael A. Cojocari 2463bc238b Channel region scope on web remote, terminal and Lua sends (#614)
The chat composer tagged a channel post with the channel's own region scope,
but termDoSend (web remote and terminal) and luaHostMeshSendChannel called
sendGroupMessage directly, so those posts went out on the default scope.
All three now go through sendGroupMessageScoped, which pushes the channel's
scope for the send and pops it afterwards.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit fa1ec9868fdee31922ee57a5ed0a940522bb86c1)
2026-10-13 09:31:37 -04:00
Michael A. Cojocari ae473ee86d Contacts: A-Z sorts by name only; Select all covers every contact (#605)
- Name (A-Z) no longer sinks "?" (never heard) contacts below the heard
  ones; that rule now applies to the Last heard / Last message modes
  only. Favourites with and without a last-heard sort together by name.
- Select all selected at most 128 contacts, in storage order, while the
  list shows the first 128 in sorted order. Past 128 contacts it picked
  mostly rows that were not on screen, so nothing visible was ticked and
  a delete seemed to do nothing. The selection is now sized to
  MAX_CONTACTS and Select all takes every non-favourite that passes the
  filter.
- Bulk delete removes without saving (uiRemoveContactNoSave) and writes
  /contacts3 once at the end, keeps only the public keys for the run,
  and matches the selection by binary search.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkSqsocfpbhCD2AtpskvQo
(cherry picked from commit f30ca794039ff11358903938d70e402019908776)
2026-10-13 09:31:37 -04:00
Michael A. Cojocari 477b7d5981 fix(touch): backup Import dialog opens on Import, loops Import/Cancel, no X (#603)
On the T-LoRa Pager the "Import this backup?" confirm started on the
close X, then stopped on the message text before reaching the buttons:
keypad navigation follows the card's tree order (X, message, Cancel,
Import), and confirmBackupImport() used none of showConfirm()'s options
for that.

- confirmBackupImport() now passes actions_only_nav (the message box is
  not a focus stop) and focus_confirm (focus opens on Import). With two
  stops, the group's wrap loops Import <-> Cancel in both directions.
- showConfirm() no longer creates the X at all for actions_only_nav
  dialogs, rather than only skipping it in navigation. Cancel and the
  Back key already dismiss them. The Purge-discovered dialog uses the
  same flag and loses its X too, which keeps the two consistent.

Fixes #603

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zE15egJdCZ9nRBZmg1YUq
(cherry picked from commit 1df7b93ef91d5ca3790271e3d216409988cabf1c)
(cherry picked from commit 29a842b4a2de8bb2ff8a849b14d57955d17b2729)
2026-10-13 09:31:37 -04:00
Michael A. CojocariandCopilot 1826e0f74c DEVICES.md: the CrowPanel 5 recovers from long flash erases (full install, OTA)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-13 09:09:54 -04:00
Michael A. CojocariandCopilot 320e5cb93b CrowPanel 5: zero chat_back's shift in the chat header; docs say what was tested
chatHeaderApply now resets chat_back's translate_y, which updateGlobalStatusBar
skips in a chat, so a chat opened from a settings or app page no longer keeps
the half-bar upward shift. DEVICES.md and the site describe the hardware
testing, and the site counts 14 boards.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-13 09:01:59 -04:00
Michael A. Cojocari b0960cdcf1 Merge remote-tracking branch 'upstream/main' into crowpanel-50
# Conflicts:
#	scripts/build/gen-touch-fonts.sh
#	src/ui-touch/ui_font_18.c
#	src/ui-touch/ui_font_20.c
#	src/ui-touch/ui_font_24.c
2026-10-13 08:59:08 -04:00
Terry Carr 9fc158499b fix: conditionally include bubbleUrlTapCb for TDECK_PRO only 2026-10-13 12:02:39 +01:00
Terry Carr 7e539c3373 Merge branch 'ALLFATHER-BV:main' into hash-channels-links 2026-10-13 11:38:23 +01:00
Terry Carr 5ac174b443 fix: refactor chat link choices management to use dynamic allocation 2026-10-13 11:20:21 +01:00
Kaj SchittecatandClaude Opus 5.5 6b6253c242 storage: the SD-fallback notice opens above the app drawer
It was shown from setupWizardMaybeOpen(), before the drawer (the default Home)
was built on the same top layer, so the drawer covered it. It now opens after
the drawer, in place of the crash-report prompt for that boot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:17:30 +02:00
Terry Carr 7d283c6ae7 fix: adjust label long mode and width in chat link picker 2026-10-13 11:11:16 +01:00
Kaj SchittecatandClaude Opus 5.5 88dc3894e4 i18n: regenerate the V4 table for the new strings
The pre-build hook picked up the track, SD and Reader strings (8 translated keys x 14).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:03:56 +02:00
Kaj SchittecatandClaude Opus 5.5 a99a582bd4 i18n + notes: the track, SD and Reader strings; beta_94 notes for the three features
Short strings in all 14 languages; the long SD-fallback and track-stopped texts
as English placeholders until translated. v25 snapshot kept equal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:49:29 +02:00
Kaj SchittecatandClaude Opus 5.5 bec56ba814 reader: page through big text files, open card documents from Files, and a tool to fill a card
Asked for on Discord (prepper use: books and documentation offline on the card).

- The Reader opens a .txt or .md file on the card as a 16 KB window at an
  offset carried in the URL (sd:/x.txt?o=49152), cut back to a line (or a whole
  UTF-8 character), with Next page / Previous page / Back to the start links and
  the position in percent. A book of any size pages through; Back works as usual.
- Files opens HTML on the card in the Reader (its links work there, so a folder of
  pages browses offline) and text or markdown too big for the 8 KB editor; small
  text files keep the editor.
- scripts/sdpack.py (standard library only): an EPUB becomes /library/<name>/ with
  one page per chapter (long chapters split under 30 KB, linked in order) and a
  contents page; .txt/.md are copied as they are; /home.htm lists everything, so
  the Reader, which opens sd:/home.htm, starts on the library. Tested with a
  generated EPUB.
ZIM (Wikipedia) files are not read on the device: their clusters are zstd/xz
blobs of 1-2 MB and the files run to tens of GB on FAT32; converting on a
computer keeps the device side small.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:48:26 +02:00
Kaj SchittecatandClaude Opus 5.5 d0b1f58024 storage: a failed SD card no longer costs the node its key; the boot says what happened
Reported on Discord: a T-Deck that kept its data on a card that died booted
into first-time setup with a NEW private key, and the next good card was then
given the new key, so the old one was gone for good.

- DataStore keeps a copy of the main identity in NVS (namespace mc_id, 96 B):
  every save writes it, a load with no identity file falls back to it and puts
  it back into the active store, and a successful load seeds it when missing. A
  different copy is never replaced silently; only an explicit save changes it.
  Verified on a T-Deck: one boot with the card seeded mc_id.
- When the SD store was chosen but the card did not mount, the boot is flagged
  (g_sd_fallback_boot) and, instead of the setup wizard (whose done-mark is on
  the card too), a dialog says the card could not be read, that the key is kept
  (or, on a device that never had a copy, that it started with a new one while
  the card keeps the old), and offers Try the card again.
The retry ladder at lower SPI clocks already existed. Not yet done: copying the
data off a card that mounts but refuses writes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:44:58 +02:00
Kaj SchittecatandClaude Opus 5.5 fd8a163421 touch(map): record a GPS track to a GPX file (Map options > Record track)
Asked for on Discord. A switch in the map options records where you go into
/meshcomod/tracks/<date-time>.gpx on the SD card, a standard GPX 1.1 track that
other mapping software opens, with elevation and time per point.

- Runs from the UI loop beside the GPS saver, so it keeps recording with the
  screen off; the GPS stays awake while it records (gpsWantedLive).
- A point once we have moved 10 m (GPS jitter stays out of the distance), or
  every five minutes after at least 3 m. Climb counts steps of 3 m or more.
- The file is complete after every point: the closing tags are rewritten behind
  each new one (r+ and seek), so a flat battery still leaves a file that opens.
- The map draws this session's track as a red line (one lv_line, thinned to 256
  points) and the status corner shows the distance and climb while recording.
- Three failed writes in a row stop it with a message; no card, no start.
Boards with an SD card and GPS (CAP_SD or the Pager). The app route was ruled out:
Lua apps stop ticking with the screen off and their files cap at 32 KB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:39:24 +02:00
Kaj SchittecatandClaude Opus 5.5 459be25e34 beta_94 notes: this round; MECP gets the bell icon
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:20:12 +02:00
Kaj SchittecatandClaude Opus 5.5 083d422a2d i18n: the V4 bakes the live translations, trimmed to keys the firmware can show
beta_93 pinned the Heltec V4 to the beta_92 .lang files because the full table
no longer fit. lang_reach.py now works out which keys any source can look up:
a .lang key is kept when it equals a C string literal anywhere in the sources
(every #if branch, adjacent literals joined, LV_SYMBOL_*/UI_ICON_* and other
string macros expanded, escapes decoded as GCC does), or that literal with TR()'s
glyph prefix stripped, or a string in any Lua app. 1818 of 1925 keys survive; the
107 dropped look unused (old dropdown strings, texts the source has reworded).

The trimmed live table is 15.5 KB smaller than the pinned one (V4 4,025,365 B
used, 37.9 KB free) and carries every translation added since beta_92.

Guards: the generator refuses to trim when an input is missing (lv_symbol_def.h,
UITask.cpp, or under 80% kept) and leaves the header alone; after linking the V4,
--check-objects reads the strings the compiler emitted and fails the build if a
translated key among them was dropped (proved by dropping a live one). The
pre-build hook regenerates on source changes too, and the header is only
rewritten when its content changes. The pinned copy is gone (git history).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:20:12 +02:00
Kaj SchittecatandClaude Opus 5.5 6ff2cdedd8 i18n: rows for the Bluetooth failure messages
"The Bluetooth chip is not responding." and "Bluetooth could not start.
Restart the device and try again." in all 14 languages, v25 snapshot kept equal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:20:12 +02:00
Kaj SchittecatandClaude Opus 5.5 cd70c91465 p4: Bluetooth starts from the toggles again; failures say what went wrong (#612)
Since beta_88 (b52c71f) the P4 stored its BLE name and PIN only when BLE was on
at boot and the C6 came up for it. A unit that booted with BLE off (or once had
it off, since that is saved) then failed every enable at once, and the UI read
every refusal as "Not enough free memory for Bluetooth", Wi-Fi off or not.

- main.cpp stores the parameters on every boot; storing them does not touch the C6.
- enableBle() brings the C6's BLE controller up on a runtime cold start
  (hostedInitBLE) before NimBLE, or NimBLE would start against no controller.
- The transport records why a cold start did not happen (no parameters, the
  memory guard, the radio) and the toast says so; only the guard is about memory.

Not run on a P4 yet: the P4 build has both strings in it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:20:12 +02:00
Kaj SchittecatandClaude Opus 5.5 821f9b2c5a apps: a Store app can ship its own files
An app's manifest can list files beside its code as one quoted string,
"files":"beep.wav,track.mp3" (the manifest reader takes strings only). The
Store installs each into /apps/<id>.d/, the folder wada.fs and
wada.audio.play("beep.wav") already read, so an app can carry its sounds. On
the T-Display P4, whose Lua SD API is off, that was the only way to get a sound
there (#lua-app-dev). Files stream to storage through a 4 KB internal buffer
(SD writes from PSRAM come back short), the server must send a length, a
short read deletes the partial file and fails the install. Up to 8 files of
4 MB, names as wada.audio's. No app uses it yet, so existing installs are
unchanged. SDK page documents it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:20:12 +02:00
Kaj SchittecatandClaude Opus 5.5 420ce5298d touch(cc): T-Deck Pro and Max control panel tiles stay visible on e-paper (#659)
Every Settings-group hue thresholds to white on the 1-bit panel, so an off tile
was white on white and its icon vanished, and an on tile was a bare icon. On
HAS_TDECK_PRO the tile is drawn in ink only: an outlined square with a black
icon when off, solid black with a white icon when on (the fix oumike proposed
in the issue). Not yet seen on a Pro.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:20:02 +02:00
Terry Carr 7517b02f9c fix: revert back to center button 2026-10-13 10:14:22 +01:00
Kaj SchittecatandClaude Opus 5.5 0394fefb22 apps: MECP 1.0, write and read Mesh Emergency Communication Protocol messages
Asked for on Discord. MECP (github.com/xiang-dev-1/MECP) codes an emergency
or everyday message as MECP/<severity>/<codes> [extras], so every reader with
a code table sees it in their own language. Compose: severity, codes from the
12 categories, head count and GPS position, a preview of the exact text, then
a channel post (MAYDAY asks first). Read: MECP lines heard while the app is
open are listed in words, a MAYDAY pops a toast, and Decode takes typed text.
English code table from MECP's languages/en.json (CC BY 4.0, credited); the
protocol is CC0. Needs sdk_ext; seed false, so not in the V4 image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:56:36 +02:00
Kaj SchittecatandClaude Opus 5.5 6504135c16 touch(home): the Advert widget is laid out like the app tiles (#637)
The solid accent button from the first fix stood out on Home. It now uses the
App shortcut tile's layout: a tinted chip in the app drawer's colour for
Advertise, the name under it in the same 12 px font and position as Apps and
Discover, and flood/nearby under that only where the tile is tall enough. The
box keeps the default widget style (glass or card), so it no longer turns into
the see-through gradient that hid its text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:49:45 +02:00
Kaj SchittecatandClaude Opus 5.5 a7a286b4e7 beta_94 prep: release notes, guide entries, language store v25
Notes for beta_94; the guide mentions the map's Favorites only and the
Fahrenheit option; the 14 languages move to v25 with their snapshot
(deploy/apps/lang/25/), for deploy-apps.sh at the slice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:58:49 +02:00
Kaj SchittecatandClaude Opus 5.5 6c6fb32d8d i18n: rows for the beta_94 strings
Temperature in Fahrenheit, Up to 15 characters and Favorites only in all 14
languages; the full-Home toast as an English placeholder like the other Home
widget strings. The V4 keeps its pinned beta_92 table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:24:59 +02:00
Kaj SchittecatandClaude Opus 5.5 3b47e089b3 touch: temperatures in Fahrenheit, as an option
"Temperature in Fahrenheit" next to "Distance in miles" (Settings > Display >
Status bar & units), asked for on Discord. Only what is shown converts: the
sensor text, Sensors widget and page, remote sensor widget, telemetry replies
and chart (its axis, shared with humidity, already spans -20..100). Stored
readings, logs, telemetry sent out and the Lua SDK stay in degrees C. Saved as
a one-byte blob (temp_f), so no settings-schema bump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:24:59 +02:00
Kaj SchittecatandClaude Opus 5.5 0b409b1f3c touch(admin): the login password field says it takes up to 15 characters
MeshCore carries at most 15: the core's sendLogin() cuts there and stock
servers store 15, on the phone app too. A longer password is used as its first
15 characters, which the empty field never said (Discord).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:24:59 +02:00
Kaj SchittecatandClaude Opus 5.5 57d1840df6 touch(home): the radar fits a two-cell slot; a full Home says how to make room
The radar was 2x2 only, and the default Glance layout fills all twelve cells
of the T-Deck grid, so adding it (or anything) only flashed "No room for it
on Home" (Discord). The radar now also comes in the two-across size (it draws
into the shorter side), and when no cell at all is free the toast says to hold
a widget to remove one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:24:59 +02:00
Kaj SchittecatandClaude Opus 5.5 46279eef92 touch(chats): a text symbol picked as a chat icon is drawn (#647)
The icon picker's last rows are text symbols (a star, a heart, a currency
sign). They were saved and announced as "Chat icon set", but the avatar only
draws colour-emoji images, so the plain avatar stayed. Now a pick with no
colour glyph is drawn as text in the colour the initials had.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:24:59 +02:00
CyrioandClaude Opus 5.5 6ee9424ece i18n: wording nits from review (nl, de, hu)
jcyrio's follow-up to #632 (ea2315e), pushed after the merge so it never
reached main. Only the .lang rows; i18n_builtin.h is generated and the V4
builds from its pinned copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:18:50 +02:00
Kaj SchittecatandClaude Opus 5.5 1130ed62e3 Merge follow-ups: Pager shortcut table says Nothing, built-in language table back to the V4 pin
#624's new Special keys row ended in an em dash; the other rows say Nothing.
#635 edited the generated i18n_builtin.h; regenerated from the pinned
beta_92 .lang copy the V4 builds from, so the V4 table is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:18:37 +02:00
Kaj Schittecat 133df4ca7f Merge PR #635 (Confituurke): feat(ui): pick your advert position on the map 2026-10-13 09:18:20 +02:00
Kaj Schittecat 01182d1be1 Merge PR #624 (jcyrio): Pager: Fn+V pastes the clipboard (no touch path to the edit menu) 2026-10-13 09:18:18 +02:00
Kaj Schittecat 4e73bcc34a Merge PR #645 (oumike): T-Deck Pro / Max: Normal, Large and Huge text sizes, as on the T-Deck (#639) 2026-10-13 09:18:17 +02:00
Kaj Schittecat f817646932 Merge PR #658 (oumike): Day mode: readable figures on the count badges (#657) 2026-10-13 09:18:15 +02:00
Kaj Schittecat 449b8abb67 Merge PR #649 (oumike): Contacts: show the chat icon, picked or the first emoji in the name (#646) 2026-10-13 09:18:14 +02:00
Kaj Schittecat 18882e975a Merge PR #650 (oumike): web app: add Reply to channel message long-press (#642) 2026-10-13 09:18:13 +02:00
Kaj Schittecat 016983e4b9 Merge PR #656 (oumike): Keyboard: readable labels on the control keys 2026-10-13 09:18:12 +02:00
Kaj SchittecatandClaude Opus 5.5 2d5e1b25ad touch(home): the Advert widget is a solid button again (#637)
hwAdvertBuild recoloured only bg_color. On glass (the aurora default) the box
is a translucent gradient into the panel colour, and the widget's dark
ON_GLOW text vanished on it. Gradient off, opacity full.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:18:05 +02:00
Kaj SchittecatandClaude Opus 5.5 793660f3fd touch(map): "Favorites only" in the map options
A switch under "Direct (0-hop) only" that leaves only favourite contacts on
the map (markers and link lines), asked for on Discord. Saved as a one-byte
blob (map_fav), so no settings-schema bump. Both filters now go through one
mapHidesContact() check, and the favourites list is read once per render
instead of a blob read per contact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:18:05 +02:00
Kaj SchittecatandClaude Opus 5.5 c8dc7f486e touch(home): the radar widget's tap opens the screensaver the way Settings' preview does
hwRadarTap called ssaverStart() directly, which never marked the screen off,
so loop() tore the saver down again on its next pass: a tap only flickered it
and dipped the backlight to 40% for a frame. It now sets the same preview
request as Settings, which starts it and marks the screen off, so it stays
until a touch ends it. ssaverStart() also refuses a second start, so the
dimmed 40% can never be saved as the user's brightness.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:18:05 +02:00
Pratik Patel 8dbb349fc6 thinknode_m9: pick GPS enable polarity from the board revision
The GPS enable pin (GPIO11) is active-LOW on the original ThinkNode-M9 but
active-HIGH on the newer revision (keyboard controller at 0x6D, e.g. Elecrow's
Meshtastic SKU LMM15409D2). Driven LOW on those units the GPS never gets a
fix: on one unit, same spot, 0 satellites with EN active-LOW vs 15 with
active-HIGH, and 34 after 20 minutes outdoors with the GPS toggled off and on
again from the UI. Meshtastic's ELECROW-ThinkNode-M9 variant makes the same
split, keyed on the same keyboard address (m9Version > 1 ? HIGH : LOW), and
Elecrow engineers have confirmed the split.

WadaNmeaLocationProvider: keep the enable level in a member that defaults to
the compile-time GPS_EN_ACTIVE, add setEnActive() to override it at runtime
(re-driving the pin so a running GPS stays on), and an optional
setPostEnableHook() run at the end of begin(). Every other board is unaffected.

thinknode_m9: m9KeyboardAddr() and a one-shot found-callback on the keyboard
driver; target.cpp uses it, when the controller is at 0x6D, to call
gps.setEnActive(true) and to register a GPIO10 wake pulse (50 ms, HIGH 5 ms,
LOW, 50 ms) after enabling, as Elecrow engineers confirmed. Any other address
keeps the existing default (LOW, no pulse), so the 0x6C path is unchanged; it
is not tested here, no 0x6C unit was available.

Depends on the keyboard 0x6D probe change.

Signed-off-by: Pratik Patel <Pratik@MakerNova.io>
2026-10-13 08:22:55 +02:00
Kaj Schittecat 9cd10dfe30 Merge PR #662 (Bfindlay): detect the ThinkNode M9 V1.1 keyboard at 0x6D (#644) 2026-10-13 08:22:30 +02:00
bfindlay 7025f79b30 no-mistakes(lint): Guard host-test runner against failed directory changes 2026-10-13 15:00:33 +11:00
bfindlay 04d8a606c5 no-mistakes(document): Consolidate M9 keyboard address documentation 2026-10-13 14:59:30 +11:00
bfindlay ad13d42a12 Fix M9 keyboard detection at the V1.1 I2C address
Signed-off-by: bfindlay <bfindlay@acm.org>
2026-10-13 14:43:28 +11:00
Michael A. CojocariandCopilot d94273bde3 Badges: paper-white with a black ring on the T-Deck Pro (#657)
The Day glow fill thresholds to solid black on e-paper, and the 11 px
anti-aliased white figures mostly fall under the one-bit cut, so every
count badge read as a black blob. On the Pro the tab-bar/launcher pills,
the Chats badge and the Discovered-nodes badge now draw white with a 1 px
black ring and black figures, like the Pro's avatars and buttons.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 19:39:51 -04:00
Michael A. CojocariandCopilot 0cba7ce037 Badges: figures in the glow's ink, readable in Day (#657)
The unread and Discovered count badges hard-coded Night's dark ink
(0x00201C) on the glow fill. Day's glow is the accent darkened, so the
figures read at 1.6-2.4:1 on every preset accent. Use COLOR_ON_GLOW,
which the theme already sets per mode and look (white in Day, the
look's own ink for Neon and Pastel). Under MORE COLORS! the chat-row
pill takes the row's hue, so its figures pick their ink by that hue's
luma, as Taste the rainbow's drifting badge already does.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:22:33 -04:00
Michael A. CojocariandCopilot 11dd04c6b7 Keyboard: theme text colour on the control keys
The control keys (1#, ABC, backspace, enter, the arrows, the tick) are
CHECKED keys. The stock theme's CHECKED key style carries its light-mode
text colour, near black, and a CHECKED style outranks the plain key text
colour, so their labels were dark on every theme. Set the theme text
colour on our CHECKED key style as well.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 11:10:23 -04:00
Michael A. CojocariandCopilot 0a7a3714f3 CrowPanel 5: pause the LCD clock during flash writes by default
Tested on hardware: while sending messages, the sideways jump during flash
writes is now a brief freeze or flicker, with no crash and no panel self-test.
The crowpanel_50 env now sets CROWPANEL50_FLASH_PAUSE and wraps
esp_flash_write and esp_flash_erase_region. DEVICES.md notes the behaviour.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 09:07:54 -04:00
Michael A. CojocariandCopilot a8a0758088 CrowPanel 5: experimental LCD clock pause around flash writes
Flash writes hold the bus shared with PSRAM, so the RGB panel's DMA underruns
and the picture jumps sideways for a frame (confirmed with a flash-write trace
while sending messages). This hook stops the LCD clock for the length of each
flash write or erase. It's off by default and not yet tested on hardware.
Also notes that 12 MHz pclk sends the panel into its self-test pattern.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 20:12:41 -04:00
Michael A. CojocariandCopilot e33eef43dc CrowPanel 5: edge fix, Wire touch, composer icons; status bar and keyboard redraw fixes
- Panel: clock edge pclk_idle_high=1 (fixes gradient striping), tunable pclk/hporch macros.
- Touch: read the GT911 through Wire with a background poll (fixes repeated-start crash).
- Chat composer: buttons sized to the input, icon labels in ui_icons_20.
- Status bar: chat header uses guarded setters and skips redundant moves/tints, so an
  open chat no longer repaints the bar every 250 ms.
- Keyboard: set/clear PRESSED in a preprocess handler with invalidation off, so a key
  press repaints only that key instead of the whole keyboard.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 20:00:37 -04:00
Terry Carr 6c99a085a4 fix: enhance hex color validation in ChatHashtag and update tests 2026-10-11 23:00:01 +01:00
Terry Carr 412324631b fix: update conditional compilation for keypad navigation and adjust event callback formatting 2026-10-11 22:16:11 +01:00
Terry Carr 9c8ac06d30 fix: remove redundant translations for "Select a link or location" in multiple language files 2026-10-11 21:48:15 +01:00
Terry Carr ace2023026 feat: add script for chat link highlighting and testing 2026-10-11 20:40:54 +01:00
Terry Carr cc4f0f318a fix: link highlighting and drawing for chat messages 2026-10-11 20:31:02 +01:00
Michael A. CojocariandCopilot 3ce6b065e4 Add Elecrow CrowPanel Advance 5.0 build target
New env crowpanel_50_companion_radio_touch for the 800x480 ST7262 RGB panel,
GT911 touch and an SX1262 in the wireless slot. Board quirks follow Camillia-MT:
14 MHz pixel clock, internal-RAM draw band, PCA9557 (V1.0) / MCU (V1.1+)
backlight controller detection with GT911 reset at 0x5D or 0x14, NSS on GPIO8
(V1.2+), no SD/GPS/battery. Large-screen layouts and UI-size fonts; browser
file transfer to the internal tiles partition. Added to the upload helper,
release script, flasher manifests, site, DEVICES.md, README and NOTICE.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 15:07:32 -04:00
Terry Carr 7c8c366b56 fix: remove redundant translations for "Channel" and "Select a link or location" 2026-10-11 19:14:06 +01:00
Terry Carr ed4f530bca feat: add RTL support for chat link text rendering 2026-10-11 19:09:09 +01:00
Michael A. CojocariandCopilot c05153ef85 web app: add Reply to the channel message long-press sheet
Long-pressing someone else's message in a channel chat in the remote web
app now offers Reply (above Copy and Info). It prefills the composer with
the device's mention syntax '@[sender] ', keeps any draft, clamps to the
160-char limit and focuses the input. DMs and our own messages are
unchanged.

Fixes #642

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 13:46:37 -04:00
Michael A. CojocariandCopilot b0a6f59ea3 Contacts: show the chat icon, picked or the first emoji in the name (#646)
The Contacts list now gives each contact the same icon as its chat: the
chat icon picked for it (keyed on the advertised name), else the first
emoji in its shown name, as #640 does for the chat list. Picking or
resetting a chat icon marks the Contacts list for a rebuild.

Fixes #646

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 13:00:53 -04:00
Terry Carr 8cc4a5aacf Merge branch 'ALLFATHER-BV:main' into hash-channels-links 2026-10-11 12:05:52 +01:00
Michael A. CojocariandCopilot 67680763ed T-Deck Pro / Max: Normal, Large and Huge text sizes, as on the T-Deck (#639)
Enable CAP_UI_SIZE and the font-only text-size presets (montserrat
12/14/16, 16/18/20, 18/20/24) for HAS_TDECK_PRO boards, compile the
18/20/24 px UI fonts for them, and size the status bar to match.
Docs list the T-Deck Pro and Max under Text size.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-10 21:44:37 -04:00
Terry Carr ae4ed9e44c fix: refactor chat link handling 2026-10-11 01:21:26 +01:00
Terry Carr 80b433bdd6 feat: enhance message action menu with link handling for URLs, hashtags, and coordinates 2026-10-11 00:37:05 +01:00
Confituurke 15ca3f8902 feat(ui): pick your advert position on the map
Typing a latitude and longitude by hand is the only way to set a
position on a node without GPS, and few people know their coordinates to
six decimals. The map already knows where its centre is, so let it do
the work.

Settings > Profile gets a "Pick on the map" row under the lat/lon fields
in the Advert location card. It opens the Map tab centred on the current
position, with a crosshair pinned over the map centre and a Cancel /
Save position bar. Pan and zoom as usual, then "Save position" saves the
centre as the advert position. Leaving the tab cancels it, and so does
Back on the M9, the pager and a Bluetooth keyboard. Auto-follow pauses
while picking so it cannot pull the centre away.

With GPS on and a fix, the GPS sets the position on every loop, so a
picked one would not stay: the row is hidden then, and a fix that
arrives while picking is reported instead of saved. A centre panned past
the antimeridian is wrapped to -180..180 before it is saved.

A node without GPS usually has no map view yet either, so it starts from
the first contact that has a position; only with none at all does it ask
for a rough location first. A typed but unsaved lat/lon is saved before
the jump. The bar sits above the zoom slider, so zooming still works
while picking, and left of the right-edge map buttons, so it never
covers them on a short screen.

The four new strings are translated in every deploy/apps/lang/*.lang
file and i18n_builtin.h is regenerated, so the i18n audit stays clean
for them.

Signed-off-by: Confituurke <9595028+Confituurke@users.noreply.github.com>
2026-10-10 23:40:51 +02:00
Kaj SchittecatandClaude Opus 5.5 2dfe3727cd docs: beta_93 in the guide and the repo docs
Guide (wadamesh.com/docs.html): contact nicknames, the redesigned control
panel with the Battery saver tile, keyboard tooltips and the amber cursor,
automatic chat icons, the default lock wallpaper, the GPS serial speed and
battery notes, the V4 built-in apps and the Lua send budget, and a new
T-LoRa Pager section (hotkeys, navigation memory, Power off confirm, power
saving). Three new screenshots, GPS position and IP masked.

Repo docs: the Pager and M9 key guides describe tooltips, per-tab memory
and Power off, and README, TLORA_PAGER_SHORTCUTS and THINKNODE_M9_SHORTCUTS
no longer use em dashes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 23:38:01 +02:00
CyrioandClaude Fable 5.1 5a3c1be63d Pager: paste chord on Fn+Enter instead of Fn+V
Review follow-up (B0N3-GD): Fn is the Pager's only symbol key, so a
held-Fn letter chord takes that letter's symbol away -- Fn+V cost the
question mark with Fn held. Fn+Enter types nothing today (Enter has no
entry in the symbol map), so the chord moves there and costs nothing.

- PagerKeyboardState: V_POS -> ENTER_POS (row 1, col 9); the chord is
  still resolved by matrix position before the symbol map, only while
  Fn is physically held, and cleared by discardAlt().
- PagerKeyboard.h/.cpp, UITask.cpp: renamed to *AltEnterChord*; the
  paste path itself (txtPasteInto, hex-key extraction, toasts, drain
  sites) is unchanged.
- test_pager_keyboard_state: chord on Enter fires once and swallows the
  press; a plain Enter still returns '\r'; tap-Fn-then-Enter is not a
  chord and types nothing, as before; Fn held + V types '?' again.
- TLORA_PAGER_SHORTCUTS.md: both rows now say Fn held + Enter, and the
  em dash is gone from the prose.

Rebuilt and flashed on a Pager (RAM 37.8 %, Flash 90.0 %, unchanged).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VKtRiAcddFBAUDogBWmZk4
2026-10-10 13:57:18 -07:00
Terry Carr c5042b71d3 fix: improve hashtag span validation and enhance test coverage for edge cases 2026-10-10 21:56:21 +01:00
Kaj SchittecatandClaude Opus 5.5 d68f276b06 touch: beta_93, contact nicknames, new control panel, tooltips, Pager shortcuts, speed and battery
Contact nicknames: TouchPrefsStore nick blob (48 x 38 B), display-only lookups
(contactShownName, threadShownName, senderShownName), Contact options >
Nickname on the shared text prompt (an empty OK clears it).
Control panel restyled in the launcher's tile language, with a Battery saver
tile; keyboard tooltips (navSetTip), an amber cursor and touch long-press
tooltips (the T-Deck touch read hid them on every poll).
T-LoRa Pager: M/C/A/S on Home, H/S tap and hold, Backspace = Back, N = jump to
new, held Enter = long press, per-tab focus memory, Power off confirm; LVGL
-O2 and IRAM, INT-gated keyboard, idle yield, status-bar setter guards, instant
tabs; PagerGps (u-blox software standby for rests, GPS rail cut when off),
panel sleep, amp off at boot, power-off parks the radio (NSS held), GPS, panel
and keyboard light.
Fixes: the DM duplicate check keys on the attempt and a path reset clears it,
the direct timeout masks the packed path byte, web remote hops (#636);
changelog over http (#638); Lua SDK send budgets and duplicate guard (#606);
GPS speed note (#613, CAP_GPS_BAUD_AUTO); emoji chat icon (#640); default
wallpaper (#626).
Heltec V4: Tetris, 2048 and Breakout unseeded and the built-in Lua stripped of
comments (identical bytecode), 27 KB freed; the baked language table is pinned
to beta_92's files (scripts/build/lang-pin-beta92) so it still fits.
Translations: 39 new keys in 14 languages, language store v24.
Site: theme, dark mode, aurora and guide refresh, SDK send limits, nicknames.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 22:08:16 +02:00
Kaj c66f70f852 Merge PR #632 (jcyrio): translate the 18 beta_90/91 strings missing from every .lang
i18n: translate the 18 beta_90/91 strings missing from every .lang
2026-10-10 21:56:48 +02:00
Kaj 45c977ff11 Merge PR #633 (oumike): LR1110 old-firmware patch applies to RadioLib 7.8.1
build(m9): the LR1110 old-firmware patch applies to RadioLib 7.8.1
2026-10-10 21:46:19 +02:00
Terry Carr 4ed3fdb52c fixed formatting 2026-10-10 17:34:47 +01:00
Terry Carr e6490b5f7b fixed formatting 2026-10-10 17:33:56 +01:00
Terry Carr dd9e47b2e0 Add translations for "Select a link or location" in multiple languages
- Updated the UI to include functionality for selecting links or locations in chat messages.
- Refactored chat link handling to improve user experience when interacting with links in chat bubbles.
2026-10-10 17:19:19 +01:00
Terry Carr bf8c2255c0 fix: handle trailing hyphens and underscores in hashtag spans 2026-10-10 16:14:04 +01:00
Terry Carr 6cbc9892cb test: enhance hashtag span tests for better coverage and edge cases 2026-10-09 20:53:53 +01:00
Terry Carr 8c31d759b5 fix: simplify hashtag picker layout and improve button styling 2026-10-09 20:53:46 +01:00
Terry Carr 301246f67b feat: add "Select a hashtag channel" option in multiple languages 2026-10-09 20:36:51 +01:00
Terry Carr e9b8811dff fix: update alert message for missing channel in hashtag chat 2026-10-09 20:15:26 +01:00
Terry Carr 68771f7860 Removed already delete comment 2026-10-09 19:49:30 +01:00
Michael A. CojocariandClaude Opus 5.5 9c0cccd2fb build(m9): the LR1110 old-firmware patch applies to RadioLib 7.8.1 too
lib_deps takes RadioLib ^7.6.0, and a fresh fetch now brings 7.8.1. Its
LR11x0::config() wraps driveDiosInSleepMode in its own skip for LR1110
firmware below 0x0306, so the call sits one level deeper and the patch
script no longer found it. It failed closed, stopping every M9 build at
its pre-script.

The script now knows both shapes. The patch still matters on 7.8.1:
firmware 0x0306 and 0x0307 get past RadioLib's own skip, send the
command, answer CMD_PERR and would abort radio init with -706.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
2026-10-09 11:26:32 -04:00
Terry Carr 6df1476922 fix: enhance bubble link navigation to support URLs and hashtags 2026-10-09 15:55:48 +01:00
CyrioandClaude Fable 5.1 9b891ff501 i18n: translate the 18 beta_90/91 strings missing from every .lang
scripts/build/i18n-audit.py reported the same 18 keys missing in all
14 language files -- the background picker (Background, Background
colour, Still/Moving aurora, Image, None, Choose an image, Choose the
colour, Set as background, the JPEG hints and the restart notice), the
GPS battery saver (Save battery, Check position every, the two
descriptions, "GPS: resting, next check in %lum%02lus") and the
"remote view open" touch-sleep block reason.

Each file gets the 18 rows in its alphabetical slot, translated rather
than left as English placeholders, reusing the terms the files already
use for the older wallpaper strings (Hintergrund, fond d'écran, sfondo,
achtergrond, háttérkép, pozadí, papel de parede/fundo, fundal, фон,
позадина, φόντο ...) and for the battery saver. "%lum%02lus" is kept
verbatim in every language. src/ui-touch/i18n_builtin.h regenerated
with scripts/build/gen-lang-builtin.py; the audit now passes for all
14 languages. "# ver: 23" left alone, since that is bumped at release.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VKtRiAcddFBAUDogBWmZk4
2026-10-09 07:05:06 -07:00
Terry Carr 3de61004e8 fix: handle extended characters in hashtag detection 2026-10-09 14:33:50 +01:00
Terry Carr 10b3674c32 fix: add support for multiple hashtags in chat bubbles 2026-10-09 14:18:15 +01:00
Terry Carr efe71684c4 Fix: clickable flag and navigation skip 2026-10-09 13:49:45 +01:00
CyrioandClaude Fable 5.1 2f811a7a6b i18n: add "Clipboard empty" to every .lang (Fn+V toast)
Review follow-up: the new TR("Clipboard empty") toast had no row in
deploy/apps/lang/*.lang, so i18n-audit.py flagged it in all 14
languages. Added in each file's alphabetical slot (after "Client
repeat") and regenerated src/ui-touch/i18n_builtin.h with
scripts/build/gen-lang-builtin.py. The audit no longer lists the key.
"# ver: 23" is left alone, since that is bumped at release.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VKtRiAcddFBAUDogBWmZk4
2026-10-09 05:36:36 -07:00
Terry Carr 357a9092d4 Merge branch 'ALLFATHER-BV:main' into hash-channels-links 2026-10-09 13:22:22 +01:00
Kaj SchittecatandClaude Opus 5.5 33ae4c61bd tanmatsu: drop the Wi-Fi OTA path so the app fits the launcher's load window
The launcher's AppFS bootloader maps only the first 50 x 64 KB of an app
while it checks and starts it (appfsBlMmap in badgeteam/appfs 2.1.0).
beta_91 (3,329,568 B) and the first beta_92 build (3,331,120 B) were past
it, so neither would start on a Tanmatsu.

On the Tanmatsu the launcher updates the app (CAP_OTA 0) and the only OTA
slots are the launcher's own 2 MB ones, so the core's "ota start" upload
page and URL install could never work there. DISABLE_WIFI_OTA stubs them
("ERR: OTA not supported in this build"), and with them go ElegantOTA,
the async web server and AsyncTCP: the app drops to 3,200,864 B, 75,936 B
under the window.

tanmatsu-appstore-publish.sh now refuses a binary over the window, and
its PR text no longer has an em dash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:40:02 +02:00
Kaj SchittecatandClaude Opus 5.5 9d0301b80a touch: beta_92, faster screens on most boards, Arabic fixes, GitHub fixes
Frame rate: full-width PSRAM draw band (6 bands a frame instead of 14 on the
T-Deck), the still aurora drawn once and copied, LVGL at -O2 with its blend and
label loops in IRAM (scripts/build/perf_lvgl.py, 1 KB more loop stack), circle
and gradient caches, newlib memcpy. Rolled out to the M9, Attaky, RAK, Wio,
CrowPanel and V4-R8; the Pager gets the band only, the V4 the caches only.
Frame benchmark tour (DOC_PERF_BENCH) for measuring it.

Fixes: hop counts read from the packed path length (#621); Arabic shaping
rows patched in LVGL (patch_lvgl_arabic.py), Arabic 101 shift layer, kaf,
symbols, text sent as standard letters (#582); info popup colour runs that
never wrap (#602); keypad nav reaches the chat header (#623); the browser
opens the changelog (#622, gen-changelog.py at every site deploy); the
Theme background colour picker no longer edits the accent; flasher entries
for P4 units with factory ESP-AT Wi-Fi chips (#612, #619).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:53:33 +02:00
CyrioandClaude Fable 5.1 e3baa774f5 Pager: Fn+V pastes the clipboard (no touch path to the edit menu)
On the T-LoRa Pager there was no way to paste: the in-RAM clipboard is
only consumed by the Cut/Copy/Paste edit menu, which opens solely from a
touch long-press on a textarea, and the Pager's encoder never reaches
LVGL's press events. Copy from a message bubble worked; nothing could
paste the result -- the 64-hex contact key and 32-hex channel secret
fields being the painful cases.

Physical Fn(Alt) held + V now pastes into the field being edited:

- PagerKeyboardState: V_POS; a held-Alt V press is resolved by matrix
  position before the symbol map, swallowed, and reported through
  alt_v_chord_pending_/consumeAltVChord() (same shape as Alt+Backspace
  and the Pro's Alt+B). discardAlt() clears it. !HAS_TDECK_PRO only.
- PagerKeyboard.h/.cpp: pagerKeyboardConsumeAltVChord().
- UITask: the Paste cell's logic is factored into txtPasteInto(ta)
  (selection replaced, pasteTextFor hex-key extraction kept) and reused
  by updatePagerAltVChord(), which uses the same "actually editing" test
  as the Alt+Shift Caps chord, hides any open edit menu/accent popup,
  and toasts "Pasted" / "Clipboard empty". Drained at the remote-mirror
  and screen-off discard sites like the other chords.
- TLORA_PAGER_SHORTCUTS.md: documented.
- test_pager_keyboard_state: chord fires once and swallows V;
  tap-Fn-then-V still returns '?'; discardAlt() drops a pending chord.

Trade-off: Fn+V is '?' on this layout, so only a HELD Fn chords. The
one-shot (tap Fn, then V) and locked (double-tap Fn) symbol layers still
type '?', the same compromise the Pro's physical Alt+B makes with '!'.

Tested on tlora_pager_sx1262_companion_radio_touch: Copy from a bubble
then Fn+V in the composer pastes; Fn+V in a key field pastes just the
key out of a copied message; empty clipboard toasts; tap-Fn-then-V
still types '?'.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VKtRiAcddFBAUDogBWmZk4
2026-10-09 02:34:07 -07:00
Terry Carr b2ecf51c38 Merge branch 'ALLFATHER-BV:main' into hash-channels-links 2026-10-09 00:30:37 +01:00
Kaj SchittecatandClaude Opus 5.5 c230820945 touch: beta_91, no picture background on e-paper (the T-Deck Pro link)
The image viewer's Set as background called into the Theme page, which the
e-paper builds do not compile. CAP_BG_IMAGE and CAP_BG_MOVING are 0 there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:24:45 +02:00
Kaj SchittecatandClaude Opus 5.5 218bc6ed3e touch: beta_91, Home becomes widgets, backgrounds, GPS in the bar
Home is a grid of arrangeable widgets (24 kinds, presets, a layout per
orientation, Lua app widgets via wada.widget); Unread, Chat and Mentions
get a large size. Theme > Background puts an aurora (still by default, or
moving), a colour, a picture or nothing behind every screen, with glass
cards and a smoked-glass top bar. A GPS icon in the bar, a GPS battery
saver (u-blox GNSS stop/start where there is no power switch), and a
battery saver that no longer waits for Bluetooth and Wi-Fi to be off.
Also: navigation bar position, every settings page on the redesign's
cards, a faster app drawer, slower screensaver drift for burn-in, text
wrapping at spaces only, the V4-R8 Store Apps tab, and SDK Test marked
extended-SDK only (keeps the 2 MB V4 at 99.8%).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:14:04 +02:00
Terry Carr 6f26a93149 fix: target hashtag taps to link text 2026-10-08 00:03:09 +01:00
Terry Carr 3d95076b3e refactor: remove unused hashtag functions and streamline hashtag handling in chat bubbles 2026-10-08 00:03:09 +01:00
Terry Carr cfa98462e6 fix: improve hashtag detection by excluding mentions and URL fragments 2026-10-08 00:03:09 +01:00
Terry Carr 3bc2acc07a feat: add support for hashtag channels links with prefill and open functionality 2026-10-08 00:03:09 +01:00
Kaj SchittecatandClaude Opus 5.5 39f342c67e site: user guide for beta_90, colour and the Theme page
A Colours & theme section: the Theme page (preview, Apply and restart on the
box and in the bar under the title), MORE COLORS! in Regular, Neon and Pastel,
Taste the rainbow, chat icons and colourful bubbles, with a gallery of the
looks. A Theme card in Settings pages; the look rows left the Display card.
Chats describes the per-chat colours and the Initials option.

The main screenshots (Home, Chats, conversation, Contacts, Settings, Map) are
retaken in the new default look. The map is fully loaded this time (the beta_89
shot was taken mid-download), with its coordinates pixelated as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:44:21 +02:00
Kaj SchittecatandClaude Opus 5.5 f1a7775d0c touch: beta_90, colour comes back
Testers said the colour was what set wadamesh apart, and the beta_89 redesign
took most of it out. This puts it back as choices on one new page.

Settings > Theme (CAT_THEME, Device group): a live preview drawn from a copy of
the palette (themeTokensFor), Night/Day cards, High contrast, the accent swatches
inline with Custom, MORE COLORS! with Regular/Neon/Pastel, Taste the rainbow, and
the chat looks. Mode, accent and look are pending until one Apply and restart on
the preview box; scrolled past it, a slim bar under the title carries the button
with six dots in the picked look. Chat icons, bubbles and compact apply live. The
look rows left Display; the control panel's theme chip long-press opens Theme.

The colour engine: ThemeTokens + lookAdjust over the mode and accent at boot
(applyBootTheme), section hues for the launcher, Settings, tab bar and status
icons, per-name colours (FNV hue) for names, glyphs, initials balls and bubble
tints, Neon lit edges and gradient primaries, Pastel tints, and a deeper Day ink
for names and glyphs on light lists. MORE COLORS! defaults on (Regular); HC and
e-paper keep their own colours. New prefs are standalone u8 keys (chat_icons,
col_more, col_style, col_rb), no schema bump.

Taste the rainbow: per-item hues round the wheel, a 10 Hz hue drift on the tab
marker, chats count and focus ring (rests when off, locked or screensaver), press
dip, an arrival flash on new bubbles (draw-only props, the virtual list never
sees it), rainbow lock mark, boot mark and Constellation nodes (CstTheme
node_sat/node_val), and a dot burst on enabling it.

Chat icons: Glyphs or Initials through one avatarApplyLook; AVATAR_SQUARE_FLAG
moved off LV_OBJ_FLAG_USER_1, which is the nav's NAV_SKIP_FLAG.

Size: wadamesh_mark_rgb.h was a static array included by main.cpp and
UITask.cpp, so every S3 image carried the 30 KB boot mark twice. One definition
now (UITask.cpp; the P4 never compiles main.cpp). The V4 goes from overflowing to
~30 KB free with all of the above.

Capture tour: wakes and ends the screensaver before shooting, DOC_LOOK forces a
look, DOC_LOOK_AUDIT walks the looks and the Theme page, DOC_MAP_SHOT waits for
Wi-Fi and for every tile before shooting the map.

25 new strings as English placeholders in every .lang file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:34:08 +02:00
Kaj SchittecatandClaude Opus 5.5 6fb66d6872 site: user guide for the redesigned interface (beta_89)
New sections for the lock screen and PIN, the Constellation screensaver,
new-message cards, the control panel, text size and the T-Deck trackball;
Home, Chats, Contacts and Settings rewritten for the new screens; Settings
pages regrouped as on the device; every screenshot recaptured on a T-Deck
running beta_89 (location, Wi-Fi name and pairing code masked).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:29:59 +02:00
Kaj SchittecatandClaude Opus 5.5 9b720401f8 release: beta_89 notes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:44:50 +02:00
Kaj SchittecatandClaude Opus 5.5 662665e6de touch: beta_89, the UI overhaul
Every screen redesigned to the Oct 2026 mockups, on one design system:
palette, type roles (generated Montserrat faces with Lucide line icons on the
LV_SYMBOL codepoints), a theme apply-cb that styles every LVGL widget, button
kinds, list rows, sheets as action lists, toasts and key-glyph avatars.

Rebuilt: status bar, Chats (filters, + and menu), conversation (route header,
hop meta, "Heard by N repeaters", delivery states, pill composer), lock screen
(mark, clock, 24 h activity, unread cards), Constellation screensaver, Home
launcher and launcher sheet, Status page, grouped Settings home, animated boot
splash, and the message glance as a notification card.

New: optional PIN lock (salted SHA-256, keyboard / touch pad / number pad per
board, backoff that survives reboot), screensaver on battery with its own
duration, T-Deck text sizes, trackball hold to switch Scroll and Cursor (Scroll
default, prefs v69/v70), hold Settings tab for the control panel. M9 bottom
notices removed. Every screen measured for the Large and Huge text sizes.

Fix: adverts over 2/3-byte path hashes never reached the recently heard table
(packed path_len compared as a byte count); ADVERT_PATH replies now carry the
whole multi-byte path.

English placeholders for the 204 untranslated keys in every .lang file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:26:41 +02:00
Kaj SchittecatandClaude Opus 5 d6e696d580 release: beta_88 notes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 13:44:42 +02:00
Kaj SchittecatandClaude Opus 5 a8f813c5f9 Merge PR #599 (oumike): battery saver, Condense Nav, Pager keyboard nav
Closes #591, #592, #593 and the M9 Bluetooth status-bar overlap museifu696
reported. Also the T-Pager battery saver zomgugoff asked for and the status-bar
clock fix.

Review notes, since this landed on top of a day of P4 and M9 work:

* The prefs change is done the right way. condense_nav is appended at the TAIL,
  CURRENT_VERSION goes 65 -> 68 with v66/v67/v68 each described, and the
  trailing static_assert moves to the new last field. v67 and v68 are one-time
  per-board resets with no new field, which is the pattern v63 and v65 used.
* Six conflicts, all in test_touch_prefs_schema.cpp, all the same shape: we had
  both repaired the same broken sums. His version is a superset (it adds
  condense_nav on top of the report_ping/report_done_n I added), so his wins.
  Verified afterwards that both sides survived rather than trusting the merge:
  the channel-send guard, the P4 tile-FS fix, statusPctOverflow, m9NoticeSet,
  m9DiscoveredUnseen and TDP4_C6_ID are all still present, as are his kbd_nav
  and Condense Nav work. Host suite 17 pass / 0 fail at v68.

Also here: PUBLISH the translations the PR added.

It added five strings to all fourteen language files with real translations, but
left "# ver: 22" alone and did not touch langs.json. Devices fetch lang/<ver>/,
so the version is the only thing that makes one re-download: without a bump the
work sits in the repo and reaches nobody, and every non-English device shows
those strings in English. The baked-in table was regenerated, so compiled-in
languages were fine -- it is the downloaded ones that would have silently
stayed behind. Bumped to 23, snapshotted into lang/23/, and langs.json points
at it. deploy-apps.sh validates: 14 languages and 14 apps, every referenced
file present.

That store still has to be pushed with the release; editing deploy/apps/ is not
publishing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 12:30:55 +02:00
Kaj 127ece5b88 Merge pull request #590 from jcyrio/fix/layout-switch-hint-physical-keyboards
touch: the Secondary keyboards hint named a key the Pager does not have
2026-10-05 12:24:25 +02:00
Kaj 02454f8142 Merge pull request #587 from jcyrio/fix/keep-cleared-dm-in-inbox
touch: "Delete history" on a DM no longer makes the chat vanish from Chats
2026-10-05 12:24:12 +02:00
Kaj SchittecatandClaude Opus 5 820bc21f46 build: verify the P4 artifacts, and make the host tests runnable
The T-Display P4 ships EIGHT images: panel (AMOLED/LCD) x radio (SX1262/LR2021)
x C6 stack (esp-hosted/ESP-AT). The C6 axis is new and it is the dangerous one,
because the two builds are identical apart from the artifact name they carry,
and flashing the wrong one at an older unit used to boot-loop the board.

scripts/build/p4-release.sh builds all eight and VERIFIES each image against the
OTA name baked into it before accepting it. That is the same string the device
builds its self-update URL from, so a disagreement with the filename is exactly
the failure that bricks people -- the T-Deck Max self-updating to Pro firmware,
and this table has been wrong three times. A mismatch aborts the release.

It earned that check immediately: building the eight by hand produced two
mislabelled images, and every mechanism meant to prevent it was itself broken.

  * build.sh fullclean has NEVER worked from a plain shell. It ran `cmake -E
    remove_directory` above the esp-idf/export.sh that puts cmake on PATH, so it
    printed that it had removed the tree, exited 0, and removed nothing. Now a
    plain rm -rf, and it verifies the directory is gone instead of assuming.
    This matters beyond tidiness: the hosted and ESP-AT builds SHARE a build dir
    (the C6 axis does not change P4_BUILD_TARGET), the backend is read from the
    environment at CMake CONFIGURE time, and CMake does not reconfigure because
    an env var changed -- so a clean that silently did nothing yields an
    incremental build of the OTHER variant, with no configure output to show for
    it. That is precisely how the two mislabelled images were produced.
  * Deleting CMakeCache.txt alone is not a clean either; it leaves CMakeFiles/
    behind and the next configure dies in CMakeDetermineCCompiler.
  * Detecting failure by grepping a log for "error:" misses a failed CMake
    configure, which prints "Configuring incomplete" and no such line -- and the
    previous application.bin is still sitting in the directory to be picked up.
    Exit codes now.

gen-flasher-meta.py gains the four ESP-AT entries, labelled "older unit, ESP-AT
C6": someone choosing in the web flasher has no serial log and needs to pick on
something they can check.

scripts/run-host-tests.sh runs test/ as one command. Nothing ever did, which is
why test_touch_prefs_schema had been failing to COMPILE since v64 appended
report_ping and report_done_n without updating its sums -- and a static_assert
that does not compile is not a guard. That one exists to catch the mid-struct
insert that broke v44 and shipped the poisoned beta_57 layout. Repaired, with a
note where the next person appending a field will read it. 17 pass, 0 fail, 2
skipped with their reasons stated rather than quietly dropped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 11:31:36 +02:00
Kaj SchittecatandClaude Opus 5 88e36e9499 touch: a private channel send must never fall out onto public (#589)
Three fixes.

#589/#588, and this one is a disclosure rather than a glitch. TippyLion28 had a
channel go invalid (#588), then sent to it from the web UI; the messages went
out on the PUBLIC channel with nothing on screen to say so. The web path did
resolve the channel by name at TX, with a comment citing the stale-slot trap --
and then threw that away:

    if (slot < 0) slot = threadMeshChannelSlot(tidx);   // fall back to cached

A name that does not resolve means the channel is gone. The cached slot then
points at some OTHER still-valid channel, in practice slot 0, public. It now
refuses and says so, which is what the touch composer already did ("Channel not
found"); the web UI had no business being the laxer path.

termDoSend() also only checked that the slot resolved to A channel, never that
it was the RIGHT one, so a stale slot sailed through it. Every caller already
passes the channel the user chose, so it compares now -- enforced at the one
point every sender passes through, not only in the callers.

#583, T-LoRa Pager losing ~30 min/day while powered. jcyrio read the source and
called it exactly: HardwareRtcClock copies the chip into the ESP32 clock once at
boot and getCurrentTime() never touches I2C again, so the software clock
free-runs at the RC oscillator's ~2% while an accurate crystal sits unread on
the bus. Keeping I2C out of the hot path is right -- it runs on every message,
UI tick and advert -- but once an hour is not that path. tick() now re-reads the
chip hourly and pulls the software clock back. Only when a chip was adopted at
boot (a read refused at boot is not a trustworthy source and must not drag a
good clock backwards), only past a 2 s threshold, and it corrects the software
clock alone: calling setCurrentTime() would write the chip's own value back to
itself and a read-back failure would then mark a healthy retention path broken.

P4 tile cache on a board with no SD card. The fallback set s_tile_fs = &FFat,
but main.cpp mounts that partition with LittleFS -- 'storage' was converted from
FAT and this line was not updated with it. The tile cache was handed an FS
object nothing had mounted, so every tile failed with "File system is not
mounted" as the only clue. Uses the filesystem that is actually mounted.

Compile-checked on T-Deck, T-LoRa Pager and ThinkNode M9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 11:31:16 +02:00
Kaj SchittecatandClaude Opus 5 b52c71f4a5 p4: stop retrying a C6 that is never coming back
7ce1b3f turned off CONFIG_ESP_HOSTED_TRANSPORT_RESTART_ON_FAILURE so a wrong-
firmware C6 could not reboot the board. It stopped the boot loop and I called
it fixed on a 45 s capture. That was the wrong test. Disabling the restart did
not stop esp-hosted, it only removed its exit: sdio_read_task posts
ESP_HOSTED_EVENT_TRANSPORT_FAILURE and then `continue`s straight back into a
register read that is going to fail again. Measured on the same board:

  731 failed register reads in 45 s, ~2.2 KB/s of log, forever

A boot loop and an unbounded retry storm are the same mistake: an unreachable
co-processor is a PERMANENT condition and the driver treats it as transient.

* The rx task now parks after the failure event is posted (build-time patch to
  the esp-hosted fork, same idempotent mechanism build.sh already uses for
  libsodium and the SD_MMC pull-ups; it fails loudly if upstream moves that
  code rather than silently not applying). No reboot, no retry: Wi-Fi and BLE
  are unavailable and everything else carries on.

* hostedInitBLE() no longer runs unconditionally. It is a full esp-hosted
  handshake and it ran on every boot even though the banner advertises lazy
  initialisation -- so on a mismatched unit it started a transport that can
  never come up, for a radio the user may have had switched off. It now
  honours the same stored intent beginBle() already checked.

* The keyboard hot-plug probe backs off from 1.5 s to 15 s once the expansion
  has been absent a few times, and returns to the fast rate the moment one
  succeeds. The probe is a real I2C transaction and on an empty bus Arduino's
  Wire layer logs two [E] lines per attempt: a permanent drip down the USB-CDC
  the companion app talks over, for an accessory most people do not own.

Measured on an ESP-AT unit running the HOSTED image, 150 s captures:

                      shipped   7ce1b3f only   this
  resets                 5           0           0
  host restarts         yes         yes          0
  failed reg reads       -        731/45s        1
  steady log rate        -        2.2 KB/s    ~14 B/s
  UI                   never      appears     stays up

The remaining ~14 B/s is the backed-off keyboard probe on a board with no
expansion attached, which is the expected floor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 10:26:17 +02:00
Kaj SchittecatandClaude Opus 5 7ce1b3f48c p4: an unreachable C6 must not take the device down with it
beta_86 flipped the T-Display P4's default C6 backend to esp-hosted. Current
V1 units ship the C6 with esp-hosted firmware and are fine; OLDER units still
carry the factory ESP-AT firmware, and on those the SDIO stream never frames.
That turned into a board that will not boot at all, reported by three people
so far (#600 jalevdev, myshoeisonfire on Discord, and Kaj's own unit), each
with the same signature:

  H_SDIO_DRV: len[25970]>max[1524] OR offset[25697] != exp[12], Drop

Two separate things made a co-processor mismatch fatal, and both had to go.

1. The UI was behind the C6. hostedInitBLE() ran unconditionally at setup, a
   hosted handshake, with ui_task.begin() ~80 lines later. The LEGACY backend
   always started its worker AFTER the UI, which is exactly why a dead C6 used
   to cost Wi-Fi and nothing else; the hosted path was wired in ahead of it.
   Both backends now start at the end of setup, after the UI, with a note
   saying nothing C6-dependent may move above it.

2. esp-hosted resets the host. CONFIG_ESP_HOSTED_HOST_RESTART_NO_COMMUNICATION_
   WITH_SLAVE=n covers one path; the one that actually fires is
   H_TRANSPORT_RESTART_ON_FAILURE in sdio_drv.c, about 10 s after the connect
   attempt, and it is on by default. Now off. The failure still arrives as
   ESP_HOSTED_EVENT_TRANSPORT_FAILURE, which is what an application should act
   on instead of the transport rebooting the board underneath it.

Measured on an ESP-AT unit, 45 s captures, same hardware:

  as shipped       5 resets, host restarts, UI never appears
  reorder only     2 resets, host restarts, UI appears then dies
  reorder + config 0 resets, 0 restarts, UI stays up

The C6 is still unreachable on that unit, as it must be - it is the wrong
firmware for it. It now costs Wi-Fi and BLE and nothing else.

Also here, both needed before this can ship:

* The ESP-AT build gets its own artifact name (tdisplay-p4-at, and the matching
  -lcd/-lr2021 combinations). Both backends used to answer "tdisplay-p4", so an
  ESP-AT unit's self-update would have fetched the hosted bin and put itself
  straight back into the loop, recoverable only over USB. That is the T-Deck
  Max bug again (a Max self-updating to Pro firmware) and would have been the
  fourth time this table shipped wrong.

* The keyboard expansion probe prints a one-shot bus scan. "It does not work"
  has three causes that are indistinguishable from outside: not clipped on,
  clipped on but unpowered, or present and failing bring-up. An unpowered board
  does not NACK cleanly either - its pull-ups are dead, the lines sit low
  through its ESD diodes, and every transaction returns ESP_ERR_INVALID_STATE,
  which reads like a driver fault. The scan says which it is in one line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 10:03:19 +02:00
Michael A. Cojocari 535f69c2ee Merge branch 'feat/web-transfer-backup-restore' into general-tweaks
# Conflicts:
#	src/helpers/esp32/WebFileTransferPage.h
2026-10-04 20:05:39 -04:00
Michael A. CojocariandCopilot 6c85948002 fix(boot): boot logo follows the saved orientation
The logo is painted before SPIFFS/SD mount, so it read rotation from the
legacy NVS settings copy, not the real file-backed settings. Any device
whose two copies disagreed (the Attaky after its v68 landscape reset, or
anyone who changed orientation since settings moved to files) showed the
logo in the old orientation until the UI took over.

Keep a raw-NVS boot copy of the rotation (touch/boot_rot): written by
touchPrefsSetUiRotation() and re-synced after touchPrefsReload() every
boot, so schema resets and restored backups catch up too. The logo reads
it, falling back to the old read when absent (first boot after update,
or Launcher installs without NVS). On the Attaky any landscape value is
treated as rotation 1, as UI init does.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-04 19:34:01 -04:00
Michael A. CojocariandCopilot 3a9b3ce055 File Transfer page: browser Backup and Restore buttons
Backup runs the same export as Settings > Backups > Export into a hidden
temp file and streams it to the browser as meshcore-<date>.json.
Restore uploads a backup .json through the existing chunked/CRC path,
then applies it via the shared backupImportAndReboot() helper (same
sections/replace flags as Import backup) and reboots. Temp files holding
key material are removed after use and on page open.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-04 19:25:58 -04:00
Michael A. CojocariandCopilot fe8fdd33cf fix(attaky): turn the panel back to portrait at UI init
Portrait came up as a shrunken landscape screen. The boot logo takes its
rotation from the settings copy in legacy NVS, read before main.cpp moves
settings to SPIFFS. On the Attaky that copy says landscape (the new
default, and the v68 reset), so the logo turned the panel to rotation 1.
The real setting, loaded from SPIFFS afterwards, said portrait: LVGL was
built at 240x320, but nothing turned the panel back, so the UI was drawn
upright for landscape, 240 px wide, with its bottom cut off.

UI init now turns the panel to the build portrait rotation
(DISPLAY_ROTATION, 2) when the UI is portrait, as the CrowPanel already
does for the same reason. Portrait there needs no touch transform, as on
the V4: the Attaky's landscape touch map is the V4's, so its touch frame
is too. The D-pad remap from the orientation commit follows from the
same map.

The boot logo still follows the NVS copy, so in portrait it shows in
landscape for a moment before the UI takes over.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-04 17:29:00 -04:00
Michael A. CojocariandClaude Opus 5.5 01346777ed feat(attaky): File Transfer on the Mesh Deck, to internal storage
File Transfer needed a removable card, and the Mesh Deck has no card
slot. Give it an internal backend (WADA_WEB_FILE_TRANSFER_INTERNAL):

- Files go to /transfer on the internal LittleFS "tiles" partition, never
  SPIFFS, which holds the settings, contacts and chats.
- The page shows "Internal storage  used / total". An upload that would
  leave less than the map tile cache's 320 KB floor is refused up front
  ("not enough free internal storage") instead of failing part-way.
- Map tile folders are refused ("map tiles need an SD card"): the map
  only reads uploaded tile libraries from a card.
- If the partition did not mount, opening the app says so instead of
  asking for an SD card.
- The browser page now says files go to "the device's storage" rather
  than "the device's SD card", which is right on every board.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M5VJ43X3nUULPqVcgrAH2
2026-10-04 15:47:12 -04:00
Michael A. CojocariandClaude Opus 5.5 32d6c0d56d feat(attaky): Orientation setting, so the Mesh Deck can run in portrait
The Mesh Deck UI was pinned to landscape (LV_DISP_ROT_90), and the
build without a keyboard is held upright, so it needs portrait too.

- CAP_ROTATABLE on the Attaky: Settings > Display > Orientation toggles
  landscape (ROT_90, panel rotation 1) and portrait (the panel's
  DISPLAY_ROTATION=2, no touch transform, as on the V4).
- Landscape stays the default: DEFAULT_UI_ROTATION is 1 on this board,
  and schema v68 (no new field) resets the stored value once, since the
  UI ignored it until now. The pre-LVGL boot logo already follows the
  setting, so it is upright in both.
- The front D-pad is named for landscape, so in portrait a press turns
  with the screen (right -> up, down -> right, left -> down, up -> left).
- The terminal (Console Mode) keeps the panel's build orientation,
  whatever the UI orientation is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M5VJ43X3nUULPqVcgrAH2
2026-10-04 15:47:12 -04:00
Michael A. CojocariandClaude Opus 5.5 6d655b8301 feat(pager): keyboard navigation and remappable tab keys, as on the T-Deck (#591)
The wheel is the Pager's only way to move the highlight, which is slow
and imprecise. Bring the T-Deck's keyboard navigation to it:

- With no text field focused, programmable letters move the highlight
  (default I up, K down, J left, L right) and scroll (U / O). Select
  and Back default to no letter, since Enter and Backspace-hold (the
  wheel's click / long-press) already do them. On the Map the letters
  move the highlight; W/A/X/D stay the map pan.
- The tab keys are programmable like the T-Deck's, defaulting to the
  Pager's M/C/H/A/S, and the bar's labels show whatever is bound.
- Settings > Keyboard gets the same "Keyboard navigation" switch (on by
  default here) and the same tap-a-row-then-press-a-key remap rows; the
  T-Deck's rows move into settingsNavKeyRows() so both boards share them.
  The Pager's switch only turns the letters on or off; it never empties
  the focus group, which the wheel always needs.
- A remap refuses letters the Pager uses for fixed actions (Q/E slider
  nudge, W/A/X/D map pan; A stays allowed as a tab key).
- In wheel edit mode on a slider, J/L preview like a turn and the
  select key closes the edit, matching the wheel.

Settings: the Pager's kbd_nav and key bindings were never shown on that
board, so they held the T-Deck defaults. Schema v67 (no new field) puts
the Pager defaults in once on that board only. Two new strings,
translated in every deploy/apps/lang file; i18n_builtin.h regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M5VJ43X3nUULPqVcgrAH2
2026-10-04 15:06:40 -04:00
Michael A. Cojocari 8adea3558e Merge branch 'fix/594-statusbar-clock-overlap' into fix/touch-ui-followups-2 2026-10-04 14:56:28 -04:00
Michael A. CojocariandClaude Opus 5.5 d7c3844ef2 fix(ui): keep the status-bar clock off the Bluetooth icon (#594)
On the M9, V4-R8 and Pager the text presets grow the status-bar offset
ladder with the font (SBX), and the builder places the Bluetooth glyph
with it. The clock placement then re-aligned the clock with plain SC(),
which leaves it at its 100% spot, so at Large / Huge the Bluetooth glyph
moved left onto the time. The charging slide also used raw 100% offsets,
so plugging in or unplugging snapped the cluster back under the grown
glyphs.

- The wide-bar clock uses SBX() like the icons it sits beside.
- The charging slide uses exactly the builder's offsets (SBX plus the
  battery-% overflow); the narrow V4 portrait bar keeps its raw values.
- Safety net for every board: after placing the clock, if it overlaps a
  visible icon on its row (Bluetooth, DND, sleep, Wi-Fi, signal,
  battery), move it just left of the leftmost one. Covers the 12-hour
  clock, the Bluetooth-keyboard glyph and the centred hide-name clock.
  Re-checked only when something that moves or resizes them changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M5VJ43X3nUULPqVcgrAH2
2026-10-04 14:56:28 -04:00
Michael A. CojocariandClaude Opus 5.5 08750ab3a1 feat(ui): Condense Nav, a landscape nav rail with icon-only Home launchers (#592)
On short landscape panels (the T-Pager is 480x222) the bottom nav bar
spends a full-width strip on five icons. New Settings > Display switch
"Condense Nav", off by default, applies after a restart:

- The nav bar becomes a rail on the right edge (LV_DIR_RIGHT tabview).
  Pager cells show the key letter under the icon; the Tanmatsu keeps its
  coloured F-key shapes; the T-Deck / P4 key hints sit under the icons.
- Home's launcher column (Advert, Terminal, Files, Apps, Control) shows
  icons only; the buttons keep their height and spacing, and the status
  text, chart and info card widen into the freed space.
- The active-tab marker turns vertical beside the active cell, the
  unread and update badges move to the top-right of their cells, and the
  rail stays solid (and the marker visible) on the Map.
- Content that kept the bottom bar visible now stops at the rail
  instead: tabContentW() subtracts the rail, and the map canvas, Settings
  landing grid, chat-list rows, Mentions and the app drawer use it.
  Swipe-left on the rail opens the drawer (swipe-up on the bottom bar).

Portrait layouts and the M9 are unchanged: the mode is decided once in
buildUiTree from the pref and the orientation at boot, the switch is
greyed out in portrait and absent on the M9.

TABBAR_H becomes a runtime value (0 with the rail) next to a new
TABBAR_W. The pref is a new trailing field, condense_nav (schema v66);
the schema test's layout asserts now count the v64 fields they missed.
Strings added to every deploy/apps/lang file and i18n_builtin.h
regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M5VJ43X3nUULPqVcgrAH2
2026-10-04 13:47:46 -04:00
Michael A. CojocariandClaude Opus 5.5 567c6599e5 fix(pager): click the wheel on a slider to adjust it with the wheel
A plain wheel turn moves focus, and a click sends Enter, which a slider
ignores, so no slider on the Pager (Screen timeout, Brightness, the
Control Center sliders, Map zoom) could be adjusted with the wheel; only
the hidden Q/E keys moved them.

Clicking the wheel (or Enter) on a focused slider now enters an edit
mode, shown with a bigger contrasting knob. Turns move the value with a
live preview (VALUE_CHANGED) and save nothing. Clicking again saves
(RELEASED, the same commit a touch drag ends with) and returns the wheel
to focus navigation. Back (long-press or Backspace-hold) restores the
original value without saving, as does the screen timing out or focus
leaving the slider. Q/E preview instead of saving while in edit mode.

The slider step moves out of navMoveDir() into navSliderNudge() so the
wheel and Q/E share it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M5VJ43X3nUULPqVcgrAH2
2026-10-04 13:35:42 -04:00
Michael A. CojocariandClaude Opus 5.5 988498b267 feat(ui): battery saver switch on every touch board (#593)
The idle power-saver (touchSleep) already ran on every touch build: the
hooks are installed in UITask::begin and main.cpp calls loopEnd() every
tick, and its gates are board-neutral. Only the Settings switch, its
callback and the amber battery tint were behind a T-Deck / V4-R8 / M9
allowlist, so on every other board (T-Pager, CrowPanel, T-Display P4,
T-Deck Pro, ...) the pref defaulted OFF with no way to turn it on --
the same bug the M9 had until 2026-09-02.

Drop the allowlist from the switch row, sleepIdleToggleCb, applyBattColor
and the status-bar re-tint. Also rejoin the lockOnScreenOffToggleCb
comment the old #if had split in two.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M5VJ43X3nUULPqVcgrAH2
2026-10-04 13:18:52 -04:00
Kaj SchittecatandClaude Opus 5 66e1f37d2c harness: model the filesystem, and stop apps running other apps' tests
Two things made a full harness run untrustworthy, and together they produced a
false accusation against a community app.

wada.fs was stubs. append threw the data away, remove always said true, and
read, write and list were not there at all — although the firmware registers
all five (LuaAppHost.cpp). Any app that writes a file and reads it back
therefore failed here while working perfectly on device. wardrive 1.2 is
exactly that app: it errors on a nil fs.read at its first log flush, long
before it renders anything, and the emoji scenario then reported the repeater
name as unrendered. Read as "1.2 regressed the beta_70 emoji fix", which it
did not. It passes untouched once fs is real.

wada.fs is now an in-memory filesystem following LuaAppHost's semantics: read
returns the data plus the file's TOTAL size so a caller can window a growing
log, an offset at or past the end gives an empty string rather than nil, a
missing file gives nil, and list is sorted so a hash order cannot make a test
flaky.

The second: the gpscompass scenario list was acting as the dispatch default,
so an app with no branch of its own was run against tests written for a
different app. Six store apps have no branch (2048, airtime, monitor, nearby,
sdktest, snake), so a full sweep came back with six red apps as its normal
state — and a real regression would not have stood out against that. Those
apps now say they have no scenarios and run none. They are untested, which is
worth saying out loud, but that is not the same as broken.

A sweep of every store app is now 8 apps passing their own scenarios, 6
reported untested, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 12:42:12 +02:00
Kaj SchittecatandClaude Opus 5 ace73911ad touch(m9): bottom notices carry a count and stop flashing
The M9 is the one board with no bottom bar (TABBAR_H == 0, tab_btns hidden
outright), so it never got the numeric badges the others have. beta_75 put
three glyph labels on layer_top across the bottom instead, OVER page content,
and blinked two of them at 1 Hz. With nothing else down there to read them
against, that came back reported as the navigation icons flashing.

They now carry a count and are shown only while they mean something:

* The person glyph was driven by discoveredCount(), a standing total that is
  non-zero on any live mesh from the first advert heard and only ever falls
  when you add or purge. It therefore blinked forever. It now counts what has
  not been looked at: watermarked at boot, because a ring restored from flash
  is not news, and again whenever the Discovered list is opened, with a clamp
  so purge, adding to Contacts and ring eviction cannot leave the next real
  arrival reading as nothing new.
* It also blinked while you were standing on Contacts with that list open.
  Suppressed there, the way the mail glyph already was on the inbox tab.
* Two style writes and a z-reorder ran on every 250 ms tick regardless of
  whether anything was pending, invalidating the strip for nothing.
  m9NoticeSet() only touches LVGL on a real change, and takes a refront flag
  so a live notice still lifts over the app drawer, which is the one overlay
  notices are allowed to sit on top of.

Also: "100%" was clipped by the signal bars. The percentage is right-anchored
at SBX(22) and grows left, the bars are pinned at SBX(54), so the text gets
SBX(32) and no more; "100%" is a whole glyph wider than "99%", and the bars
are created after the label so they paint over the leading 1. Reported by
buko84 and confirmed by jd1227, both on the M9, which is a board whose text
presets grow the font and leave the geometry at 100%, so it runs out of room
first. statusPctOverflow() measures the real width and slides the bars, the
Wi-Fi glyph and the SD dot left by the overflow, capped, and gated to those
font-only-preset boards. Zero where the text already fits, so no board moves
that did not have to, and the clock, the DND moon and the BLE glyph never
move. The round panel already solved this with statusClusterStart(); this is
the same idea for the single-row ladder.

Built for the M9, the V4-R8, the Pager and the T-Deck. The T-Deck binary is
byte-identical, which is the check that the ladder change reaches nothing
outside its gate. Not run on hardware: needs an M9 tester.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 12:36:53 +02:00
jcyrioandClaude Fable 5.1 5249901555 touch: the Secondary keyboards hint named a key the Pager does not have
Switching on a secondary layout changes nothing until you cycle to it with
double-tap SPACE, and the sentence under "Secondary keyboards" is where that
is explained. It was gated on HAS_TDECK_KEYBOARD || HAS_M9_KEYBOARD, so the
T-LoRa Pager, T-Deck Pro and T-Deck Max (HAS_PAGER_KEYBOARD) fell through to
the on-screen wording and told the user to tap an "EN" key their physical
keyboard does not show, while their keys go through the same handleHwKey()
double-space path as the T-Deck. The setting read as broken.

Gate the hint on CAP_KEYBOARD instead, which is exactly the set of boards
whose keys reach that handler (the Attaky module keyboard has its own loop
and really does type next to the on-screen EN key, so it keeps the touch
wording). And make the "on" toast say how to switch, since "Russian: on"
alone was the moment the user was left guessing; "off" keeps the short form.
The cycle itself is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: jcyrio <50239349+jcyrio@users.noreply.github.com>
2026-10-03 14:13:56 -07:00
Kaj SchittecatandClaude Opus 5 1348910415 release: beta_87 notes say what happened and what fixed it
Dropped the fault framing. The notes describe the mechanism and the change,
not whose code it was.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 19:49:22 +02:00
Kaj SchittecatandClaude Opus 5 2576b7e369 release: beta_87 notes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 19:42:58 +02:00
Kaj SchittecatandClaude Opus 5 7e1f5af904 lua: wada.ui.emoji(cb) for apps that draw their own text entry
beta_86 put the emoji picker on the long-press menu, which covers the firmware's
own field and so covers wada.ui.input(). It does not cover an app that builds its
own buffer from key events, because there is no textarea to long-press -- and
that is exactly the case Jade asked about: their composer shows a pad-bytes
counter the firmware's field cannot, so it takes the custom path and the menu
never applies to it.

wada.ui.emoji(cb) opens the picker and hands the glyph back as UTF-8, nil if
dismissed. Same discipline as wada.ui.input's callback: one ref, one call,
released either way, refused if one is already open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 19:41:51 +02:00
Kaj SchittecatandClaude Opus 5 defbd1d478 lua: wada.ui.clear() leaked a closure per button and per list row
Reported by Jade as "screen memory errors, worse the longer the app has been
open", with an app that rebuilds its screens on a 120 ms debounce.

Button and list-row callbacks live in one registry table keyed by the LVGL
object POINTER. clear() calls lv_obj_clean(), which frees every one of those
objects, and bumps s_ui_gen so stale handles go inert, but it never touched the
table. So each rebuild added an entry and kept the Lua closure, with its
upvalues, alive forever. Unbounded, and worst for exactly the apps that are
built properly: a list of twenty contacts redrawn on a timer leaks twenty
closures a go.

It was also a correctness bug in waiting. LVGL reuses freed addresses, so a new
button created WITHOUT a callback could land on an address a dead one left
behind and fire that stale closure on the next tap.

clear() now replaces the table. Every entry in it describes an object
lv_obj_clean just destroyed, so there is nothing there worth keeping.

Found by reading the host after Jade asked what the memory budget was; the
budget was never the problem.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 19:33:33 +02:00
Kaj SchittecatandClaude Opus 5 890a250a02 release: cumulative stable notes for beta_85 (beta_84..85)
Curated from the two beta notes. Leads with the two things a user must know
before this reaches their device: do not switch a T-Display P4 to landscape on
this build (fixed in beta_86), and a T-Deck Max on beta_84 must be updated over
USB rather than from the device.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 19:14:07 +02:00
Kaj Schittecat f4693f7c35 chore: refresh Mesh America catalog for beta_85 2026-10-03 19:01:29 +02:00
Kaj SchittecatandClaude Opus 5 607c761efa release: beta_86 notes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 18:48:13 +02:00
Kaj SchittecatandClaude Opus 5 76a6e86896 touch: emoji in any text field, and the status bar fits the bigger type
Jade asked for three things for Lua apps. Two already existed and the answer is
documentation, not code: wada.sys.epoch() and wada.sys.datetime() have returned
the clock since #245, and wada.ui.input() is the firmware's own field, so it
already carries the long-press Cut/Copy/Paste/All/Sym menu, which is exactly the
"copy/paste from chats into a Lua text field" that was asked for.

The third is here. The long-press menu gains Emoji, so the picker reaches every
field in the firmware rather than only the chat composer: a Lua app's text entry,
a node name, a Wi-Fi password. The row is now sized to the panel instead of a
fixed 48 px per cell. It was already 268 px at five cells, wider than a 240 px
screen, so a sixth would have hung off the M9 and the V4 entirely.

Status bar: the right-hand ladder (battery, signal bars, Wi-Fi, Bluetooth, sleep,
clock) is a chain of fixed offsets tuned for 12 px text. The boards with
font-only UI-size presets grow the font and deliberately leave geometry at 100%,
so the labels widened and the ladder did not, and the battery percentage ran into
the signal bars (jd1227, M9, beta_85 -- the release that gave that board the
presets). SBX() scales those offsets by how much the text actually grew and
returns exactly SC() at Normal, so nobody who kept the default size sees any
change. Verified on the M9, V4-R8, Pager, T-Deck and V4.

SDK docs: corrected the app path. They said to copy the file to /apps/ on the SD
card; a card-backed device roots everything at /meshcomod, so it is
/meshcomod/apps/ there and a bare /apps/ only on boards that store internally.
Following the old wording cost gottlieb8849 an evening.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 18:40:58 +02:00
jcyrioandClaude Fable 5.1 3eb3fa8c71 touch: "Delete history" on a DM no longer makes the chat vanish from Chats
The confirm says "The chat itself stays", and beta_38's notes say the
contact stays, but on a DM the row disappeared: the inbox lists a DM only
while threadHasMessageHistory() is true, and refreshThreadsFromMesh() (every
4 s) reclaims any DM slot with no history, so the emptied thread was hidden
and then freed. Channels were unaffected because the inbox lists them
regardless.

The thread record had nothing to tell an emptied chat from a contact never
chatted with, so this adds keep_when_empty: set by clearThreadHistory() on
a DM, honoured by getCombinedInboxCount(), getUnreadTotal() and the reclaim
pass, cleared when the slot is reused, removed with the thread by "Delete
chat". It is appended to UiHistoryThread; thread_rec_size already lets older
records read it back as 0 and older builds skip it, so the history version
is not bumped (a bump would make a downgrade quarantine both history files).

Only threads the user explicitly emptied are pinned, so contacts without a
conversation still stay out of the inbox and clearThreadHistory() still
removes every message.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: jcyrio <50239349+jcyrio@users.noreply.github.com>
2026-10-03 09:34:47 -07:00
Kaj SchittecatandClaude Opus 5 1247563110 apps: bake the new games, not the GPS ones
The five apps from #585 and #586 all defaulted to being seeded, which bakes
their source into the boards that cannot reach the Store. That is 40 KB of Lua,
and it took the Heltec V4 from 95.4% of flash to 96.7%: the tightest board in
the matrix paying for apps two of which it cannot use.

gen-lua-builtin.py states the rule: an app that is large, or that needs hardware
those boards do not have, earns its place rather than getting it by being in the
catalog. ProTreck (17 KB, altimeter/chrono/sensors) and Trip Odometer (GPS-only)
are both that, so they are seed:false, exactly like gpscompass and sdscan. They
remain in the Store for every board that can download.

Tetris, Breakout and Ping stay baked: no hardware needs, small, and the boards
that get seeded apps are precisely the ones that cannot fetch them otherwise.
V4 lands at 96.2%.

lua_builtin.h is regenerated here because it is tracked and the build rewrites
it. Leaving it out is how Wardrive and Nearby ended up missing from the seeded
boards after beta_68.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 17:27:40 +02:00
Kaj Schittecat 3171f5adea Merge PR #586 from samuelcoustet: Breakout and Trip Odometer Lua apps
Stacked on #585 and merged after it, as the PR asks. Two conflicts, both from
the stack base moving:

  apps.json: unioned by id, so all fourteen apps survive.
  harness.lua: the additive blocks came from #586. Where both sides edited the
  same code, #585's is newer and wins: its ping scenarios pass cfg.gps as a
  FUNCTION, which is what the harness calls (cfg.gps()); #586 still passed the
  table and would have failed. Taking #586 there would have broken ping.

  Then restored the two dispatch branches my first resolution dropped. The
  breakout and tripodometer scenarios existed but nothing ran them, so both
  apps silently fell through to the gpscompass scenarios and "failed".

All five new apps pass their own scenarios, and so do gpscompass and sdscan.
2026-10-03 17:24:15 +02:00
Kaj Schittecat ee20089e30 Merge PR #585 from samuelcoustet: Tetris, Ping and ProTreck Lua apps 2026-10-03 17:20:09 +02:00
Kaj Schittecat 83303fc297 Merge PR #566 from samdems: the trackball no longer pans the map while the screen is locked
Closes #132. Touch was already gated on isScreenOff() || isManualLock(); the
trackball path was not, so the map had moved by the time you unlocked.
2026-10-03 17:20:09 +02:00
Kaj Schittecat 34e6456b1a Merge PR #565 from oumike: T-Display P4, T-Deck Pro, M9 and UI follow-ups, plus the CrowPanel 3.5
Closes #562, #563, #564, #568, #569, #570, #571, #574, #576 and adds the
Elecrow CrowPanel Advance 3.5 (#561). Carries the fix for the beta_85 P4
landscape touch, which was rotated twice: once by the driver and once by LVGL.
2026-10-03 17:20:02 +02:00
Michael A. CojocariandClaude Opus 5.5 dbecef0b6f feat(crowpanel-35): add Elecrow CrowPanel 3.5" (ESP32-S3, ILI9488 + GT911) touch target
- New crowpanel_35 variant: display, SD, board and target files, plus board JSON
- Portrait mode is drawn rotated 180°; landscape stays at rotation 3
- Share the GT911 touch driver between Wio Tracker L2 and CrowPanel
  (WioTrackerL2Touch.cpp -> src/helpers/input/LgfxGt911Touch.cpp)
- platformio envs, flasher/site entries, release and merge-bin support
- Tests for the CrowPanel SD codec and merge-bin

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132MkJ7RUV32mV9F7o4PJMm
2026-10-02 20:16:06 -04:00
SamandClaude Sonnet 4.6 06f9c6e549 feat(apps): ProTreck 1.0 — outdoor watch Lua app (Astro/Chrono/Timer/Alti)
Sun/Moon rise+set from GPS lat/lon + date (Spencer 1971), moon phase 0-29.
Chronometer with lap times, countdown timer with alarm beep and preset
cycling, altimeter with GPS alt_m, min/max persistence and trend graph.
Swipe left/right to switch tabs. 4 tabs fit any screen size.

test(harness): add 7 ProTreck scenarios (tdeck, v4, no_gps, chrono,
timer, alti, cost). ALL OK 7/7.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-02 22:52:51 +02:00
rootandClaude Sonnet 4.6 7254b9cca6 feat(apps): Ping 1.0 — mesh DM ping with GNSS position and RTT
Send a DM to any contact, measure round-trip time.  Both devices
exchange their GPS coordinates; the result shows RTT, distance and
cardinal bearing when both have a fix.  The app also auto-replies to
incoming pings so two devices running Ping can time each other.

Harness additions: send_dm mock, wada.geo (haversine), deliver_dm
helper, and 6 scenarios (tdeck, v4, no_contacts, round_trip,
auto_reply, cost) — all pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-02 22:50:22 +02:00
SamandClaude Sonnet 4.6 9b7ca27747 feat(apps): Trip Odometer 1.0 — GPS trip computer as a Lua app
Tracks total distance (Haversine accumulation), elapsed time, current
speed, max speed, and altitude from live GPS fixes.  Start/Stop button
pauses accumulation; Reset clears the trip.  The current trip persists
across app restarts via wada.store.

Also adds 6 harness scenarios (no-fix, T-Deck, moving, persist, reset,
cost) and registers the app in apps.json.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-02 22:41:14 +02:00
SamandClaude Sonnet 4.6 ec57effffb feat(apps): Breakout 1.0 — classic brick-breaker as a Lua app
Ball + paddle + 8×5 brick grid. Swipe left/right (or arrow keys) to
move the paddle; swipe up / Enter / Space to launch. Angle on bounce
varies by paddle-hit position. Speed increases each level; high score
persists via wada.store.

Adds 6 harness scenarios (tdeck, v4, keyboard, logic, hiscore, cost).
Worst tick ~8k instructions, 8% of the 100k budget.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-02 22:37:57 +02:00
SamandClaude Sonnet 4.6 44f5dae4eb test(harness): add Tetris scenarios — tdeck, v4, keyboard, logic, hiscore, cost
Detects /tetris/ in the app path and routes to 6 dedicated scenarios:
- tetris_tdeck/v4: open + swipe input on touch screens
- tetris_keyboard: arrow + enter input on keyboard boards
- tetris_logic: hard-drop to game-over, verify restart resets timer to 700 ms
- tetris_hiscore: verify high score is stored as a number after game-over
- tetris_cost: 100 ticks, worst ~6000 instructions (budget 100000)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-02 22:21:44 +02:00
SamandClaude Sonnet 4.6 b4d5e1ff81 feat(apps): Tetris 1.0 — classic falling-block puzzle as a Lua app
Adds Tetris to the app catalog. Full 7-tetromino implementation with:
- Correct piece rotations for all 7 tetrominos (I, O, T, S, Z, J, L)
- Wall kick on rotation (tries +/-1 col when center position collides)
- Hard-drop on swipe-down or Enter/Space
- Soft-drop on keyboard arrow-down
- Score scaling by level (100/300/500/800 x level per 1/2/3/4 lines)
- Speed increase every 10 lines cleared (700ms to 80ms floor)
- Next-piece preview and high score persistence via wada.store
- Game-over overlay with tap-to-restart
- Centered layout adapts to any screen width/height

Tested against the wada.* API contract (integer coords, canvas bounds,
store string/number restriction, instruction budget per tick).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-02 22:13:07 +02:00
Michael A. CojocariandClaude Opus 5.5 f7402e628c fix(tdisplay-p4): Pick colour button no longer runs into the accent swatch
The P4's larger UI scale grew the button over the swatch, which sat at a fixed
x. On the P4 the button is narrower and the swatch sits a gap past its end, in
every orientation. Other boards are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:32:37 -04:00
Michael A. CojocariandClaude Opus 5.5 1cad071bd0 fix(ui): opening a chat no longer pops up the on-screen keyboard
On touch boards without a built-in keyboard, opening a chat focused the
composer and that focus raised the on-screen keys, covering the conversation.
The focus now only binds the composer (cursor shown, a Bluetooth, clip-on or
CardKB keyboard types straight in); tapping the composer brings the keys up.
T-Deck, T-Deck Pro and M9 are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:32:37 -04:00
Michael A. CojocariandClaude Opus 5.5 3378df5494 feat(tdisplay-p4): clipping the keyboard on in portrait offers a landscape restart
When the keyboard expansion arrives while the UI is portrait, a dialog counts
down 15 s and then saves landscape and restarts (the Settings orientation
switch's path); "Reboot Now" does it at once. Cancel, or taking the keyboard
off again, keeps portrait. A keyboard already attached at boot is not an
arrival, so a Cancel is not asked again on every start, and nothing is offered
under the lock screen. showConfirm() gains confirmSetMessage() to update the
open dialog's text in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GRWzzRs7oRvFRNcHUZF4c
2026-10-01 11:31:27 -04:00
Michael A. CojocariandClaude Opus 5.5 b078df52e9 feat(ui): lock screen shows the battery, laid out for every panel; P4 BOOT button locks
Lock screen (all boards, #579): the default view is now drawn from parts
instead of the old 320x240 bitmap with the padlock and wordmark baked in,
which cover-scaled into a jumble on any other panel shape. WADAMESH sits at
the top, the clock below it (clear of the status bar), a smaller padlock in
the centre with the unread count to its left and a new battery readout to its
right, each centred between the padlock and the screen edge. Short panels get
a smaller wordmark and padlock. The clock's burn-in drift now moves around its
new place instead of snapping it back to y=30 every minute. T-Deck Pro keeps
its black-on-white e-paper colours; custom wallpapers are unchanged.

T-Display P4: the BOOT button (GPIO35) now works like the Wio Tracker L2's
wake button -- a press wakes or lights the lock screen, a 2 s hold locks or
unlocks -- so the P4 gets the lock screen and the "Lock when screen off"
setting. It gets no Power-off row: GPIO35 cannot wake the P4 from deep sleep.

Also: the chat list's per-row gear clears the list scrollbar on large panels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:54:15 -04:00
Michael A. CojocariandClaude Opus 5.5 ad91b1cb3f feat(tdisplay-p4): clip-on keyboard expansion, plus keyboard and layout fixes
The T-Display P4 keyboard expansion (TCA8418 + its own XL9555, ported from
camillia-mt) now works. It is looked for every 1.5 s, so it can be clipped on
or off while running, and is routed like the CardKB and Bluetooth keyboards:
it types into the focused field and keeps the on-screen keyboard down. The
external-keyboard routing was gated on Bluetooth, which the P4 lacks, so it
now follows a new CAP_EXT_KEYBOARD; the Bluetooth-only parts stay on
CAP_BLE_KEYBOARD.

- Keys: Shift/Sym/Alt one-shot, Caps Lock lights the expansion LEDs, arrows
  and Backspace repeat, Alt+Backspace backs out, F1-F5 open the tabs left to
  right, and F11 steps the keyboard light through off/low/medium/high (saved
  across reboots).
- Wire1 is shared with the ES8311 codec on other pins; P4I2c1 switches it
  under a lock, and the codec's lazy bring-up now goes through it.
- With the keyboard attached the tab bar labels F1-F5 beside each icon, and
  Enter on a chat bubble opens its action menu.

Also, on any keyboard build, Enter runs the command in the terminal and the
repeater admin console (the admin console's Enter hook sat on its Send button,
where nothing raised it). And on the P4: the status-bar name shrinks to fit the
bar, the Discover buttons are spaced out, contact names clear their icon in
landscape, and leaving an app returns the focus to its tile in the app drawer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:10:12 -04:00
Michael A. CojocariandClaude Opus 5.5 d6bc169017 fix(ui): Map side buttons show which one is selected (#576)
The keyboard/trackball cursor is a 3 px accent outline, and on an accent
fill it disappears. setSelectionGlow already swaps to the contrast
outline (white, black on Day) there, but only for a fully opaque fill;
the Map's side buttons are accent at 70% over the tiles, so they kept
the accent outline and the selected one barely stood out. Any fill that
is at least half accent now takes the contrast outline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:20:18 -04:00
Michael A. Cojocari 500b24e38c feat(tdisplay-p4): support LR2021 radio variants (#504) 2026-09-26 21:59:58 -04:00
Michael A. CojocariandClaude Opus 5.5 b408487dec fix(ui): tapping the accent popup after its text box closed crashed (#574)
The accent popup keeps a pointer to the text box it edits, and about 25
separate paths close the popup by hand. A path that deleted the text
box without closing it (e.g. a form closing after its send) left the
popup on screen pointing at freed memory; tapping an accent letter then
ran lv_textarea_del_char on it. That is the beta_85 T-Deck crash in
#574 (loopTask LoadProhibited at 0x3, accentBoxCellCb ->
lv_textarea_del_char), matched against the release image.

Tie the popup to its text box with an LV_EVENT_DELETE hook that clears
the pointer and hides the popup (hide only: in a layer clean the popup
may be deleted in the same pass). The cell handler also checks the text
box is still valid, and accentBoxHide no longer deletes a popup that
something else already removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:41:48 -04:00
Michael A. CojocariandClaude Opus 5.5 edf465ab19 fix(tdisplay-p4): Spectrum peak readout no longer overlaps the RBW/span line
The right-aligned peak readout shares the RBW/span line, and on the
narrow portrait panel it ran over that text. In portrait the RBW/span
line, chart and waterfall start one text line lower; the peak stays put
and the tall panel has the room. Landscape, the T-Deck Pro (peak on its
own line already) and the other boards are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 20:57:49 -04:00
Michael A. CojocariandClaude Opus 5.5 df18b3bf56 fix(tdisplay-p4): Send advert starts under the status bar; stacked buttons in portrait
The page offset its content by a second STATUSBAR_H to clear the tall
status bar's lower row, which overlaps the page on square panels. The
round panel's bar is already its full two rows and never doubles, so on
the P4 that only left an empty band above the Home button, in both
orientations. Start 8 px below the bar there instead.

In portrait, Flood and Zero-hop stack as two full-width buttons instead
of sharing a cramped row. Landscape and the other boards are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 20:57:49 -04:00
Michael A. CojocariandClaude Opus 5.5 f3ee043b5a fix(tdisplay-p4): contact action sheet is one column in portrait
The sheet's two-column grid of half-width buttons cut most labels off
with "..." on the narrow portrait panel. In portrait every action gets
its own full-width row; the card grows to fit and its body scrolls past
the screen height like any tall sheet. Delete stays the red bottom row.
Landscape and the other boards keep the grid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 20:57:49 -04:00
Michael A. CojocariandClaude Opus 5.5 d902cc8153 fix(tdisplay-p4): composer buttons one size, centred on the text box
The P4 is a large-screen board, so its quick-reply/emoji chips were
56 px against a 34x30 send button, all pinned to the composer's bottom
edge, where the landscape row sat under the panel's bottom corner arcs.

All three buttons are now 45 px (the average) in both orientations and
vertically centred on the text box, re-centred as it grows. That lifts
them ~9 px off the screen edge, with a floor matching the status bar's
top-corner clearance. In landscape the quick-reply button also moves
16 px in from the bottom-left corner. Replaces the 32 px the portrait
two-row change assumed from the small boards' sizes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 19:43:49 -04:00
Michael A. CojocariandClaude Opus 5.5 d217b33667 fix(ui): leaving an unedited Profile field no longer says it saved (#570)
Node name and advert location save on blur, and every blur saved and
showed "Name saved" / "Position saved", edited or not. On the M9 (no
touch) moving focus past a field with the d-pad is a blur, so the
alert flashed for each field crossed.

On blur, skip the save and the alert when the field still holds what
it was filled with: the name with missing glyphs replaced, the
position as "%.6f". An edit still saves and says so. This also stops a
bare blur writing the glyph-replaced display form over the real name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:22:39 -04:00
Michael A. CojocariandClaude Opus 5.5 d9794dc8bf feat(terminal): add and delete channels from the terminal
chan add <name> [key], chan add #tag, chan del <slot|name> and
chan key <slot|name>, in the on-device console and remote terminal.
They reuse the Channels screen's save and delete paths, so the chats
list updates at once. No rename: chat threads are matched to channels
by name, and the Channels screen has no rename for the same reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 12:17:49 -04:00
Michael A. CojocariandClaude Opus 5.5 e6b89ffc61 fix(ui): at-a-glance title and message centred, with a gap between them
The channel/sender line was pinned at y=30 and the message at y=60,
so a title taller than 30 px (larger UI-size presets, bigger panels)
was drawn over the message, and the rest of a tall screen stayed empty.
Centre both horizontally and centre the pair vertically with 16 px
between them; the message box takes the height its text needs, capped
to what fits and cut at a whole line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 12:17:49 -04:00
Michael A. CojocariandClaude Opus 5.5 594c6b12a3 fix(tdisplay-p4): contact names clear of their type icon in portrait
The name started a fixed 24 px after the icon, but the antenna and room
glyphs are wider than the person glyph and all three grow with the
UI-size preset, so some names touched their icon. Start the name 8 px
past the widest of the three, never closer than before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 12:17:49 -04:00
Michael A. CojocariandClaude Opus 5.5 85296b07a7 fix(tdisplay-p4): composer send button clear of the corner; two rows in portrait
The send button sat at the composer's bottom-right, under the panel's
rounded corner. Pull it in by the same 16 px safe-area the tab bar
uses, and narrow the text box to match.

Portrait: the single row (QR | emoji | text | send) was crunched on the
284-px panel. Split it into two rows: quick-reply and emoji
right-aligned on top, text and send below. All three buttons are 32 px,
midway between the old 30-px chips and the 34-px send button.
Landscape keeps the single row, and chatComposerApplyRows re-lays it
out when the keyboard's rotate button flips orientation with a chat
open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 12:17:49 -04:00
Michael A. CojocariandClaude Opus 5.5 94ac4beccf fix(lua): ui.input text box takes typing on keyboard boards (#571)
While a Lua app was open, every physical-keyboard key went to the app,
and luaAppKey always claims the key, including while the app's own
wada.ui.input prompt was open. The prompt's text box never received a
character on the T-Deck, Pager, M9 or Attaky, or from a Bluetooth
keyboard (reported with Wardrive's "Name this run").

Don't forward keys to the app while that prompt is open, in
handleHwKey and both bleKbdDispatch paths; they then type into the
bound text box like any other prompt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 12:17:49 -04:00
Claude e1f6c75091 docs(tdisplay-p4): full-reset restarts, landscape touch and the landscape Cmdr layout
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbY4FsGuX2fNDpNttB6a7R
2026-09-25 01:28:50 +00:00
Claude 6c6600add1 Merge remote-tracking branch 'origin/main' into fix/touch-ui-followups
# Conflicts:
#	variants/tdisplay_p4/target.cpp
2026-09-25 01:17:45 +00:00
Claude 81f5de8ef8 T-Display P4: nav bar taps land where they are made in landscape
In landscape the P4's tab bar barely responded to taps, while swipes between
tabs still worked.

P4 landscape is LVGL software rotation, and LVGL 8 rotates pointer input for a
rotated display itself (indev_pointer_proc): it expects points in the
unrotated panel frame. The touch driver was also mapping points to landscape
(heltecV4CapTouchSetPointRotation), on the belief that LVGL does not, so every
point was rotated twice. In ROT_90, the rotation the Orientation button picks,
a tap at (X, Y) reached LVGL at (615 - Y, X): a tap on the bar at the bottom
landed in the middle of the screen or below it, and never on a tab button.
Swipes were unaffected because the driver detects them in its own mapped
coordinates.

The driver keeps its landscape mapping, which its swipe detection and the
direct readers (tab-bar guard, remote-mode hold) need. lvglTouchRead now
converts the point back to the panel frame before giving it to LVGL, keyed on
the driver's rotation, so the chat keyboard's rotate trick on a portrait UI
(LVGL rotated, driver not) passes through unchanged. Also corrects the comment
in UITask::begin that said LVGL does not rotate input.

Not built or tested on hardware.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbY4FsGuX2fNDpNttB6a7R
2026-09-25 01:13:46 +00:00
Claude 14e78aeaa7 T-Display P4: lay out the landscape Cmdr screen like the Wio Tracker L2
In landscape the P4 home screen mixed its portrait column-flow layout with the
landscape right-hand button column: the TX/RX chart ran full width underneath
the buttons, and the info card was placed below the portrait launcher grid,
off the bottom of the 284-px-tall screen.

Landscape now follows the Wio Tracker L2's arrangement: status lines and a
chart sized to the remaining height on the left, clear of a right-hand column
of Advert / Terminal / Apps / Control spread evenly down the content height
(6-px gaps, each 22..46 px). The info card is portrait-only; its eight rows do
not fit landscape. The height is taken from the measured tab, since the
round-corner status bar leaves less than tabContentH() reports. Portrait is
unchanged.

Not built or tested on hardware.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbY4FsGuX2fNDpNttB6a7R
2026-09-25 01:09:31 +00:00
Claude 1501394fe5 T-Display P4: a settings reboot no longer leaves the screen off
After a setting that reboots (theme, rotation, language, remote mode...) the
P4 came back with the panel dark, and only a second reboot brought it back.

esp_restart() on the ESP32-P4 is a CPU reset: esp_system_reset_modules_on_exit()
resets DMA, UARTs, SDMMC and crypto, but not the MIPI-DSI host or its DPI path.
The display driver then initialises over a host still half-configured from the
previous session and the panel stays dark. A full chip reset always recovers it.

So every software restart now becomes a full system reset: a shutdown handler,
installed first thing in app_main so it runs last, arms the RTC watchdog with
RESET_SYSTEM. Being a shutdown handler it covers rebootDevice(), OTA, the
console and factory reset without touching any of them. Same fix as
camillia-mt's T-Display P4 port.

Those restarts now arrive as ESP_RST_WDT, so an RTC_NOINIT marker records that
the restart was deliberate, and the crash-report prompt and Settings -> About
read the reset reason through bootResetReason(), which reports ESP_RST_SW for
them. Otherwise a lingering coredump would re-prompt "restarted after a crash"
on every settings reboot.

Not built here (no ESP-IDF toolchain in this environment); needs a P4 build and
an on-device check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbY4FsGuX2fNDpNttB6a7R
2026-09-25 01:08:59 +00:00
Michael A. CojocariandClaude Opus 5.5 1c3576e1d6 fix(tdisplay-p4): GPS the camillia-mt way, 9600 start with baud probing
camillia-mt's working T-Display P4 GPS wakes the L76K through the XL9535
before opening UART1 on RX 22 / TX 23, starts at 9600 and probes other
rates until valid NMEA arrives. Wake and pins already match here; the
difference was the rate. The P4 used the core MicroNMEALocationProvider
locked to one rate (115200), so a receiver at any other rate gave an
open but silent UART.

Build the P4's provider as WadaNmeaLocationProvider with UART probing
(already on main via PR #517, used by the T-Deck Pro), starting at 9600:
it walks 9600/19200/38400/57600/115200 until a checksummed sentence
arrives and saves the working rate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:28:50 -04:00
Michael A. CojocariandClaude Opus 5.5 8f97db532c feat(tdisplay-p4): cold-boot clock sync over saved Wi-Fi
The "Sync time from saved Wi-Fi after cold boot" option (#383) existed
only on the T-Deck and ThinkNode M9. The P4's PCF8563 has come back with
impossible dates too, so enable it there: the Settings > Clock switches
(CAP_BOOT_TIME_SYNC), the runtime gate in BootTimeSync.cpp, and a call
in the P4's own startup at the same point the S3 main.cpp uses, before
the TCP/WS/BLE transports start.

Esp-hosted builds only: the sync drives Arduino's real WiFi, which the
legacy ESP-AT build must never touch. As elsewhere it is opt-in, runs
only after a true power-on, and skips when the RTC already gave a
trustworthy time or Wi-Fi is on anyway.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:28:50 -04:00
Michael A. CojocariandClaude Opus 5.5 e469993295 fix(ui): keep the screen-timeout slider inside the settings page
The track started 4 px in at 96% width, but the knob overhangs each end
by its 6 px padding plus half the track height, so at the lowest setting
it was drawn past the left edge. Inset the track by that overhang on both
sides of the content width.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:28:50 -04:00
Michael A. CojocariandClaude Opus 5.5 4aafab4b1c feat(web): paste text into the device from the browser (VNC / Remote UI)
The mirror page only forwarded key presses: its key handler skips
anything with Ctrl/Cmd held, so Ctrl+V / Cmd+V did nothing, and a paste
into the hidden input phones use was discarded. Catch the document
'paste' event and type the clipboard text into the device's focused
field, and add a Paste button (a prompt box) for phones, where a
plain-HTTP page cannot read the clipboard from script.

Pasted text goes out in batches of 16 keys every 80 ms so the device's
256-entry key ring is never overrun; line breaks become spaces so a
multi-line paste cannot press Enter and send a half-typed message;
characters outside the BMP are dropped (keys are 16-bit). Same page on
every board.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:28:50 -04:00
Michael A. CojocariandClaude Opus 5.5 005e4ff5a6 fix(tdisplay-p4): show the real web UI port (8765) on esp-hosted builds
WEB_UI_PORT_STR was 5000 on every P4 build, from the ESP-AT days when the
single AT listener shared the companion port. The esp-hosted build serves
the web UI on WS_PORT (8765) like the other boards, so the VNC, Remote,
Files, terminal and remote-placeholder addresses pointed at the wrong
port. Keep 5000 only for WADA_P4_LEGACY_AT builds; other boards were
already correct.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:28:50 -04:00
Michael A. CojocariandClaude Opus 5.5 36f96e0b7c fix(ui): smaller "Enable web access" label and address on the VNC page
The switch label drops from the 14 px to the 12 px body font and the
browser address from 16 px to 14 px, so both fit comfortably.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:28:50 -04:00
Michael A. CojocariandClaude Opus 5.5 0bff47286c fix(ui): Join private channel form sits just above the keyboard
The form hugged the header and left most of the tall page empty, so on
on-screen-keyboard boards start it where its Join button ends just above
the keyboard's top edge (keyboard height as kbApplyLayoutForRotation sets
it: CHAT_KB_H portrait, half the screen landscape). Physical-keyboard
boards, and screens without the spare room, keep the old layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:28:50 -04:00
Michael A. CojocariandClaude Opus 5.5 7e39b7c853 fix(ui): shorter Add channel options in a smaller font
"Create a private channel" / "Join a private channel" / "Join the public
channel" / "Join a hashtag channel" become "Create private channel",
"Join private channel", "Join Public channel" and "Join # channel", in
the body font a step below the title. The first two reuse existing
translated strings; the other two fall back to English until translated.
The labels were also wrapped in TR() twice; now translated once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:28:50 -04:00
Michael A. CojocariandClaude Opus 5.5 dfc44b9d11 fix(ui): fit the Power menu's Download mode label
"Download mode (wait for USB flash)" ran past its button. Use the smaller
font for that button and wrap it inside the button, centred on two lines.
The text (and its translations) is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:28:50 -04:00
Michael A. CojocariandClaude Opus 5.5 164c6d05d2 fix(backup): exported settings were invalid JSON
396e818 added rx_delay_base, buzzer_quiet, client_repeat, ble_pin and
gps_fuzz_m after the "region_scope": " opening quote had already been
printed, so they landed inside that string and every backup exported
since then failed to import ("bad/unreadable JSON"). Print region_scope
last, opened and closed in one place. Backups made with the broken build
must be exported again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:28:50 -04:00
Michael A. CojocariandClaude Opus 5.5 5cdbf96cff fix(ui): smaller text and more room above stacked confirm buttons
In the stacked (portrait) confirmation dialog, e.g. the backup Import
prompt, the multi-line message sat hard against the buttons. Use the
smaller body font there and widen the message-to-buttons gap; the card
height follows. Landscape dialogs are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:28:50 -04:00
Michael A. CojocariandClaude Opus 5.5 1467184d79 fix(ui): tighter Cmdr info panel; round-panel breadcrumb and status row
Cmdr: the info panel's values column started at a fixed SC(100), leaving
a wide gap after short labels like "Node". Size it from the labels' real
width and draw a thin divider between the two columns.

Round-cornered status bar (T-Display P4):
- The settings/tool breadcrumb moves from row 1 to row 2 so its top is
  level with the battery readout, at row 2's text size instead of the
  tall bar's larger one, and is capped (with an ellipsis) short of the
  status icons it now shares the row with.
- The signal/SD/Wi-Fi/BLE icons start after the widest the battery
  percentage can get ("100%" in its font) instead of a fixed 64 px, so
  they no longer cover it; the charging close-up keeps its old position.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:28:50 -04:00
Michael A. CojocariandClaude Opus 5.5 237a33e68e feat(backup): include screen and app preferences in settings backups
The backup carried node settings but none of the touch UI's own
preferences. Add an "app_settings" block: the active Wi-Fi connection
(SSID, password, radio/BLE switches) and every touch preference key as
["key","type","value"] triples: the packed cfg blob (display, theme,
keyboard, map, UI scale, notifications, advert timers...), sound and
DND settings, tile server, lock wallpaper, notification sounds, quick
replies, channel scopes, favourites, ignore lists, channel mute and
emoji, repeater passwords, the last radio preset and saved Wi-Fi
networks.

Import restores them as section bit 0x20 (the UI's Import now applies
it). Only keys and types this build writes itself are accepted. The cfg
blob is merged field by field and keeps what belongs to the unit:
battery calibration, GPS baud, clock floor, FEM/antenna, the paired BLE
keyboard, remote/console boot modes and beta-report counters. Storage
choice, the setup flag and the discovered-nodes cache are not exported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:28:50 -04:00
Michael A. CojocariandClaude Opus 5.5 b00deeffd3 fix(ui,backup): 2x2 location-privacy buttons; export the remaining settings
GPS screen: the four location-privacy accuracy buttons sat in one row and
were too narrow for their labels. Lay them out two columns by two rows.

Backup export: the wadamesh_settings block now also carries the
location-privacy radius, BLE pairing PIN, RX delay, buzzer quiet and
client repeat, and import restores them with range checks (the radius
only to the values the UI offers, the PIN to six digits). Every other
NodePrefs field was already exported; identity, radio, position,
channels and contacts are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:28:50 -04:00
Michael A. CojocariandClaude Opus 5.5 b60c26ac85 feat(tdisplay-p4): esp-hosted C6 backend for Wi-Fi
Current T-Display P4 V1 units ship the C6 with esp-hosted firmware
(2.12.3), and main's P4 build only spoke ESP-AT, so Wi-Fi could not come
up on them at all. This is a one-time import of the esp-hosted backend

- TDP4_C6_HOSTED build switch (default hosted; WADA_P4_LEGACY_AT=1 keeps
  the ESP-AT path for older units), with the ESP-AT socket/WiFi facades
  and the single-listener TCP router guarded to the legacy build.
- tdisplay_p4: build.sh hosted patches and fullclean handling,
  idf_component.yml esp-hosted pin, sdkconfig, main.cpp hosted bring-up
  and SNTP, c6_at updates, and the C6 reset hook in target.cpp.
- P4 Wi-Fi scan wait in UITask.
- TDISPLAY_P4_C6_FIRMWARE_PLAN.md.

Not imported from that commit: UI-scale default, GPS log lines, UI

build-upload-monitor.sh: run build.sh's own fullclean for the P4 (idf.py
fullclean rejects the patched managed components).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:28:50 -04:00
Michael A. CojocariandClaude Opus 5.5 4d86f66c39 fix(tdisplay-p4): draw the Remote UI placeholder on the physical panel
In Remote UI mode the UI renders off-screen for the browser and the
physical panel shows a status placeholder drawn through the
DisplayDriver API. On the P4 every fill/text call was an empty stub, and
writePixelsRGB565 takes half-resolution LVGL coordinates and upscales
2x, so only the logo appeared, twice as big and pushed towards the top
right, over whatever was on screen before.

Add P4PanelPainter: native-resolution fills, rectangles and text for
both P4 panels (RM69A10 AMOLED, HI8561 LCD), rasterising LVGL's
Montserrat fonts at the UI's 2x scale, written through a new
non-upscaling writeNativeRGB565. Give the placeholder a layout for the
tall portrait panel: logo, title, browser address or Wi-Fi status, and
the touch-and-hold exit hint, matching the other builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:28:04 -04:00
Michael A. CojocariandClaude Opus 5.5 b932c13e34 fix(ui): stack confirm-dialog buttons in portrait
The shared confirmation dialog placed a fixed-width Cancel and OK side by
side, and on portrait panels they overran each other (reported on the
backup Import prompt). In portrait, or whenever the card is too narrow
for both, stack them full width with OK above Cancel, and grow the card
to fit. Landscape layouts are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:28:04 -04:00
Michael A. CojocariandClaude Opus 5.5 485436daa2 fix(ui): centre the onboarding welcome message and buttons
The first setup step pinned "Let's set up your device" under the title and
its Skip / Get Started buttons to the bottom edge, leaving a large empty
gap on tall panels like the T-Display P4. Lay them out as one block in the
middle of the screen with centred text, clamped so it never overlaps the
title or drops below the old bottom-edge position on short panels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:28:04 -04:00
Michael A. CojocariandClaude Opus 5.5 ed9a49c506 scripts: T-Display P4 targets in build-upload-monitor and build-all
build-upload-monitor.sh gains --tdisplay-p4 (AMOLED) and
--tdisplay-p4-lcd (HI8561 LCD SKU). The P4 is an ESP-IDF app, so these
drive tdisplay_p4/build.sh (build/flash/monitor, --erase, --fullclean,
PORT= for the serial port) instead of pio; both appear in the interactive
menu and the --just-build sweep, which skips them when ESP-IDF is absent.
PlatformIO is only required when a PlatformIO board is chosen.

build-all-targets.sh also builds the LCD SKU. Both SKUs share
build/tdisplay_p4 and WADA_P4_LCD is read only at configure time, so each
P4 build reconfigures first; before this, the AMOLED entry rebuilt
whichever panel had been configured last.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:28:04 -04:00
Claude d2ee8cea5f fix(ui): Mentions breadcrumb on the left like every other screen
The Mentions screen drew its own header with the back chevron at the top
right, the only screen whose back breadcrumb was on the right. Use the
shared app-page chrome instead (appPageBegin/appPageEnd, as Send advert,
Spectrum and the settings pages do): "‹ @ Mentions" on the left of the
status bar, tap to go back. The list starts below the tall bar on square
panels. Keeps the existing translated "@  Mentions" key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBQtHzNKA9Q55ogrULN6JJ
2026-09-24 15:28:04 -04:00
Claude fc631be52c fix(tdisplay-p4): GPS baud, nav bar, map chrome, presets, export
- GPS: the P4 listened at 9600 (the L76K power-on default), but LilyGo's
  factory firmware leaves the receiver at 115200, so the UART opened and
  never parsed a sentence. Set GPS_BAUD_RATE=115200 for the P4. A GPS baud
  saved in Settings still overrides it.
- Nav bar: the active-tab bar assumed a full-width tab row. The P4's round
  corners inset the row by SB_INSET_X on each side, leaving the bar ~13 px
  off under the outer tabs. Measure the real button row instead; move the
  unread and update badges in by the same inset.
- Map: with no tiles rendered the canvas shows the COLOR_FIELD
  placeholder, but the chrome followed only the night setting (except on
  the Wio Tracker L2). Pick the chrome colour from the placeholder's
  brightness on every board, and refresh it when tiles appear or vanish.
- Radio & mesh: picking a community preset only filled the fields and a
  dropdown never blurs a field, so nothing was saved; leaving the screen
  lost it. Apply on pick. Also remember the picked preset by label, since
  several presets share identical parameters and the first match
  (e.g. EU/UK instead of Switzerland) was shown on return.
- Export: add a wadamesh_settings block with airtime factor, region
  scope, auto-add rules, advert/telemetry policy, multi-ACKs, GPS and
  RX boosted gain, and restore it on import. The stock-app fields keep
  their exact shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:28:04 -04:00
Michael A. Cojocari 2fde038f08 fix(m9): streamline mark-all-read action
Closes #569\n\nCo-Authored-By: GitHub Copilot <noreply@github.com>
2026-09-24 15:16:09 -04:00
Michael A. Cojocari a3090aebbe fix(tdeck-pro): split v1.0 and v1.1 targets 2026-09-23 16:43:29 -04:00
sam cross 23bd6ee571 fix: ignore trackball pan on map when screen is locked or off
The trackball map-panning block in updateTrackball() ran without
checking the lock state, so rolling the trackball while the screen
was locked (or idle-off) moved the underlying map. On unlock the
map would be in a different location. Bail out early when locked,
committing any pending pan first so it doesn't jump on unlock.
Fixes #132.
2026-09-23 17:33:51 +01:00
Michael A. Cojocari 98dd4dadea fix(m9): preserve locked drawer through shortcuts
Keep the app-drawer Home root open beneath Messages, Map, Mentions, and Advert function-key navigation while still dismissing overlays and respecting blocker dialogs.

Fixes #564
2026-09-23 10:34:28 -04:00
Michael A. Cojocari a8067c921b fix(tdeck-pro): separate compact chat messages
Measure the same vertical padding that compact rows render and increase the blank gap between separate messages without changing wrapped-line spacing.

Fixes #563
2026-09-23 10:23:59 -04:00
Michael A. Cojocari 31a6a9798e fix(tdeck-pro): make console readable on e-paper
Use white paper and black ink from the first boot frame, commit sprite-backed input updates, and keep the e-paper cursor solid.

Fixes #562
2026-09-23 10:23:45 -04:00
Kaj SchittecatandClaude Opus 5 c9ecaf383f reports: say how long they ran it, not just a span
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>
2026-09-23 15:58:23 +02:00
461 changed files with 237711 additions and 25234 deletions

No files matched your search

+3
View File
@@ -46,3 +46,6 @@ CLAUDE.md
# device's AppFS partition and is both large and device-specific.
tanmatsu/tools/appfs-src/
tanmatsu/tools/dev_appfs.bin
# Generated by scripts/build/gen-changelog.py at every site deploy
deploy/site/changelog.html
src/ui-touch/doc_radio_apps.h
+88 -8
View File
@@ -14,10 +14,13 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases
| Heltec V4 + TFT | ESP32-S3, SX1262 | 2.4" 240x320 touch (CHSC6x) | Web flasher | Stable | Fully supported; Expansion Kit sensors, V4.3 high-gain RX toggle |
| Tanmatsu | ESP32-P4 + ESP32-C6, SX1262 | 4" 800x480, 69-key keyboard (no touch) | Tanmatsu app store on the device (runs under the badge.team launcher, not web-flashable) | Store tracks the test channel | Fully supported; LoRa + Wi-Fi + Bluetooth simultaneously, standalone and companion in one |
| Elecrow ThinkNode M9 | ESP32-S3, LR1110 | 2.4" 240x320 (no touch), I2C QWERTY + d-pad | Web flasher | Beta (new in beta_38) | Hardware-complete community port by ded (#138): GPS, microSD, buzzer, lock screen, d-pad navigation |
| Elecrow CrowPanel Advance 3.5 | ESP32-S3, external SX1262 | 3.5" ILI9488 480x320 touch (GT911) | Web flasher (beta) | Beta, hardware validation pending | Requires an SX1262 module in the LoRa expansion slot; not for other CrowPanel models |
| Elecrow CrowPanel Advance 5.0 | ESP32-S3, external SX1262 | 5" ST7262 RGB 800x480 touch (GT911) | Web flasher (beta) | Beta, hardware-tested (display, touch, radio) | V1.1+ boards; SX1262 in the wireless slot with the function DIP switch on the wireless module; no SD, GPS or battery gauge |
| RAK WisMesh Tap V2 (RAK3312) | ESP32-S3, SX1262 | Touch display (LovyanGFX, 30+ fps) | Web flasher | Beta (new in beta_38) | Early community port by Ethac.chen (#136); core mesh, chat and map working |
| LilyGo T-Lora Pager | ESP32-S3, LR1121 or SX1262 | 2.33" IPS LCD, 480x222 (no touch), QWERTY + rotary encoder | Web flasher (pick the build matching your radio) | Beta (new in beta_45) | Fully supported; keyboard-first navigation, microSD, map tile packs |
| Heltec V4-R8 + Expansion Kit V2 | ESP32-S3 (8 MB octal PSRAM), SX1262 | 2.4" touch (CHSC6x) | Web flasher | Beta (new in beta_45) | As the V4 plus microSD and the Expansion Kit sensors; buzzer supported |
| LilyGo T-Display P4 | ESP32-P4 + ESP32-C6, SX1262 | AMOLED or TFT-LCD, touch | Web flasher (AMOLED) or the .bin for the LCD SKU | Beta (new in beta_45) | Two screen SKUs; fuel-gauge battery reporting, full TX power |
| LilyGo T-Display P4 | ESP32-P4 + ESP32-C6, SX1262 or LR2021 | AMOLED or TFT-LCD, touch | Web flasher | Beta (new in beta_45) | Four panel/radio builds; AMOLED + SX1262 hardware-tested, the other combinations need validation |
| Elecrow CrowPanel Advanced 10.1 | ESP32-P4 + ESP32-C6, external SX1262 | 10.1" EK79007 MIPI-DSI 1024x600 touch (GT911) | Web flasher (beta) | Experimental (new in beta_97), not yet tested on hardware | One image per board revision (V1.0, V1.1, V1.2); SX1262 in the wireless slot; no battery reading, sound or GPS |
| Attaky Mesh Series | ESP32-S3, SX1262 | Touch display + front D-pad | Web flasher | Beta (new in beta_47) | Community port by attakygit (#158/#169); detachable keyboard supported; the front D-pad + SELECT navigate the UI |
## Feature notes per board
@@ -33,12 +36,14 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases
this needs [LilyGO keyboard-controller firmware with raw matrix mode](https://github.com/Xinyuan-LilyGO/T-Deck/tree/master/examples/Keyboard_ESP32C3)
(June 2025 or newer). Older controller firmware keeps normal typing and
reports the unavailable latch mode on Serial.
- **T-Deck Pro**: dedicated `LilyGo_TDeck_Pro_companion_radio_touch` development
target for the GDEQ031T10 e-paper model. It uses a monochrome shadow buffer,
coalesced partial refreshes, CST328/CST3530 touch detection and the Pro-specific
TCA8418 matrix. Keyboard events are drained during the panel BUSY interval so
a refresh cannot overflow the controller FIFO. Hardware validation is pending;
see [variants/lilygo_tdeck_pro/PORT.md](variants/lilygo_tdeck_pro/PORT.md).
- **T-Deck Pro**: separate `LilyGo_TDeck_Pro_v1_0_companion_radio_touch` and
`LilyGo_TDeck_Pro_v1_1_companion_radio_touch` targets for the incompatible PCB
revisions. V1.0 has no frontlight and uses GPIO45 for touch reset; V1.1 uses
GPIO45 for its frontlight and GPIO38 for touch reset. Flashing the V1.1 image
onto V1.0 holds touch in reset. Both use the GDEQ031T10 e-paper panel,
CST328/CST3530 touch detection, and the Pro-specific TCA8418 matrix. See
[variants/lilygo_tdeck_pro/PORT.md](variants/lilygo_tdeck_pro/PORT.md) before
choosing a target.
- **T-Deck Max**: `LilyGo_TDeck_Max_companion_radio_touch`. The same e-paper,
keyboard and radio as the Pro, whose display and touch drivers it reuses; the
differences are an XL9555 expander in front of the peripheral power enables and
@@ -79,12 +84,87 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases
validated at boot; if it reports lost integrity, the same optional saved-Wi-Fi
cold-boot sync offered on T-Deck is available in Clock settings. New in beta_38;
report anything that feels off.
- **T-Display P4**: touch UI with the on-screen keyboard. The BOOT button wakes
the screen (or lights the lock screen) and holding it for two seconds locks or
unlocks, as the Wio Tracker L2's wake button does; "Lock when screen off" is
under Settings, Lock screen. The optional clip-on
keyboard expansion can be attached or removed while running and is picked up
within a couple of seconds; while it is on, it types into the focused field
and the on-screen keyboard stays down (tap the field again to bring it up).
Arrows move the caret or the focus highlight, Esc goes back, F1-F5 open the
tabs left to right, F11 steps the keyboard light through off, low, medium and
high (the level is kept across reboots), Caps Lock lights its LEDs,
and Shift, Sym and Alt apply to the next key. Alt+Backspace goes back.
- **RAK WisMesh Tap V2**: newest port, touch-driven. The browser-flash path is
fresh; if the flasher cannot open the serial port, put the board in download
mode manually and retry, and please report it.
- **Seeed Wio Tracker L2**: pre-release touch target under active bring-up. Builds are for
development and hardware validation only; no public release artifact is
promised until the port is verified.
promised until the port is verified. It has no battery-backed clock, so the
same optional saved-Wi-Fi cold-boot time sync offered on T-Deck is available
in Clock settings.
- **CrowPanel Advance 3.5**: `crowpanel_35_companion_radio_touch` is a
16 MB flash / 8 MB OPI PSRAM build for the ILI9488/GT911 panel and an SX1262
in the expansion slot. It starts upright in 480x320 landscape; Display
settings can switch to 320x480 portrait, rotated 180 degrees. The selected
orientation persists across reboots; landscape is only the default when no
saved choice exists.
It uses the same compact font and control sizing as Wio Tracker L2.
Fit the radio before booting. GPIO0 is shared by BOOT and the radio's chip
select, so BOOT is not configured as a wake/unlock input on stock wiring.
Screen-off remains touch-wakeable; hard lock is disabled without a separate
unlock button. See the
[official schematic](https://github.com/Elecrow-RD/CrowPanel-Advance-HMI-ESP32-AI-Display/blob/master/3.5/schematic/ESP32%20Display%203.5%20inch%20V1.0.sch)
and [reference radio pin map](https://github.com/meshtastic/firmware/blob/master/variants/esp32s3/elecrow_panel/variant.h).
An optional external GPS can be connected to UART1 (CrowPanel RX GPIO18 /
TX GPIO17, 9600 baud). Data
can use a FAT32 microSD card for data, offline maps, File Manager and browser
Transfer. The card uses software SPI (SCLK 5, MISO 4, MOSI 6, CS 7), leaving
the display and radio SPI hosts untouched; missing or unreadable cards fall
back to SPIFFS. The dedicated LittleFS partition remains the internal tile
cache. Cards are never auto-formatted; format them on a computer. Serial
uses the board's USB-to-UART bridge. Battery voltage monitoring is not
supported. Display, touch, radio and SD still need validation on a physical
CrowPanel. Like Wio Tracker L2, it is included in the all-target builds and local beta release
scripts, not the two-board stable CI workflow. Select it in the upload
helper with `scripts/build-upload-monitor.sh --crowpanel-35`.
- **CrowPanel Advance 5.0**: `crowpanel_50_companion_radio_touch` is a
16 MB flash / 8 MB OPI PSRAM build for the 800x480 ST7262 RGB panel, the
GT911 touch controller and an SX1262 in the wireless-module slot (J8/J9). The
board quirks follow Camillia-MT's port:
- The panel is driven over the 16-bit RGB bus from a PSRAM framebuffer at a
14 MHz pixel clock (Elecrow's demo uses 21 MHz); the slower clock leaves
PSRAM bandwidth for WiFi and LVGL so the picture does not drift or tear.
LVGL's draw band stays in internal RAM for the same reason.
- Flash writes (saving chats and settings) take the bus that the panel's DMA
shares with PSRAM, which made the picture jump sideways. The firmware stops
the LCD clock for each flash write or erase, so the screen briefly freezes
or flickers instead.
- Backlight and touch reset sit behind an I2C controller on GPIO15/16 that
changed between revisions: a PCA9557 at 0x18 on V1.0, a board MCU at 0x30
from V1.1. The driver detects which one answers, resets the GT911 through
it and accepts the touch controller at either 0x5D or 0x14. Below full
brightness the MCU dims by PWM, which can whine; 100% is steady.
- The function DIP switch on the back routes GPIO 2/4/5/6/19/20 to the
wireless slot, the microSD slot or the speaker/microphone. Set it to the
wireless module (S1=0, S0=1), or the radio is not found (the serial log
says so). The microSD slot is therefore unusable; browser Transfer uses the
internal tiles partition instead, as on the Attaky.
- The radio's chip select is GPIO8 on V1.2/V1.3 boards (current production)
and GPIO0 on V1.0. The build targets GPIO8; a V1.0 board is detected at boot
and logged, and needs `P_LORA_NSS=0`.
- No GPS (no free GPIO pair for a UART; 17/18 are panel data), no battery
voltage sensing and no free user button, so hard lock is disabled and the
screen wakes by touch.
It starts in native 800x480 landscape; Display settings can switch to
480x800 portrait. It uses the large-screen layouts and UI-size presets of the
Tanmatsu. Serial uses the board's CH340 USB-to-UART bridge. Tested on a physical
unit: a steady picture with smooth gradients, responsive typing with key
highlighting, the chat header and composer, and sending and receiving messages
over the radio (flash writes show as a brief freeze or flicker), and a fresh
full install and an on-device update, whose long flash erases the panel
recovers from. Select it in
the upload helper with `scripts/build-upload-monitor.sh --crowpanel-50`.
## Requested boards
+3
View File
@@ -53,6 +53,9 @@ We do NOT bind LVGL wholesale. We bind a stable, versioned surface we own
trackball, gps), `version()`, `toast(msg)`, `tr(s)` (the firmware's own
translation table, so an app is not stuck in English).
- `wada.timer` — `every(ms)` drives `on_tick` cadence (min 33 ms).
- `wada.widget` (extended SDK) — `set{title, value, line, bar, spark}` publishes the app's
Home widget (at most once a second), `clear()` empties it. The last one is kept after the
app closes and written to `<root>/apps/<id>.wgt` at teardown; nothing runs headless.
Sandbox env (no `io`, `os`, `require`, `dofile`, `load` of new chunks;
whitelisted stdlib: `math`, `string`, `table`, `pairs/ipairs/select/pcall/
+13
View File
@@ -91,5 +91,18 @@ driver, variants/tdisplay_p4/P4Audio.cpp)
LovyanGFX (RAK WisMesh Tap V2 display backend)
Copyright (c) lovyan03 — FreeBSD License — https://github.com/lovyan03/LovyanGFX
Camillia-MT (CrowPanel Advance 3.5 software-SPI microSD backend)
Copyright (c) 2025-2026 Michael A. Cojocari and contributors
GNU General Public License v3.0 or later
Adapted from src/sd_soft_spi.cpp; integrated with WadaMesh's filesystem
API and concurrent storage consumers.
Camillia-MT (CrowPanel Advance 5.0 display, backlight and touch bring-up)
Copyright (c) 2025-2026 Michael A. Cojocari and contributors
GNU General Public License v3.0 or later
The RGB panel configuration, revision detection, backlight controller and
GT911 reset in variants/crowpanel_50/CrowPanel50Display.cpp are adapted
from src/hal/crowpanel_50_display.h and src/hal/hw_crowpanel_50.h.
This list is not exhaustive and is expanded as dependencies are vendored or
added.
+22 -17
View File
@@ -8,10 +8,10 @@
<p align="center"><b>A real touchscreen UI for your mesh radio.</b> &middot; open source &middot; GPL-3.0</p>
Touch-UI [MeshCore](https://github.com/meshcore-dev/MeshCore) companion-radio
firmware for the **LilyGo T-Deck / T-Deck Plus**, **Heltec V4 + TFT** and eight other boards
firmware for the **LilyGo T-Deck / T-Deck Plus**, **Heltec V4 + TFT** and other boards
(ESP32-S3).
An LVGL touch UI — map, chat, contacts, channels, settings — split out of
An LVGL touch UI (map, chat, contacts, channels, settings) split out of
[meshcomod](https://github.com/ALLFATHER-BV/meshcomod). The app depends on a
MeshCore fork via PlatformIO `lib_deps`.
@@ -20,21 +20,24 @@ MeshCore fork via PlatformIO `lib_deps`.
See **[DEVICES.md](DEVICES.md)** for the full support matrix, install paths and
per-board status.
- LilyGo T-Deck / T-Deck Plus — env `LilyGo_TDeck_companion_radio_touch` (stable)
- Heltec V4 + TFT + CHSC6x touch — env `heltec_v4_tft_companion_radio_usb_tcp_touch` (stable)
- Tanmatsu (ESP32-P4) — built from `tanmatsu/` (ESP-IDF), ships via the Tanmatsu app store — [sideload guide](TANMATSU_SIDELOAD.md) for running your own build
- Elecrow ThinkNode M9 — env `ThinkNode_M9_companion_radio_touch` (beta) — [keyboard & d-pad guide](THINKNODE_M9_SHORTCUTS.md)
- RAK WisMesh Tap V2 (RAK3312) — env `rak_tap_v2_companion_radio_touch` (beta)
- LilyGo T-Lora Pager — envs `tlora_pager_lr1121_companion_radio_touch` / `tlora_pager_sx1262_companion_radio_touch` (beta) — [keyboard shortcuts](TLORA_PAGER_SHORTCUTS.md)
- Heltec V4-R8 + Expansion Kit V2 — env `heltec_v4_r8_tft_companion_radio_usb_tcp_touch` (beta)
- LilyGo T-Display P4 — built from `tdisplay_p4/` (ESP-IDF); AMOLED by default, `WADA_P4_LCD=1` for the TFT-LCD SKU (beta)
- Attaky Mesh Series — env `attaky_mesh_series_companion_radio_touch` (beta)
- LilyGo T-Deck / T-Deck Plus: env `LilyGo_TDeck_companion_radio_touch` (stable)
- Heltec V4 + TFT + CHSC6x touch: env `heltec_v4_tft_companion_radio_usb_tcp_touch` (stable)
- Tanmatsu (ESP32-P4): built from `tanmatsu/` (ESP-IDF), ships via the Tanmatsu app store; [sideload guide](TANMATSU_SIDELOAD.md) for running your own build
- Elecrow ThinkNode M9: env `ThinkNode_M9_companion_radio_touch` (beta); [keyboard & d-pad guide](THINKNODE_M9_SHORTCUTS.md)
- RAK WisMesh Tap V2 (RAK3312): env `rak_tap_v2_companion_radio_touch` (beta)
- LilyGo T-Lora Pager: envs `tlora_pager_lr1121_companion_radio_touch` / `tlora_pager_sx1262_companion_radio_touch` (beta); [keyboard shortcuts](TLORA_PAGER_SHORTCUTS.md)
- Heltec V4-R8 + Expansion Kit V2: env `heltec_v4_r8_tft_companion_radio_usb_tcp_touch` (beta)
- LilyGo T-Display P4: built from `tdisplay_p4/` (ESP-IDF); AMOLED + SX1262 by default, with `WADA_P4_LCD=1` for TFT-LCD and `WADA_P4_LR2021=1` for LR2021 (beta)
- Elecrow CrowPanel Advanced 10.1 (ESP32-P4): built from `crowpanel_p4_101/` (ESP-IDF), one image per board revision with `WADA_CP_REV=1.0|1.1|1.2` (experimental, not yet tested on hardware; requires an SX1262 in the wireless slot)
- Attaky Mesh Series: env `attaky_mesh_series_companion_radio_touch` (beta)
- Elecrow CrowPanel Advance 3.5: env `crowpanel_35_companion_radio_touch` (beta, hardware validation pending; requires an SX1262 in the expansion slot)
- Elecrow CrowPanel Advance 5.0: env `crowpanel_50_companion_radio_touch` (beta, hardware validation pending; requires an SX1262 in the wireless slot and the DIP switch set to it)
## Architecture
This repo holds only the **app**: the `companion_radio` glue, the `ui-touch`
LVGL UI, each board's glue/variants, and `platformio.ini`. The **MeshCore
core is not vendored here** — it's pulled as a library via `lib_deps` from the
core is not vendored here**: it's pulled as a library via `lib_deps` from the
[`ALLFATHER-BV/meshcomod`](https://github.com/ALLFATHER-BV/meshcomod) monorepo
(the same repo as the non-touch firmware), pinned by a lean source-only `core-*`
git tag. The touch-app files this repo owns (TouchPrefsStore, WifiRuntimeStore,
@@ -98,28 +101,30 @@ automatically:
```bash
pio run -e heltec_v4_tft_companion_radio_usb_tcp_touch # Heltec V4 TFT
pio run -e LilyGo_TDeck_companion_radio_touch # LilyGo T-Deck
# or just `pio run` to build both
pio run -e crowpanel_35_companion_radio_touch # CrowPanel Advance 3.5
pio run -e crowpanel_50_companion_radio_touch # CrowPanel Advance 5.0
# or just `pio run` to build the configured default environments
```
Flash with the NVS-preserving 4-component chain (bootloader / partitions /
boot_app0 / firmware at `0x0 / 0x8000 / 0xe000 / 0x10000`) so saved Wi-Fi
credentials survive — not a merged image, which 0xFF-pads and wipes NVS.
credentials survive, not a merged image, which 0xFF-pads and wipes NVS.
## Contributing
Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). One topic per
Contributions are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md). One topic per
PR; inbound contributions are accepted under the project's GPL-3.0 license.
## License
**GPL-3.0-or-later** — see [LICENSE](LICENSE). wadamesh is copyleft: anyone who
**GPL-3.0-or-later**: see [LICENSE](LICENSE). wadamesh is copyleft: anyone who
distributes a build or a fork must also make their source available under the GPL.
This keeps the UI open and concentrates community effort instead of fragmenting it
into closed forks.
wadamesh incorporates and depends on
[MeshCore](https://github.com/meshcore-dev/MeshCore) (MIT, © Scott Powell /
rippleradios.com) and other third-party components — see [NOTICE](NOTICE) for the
rippleradios.com) and other third-party components; see [NOTICE](NOTICE) for the
full list and their licenses. MeshCore-derived files keep their MIT notices; the
combined work is distributed under the GPL (MIT is GPL-compatible). The MeshCore
fork that wadamesh builds against stays **MIT** on purpose, so its Wi-Fi/BLE hooks
+11
View File
@@ -3,6 +3,17 @@
These post-flash checks verify the setup-wizard keyboard fallback and CST3530
touch fix on a LilyGo T-Deck Pro 4G.
## Choose the hardware revision
Flash `LilyGo_TDeck_Pro_v1_0_companion_radio_touch` on V1.0 hardware and
`LilyGo_TDeck_Pro_v1_1_companion_radio_touch` on V1.1 hardware. Do not use the
V1.1 image on V1.0: GPIO45 is touch reset on V1.0 but frontlight PWM on V1.1,
so the wrong image can hold the touch controller in reset. V1.0 has no
frontlight; V1.1 identifies a DRV2605 at I2C address `0x5A`.
Run the checks below separately on each available PCB revision and include the
revision and firmware target in the report.
## Boot check
With a 115200-baud serial monitor attached, reboot the device and record this
+412
View File
@@ -0,0 +1,412 @@
# T-Display P4 C6 firmware and Bluetooth plan
## Decision
Use Espressif esp-hosted as the normal WadaMesh backend for current T-Display P4
V1 units, which ship C6 hosted firmware 2.12.3. Retain the existing ESP-AT
backend only for older units carrying LilyGo's now-deprecated AT images.
Older AT units need a guided two-stage conversion because the P4 can reset the
C6 but cannot press its documented physical BOOT control. Current hosted units
need no C6 reflash. After any legacy conversion, C6 firmware updates should be
delivered and verified by the P4 over SDIO so normal WadaMesh updates remain a
one-device operation.
## Goals
- Expose the C6 Bluetooth LE radio to WadaMesh.
- Run the existing MeshCore Bluetooth companion transport without an AT-to-BLE
protocol translation layer.
- Preserve P4 Wi-Fi scanning, station mode, TCP clients, the companion TCP
server, HTTP, SNTP, and simultaneous Wi-Fi/BLE operation.
- Make the one-time C6 conversion understandable from the WadaMesh installer.
- Make every later P4 installation or update automatically enforce a compatible
C6 firmware version.
- Provide A/B update, verification, rollback, and a wired recovery route for the
C6.
- Keep the P4 usable for LoRa and local UI if the C6 is absent or broken.
## Non-goals
- Classic Bluetooth. The ESP32-C6 supports Bluetooth LE only.
- Silently rewriting the factory C6 through undocumented raw flash commands.
- Depending on deprecated factory ESP-AT images for long-term update support.
- Removing the existing AT implementation before the hosted replacement has
equivalent Wi-Fi and socket coverage.
## Current architecture
### Processors
- ESP32-P4: runs WadaMesh, LVGL, LoRa, GPS, storage, and the companion logic.
- ESP32-C6: owns the physical 2.4 GHz Wi-Fi and Bluetooth LE radio.
The P4 itself has no Wi-Fi or Bluetooth radio.
### Current C6 connection
The C6 is connected to the P4 over SDIO slot 1:
| Signal | P4 GPIO |
|---|---:|
| CLK | 18 |
| CMD | 19 |
| D0 | 14 |
| D1 | 15 |
| D2 | 16 |
| D3 | 17 |
C6 enable/reset is `XL9535 IO14`. The P4 can pulse this line but does not have a
connection to the C6 IO9 boot strap.
### Current software generations
- Current V1 factory/release firmware uses esp-hosted MCU 2.12.3. WadaMesh uses
the matching local host fork (`2.12.3~1`) and standard remote Wi-Fi/NimBLE
paths.
- Older V1 units may carry LilyGo's deprecated ESP-AT SDIO firmware.
- `tdisplay_p4/components/c6_at/` and `C6WifiShim.h` remain as an explicit
`WADA_P4_LEGACY_AT=1` compatibility backend for those units.
- Legacy AT Wi-Fi scans, joins, sockets, server mode, and SNTP are implemented
in that worker. Its BLE commands cannot reliably expose the MeshCore service.
- `AT+CIUPDATE` and `AT+USEROTA` are unavailable on the tested legacy image.
- `C6_FLASH_HELPER=1 ./tdisplay_p4/build.sh build` creates a P4 image that does
not reset or start the C6. It is useful for development while the C6 is placed
in ROM download mode and flashed through its own USB connection.
### P4 flash layout
The current 16 MB P4 layout contains:
- 4 MB `ota_0`
- 4 MB `ota_1`
- approximately 8 MB FAT `storage`
- NVS, OTA metadata, PHY data, and coredump partitions
The P4 application currently has less than 1 MB free in each app slot. A full C6
image should therefore not be linked into the P4 application. If offline C6
updates are required, reserve a dedicated data partition by reducing the FAT
partition after measuring the final C6 image size.
## Target architecture
### C6 firmware: `wadamesh-c6`
Create a separately versioned ESP-IDF project for the ESP32-C6. Start from the
same esp-hosted slave generation already proven by the Tanmatsu target.
Required C6 features:
- esp-hosted SDIO slave transport
- Wi-Fi station and scan support
- Bluetooth LE HCI transport for NimBLE on the P4
- a small management endpoint for firmware identity and updates
- A/B application partitions with rollback
- signed-image verification
- watchdog and boot-health reporting
- a stable protocol version independent of the human firmware version
The C6 firmware should not implement the MeshCore GATT service itself. It should
transport Bluetooth HCI to the P4, allowing the existing P4-side NimBLE and
MeshCore `SerialBLEInterface` code to own advertising, pairing, GATT, and packet
routing. This keeps one MeshCore BLE implementation across boards.
### Management protocol
Add a narrow WadaMesh management channel alongside the hosted data path. It must
support at least:
- `GET_INFO`: protocol version, firmware version, build hash, active OTA slot,
rollback state, and capabilities
- `BEGIN_UPDATE`: image size, version, SHA-256, and signature metadata
- `WRITE_CHUNK`: offset, bytes, and per-chunk integrity check
- `END_UPDATE`: full hash/signature verification and boot-slot selection
- `REBOOT`: restart into the pending slot
- `CONFIRM_BOOT`: mark the new C6 image valid after host-side health checks
- `ABORT_UPDATE`: discard a partial update
Commands need sequence numbers and idempotent replies so a P4 reset or SDIO
retry cannot corrupt the inactive slot.
### P4 host changes
Introduce one explicit C6 backend boundary instead of allowing both transports
to initialize:
- `C6_BACKEND_AT`: existing factory ESP-AT support, retained for migration and
recovery builds
- `C6_BACKEND_HOSTED`: production backend after conversion
For the hosted backend:
- stop compiling or hard-stubbing the duplicate hosted implementations
- use exactly one esp-hosted component, following the Tanmatsu collision fix
- route Wi-Fi through `esp_wifi_remote` and the standard Arduino Wi-Fi facade
- route Bluetooth HCI into NimBLE
- enable the existing MeshCore BLE companion interface
- expose real Bluetooth capability to the UI
- keep C6 reset under the XL9535 callback rather than a fake host GPIO
- leave LoRa/UI startup independent from hosted initialization success
The host must query `GET_INFO` before enabling Wi-Fi or Bluetooth. An unsupported
protocol should produce a clear Settings/About diagnostic and keep the rest of
WadaMesh operational.
## First-time conversion
### Why it cannot currently be one P4 flash
The factory C6 has no working self-update command, and the P4 cannot force C6
IO9 low to enter the ROM downloader. A P4 image cannot safely replace the first
C6 firmware through the current wiring.
`AT+SYSFLASH` or other undocumented raw-partition writes must not be used for the
public migration unless a sacrificial-device test proves all of the following:
- the required C6 partitions can be written while ESP-AT is running
- bootloader, partition table, OTA metadata, and app offsets are known
- power loss cannot leave the C6 unrecoverable without opening the device
- image verification and rollback are available
Until then, use the C6 USB downloader.
### Guided web installer
Extend the WadaMesh install page with a P4-specific two-stage flow:
1. **Install C6 radio firmware**
- Explain which USB connector reaches the C6.
- Explain the required BOOT/IO9 and reset sequence with board photographs.
- Launch an ESP Web Tools manifest with `chipFamily: ESP32-C6`.
- Flash a full merged `wadamesh-c6` image containing bootloader, partition
table, OTA metadata, and app.
- Verify the chip and image before showing success.
2. **Install WadaMesh P4 firmware**
- Move the cable to the P4 USB connector.
- Launch the existing `ESP32-P4` WadaMesh manifest.
- On first boot, require the expected C6 protocol handshake and show the
detected C6 version in About/Diagnostics.
The page should remember only browser-local completion state. The device
handshake remains authoritative.
### Development flow
Use the existing helper for early testing:
```bash
C6_FLASH_HELPER=1 ./tdisplay_p4/build.sh build
./tdisplay_p4/build.sh flash -p <P4-port>
```
Then place the C6 in ROM download mode and flash `wadamesh-c6` through the C6
USB port. After the C6 image is installed, flash a normal hosted-backend P4
build.
Document a full factory-image backup and restore command before the first C6
write.
## Future seamless C6 updates
Once `wadamesh-c6` is installed, subsequent updates should not require the C6
USB connector.
### Image delivery
Preferred production path:
- publish immutable, versioned C6 OTA images beside each P4 release
- include required C6 protocol/version metadata in the P4 release manifest
- let the P4 download the C6 image over its working C6 Wi-Fi connection
- stream directly into the C6 inactive OTA slot over the management channel
Offline/full-install option:
- add a dedicated P4 `c6_fw` data partition
- place the matching C6 OTA image there in the full merged P4 artifact
- reduce the P4 FAT storage partition by the measured partition size
- do not embed the image in either 4 MB P4 application slot
The online path should remain available for P4 OTA updates where the full flash
image and data partition are not rewritten.
### Update state machine
1. P4 reads C6 identity and compatibility.
2. If compatible, continue boot without rewriting anything.
3. If an update is required, keep Wi-Fi/BLE user transports disabled.
4. Select the C6 inactive OTA slot.
5. Stream chunks with retries and a visible progress state.
6. Verify SHA-256 and signature on the C6.
7. Mark the inactive slot pending and reboot only the C6.
8. Re-establish SDIO and run health checks:
- management handshake
- Wi-Fi scan
- Wi-Fi join where credentials exist
- BLE controller initialization
- BLE advertising start
9. Confirm the C6 slot only after health checks pass.
10. If the handshake does not recover, reset the C6 and allow its bootloader
rollback. Keep the P4 UI and LoRa available with a recovery message.
Never update both P4 and C6 active slots without an independently recoverable
checkpoint between them.
## Release pipeline
Add a dedicated C6 build/release surface rather than hiding it inside the P4
build script:
```text
tdisplay_p4/c6_firmware/
CMakeLists.txt
sdkconfig.defaults
partitions.csv
main/
build.sh
```
Release artifacts:
- `wadamesh-c6-full.bin`: first-time USB conversion
- `wadamesh-c6-ota.bin`: later in-band update
- `manifest-tdisplay-p4-c6.json`: ESP Web Tools C6 manifest
- `c6-version.json`: protocol, firmware version, hash, signature, minimum P4
version, and release channel
Update these existing surfaces:
- `scripts/build-all-targets.sh`: build the C6 project in IDF sweeps
- `scripts/release.sh`: stage C6 artifacts with the P4 release
- `scripts/build/gen-flasher-meta.py`: create the ESP32-C6 manifest
- `deploy/site/index.html`: guided two-stage P4 install
- P4 About/Diagnostics: show C6 backend, protocol, firmware, active slot, and
last update result
A release gate should reject a P4 build whose required C6 protocol is not
present in the staged C6 metadata.
## Security and integrity
- Sign C6 images with a WadaMesh release key.
- Verify the signature on the C6 before selecting the pending slot.
- Pin the expected SHA-256 in release metadata and report it in diagnostics.
- Reject downgrade unless a physical recovery flow explicitly requests it.
- Do not carry Wi-Fi credentials inside C6 update images.
- Clear partial-update state after abort or rollback.
- Rate-limit and authenticate update commands so a network client cannot invoke
the management endpoint.
Secure Boot and flash encryption should be evaluated for both processors as a
separate rollout because enabling eFuses changes recovery procedures.
## Compatibility and fallback
During migration, a P4 build should detect three states:
| State | Behavior |
|---|---|
| Factory ESP-AT | Keep AT Wi-Fi available, report Bluetooth unavailable, offer C6 conversion instructions |
| Supported hosted protocol | Enable Wi-Fi and Bluetooth normally |
| Unknown/unresponsive C6 | Keep LoRa/UI running, disable wireless transports, expose diagnostics and wired recovery |
Do not infer the backend from one failed packet. Use a bounded handshake, reset
once through XL9535 IO14, then classify the C6.
## Test plan
### C6 unit/integration
- version/capability handshake
- chunk replay, duplication, and out-of-order rejection
- full hash and signature failure
- power loss at each update stage
- pending-slot rollback
- management request timeout and retry
### Wi-Fi
- scan in sparse and dense RF environments
- hidden SSIDs
- open, WPA2, WPA3, and wrong-password behavior
- reconnect after C6 reset
- TCP client and inbound companion server
- HTTP downloads, tiles, OTA, and SNTP
### Bluetooth LE
- advertising name and MeshCore service UUID
- Android and iOS discovery
- connect/disconnect/reconnect
- bidirectional MeshCore packet transport
- MTU negotiation and fragmented writes
- simultaneous Wi-Fi traffic
- BLE availability after P4 and C6 independent resets
### System
- LoRa receive/transmit while Wi-Fi and BLE are active
- SD card and C6 sharing separate SDMMC hosts
- display underrun/flicker during C6 update and heavy traffic
- screen-off and wake behavior
- low-memory operation
- C6 absent, factory, supported, outdated, and corrupt states
- P4 OTA with matching and mismatched C6 versions
- first-time web installation on macOS, Windows, Linux, Android Chrome, and
ChromeOS where supported
## Rollout phases
### Phase 0: preserve and measure
- Back up the complete factory C6 flash.
- Record chip revision, flash size, partition table, MAC addresses, and USB IDs.
- Confirm the C6 USB/BOOT procedure on at least two boards.
- Capture current AT Wi-Fi performance as a regression baseline.
### Phase 1: hosted proof of life
- Build and manually flash the smallest esp-hosted C6 slave.
- Establish the SDIO handshake from P4.
- Validate Wi-Fi scan/join and Bluetooth controller discovery.
- Keep the existing AT build selectable for recovery.
### Phase 2: transport parity
- Port every P4 Wi-Fi/socket use away from `c6_at`.
- Enable the existing MeshCore BLE companion transport.
- Pass Wi-Fi, BLE, TCP server, OTA, tile, and time-sync tests.
### Phase 3: public first-time installer
- Publish signed full C6 artifacts and an ESP32-C6 web manifest.
- Add the two-stage installer UI and connector/BOOT instructions.
- Add backend/version diagnostics to P4.
### Phase 4: in-band updates
- Implement C6 A/B management protocol and host updater.
- Add online C6 artifact delivery.
- Add an optional offline P4 `c6_fw` partition after measuring image size.
- Exercise interruption and rollback tests before enabling automatic updates.
### Phase 5: default hosted backend
- Make hosted the normal P4 release backend.
- Retain an explicit AT recovery build for at least one stable release cycle.
- Remove temporary ESP-AT GATT provisioning only after migration coverage is
acceptable.
## Acceptance criteria
The migration is complete when:
- a fresh user can follow one WadaMesh page to flash both chips without command
line tools
- normal later WadaMesh updates need only the P4 connection or on-device OTA
- About reports compatible P4 and C6 versions
- Wi-Fi feature parity is maintained
- the MeshCore phone app can discover, connect, exchange packets, and reconnect
over Bluetooth LE
- failed or interrupted C6 updates roll back without losing P4 LoRa/UI access
- a documented wired restore path returns the C6 to a known image
+46 -28
View File
@@ -1,22 +1,22 @@
# ThinkNode M9: Keyboard & D-pad Guide
The Elecrow ThinkNode M9 has **no touchscreen** — every screen, every setting,
The Elecrow ThinkNode M9 has **no touchscreen**: every screen, every setting,
and every chat is driven by the d-pad, the dedicated function keys, and the
QWERTY keyboard. If you're coming from the T-Deck or Heltec V4 (touch-first),
this page is the five-minute tour that makes the board feel native.
## The inputs
- **D-pad** — four arrows around a centre button (**OK**). Arrows move the
- **D-pad**: four arrows around a centre button (**OK**). Arrows move the
on-screen focus highlight; OK activates whatever is focused. **Holding OK**
is the board's long-press: it fires the held item's long-press action (a
chat message's context menu, the SD row's hold-to-format) and unlocks the
lock screen.
- **Function row** — dedicated keys for Chats, Home, Mentions, Advert, Map,
Back, Mic and Ctrl (see the table below).
- **QWERTY keyboard** — the keyboard controller resolves Shift/symbol layers
- **Function row**: dedicated keys for Chats, Home, Mentions, Advert, Map,
Back, Ctrl and Triangle (see the table below).
- **QWERTY keyboard**: the keyboard controller resolves Shift/symbol layers
itself, so keys always deliver their final character. It latches one key at
a time, which is why there are no chords on this board — long-press carries
a time, which is why there are no chords on this board; long-press carries
the second layer instead. Start typing in a chat and the composer focuses
itself; any key wakes the screen from idle.
@@ -28,7 +28,7 @@ dropdown → the power menu → the Control Center → a full-screen app → a d
else.
Once there is nothing left covering the screen, **Back returns you to the
previous screen** — the tab you came from, however you got there: a function
previous screen**, the tab you came from, however you got there: a function
key, an app-drawer tile, "Show on map", a contact's Send-msg button. Keep
pressing and you keep walking back through where you have been. When the trail
runs out Back lands on Home, and on Home with nothing open it does nothing,
@@ -42,21 +42,21 @@ the root after closing anything above it. The **Cmdr** tile still opens the
Commander screen when you need it.
The only exception is deliberate: progress overlays (SD format, bulk delete)
block all keys until the operation finishes — Back will not navigate out from
block all keys until the operation finishes; Back will not navigate out from
under a running operation.
## Function keys
| Key | Press | Hold |
|---|---|---|
| **MSG** | Jump to the Chats tab; inside a conversation, return to the chat list (closes an open app first) | — |
| **HOME** | Peel one layer off an open app; on the Home tab, toggle the app drawer; otherwise jump Home (and clear the Back trail) | — |
| **@ (Mentions)** | Open the Mentions screen | — |
| **MSG** | Jump to the Chats tab; inside a conversation, return to the chat list (closes an open app first) | None |
| **HOME** | Peel one layer off an open app; on the Home tab, toggle the app drawer; otherwise jump Home (and clear the Back trail). **Double-press** to open the launcher settings (what Home opens, icon size) | None |
| **@ (Mentions)** | Open the Mentions screen | None |
| **ADV** | Open the Send Advert page | **Toggle GPS on/off** |
| **MAP** | Jump to the Map tab — press again *on* the map to toggle pan mode | — |
| **BACK** | Close the top layer (see above) | — |
| **CTRL** | Open the Control Center (quick toggles, incl. the keyboard light: off / on / auto) | — |
| **MIC** | Nothing yet — deliberately reserved | — |
| **MAP** | Jump to the Map tab; press again *on* the map to toggle pan mode | None |
| **BACK** | Close the top layer (see above) | None |
| **CTRL** | Open the Control Center (quick toggles, incl. the keyboard light: off / on / auto) | None |
| **Triangle** | The focused item's hold action, e.g. a chat row's menu | None |
| **OK / Enter** | Activate the focused item; send a message; run a terminal command; newline in the editor | **Long-press the focused item / unlock the lock screen** |
The **MSG**, **HOME**, **@**, **ADV**, **MAP**, and **CTRL** shortcuts remain
@@ -84,9 +84,18 @@ select OSM, and enable **Tiles from SD card**. See the
[offline map tile guide](README.md#offline-map-tiles) for the QGIS export steps
and map-provider policy requirements.
## Web pages
In the Web browser, **Up** and **Down** read the page: each press moves the
highlight to the next link already on screen, or, when there is none, scrolls
the text by about two-thirds of a screen. If the highlighted link scrolls out
of view, the highlight moves to a link coming into view. Only at the top or
bottom of the page does focus leave for the address bar or the toolbar.
**OK** opens the highlighted link.
## Typing & editing
Inside a text field the left/right arrows move the caret — and when the caret
Inside a text field the left/right arrows move the caret, and when the caret
is already at the edge of the text, the same press steps focus *out* of the
field, so you're never stuck. Enter sends (toggleable under Settings), Back
leaves the field. In the Terminal, Enter runs the command; in the text editor
@@ -96,9 +105,10 @@ While a chat message is focused and newer messages are below it, a down arrow
appears at the right edge. Press **Right** to select it, then **OK** to jump to
the newest message.
Channels, direct messages, and room conversations place **#** and emoji to the
right of the message input. Press Right to move from the input to **#**, and
Right again to move to emoji. Press **OK** on either button to open its picker.
Channels, direct messages, and room conversations place emoji and **#** to the
right of the message input, next to Send. With the caret at the end of the
input, press Right to move to emoji, and Right again to move to **#**; Left
walks back to the input. Press **OK** on either button to open its picker.
In the symbol grid, use all four arrows to move the highlight and **OK** to
insert the selected character.
@@ -110,7 +120,7 @@ After inserting a symbol, focus returns to the originating text field.
## Lock screen
**Hold OK** to unlock — the keyboard's hardware long-press stands in for the
**Hold OK** to unlock: the keyboard's hardware long-press stands in for the
hold-to-unlock other boards do by touch. With "Flash on new message" enabled,
an incoming message wakes (or lock-reveals) the screen and pulses the keyboard
backlight.
@@ -118,26 +128,26 @@ backlight.
## Inside apps
Store apps (Snake and friends) get the d-pad natively: arrows steer, OK taps
the centre — start, steer, and tap-to-retry all work. Display-only apps
the centre, so start, steer, and tap-to-retry all work. Display-only apps
(Airtime, RF Monitor) keep normal navigation: arrows move between the app's
own buttons or scroll the feed. **Back and Home always escape an app** — no
own buttons or scroll the feed. **Back and Home always escape an app**: no
app can trap the keys, even while the screen is locked.
## Recent improvements (beta_66 branch)
If you last used the M9 on an earlier beta, the notable navigation changes:
- **Back now actually goes back** — once everything covering the screen was
- **Back now actually goes back**: once everything covering the screen was
closed, Back had nothing left to do and the press simply died on any bare
tab. It now walks back through the screens you visited, then Home. Jumps that
used to strand you ("Show on map" from Discover or Contacts, a route replay
from a chat, the Home unread line, the ✉ badge, app-drawer tiles) all return
properly. **Back also works in the first-boot wizard** now, stepping back
through its screens like the on-screen Back button.
- **Back restored and made consistent** — it had regressed to doing nothing
- **Back restored and made consistent**: it had regressed to doing nothing
outside text fields, and could close hidden popups beneath the Control
Center. Both fixed; the close-what-you-see rule above now holds everywhere.
- **The map opens faster the first time** — the cold open was doing a
- **The map opens faster the first time**: the cold open was doing a
whole-card zoom scan whose result was thrown away, and (with no GPS fix) was
repeating that scan on *every* open. The "Loading map…" hint now appears
immediately instead of after the scan, so the board no longer looks frozen on
@@ -145,14 +155,14 @@ If you last used the M9 on an earlier beta, the notable navigation changes:
- **The d-pad no longer goes dead for a second** when a message arrives, and
the Spectrum page no longer steers an invisible cursor around the app drawer
underneath it.
- **Apps can't strand you** — opening Advert/Mentions over a running app used
- **Apps can't strand you**: opening Advert/Mentions over a running app used
to orphan it with no key path back; app permission dialogs are now
answerable with the d-pad.
- **Map pan cleans up after itself** (see above — it used to leak across tab
- **Map pan cleans up after itself** (see above; it used to leak across tab
jumps).
- **Keyboard light "On" applies at boot**, not after the first dim cycle.
- **Terminal chat shows incoming replies**, not just your own sent lines.
- **GPS works from a cold boot** — the toggle (and ADV-hold) no longer depends
- **GPS works from a cold boot**: the toggle (and ADV-hold) no longer depends
on a boot-time detection race.
- **Spectrum analyzer** sweeps roughly twice as fast, survives read glitches
without flattening the trace, and waits for an in-flight transmission
@@ -160,3 +170,11 @@ If you last used the M9 on an earlier beta, the notable navigation changes:
The engineering record behind these lives in
[`variants/thinknode_m9/M9_PORT.md`](variants/thinknode_m9/M9_PORT.md).
## Tooltips and the cursor
The focus highlight is amber, so the selected control stands out on any
background. Icon buttons say what they are: rest on one for a moment and a
small card names it. On a control panel tile the card adds "Hold Enter:
settings": holding OK (the d-pad's Enter) there opens that tile's settings
page.
+87 -59
View File
@@ -1,19 +1,19 @@
# T-LoRa Pager: Keyboard & Rotary Encoder Guide
The LilyGo T-LoRa Pager has **no touchscreen and no trackball** — every
The LilyGo T-LoRa Pager has **no touchscreen and no trackball**: every
screen, every setting, and every chat is driven entirely by the physical
QWERTY keyboard and the rotary encoder knob. If you're coming from the
T-Deck or Heltec V4 (touch-first), this is the one board where learning a
handful of gestures up front pays off immediately. This guide covers both
radio variants (LR1121 and SX1262) — the UI and input handling are identical
radio variants (LR1121 and SX1262); the UI and input handling are identical
on both.
## The two inputs
- **Keyboard** — a physical QWERTY matrix with a Space bar, Backspace, Enter,
- **Keyboard**: a physical QWERTY matrix with a Space bar, Backspace, Enter,
a Shift key, and an orange **Fn/Alt** key (bottom-left). No dedicated Esc,
arrow keys, or number row — those are covered by combos below.
- **Rotary encoder** — turn to move the on-screen focus highlight; click
arrow keys, or number row; those are covered by combos below.
- **Rotary encoder**: turn to move the on-screen focus highlight; click
(press the knob) to select. It's your only pointing device, so almost
every screen is navigable with turn + click alone.
@@ -36,20 +36,40 @@ They do nothing inside an open chat, settings detail, app page, or popup, so a
letter cannot unexpectedly close the inner screen. They also remain inactive
while editing a text field, where the keys type normally.
**H** and **S** also work like holding those buttons on a touchscreen:
- **Tap H** on Home switches between the app drawer and your widgets.
- **Hold H** (about half a second) opens the launcher options.
- **Hold S** opens the control panel.
A hold opens them over the screen you are on.
Each tab remembers where you were: coming back puts the cursor on the item
you left. Contacts opens on the first contact, and a chat with unread
messages opens on the first of them (see [N](#n-catching-up)).
Icon buttons say what they are: rest the cursor on one for a moment and a
small card names it, sometimes with a line on what it does or, on a
control-panel tile, "Hold Enter: settings".
**Power off** (control panel, power button) asks first, with Cancel
selected, so one stray click never switches the Pager off. Press **BOOT** to
wake it again.
## Rotary encoder
| Gesture | Action | Keyboard equivalent |
|---|---|---|
| Turn | Move focus to the next/previous item on screen | none — encoder only |
| Turn | Move focus to the next/previous item on screen | none (encoder only) |
| Short click | Select / confirm the focused item | **Enter** |
| Hold ~1 s, then release | **Back**: closes a popup → closes an open chat → goes Home → Esc (whichever applies first) | **Backspace held ~1 s** |
| Hold ~1 s, then release | **Back**: closes a popup → closes an open chat → goes Home → Esc (whichever applies first). A ring fills while you hold | **Backspace** (tap) |
| **Fn (Alt) + turn**, on a main tab | Move between the 5 main tabs (Mail / Contacts / Home / Map / Settings) | **M / C / H / A / S** jumps directly |
| **Fn (Alt) + turn**, inside a settings page or chat | Scroll the page up/down | none — encoder only |
| **Fn (Alt) + short click**, with a text field focused | Open the special-character picker | none — encoder only |
| Turn, with a dropdown open | Scroll through the dropdown's options | none — encoder only |
| Turn, with the accent picker open | Cycle through the accent variants (see below) | none — encoder only (Fn+Space to enter is shared) |
| Turn, with the @-mention list open | Cycle through matching contacts (see below) | none — encoder only |
| Turn, at the oldest/newest **loaded** message in an open chat | Loads more history in that direction and keeps going — see [Chat screen](#chat-screen) | none — encoder only |
| **Fn (Alt) + turn**, inside a settings page or chat | Scroll the page up/down | none (encoder only) |
| **Fn (Alt) + short click**, with a text field focused | Open the special-character picker | none (encoder only) |
| Turn, with a dropdown open | Scroll through the dropdown's options | none (encoder only) |
| Turn, with the accent picker open | Cycle through the accent variants (see below) | none (encoder only; Fn+Space to enter is shared) |
| Turn, with the @-mention list open | Cycle through matching contacts (see below) | none (encoder only) |
| Turn, at the oldest/newest **loaded** message in an open chat | Loads more history in that direction and keeps going (see [Chat screen](#chat-screen)) | none (encoder only) |
Since there's no touch to tap the bottom bar, use its visible **M / C / H / A /
S** shortcuts to jump directly or **Fn+turn** to move through the tabs in
@@ -58,7 +78,7 @@ order.
## Chat screen
An open chat has no touch-only jump-to-oldest/jump-to-latest buttons on this
board — everything is reachable by turning and clicking, and the encoder
board: everything is reachable by turning and clicking, and the encoder
behaves a bit differently here than anywhere else in the app.
### Turning through messages
@@ -67,16 +87,16 @@ Message history is loaded lazily: only the messages currently near the
viewport actually exist as on-screen items, and older/newer ones are pulled in
as you approach them. Turning the encoder normally moves the focus highlight
one message at a time, and **at the edge of what's currently loaded, it keeps
going** — the turn instead scrolls the list to pull in the next batch of
going**: the turn instead scrolls the list to pull in the next batch of
history, then continues moving the highlight onto it. You'll only ever land
outside the message list (see below) once you've genuinely reached the very
oldest or very newest message in the whole conversation — never mid-history.
oldest or very newest message in the whole conversation, never mid-history.
### Leaving the message list
Turning **past the oldest loaded message** (top edge), once there's truly
nothing more to load, wraps around to whatever the last focusable item on the
screen is — the channel-settings gear in an open **channel**, or the Send
screen is: the channel-settings gear in an open **channel**, or the Send
button in a **DM** (a DM has no gear to reach). This is plain focus wrap, the
same as reaching the end of any other list in the app; there's no chat-specific
handling for this edge.
@@ -90,23 +110,23 @@ newest message instead.
### The reply row
Below the messages, turning steps through: **quick-reply (△) → emoji →
text box → Send**, in that order, in both directions — nothing is skipped.
text box → Send**, in that order, in both directions; nothing is skipped.
Turning past the △ chip (toward the messages) always jumps straight to the
newest message, regardless of channel or DM.
### Backspace — catching up
### N: catching up
**Backspace (tap)** jumps to whichever message needs your attention:
A chat with unread messages opens on the first of them. **N** jumps to
whichever message needs your attention:
- If there are unread messages, it jumps to the first one — right below the
**"NEW ----"** divider — and selects it, so you can then turn forward
- If there are unread messages, it jumps to the first one, right below the
**"NEW ----"** divider, and selects it, so you can then turn forward
through your unread messages in order, oldest-of-the-unread first.
- If nothing is unread, it jumps to the newest message instead (same
destination, with focus left on the message).
Backspace's override above only applies while you're inside an open chat and
*not* actively editing the text box — the usual rule from the top of this
guide; with a field focused, Backspace deletes a character as normal.
N only does this inside an open chat and while you are not typing in the
reply box. While you type, it is just the letter n.
## Keyboard layout
@@ -142,14 +162,14 @@ While any text field is focused, including a Wi-Fi or repeater password:
### Shift and Caps Lock
- **Hold Shift + a letter**: that letter (or letters, for as long as you
hold Shift) types uppercase — released, typing goes back to lowercase.
hold Shift) types uppercase; released, typing goes back to lowercase.
Real momentary Shift, just like a normal keyboard.
- **Hold Fn (Alt), then press Shift**: while you're editing a text field,
toggles **Caps Lock** on/off — stays uppercase until you repeat the chord.
toggles **Caps Lock** on/off; it stays uppercase until you repeat the chord.
Anywhere else (not editing a field), the chord does nothing.
- Shift alone, tapped with nothing else, does nothing (as expected).
- **Hold Fn (Alt), then press Backspace**: jumps straight to the **Home**
screen — works everywhere, including while you're actively editing a field
screen; it works everywhere, including while you're actively editing a field
(unlike Alt+Shift above, this one isn't context-dependent). Doesn't delete
a character or trigger the plain-Backspace gestures below.
@@ -157,18 +177,21 @@ While any text field is focused, including a Wi-Fi or repeater password:
| Key | While editing a text field | Otherwise |
|---|---|---|
| **Enter** | Send / submit / newline | Select the focused item — or, if a chat message bubble is focused, opens its action menu (Ack / Mention / Copy / Info / Block) |
| **Backspace** (tap) | Delete a character | In an open chat: jump to your first unread message, or the newest message if nothing's unread — see [Chat screen](#chat-screen) |
| **Backspace** (hold ~1 s) | — | Same as the encoder's long-press: **Back** |
| **Backspace** (hold ~1 s) *while the screen is locked* | — | Unlock the screen |
| **Space** (tap) | Types a space | — |
| **Space** (double-tap, within 250 ms) | Switches between English and your configured secondary keyboard layout | — |
| **Space** (hold ~1 s) | — | Locks the screen (shows a "Locking…" progress bar; tapping any key cancels) |
| **Enter** | Send / submit / newline | Select the focused item. On a chat message bubble, opens its action menu (Ack / Mention / Copy / Info / Block) |
| **Enter** (hold about half a second) | Nothing | What a long press does on a touchscreen: a chat's actions, hiding an app in the drawer, a control-panel switch's settings page |
| **Backspace** (tap) | Delete a character | **Back**: closes a popup, a page or the chat, otherwise goes Home |
| **Backspace** (hold ~1 s) | **Back**, leaving the screen the field is on | **Back** |
| **N** | Types n | In an open chat: jump to your first unread message, or the newest if nothing is unread. See [Chat screen](#chat-screen) |
| **Backspace** (hold ~1 s) *while the screen is locked* | Nothing | Unlock the screen |
| **Space** (tap) | Types a space | Nothing |
| **Space** (double-tap, within 250 ms) | Switches between English and your configured secondary keyboard layout | Nothing |
| **Space** (hold ~1 s) | Nothing | Locks the screen (shows a "Locking…" progress bar; tapping any key cancels) |
| **Fn (Alt)** (tap / double-tap) | Next key uses symbols / lock symbols until tapped again | Same |
| **Fn (Alt) held + Enter** | **Paste** the clipboard at the cursor (replaces a highlighted range). The clipboard is filled by **Copy** in a message bubble's action menu. Fn must be held, not tapped | Nothing |
The **BOOT** button (top of the device) instantly wakes the screen from
idle-dim. It does *not* unlock a screen you've manually locked with the
Space-hold gesture above — for that, hold Backspace.
Space-hold gesture above; for that, hold Backspace.
## Accented characters
@@ -176,30 +199,30 @@ Typing a letter that has accent variants (all vowels, plus `n c s y t z l r`,
both cases) pops up a small row of alternatives above the field. To pick one
without touch:
1. **Fn (Alt) + Space** — jumps focus into the popup.
2. **Turn the encoder** — cycles through the variants.
3. **Enter** — confirms your choice and replaces the letter you just typed.
4. **Backspace** — dismisses the popup and keeps the plain letter.
1. **Fn (Alt) + Space**: jumps focus into the popup.
2. **Turn the encoder**: cycles through the variants.
3. **Enter**: confirms your choice and replaces the letter you just typed.
4. **Backspace**: dismisses the popup and keeps the plain letter.
## @-mentions
Typing `@` followed by a few letters of a contact's name pops up a matching
list in a channel/group chat. Unlike the accent picker above, this one grabs
the encoder **immediately** — no Fn+Space needed, since it only appears once
the encoder **immediately**, no Fn+Space needed, since it only appears once
you've deliberately started typing a mention:
1. **Turn the encoder** — cycles through the matching contacts.
2. **Enter / encoder click** — confirms the highlighted one, replacing
1. **Turn the encoder**: cycles through the matching contacts.
2. **Enter / encoder click**: confirms the highlighted one, replacing
`@partial` with `@FullName ` in your message.
3. **Backspace** — dismisses the list without touching what you typed.
3. **Backspace**: dismisses the list without touching what you typed.
## Sliders
Any focused slider (Control Center brightness, a Settings slider, the Map
zoom bar) can't be dragged without touch — instead:
zoom bar) can't be dragged without touch. Instead:
- **Q** — decrease
- **E** — increase
- **Q**: decrease
- **E**: increase
Each press moves it proportionally (about 20 presses end-to-end) and
persists the new value immediately, the same as releasing a drag.
@@ -208,10 +231,10 @@ persists the new value immediately, the same as releasing a drag.
While the **Map** tab is active:
- **W** — pan north
- **A** — pan west
- **X** — pan south
- **D** — pan east
- **W**: pan north
- **A**: pan west
- **X**: pan south
- **D**: pan east
`X` replaces the usual `S` position because **S** is the visible Settings
shortcut. Pressing **A** on another main screen opens Map; once Map is already
@@ -225,9 +248,9 @@ needed.
**Settings → Keyboard → Keyboard backlight** cycles through three modes
(tap to advance):
- **Off** — always dark
- **On** — always lit
- **Auto** (default) — lights up on any keypress and follows the configured
- **Off**: always dark
- **On**: always lit
- **Auto** (default): lights up on any keypress and follows the configured
**Screen timeout**; `0 = never` keeps it lit until the screen is turned off
Unlike the T-Deck, this is a plain on/off strip under the keys, not a
@@ -252,9 +275,9 @@ that changes keyboard language.
|---|---|
| M / C / H / A / S (top-level main screens only) | Open Mail / Contacts / Home / Map / Settings |
| Turn encoder | Move focus |
| Turn, at the loaded edge of a chat | Load more history, keep moving — only exits the list at the true oldest/newest message |
| Turn, at the loaded edge of a chat | Load more history, keep moving; only exits the list at the true oldest/newest message |
| Click encoder (or Enter) | Select / confirm |
| Hold encoder ~1s (or hold Backspace ~1s) | Back |
| Hold encoder ~1 s, or tap Backspace | Back |
| Fn + turn (main tab) | Switch tabs |
| Fn + turn (page/chat) | Scroll |
| Fn + encoder click (text field focused) | Open special-character picker |
@@ -264,11 +287,16 @@ that changes keyboard language.
| Fn + Shift (editing a field) | Toggle Caps Lock |
| Fn + Shift (not editing a field) | Nothing |
| Fn + Backspace (anywhere) | Jump to Home |
| Fn held + Enter (editing a field) | Paste the clipboard |
| Fn + Space | Enter accent picker |
| @ + letters (composer) | Auto-focuses the mention list — no Fn+Space needed |
| @ + letters (composer) | Auto-focuses the mention list, no Fn+Space needed |
| Enter | Select / send / message action menu |
| Backspace (tap) | Delete / in a chat: jump to first unread message, or newest if none |
| Backspace (hold 1s) | Back, or unlock if locked |
| Hold Enter | Long press: a chat's actions, app options, a switch's settings |
| Backspace (tap) | Delete while typing, otherwise Back |
| Backspace (hold 1s) | Back, also while typing; or unlock if locked |
| N (in a chat) | Jump to the first unread message, or the newest if none |
| H on Home | Switch between the app drawer and widgets |
| Hold H / hold S | Launcher options / control panel |
| Space (tap) | Space |
| Space (double-tap) | Switch keyboard language |
| Space (hold 1s) | Lock screen |
+45
View File
@@ -0,0 +1,45 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=0",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"mcu": "esp32s3",
"variant": "esp32s3"
},
"connectivity": ["wifi", "bluetooth", "lora", "gps"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": [
"arduino",
"espidf"
],
"name": "Elecrow CrowPanel Advance 3.5 (N16R8)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"use_1200bps_touch": false,
"wait_for_upload_port": false,
"require_upload_port": true,
"speed": 921600
},
"url": "https://www.elecrow.com/",
"vendor": "Elecrow"
}
+45
View File
@@ -0,0 +1,45 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=0",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"mcu": "esp32s3",
"variant": "esp32s3"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": [
"arduino",
"espidf"
],
"name": "Elecrow CrowPanel Advance 5.0 (N16R8)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"use_1200bps_touch": false,
"wait_for_upload_port": false,
"require_upload_port": true,
"speed": 921600
},
"url": "https://www.elecrow.com/",
"vendor": "Elecrow"
}
+8
View File
@@ -0,0 +1,8 @@
# build output + component manager cache (project-local ESP-IDF comes in via the ../tanmatsu symlinks)
build/
managed_components/
sdkconfig
sdkconfig.old
dependencies.lock
# flashable images produced by build.sh merge
*.bin
+3
View File
@@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.10)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(application)
+204
View File
@@ -0,0 +1,204 @@
#!/usr/bin/env bash
# wadamesh / Elecrow CrowPanel Advanced 10.1" ESP32-P4 HMI build wrapper. EXPERIMENTAL.
# Standalone ESP-IDF app modelled on tdisplay_p4/build.sh: reuses the Tanmatsu's project-local
# ESP-IDF 5.5.1 (esp32p4) via the ../tanmatsu symlinks and the shared P4 components.
# ./build.sh build # board revision V1.2 (default, newest)
# WADA_CP_REV=1.1 ./build.sh build # V1.1 (SX1262 DIO1/RST on GPIO53/54)
# WADA_CP_REV=1.0 ./build.sh build # V1.0 (same LoRa pins as V1.1, reversed 1-bit C6 SDIO)
# ./build.sh merge # build + one flashable image at 0x0 (build/<target>/*-merged.bin)
# ./build.sh flash -p /dev/cu.usbserial-XXXX
# ./build.sh fullclean # remove this revision's build tree only
set -e
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
CP_REV="${WADA_CP_REV:-1.2}"
case "$CP_REV" in
1.2) CP_SDIO_FRAG="sdio_v11" ;;
1.1) CP_SDIO_FRAG="sdio_v11" ;;
1.0) CP_SDIO_FRAG="sdio_v10" ;;
*) echo "[build.sh] WADA_CP_REV must be 1.0, 1.1 or 1.2 (got '$CP_REV')" >&2; exit 1 ;;
esac
export WADA_CP_REV="$CP_REV"
P4_BUILD_TARGET="crowpanel_p4_101_v${CP_REV/./}"
P4_BUILD_DIR="$PROJECT_DIR/build/$P4_BUILD_TARGET"
# This project intentionally applies compatibility fixes inside managed_components
# before every build. ESP-IDF's stock fullclean also runs remove_managed_components,
# which rejects those expected hash changes and aborts. A P4 full clean therefore
# removes only the generated CMake/Ninja tree; dependencies stay in place and the
# idempotent patches below remain valid.
if [ "$#" -eq 1 ] && [ "$1" = "fullclean" ]; then
# Plain rm -rf: cmake is not on PATH before export.sh. Each revision has its own build tree
# (and its own sdkconfig inside it), so this removes only the selected one.
if [ -d "$P4_BUILD_DIR" ]; then
rm -rf "${P4_BUILD_DIR:?}" || { echo "[build.sh] FAILED to remove $P4_BUILD_DIR" >&2; exit 1; }
fi
if [ -d "$P4_BUILD_DIR" ]; then
echo "[build.sh] FAILED to remove $P4_BUILD_DIR" >&2
exit 1
fi
echo "[build.sh] removed build/$P4_BUILD_TARGET (patched managed components preserved)"
exit 0
fi
# Keep the baked-in translation + Lua app tables in step with deploy/apps/.
# Same for the seeded Lua apps. The PlatformIO envs get this from a pre: hook;
# the IDF builds need it here or the P4 boards ship stale copies.
python3 "$(cd "$(dirname "$0")/.." && pwd)/scripts/build/pre_gen_baked.py"
# LVGL is vendored (gitignored) by fetch-deps.sh, so a copy vendored before the
# anim_timer use-after-free fix (#428) still needs it. Idempotent; fails on drift.
python3 "$(cd "$(dirname "$0")/.." && pwd)/scripts/build/patch_lvgl_anim_uaf.py" \
--patch-file "$(cd "$(dirname "$0")" && pwd)/components/lvgl/upstream/src/misc/lv_anim.c"
python3 "$(cd "$(dirname "$0")/.." && pwd)/scripts/build/patch_lvgl_arabic.py" \
--patch-file "$(cd "$(dirname "$0")" && pwd)/components/lvgl/upstream/src/misc/lv_txt_ap.c"
cd "$PROJECT_DIR"
export IDF_TOOLS_PATH="$PWD/esp-idf-tools"
# VS Code may launch this wrapper from PlatformIO's virtualenv. Pin the
# project-local IDF environment before export.sh inspects that unrelated Python.
if [ -z "${IDF_PYTHON_ENV_PATH:-}" ]; then
for idf_py_env in "$IDF_TOOLS_PATH"/python_env/idf5.5_py*_env; do
if [ -x "$idf_py_env/bin/python" ]; then
export IDF_PYTHON_ENV_PATH="$idf_py_env"
break
fi
done
fi
unset VIRTUAL_ENV CONDA_PREFIX
# shellcheck disable=SC1091
source esp-idf/export.sh >/dev/null 2>&1
WADA_FW_TAG="${WADA_FW_TAG:-$(git describe --tags --match 'beta_*' --always 2>/dev/null || echo dev)}"
WADA_FW_DATE="$(date '+%-d %b %Y')"
IDF_ARGS=(-B "build/$P4_BUILD_TARGET" \
-DDEVICE=crowpanel_p4_101 \
-DSDKCONFIG="$P4_BUILD_DIR/sdkconfig" \
-DSDKCONFIG_DEFAULTS="sdkconfigs/general;sdkconfigs/wadamesh;sdkconfigs/crowpanel_p4_101;sdkconfigs/$CP_SDIO_FRAG" \
-DWADA_FW_TAG="$WADA_FW_TAG" -DWADA_FW_DATE="$WADA_FW_DATE" \
-DIDF_TARGET=esp32p4)
# A fresh clone has no managed_components yet. Configure once to download them,
# then apply the compatibility patches below before the first compilation.
if [ ! -f managed_components/espressif__libsodium/CMakeLists.txt ]; then
idf.py "${IDF_ARGS[@]}" reconfigure
fi
# --- Build-time patch: libsodium forced includes in paths with spaces -----------------------------
# The managed component emits `SHELL:-include <absolute path>`. CMake leaves that
# path unquoted in Ninja, so a checkout such as "T7 Shield" reaches GCC as two
# input files. GCC accepts the joined -include<path> form, which cannot split.
SODIUM_CMAKE="managed_components/espressif__libsodium/CMakeLists.txt"
if [ -f "$SODIUM_CMAKE" ] && grep -q 'SHELL:-include' "$SODIUM_CMAKE"; then
sed -i '' -e 's|SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}|-include${CMAKE_CURRENT_SOURCE_DIR}|g' \
-e 's|SHELL:-include${CMAKE_CURRENT_SOURCE_DIR}|-include${CMAKE_CURRENT_SOURCE_DIR}|g' "$SODIUM_CMAKE"
echo "[build.sh] patched libsodium forced includes (space-safe paths)"
fi
# --- Build-time patch: SD_MMC internal pull-ups (P4 boards) ------------------------------------------
# The board has no external pull-ups on the SD data lines; the IDF sdmmc host explicitly FLOATS the
# pads unless SDMMC_SLOT_FLAG_INTERNAL_PULLUP is set (so pre-begin gpio_pullup_en gets undone).
# Meck-P4's working SD init sets the flag; Arduino's SD_MMC never does and has no API for it. Patch
# the P4's own managed copy. Idempotent.
SDMMC_C="managed_components/espressif__arduino-esp32/libraries/SD_MMC/src/SD_MMC.cpp"
if [ -f "$SDMMC_C" ] && ! grep -q 'wadamesh P4: internal pullups' "$SDMMC_C"; then
sed -i '' 's|sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();|sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();\n slot_config.flags \|= SDMMC_SLOT_FLAG_INTERNAL_PULLUP; // wadamesh P4: internal pullups (no external ones on the SD lines)|' "$SDMMC_C"
echo "[build.sh] patched arduino SD_MMC.cpp (internal pull-ups on the SD slot)"
fi
# --- Build-time patch: a dead C6 must not spin the SDIO rx task -----------------------------------
# With CONFIG_ESP_HOSTED_TRANSPORT_RESTART_ON_FAILURE=n (see sdkconfigs/tdisplay_p4 for why it is
# off), sdio_read_task posts ESP_HOSTED_EVENT_TRANSPORT_FAILURE and then `continue`s: straight back
# into a register read that is going to fail again. On a unit whose C6 runs factory ESP-AT the
# transport NEVER comes up, so that is an unbounded retry storm: measured at ~16 failed reads per
# second, forever, with the whole transport re-initialising on top of it. It floods the USB-CDC the
# companion app talks over and keeps a task busy for the life of the session.
#
# Turning the restart back on is not the answer: that is the boot loop this board had before.
# Both are the same mistake: a co-processor that cannot be reached is a permanent condition, and
# the driver treats it as transient. Park the task instead. The failure event has already been
# posted, Wi-Fi and BLE are simply unavailable, and everything else on the device keeps running.
# Idempotent.
HOSTED_SDIO="managed_components/nicolaielectronics__esp-hosted-tanmatsu/host/drivers/transport/sdio/sdio_drv.c"
if [ -f "$HOSTED_SDIO" ] && ! grep -q 'wadamesh: park instead of spinning' "$HOSTED_SDIO"; then
python3 - "$HOSTED_SDIO" <<'PYEOF'
import re, sys
p = sys.argv[1]
s = open(p).read()
# Both failure arms look identical apart from the log line: event post, optional restart, continue.
old = """#if H_TRANSPORT_RESTART_ON_FAILURE
ESP_LOGI(TAG, "Host is resetting itself, to avoid any sdio race condition");
g_h.funcs->_h_restart_host();
#endif
continue;"""
new = """#if H_TRANSPORT_RESTART_ON_FAILURE
ESP_LOGI(TAG, "Host is resetting itself, to avoid any sdio race condition");
g_h.funcs->_h_restart_host();
#else
/* wadamesh: park instead of spinning. The transport is down and we are
* not allowed to reboot the host, so retrying can only fail again at
* full speed. The failure event is already posted; stop here and let the
* rest of the device run without Wi-Fi/BLE. */
ESP_LOGE(TAG, "transport down, host restart disabled: parking rx task (Wi-Fi/BLE unavailable)");
while (1) g_h.funcs->_h_msleep(60000);
#endif
continue;"""
n = s.count(old)
if n == 0:
sys.exit("esp-hosted sdio_drv.c: failure arm not found; patch needs updating")
s = s.replace(old, new)
open(p, "w").write(s)
print(" patched %d failure arm(s)" % n)
PYEOF
echo "[build.sh] patched esp-hosted sdio_drv.c (park the rx task on a dead transport)"
fi
# ⚠️ The P4/slot-0 branch REBUILDS slot_config from scratch with `.flags = 0`, silently discarding
# the flag added above (that's the branch that actually runs with BOARD_SDMMC_SLOT=0): so patch that
# struct literal too. Without the flag the IDF host explicitly FLOATS the pads -> deterministic 0x109
# CRC at the first data read regardless of card/frequency.
if [ -f "$SDMMC_C" ] && ! grep -q 'wadamesh P4: slot0 pullups' "$SDMMC_C"; then
sed -i '' 's|^ .flags = 0,$| .flags = SDMMC_SLOT_FLAG_INTERNAL_PULLUP, // wadamesh P4: slot0 pullups (struct rebuilt here; the earlier flag is discarded)|' "$SDMMC_C"
echo "[build.sh] patched arduino SD_MMC.cpp (slot-0 struct pull-up flag)"
fi
# --- Build-time patch: this board's C6 ships esp-hosted, so Arduino's hostedInit() must be the real
# one. A managed_components tree copied from a legacy ESP-AT T-Display build may carry the stub.
HOSTED_C="managed_components/espressif__arduino-esp32/cores/esp32/esp32-hal-hosted.c"
if [ -f "$HOSTED_C" ] && grep -q 'wadamesh P4: never start esp_hosted' "$HOSTED_C"; then
sed -i '' 's|^static bool hostedInit() { return false; // wadamesh P4: never start esp_hosted.*$|static bool hostedInit() {|' "$HOSTED_C"
echo "[build.sh] restored arduino hostedInit() (hosted C6 build)"
fi
BLEDEV_CPP="managed_components/espressif__arduino-esp32/libraries/BLE/src/BLEDevice.cpp"
if [ -f "$BLEDEV_CPP" ] && grep -q 'int rc = ble_gap_read_local_irk(irk);' "$BLEDEV_CPP"; then
sed -i '' 's|int rc = ble_gap_read_local_irk(irk);|int rc = 0; (void)irk; // wadamesh: arduino BLE unused; symbol renamed in IDF 5.5 NimBLE|' "$BLEDEV_CPP"
echo "[build.sh] patched arduino BLEDevice.cpp (stub renamed ble_gap_read_local_irk)"
fi
# --- Build-time patch: neutralize esp-hosted's auto-init constructor (P4-ONLY) --------------------
# Initialization must not run before app_main: the C6 is brought up lazily through Arduino
# hostedInit() after the UI, so a C6 that does not answer can only cost Wi-Fi/BLE.
# Two copies get pulled in (espressif__esp_hosted via arduino-esp32, and the nicolaielectronics__
# esp-hosted-tanmatsu fork via tanmatsu-wifi/badge-bsp). The build actually links the fork, so patch
# BOTH so whichever is linked never grabs the SDIO.
for HDIR in espressif__esp_hosted nicolaielectronics__esp-hosted-tanmatsu; do
HINIT_C="managed_components/$HDIR/host/port/esp/freertos/src/port_esp_hosted_host_init.c"
if [ -f "$HINIT_C" ] && grep -q 'ESP_ERROR_CHECK(esp_hosted_init());' "$HINIT_C"; then
sed -i '' 's|ESP_ERROR_CHECK(esp_hosted_init());|/* wadamesh P4: C6 initialized lazily via hostedInit() after the UI */|' "$HINIT_C"
echo "[build.sh] neutralized $HDIR auto-init constructor (defer until board power)"
fi
done
if [ "$#" -eq 1 ] && [ "$1" = "merge" ]; then
idf.py "${IDF_ARGS[@]}" build
OUT="$P4_BUILD_DIR/wadamesh-crowpanel-p4-101-v${CP_REV/./}-merged.bin"
# Same offsets and flash settings as scripts/build/p4-release.sh (bootloader 0x2000 on the P4).
python -m esptool --chip esp32p4 merge_bin --flash_mode dio --flash_size 16MB --flash_freq 80m \
-o "$OUT" \
0x2000 "$P4_BUILD_DIR/bootloader/bootloader.bin" \
0x8000 "$P4_BUILD_DIR/partition_table/partition-table.bin" \
0xf000 "$P4_BUILD_DIR/ota_data_initial.bin" \
0x20000 "$P4_BUILD_DIR/application.bin"
echo "[build.sh] merged image: $OUT (flash at 0x0)"
exit 0
fi
exec idf.py "${IDF_ARGS[@]}" "$@"
+1
View File
@@ -0,0 +1 @@
../../tdisplay_p4/components/RadioLib
+1
View File
@@ -0,0 +1 @@
../../tanmatsu/components/ardlibs
+1
View File
@@ -0,0 +1 @@
../../tanmatsu/components/esp_hosted
+1
View File
@@ -0,0 +1 @@
../../tanmatsu/components/lvgl
+1
View File
@@ -0,0 +1 @@
../../tdisplay_p4/components/meshcore
@@ -0,0 +1,22 @@
# The wadamesh app layer (MyMesh + DataStore + helpers + transports + the LVGL UITask), compiled from
# the repo's src/ plus variants/crowpanel_p4_101/. Mirrors tdisplay_p4/components/wadamesh_app, minus
# the ESP-AT c6_at component (this board's C6 ships esp-hosted firmware).
set(REPO "${CMAKE_CURRENT_LIST_DIR}/../../..")
set(APP "${REPO}/src")
file(GLOB_RECURSE APP_SRCS "${APP}/*.cpp" "${APP}/*.c" "${REPO}/variants/crowpanel_p4_101/*.cpp" "${REPO}/variants/crowpanel_p4_101/*.c")
list(FILTER APP_SRCS EXCLUDE REGEX "/main\\.cpp$")
list(FILTER APP_SRCS EXCLUDE REGEX "/helpers/input/(HeltecV4CapTouch|TDeckKeyboard|TDeckTouch|TDeckTrackball|RakTapV2Touch)\\.cpp$")
file(GLOB LUA_SRCS "${REPO}/lib/lua/src/*.c")
idf_component_register(
SRCS ${APP_SRCS} ${LUA_SRCS}
INCLUDE_DIRS "${APP}" "${APP}/ui-touch" "${REPO}/include" "${REPO}/variants/crowpanel_p4_101" "${REPO}/lib/lua/src"
REQUIRES espressif__arduino-esp32 meshcore lvgl ardlibs RadioLib nvs_flash esp_lcd fatfs
esp_driver_i2c esp_driver_gpio esp_driver_ledc esp_hosted h2zero__esp-nimble-cpp bt
soc esp_hw_support app_update driver esp_timer esp_partition spi_flash esp_adc esp_pm espcoredump
espressif__esp_lcd_ek79007 esp_mm # esp_mm: esp_cache_msync (framebuffer clear)
WHOLE_ARCHIVE
)
target_compile_options(${COMPONENT_LIB} PRIVATE -w
-include "${REPO}/variants/tdisplay_p4/tdisplay_p4_compat.h") # adcAttachPin() shim (arduino 3.x dropped it)
+1
View File
@@ -0,0 +1 @@
../tanmatsu/esp-idf
+1
View File
@@ -0,0 +1 @@
../tanmatsu/esp-idf-tools
+94
View File
@@ -0,0 +1,94 @@
idf_build_get_property(idf_target IDF_TARGET)
idf_component_register(
SRCS
"main.cpp"
PRIV_REQUIRES
esp_lcd # MIPI-DSI (EK79007)
esp_driver_i2c
esp_driver_gpio
fatfs # SD_MMC storage
nvs_flash
bt # NimBLE companion (BLE over the C6)
espressif__arduino-esp32
esp_hosted # ESP32-C6 Wi-Fi/BLE over SDIO slot 1
meshcore
lvgl
RadioLib # target.h (included by main.cpp) pulls <RadioLib.h>
wadamesh_app
INCLUDE_DIRS
"."
)
# Same warning policy as tdisplay_p4/main: vendored code must not fail the build on warnings.
idf_build_set_property(COMPILE_OPTIONS "-Wno-error=unused-variable" APPEND)
idf_build_set_property(COMPILE_OPTIONS "-Wno-error=unused-const-variable" APPEND)
idf_build_set_property(COMPILE_OPTIONS "-Wno-error=stringop-truncation" APPEND)
idf_build_set_property(COMPILE_OPTIONS "-Wno-error=stringop-overflow" APPEND)
idf_build_set_property(COMPILE_OPTIONS "-Wno-error=array-bounds" APPEND)
idf_build_set_property(COMPILE_OPTIONS "-Wno-error=restrict" APPEND)
idf_build_set_property(COMPILE_OPTIONS "-Wno-error=maybe-uninitialized" APPEND)
idf_build_get_property(_co COMPILE_OPTIONS)
list(REMOVE_ITEM _co "-Werror=all")
idf_build_set_property(COMPILE_OPTIONS "${_co}")
# ---- Elecrow CrowPanel Advanced 10.1" ESP32-P4 (EXPERIMENTAL) app define set. -------------------
# Pins: Elecrow schematics V1.0/V1.1/V1.2 + their ESP-IDF lessons; per-pin status and sources in
# the pin table at the top of variants/crowpanel_p4_101/target.h.
#
# Board revision (build.sh WADA_CP_REV=1.0|1.1|1.2, default 1.2). Only two things differ:
# - the SX1262 DIO1/NRST pair (J9.1 / J11.1): GPIO53/54 on V1.0 + V1.1, GPIO27/28 on V1.2;
# - the C6 SDIO data-pin order (sdkconfig fragment, build.sh).
if(NOT DEFINED ENV{WADA_CP_REV} OR "$ENV{WADA_CP_REV}" STREQUAL "1.2")
set(CP_REV_DEFS CP_REV=12 P_LORA_DIO_1=27 P_LORA_RESET=28)
message(STATUS "wadamesh: CrowPanel P4 10.1 board revision V1.2")
elseif("$ENV{WADA_CP_REV}" STREQUAL "1.1")
set(CP_REV_DEFS CP_REV=11 P_LORA_DIO_1=53 P_LORA_RESET=54)
message(STATUS "wadamesh: CrowPanel P4 10.1 board revision V1.1")
elseif("$ENV{WADA_CP_REV}" STREQUAL "1.0")
set(CP_REV_DEFS CP_REV=10 P_LORA_DIO_1=53 P_LORA_RESET=54)
message(STATUS "wadamesh: CrowPanel P4 10.1 board revision V1.0")
else()
message(FATAL_ERROR "WADA_CP_REV must be 1.0, 1.1 or 1.2 (got '$ENV{WADA_CP_REV}')")
endif()
set(WADA_DEFS
HAS_CROWPANEL_P4_101=1 ESP32_PLATFORM MC_VENDORED_TOUCH_APP UI_LVGL=1 HAS_TOUCH_UI=1
${CP_REV_DEFS}
USE_SX1262 RADIO_CLASS=CustomSX1262 WRAPPER_CLASS=CustomSX1262Wrapper
SX126X_RX_BOOSTED_GAIN=1 SX126X_CURRENT_LIMIT=140
SX126X_DIO2_AS_RF_SWITCH=true SX126X_DIO3_TCXO_VOLTAGE=1.6f # Elecrow Lesson14: RadioLib defaults + 1.6 V TCXO
MULTI_TRANSPORT_COMPANION TCP_PORT=5000 WS_PORT=8765 BLE_PIN_CODE=123456
LORA_FREQ=869.618 LORA_BW=62.5 LORA_SF=8 # EU 868 default like the other P4 boards; region is a setting
LORA_TX_POWER=22 MAX_LORA_TX_POWER=22
MAX_CONTACTS=2000 MAX_GROUP_CHANNELS=40 # 32 MB PSRAM
ENABLE_PRIVATE_KEY_IMPORT=1 ENABLE_PRIVATE_KEY_EXPORT=1
# --- SX1262 on the external wireless header J9/J11 (SPI3). DIO1/NRST from the revision block. ---
P_LORA_NSS=10 P_LORA_BUSY=9 P_LORA_SCLK=8 P_LORA_MISO=7 P_LORA_MOSI=6
# --- Display: EK79007 1024x600 MIPI-DSI, native landscape, LVGL at native resolution. ---
DISPLAY_CLASS=EK79007Display SCREEN_WIDTH=1024 SCREEN_HEIGHT=600
# --- Touch GT911 + shared board I2C0 (codec, HY2.0 port). Wire is begun by ESP32Board::begin. ---
PIN_BOARD_SDA=45 PIN_BOARD_SCL=46 PIN_TOUCH_SDA=45 PIN_TOUCH_SCL=46 PIN_TOUCH_RST=40 PIN_TOUCH_INT=42
# --- microSD: SDMMC slot 0, 1-bit (only D0 is wired): CLK43 / CMD44 / D0 39. C6 is slot 1. ---
BOARD_HAS_SDMMC=1 BOARD_SDMMC_SLOT=0
)
foreach(_d ${WADA_DEFS})
idf_build_set_property(COMPILE_OPTIONS "-D${_d}" APPEND)
endforeach()
# Firmware version/tag (build.sh passes WADA_FW_TAG/-DATE from git describe + date).
if(NOT WADA_FW_TAG)
set(WADA_FW_TAG "dev")
endif()
if(NOT WADA_FW_DATE)
set(WADA_FW_DATE "")
endif()
idf_build_set_property(COMPILE_OPTIONS "-DFIRMWARE_RELEASE_TAG=\"${WADA_FW_TAG}\"" APPEND)
idf_build_set_property(COMPILE_OPTIONS "-DFIRMWARE_BUILD_DATE=\"${WADA_FW_DATE}\"" APPEND)
idf_build_set_property(COMPILE_OPTIONS "-DFIRMWARE_VERSION=\"wadamesh ${WADA_FW_TAG}\"" APPEND)
# NOTE: no FIRMWARE_OTA_ENV on purpose. This is an experimental board with no release artifact on
# firmware.wadamesh.com yet, so the on-device update UI stays hidden (CAP_OTA 0 in device_caps.h).
+12
View File
@@ -0,0 +1,12 @@
dependencies:
idf: ">=5.5.1"
# Same component set as the T-Display P4 (esp-hosted 2.12.3 fork via tanmatsu-wifi, arduino-esp32
# for the shared Arduino src/, esp-nimble-cpp for BLE over the C6). badge-bsp is not initialised on
# this board; it is here because tanmatsu-wifi depends on it.
badgeteam/badge-bsp: "^0.9.9"
nicolaielectronics/tanmatsu-wifi: "^1.1.2"
nicolaielectronics/tanmatsu-lora: "^0.3.0"
espressif/arduino-esp32: "3.3.10"
h2zero/esp-nimble-cpp: "^2.3.0"
# EK79007 MIPI-DSI panel driver (Elecrow's lessons pin ^1.0.2).
espressif/esp_lcd_ek79007: "^1.0.2"
+238
View File
@@ -0,0 +1,238 @@
// wadamesh on the Elecrow CrowPanel Advanced 10.1" ESP32-P4 HMI (1024x600). EXPERIMENTAL.
//
// Standalone ESP-IDF app, modelled on tdisplay_p4/main/main.cpp (same P4 + C6 esp-hosted pairing,
// same storage law: hot store on the internal LittleFS 'storage' partition, the microSD for tiles and
// bulk). The board has no I/O expander, so bring-up is: on-chip LDO rails -> EK79007 panel -> radio
// -> storage -> UI, and the C6 last. Board globals + radio_init() live in
// variants/crowpanel_p4_101/target.cpp. Pin map: the table in variants/crowpanel_p4_101/target.h.
#include "../../variants/tdisplay_p4/tdisplay_p4_compat.h" // adcAttachPin() shim, BEFORE target.h
#include <Arduino.h>
#include <Mesh.h>
#include "MyMesh.h"
#include <new>
#include "esp_heap_caps.h"
#include "UITask.h"
#include "target.h"
#include <LittleFS.h>
#include <SD_MMC.h>
#include <WiFi.h>
#include "../../src/helpers/esp32/MultiTransportCompanionInterface.h"
#include <helpers/esp32/WifiRuntimeStore.h>
#include <helpers/esp32/TouchPrefsStore.h>
#include "../../src/helpers/esp32/BootTimeSync.h"
#include "esp_hosted.h"
#include "esp_netif.h"
#include "esp_event.h"
#include "driver/gpio.h"
#include "lvgl.h"
#ifndef TCP_PORT
#define TCP_PORT 5000
#endif
#ifndef WS_PORT
#define WS_PORT 8765
#endif
#if defined(CONFIG_BT_NIMBLE_TRANSPORT_UART)
#error "CrowPanel P4 C6 requires NimBLE hosted HCI; disable CONFIG_BT_NIMBLE_TRANSPORT_UART"
#endif
extern "C" bool hostedInitBLE(); // arduino-esp32 BLE controller bring-up over esp-hosted
extern "C" bool hostedSetPins(int8_t clk, int8_t cmd, int8_t d0, int8_t d1, int8_t d2, int8_t d3, int8_t rst);
// C6 SDIO pin set. Arduino's hostedInit() does NOT take these from sdkconfig: it passes the generic
// esp32p4 variant's BOARD_SDIO_ESP_HOSTED_* (CLK18 CMD19 D0-D3 = 14,15,16,17, RST 54) to
// esp_hosted_sdio_set_config(), which overrides Kconfig. That order happens to match the T-Display
// P4 and the CrowPanel V1.0, but V1.1/V1.2 route D0..D3 to GPIO17..14, and C6_EN is GPIO32 on every
// revision. Must run before anything can start esp-hosted (Wi-Fi, BLE, boot time sync).
// The bus width (4-bit, or 1-bit on V1.0) still comes from the sdkconfig fragment.
static void crowpanelC6Pins() {
#if CP_REV == 10
const bool ok = hostedSetPins(18, 19, 14, 15, 16, 17, 32);
#else
const bool ok = hostedSetPins(18, 19, 17, 16, 15, 14, 32);
#endif
printf("[BOOT] C6 SDIO pins set for V%d.%d: %s\n", CP_REV / 10, CP_REV % 10, ok ? "ok" : "FAILED");
}
// Boot-trace hooks the shared code defines on the S3 boards (src/main.cpp, excluded here).
volatile int g_boot_phase = 0;
extern "C" void set_boot_phase(int phase) { g_boot_phase = phase; }
// App globals: src/main.cpp declares these on the S3 boards; this build owns them.
DataStore store(LittleFS, rtc_clock);
bool g_fs_ok = false; // internal 'storage' (LittleFS) mounted
bool g_sd_ok = false; // microSD mounted
MultiTransportCompanionInterface serial_interface;
StdRNG fast_rng;
SimpleMeshTables tables;
UITask ui_task(&board, &serial_interface);
static MyMesh& makeTheMesh() {
void* mem = heap_caps_malloc(sizeof(MyMesh), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!mem) mem = malloc(sizeof(MyMesh));
return *new (mem) MyMesh(radio_driver, fast_rng, rtc_clock, tables, store, &ui_task);
}
MyMesh& the_mesh = makeTheMesh();
static void wadameshSetup() {
Serial.begin(115200);
delay(150);
printf("[BOOT] wadamesh / CrowPanel P4 10.1 (board revision V%d.%d, EXPERIMENTAL)\n", CP_REV / 10, CP_REV % 10);
// 1. On-chip LDO rails (DSI PHY + the 3.3 V pad domain of touch/LCD-reset/SD), backlight + amp off.
crowpanelP4PowerOn();
// 2. Shared I2C0 (GT911, codec, HY2.0 port) on 45/46.
board.begin();
// 3. EK79007 panel, before LVGL flushes to it.
if (!display.begin()) printf("[BOOT] EK79007 display init FAILED\n");
// 4. lwIP/tcpip + default event loop before any Wi-Fi/netif call (esp_wifi_remote needs it).
esp_netif_init();
esp_event_loop_create_default();
crowpanelC6Pins();
printf("[BOOT] C6 = esp-hosted (lazy Wi-Fi/BLE initialization)\n");
// 5. Radio (SX1262 on J9/J11).
if (!radio_init()) printf("[BOOT] radio_init FAILED\n");
DisplayDriver* disp = &display;
// 6. Storage: internal LittleFS 'storage' = the hot store; the microSD = tiles, backups, bulk.
g_fs_ok = LittleFS.begin(true, "/lfs", 10, "storage");
printf("[storage] LittleFS(storage) = %s\n", g_fs_ok ? "OK" : "FAILED");
if (g_fs_ok) { LittleFS.mkdir("/identity"); LittleFS.mkdir("/bl"); }
// microSD on SDMMC slot 0, 1-bit: the J5 slot only has D0 routed (CLK43 / CMD44 / D0 39), as in
// Elecrow's Lesson08. Pad power is LDO4, which crowpanelP4PowerOn() already holds, so SD_MMC must
// not try to claim it again (-1 = external power). The card has pull-ups on CMD/D0 (R27/R28); the
// build also enables the internal ones (build.sh patch), as Elecrow does.
SD_MMC.setPowerChannel(-1);
SD_MMC.setPins(43, 44, 39);
for (int p : {44, 39}) gpio_pullup_en((gpio_num_t)p);
{
struct { int khz; const char* tag; } tries[] = {
{ SDMMC_FREQ_DEFAULT, "1-bit 20MHz" },
{ 10000, "1-bit 10MHz (Elecrow)" },
{ 5000, "1-bit 5MHz" },
};
sdMountDiagBegin();
uint32_t mounted_hz = 0;
for (auto& t : tries) {
const bool begin_ok = SD_MMC.begin("/sdcard", true, false, t.khz);
g_sd_ok = begin_ok && SD_MMC.cardType() != CARD_NONE;
sdMountDiagAttempt((uint32_t)t.khz * 1000u, begin_ok, g_sd_ok);
printf("[storage] SD_MMC try %s -> %s\n", t.tag, g_sd_ok ? "OK" : "fail");
if (g_sd_ok) { mounted_hz = (uint32_t)t.khz * 1000u; break; }
SD_MMC.end();
delay(120);
}
sdMountDiagSetMounted(g_sd_ok, mounted_hz);
}
printf("[storage] SD_MMC = %s\n", g_sd_ok ? "OK" : "no card");
store.begin(); // internal LittleFS is the DataStore root (P4 storage law)
the_mesh.begin(disp != NULL);
wifiConfigBegin(); // NVS only, no radio
serial_interface.begin(Serial, TCP_PORT, WS_PORT);
serial_interface.setBroadcastResponses(true);
the_mesh.startInterface(serial_interface);
sensors.begin();
ui_task.begin(disp, &sensors, the_mesh.getNodePrefs());
board.onBootComplete();
printf("[BOOT] setup done\n");
// ---- C6 bring-up, AFTER the UI ----------------------------------------------------------------
// A C6 that does not answer (wrong firmware, wrong SDIO pin set for the revision) must cost Wi-Fi
// and BLE only: display, touch and LoRa are already up by now.
#if defined(BLE_PIN_CODE)
char* nm = the_mesh.getNodePrefs()->node_name;
serial_interface.prepareBle("wadamesh-", nm, the_mesh.getBLEPin());
if (wifiConfigGetBleEnabled()) {
if (hostedInitBLE()) serial_interface.beginBle("wadamesh-", nm, the_mesh.getBLEPin());
else printf("[BOOT] hostedInitBLE FAILED\n");
} else {
printf("[BOOT] BLE off in settings: C6 not started for it\n");
}
#endif
{
uint32_t synced_epoch = 0;
const BootTimeSyncResult r = bootTimeSyncRun(rtc_clock.timeIsCurrent(), synced_epoch);
if (r == BootTimeSyncResult::Ok) {
rtc_clock.setCurrentTime(synced_epoch);
printf("[BOOT] cold-boot time sync ok: %lu\n", (unsigned long)synced_epoch);
} else if (r != BootTimeSyncResult::Skipped) {
printf("[BOOT] cold-boot time sync: %s\n", bootTimeSyncResultName(r));
}
}
}
extern "C" void app_main(void) {
crowpanelP4InstallFullRestart(); // before anything else registers a shutdown handler
initArduino();
wadameshSetup();
// Wi-Fi state machine + SNTP + TCP/WS companion server, as in tdisplay_p4/main/main.cpp
// (esp-hosted backend: Arduino's real WiFi over esp_wifi_remote).
bool wifi_started = false, wifi_radio_prev = true, wifi_radio_inited = false;
bool sntp_kicked = false, sntp_pushed = false, modem_sleep_set = false;
uint32_t last_wifi_retry_ms = 0;
const uint32_t WIFI_RETRY_INTERVAL_MS = 10000;
for (;;) {
ui_task.loop();
bool wifi_radio_en = wifiConfigWantsWifi();
if (!wifi_radio_inited) {
wifi_radio_inited = true;
wifi_radio_prev = wifi_radio_en;
} else if (wifi_radio_en != wifi_radio_prev) {
wifi_radio_prev = wifi_radio_en;
if (!wifi_radio_en) { WiFi.disconnect(true); delay(50); WiFi.mode(WIFI_OFF); }
wifi_started = false;
}
if (wifiConfigConsumeApplyRequest()) {
if (wifi_started) {
if (!wifi_radio_en) { WiFi.disconnect(true); delay(50); WiFi.mode(WIFI_OFF); }
else { WiFi.disconnect(false, false); delay(50); }
}
wifi_started = false; last_wifi_retry_ms = 0;
}
if (wifi_radio_en) {
if (!wifi_started) {
wifi_started = true;
WiFi.mode(WIFI_STA);
if (wifiConfigHasRuntime()) {
char ssid[WIFI_CONFIG_SSID_MAX], pwd[WIFI_CONFIG_PWD_MAX];
wifiConfigGetSsid(ssid, sizeof(ssid)); wifiConfigGetPwd(pwd, sizeof(pwd));
if (strlen(ssid) > 0) { WiFi.begin(ssid, pwd[0] ? pwd : nullptr); last_wifi_retry_ms = millis(); }
}
}
if (wifiConfigHasRuntime() && WiFi.status() != WL_CONNECTED) {
uint32_t now = millis();
if ((uint32_t)(now - last_wifi_retry_ms) >= WIFI_RETRY_INTERVAL_MS) {
last_wifi_retry_ms = now;
char ssid[WIFI_CONFIG_SSID_MAX], pwd[WIFI_CONFIG_PWD_MAX];
wifiConfigGetSsid(ssid, sizeof(ssid)); wifiConfigGetPwd(pwd, sizeof(pwd));
if (strlen(ssid) > 0) { WiFi.disconnect(false, true); WiFi.begin(ssid, pwd[0] ? pwd : nullptr); }
}
}
if (WiFi.status() == WL_CONNECTED) {
if (!modem_sleep_set) { WiFi.setSleep(true); modem_sleep_set = true; }
serial_interface.startTcpServer(true);
if (!sntp_pushed) {
if (!sntp_kicked) { configTime(0, 0, "pool.ntp.org", "time.nist.gov"); sntp_kicked = true; }
const uint32_t e = (uint32_t)time(nullptr);
if (e > 1700000000) { rtc_clock.setCurrentTime(e); sntp_pushed = true; printf("[TIME] SNTP -> rtc %lu\n", (unsigned long)e); }
}
}
}
serial_interface.tickWebSocketHandshake();
sensors.loop();
the_mesh.loop();
vTaskDelay(1);
}
}
@@ -0,0 +1,12 @@
# Elecrow CrowPanel P4 10.1: standalone 16 MB partition table (same layout as the T-Display P4).
# NOT an AppFS/launcher app (that's the Tanmatsu): this flashes bootloader+app directly.
# Dual OTA app slots (4 MB each; the P4 wadamesh image is ~2.5-3 MB) + a big FAT for
# identity/prefs/contacts/chat history + map tiles.
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 24K,
otadata, data, ota, 0xF000, 8K,
phy_init, data, phy, 0x11000, 4K,
ota_0, app, ota_0, 0x20000, 4M,
ota_1, app, ota_1, 0x420000, 4M,
storage, data, fat, 0x820000, 8000K,
coredump, data, coredump, 0xFF0000, 64K,
1 # Elecrow CrowPanel P4 10.1: standalone 16 MB partition table (same layout as the T-Display P4).
2 # NOT an AppFS/launcher app (that's the Tanmatsu): this flashes bootloader+app directly.
3 # Dual OTA app slots (4 MB each; the P4 wadamesh image is ~2.5-3 MB) + a big FAT for
4 # identity/prefs/contacts/chat history + map tiles.
5 # Name, Type, SubType, Offset, Size, Flags
6 nvs, data, nvs, 0x9000, 24K,
7 otadata, data, ota, 0xF000, 8K,
8 phy_init, data, phy, 0x11000, 4K,
9 ota_0, app, ota_0, 0x20000, 4M,
10 ota_1, app, ota_1, 0x420000, 4M,
11 storage, data, fat, 0x820000, 8000K,
12 coredump, data, coredump, 0xFF0000, 64K,
@@ -0,0 +1,58 @@
# Elecrow CrowPanel Advanced 10.1" ESP32-P4 HMI (1024x600) board sdkconfig fragment. EXPERIMENTAL.
# Modelled on sdkconfigs/tdisplay_p4 (same ESP32-P4NRW32 + ESP32-C6-MINI-1 esp-hosted pairing). The
# C6 SDIO pin/width set lives in a per-revision fragment (sdio_v10 / sdio_v11), layered after this.
CONFIG_IDF_TARGET="esp32p4"
# 32 MB HEX PSRAM @ 200 MHz (in-package on the P4NRW32; Elecrow's V1.0/V1.1 lessons run HEX 200M).
# 200 MHz is behind CONFIG_IDF_EXPERIMENTAL_FEATURES on IDF 5.5; without it Kconfig drops to 20 MHz
# and the DPI panel underruns fetching its PSRAM framebuffer.
CONFIG_SPIRAM=y
CONFIG_IDF_EXPERIMENTAL_FEATURES=y
CONFIG_SPIRAM_SPEED_200M=y
CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=65536
# 16 MB W25Q128 + our standalone dual-OTA partition table (same layout as the T-Display P4).
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partition_tables/crowpanel_p4_101_16M.csv"
# MIPI-DSI (EK79007)
CONFIG_LCD_DSI_ISR_IRAM_SAFE=y
# ESP32-C6-MINI-1 co-processor over SDIO slot 1 (CLK18 / CMD19; data pins per revision fragment).
# Factory C6 firmware is esp-hosted MCU 2.12.3 (Elecrow "Advance_P4_On-Board_ESP32C6_Firmware_V2.12.3"),
# the same version as the hosted fork tanmatsu-wifi pulls in.
CONFIG_SLAVE_IDF_TARGET_ESP32C6=y
CONFIG_ESP_HOSTED_CP_TARGET_ESP32C6=y
CONFIG_ESP_HOSTED_SDIO_CLOCK_FREQ_KHZ=40000
# C6_EN = GPIO32 (schematic, all revisions). Reset through hosted_reset_slave_callback() in
# variants/crowpanel_p4_101/target.cpp, so the polarity and the GPIO are explicit in code.
CONFIG_ESP_HOSTED_SLAVE_RESET_ONLY_IF_NECESSARY=y
CONFIG_ESP_HOSTED_RESET_SLAVE_USING_CALLBACK=y
CONFIG_ESP_HOSTED_SDIO_RESET_SLAVE_USING_CALLBACK=y
CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE=y
# The P4 has no Bluetooth controller of its own: NimBLE's HCI runs over esp-hosted SDIO.
CONFIG_BT_ENABLED=y
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_CONTROLLER_DISABLED=y
# CONFIG_BT_NIMBLE_TRANSPORT_UART is not set
# A C6 that cannot be reached must cost Wi-Fi and BLE, never the device (see sdkconfigs/tdisplay_p4).
CONFIG_ESP_HOSTED_HOST_RESTART_NO_COMMUNICATION_WITH_SLAVE=n
CONFIG_ESP_HOSTED_TRANSPORT_RESTART_ON_FAILURE=n
# Panic coredump -> the 64K 'coredump' partition (ELF, decodable against the build's elf).
CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH=y
CONFIG_ESP_COREDUMP_DATA_FORMAT_ELF=y
CONFIG_ESP_COREDUMP_CHECKSUM_CRC32=y
CONFIG_ESP_COREDUMP_MAX_TASKS_NUM=64
# DPI/video panel: the 1.2 MB framebuffer lives in PSRAM and is streamed continuously, so the same
# underrun mitigations as the T-Display P4: run code from PSRAM (flash writes no longer drop the
# cache under the DSI DMA), a 256 KB L2 cache with 128 B lines, -O2.
CONFIG_SPIRAM_XIP_FROM_PSRAM=y
CONFIG_COMPILER_OPTIMIZATION_SIZE=n
CONFIG_COMPILER_OPTIMIZATION_PERF=y
CONFIG_CACHE_L2_CACHE_128KB=n
CONFIG_CACHE_L2_CACHE_256KB=y
CONFIG_CACHE_L2_CACHE_LINE_64B=n
CONFIG_CACHE_L2_CACHE_LINE_128B=y
+1
View File
@@ -0,0 +1 @@
../../tanmatsu/sdkconfigs/general
+9
View File
@@ -0,0 +1,9 @@
# CrowPanel P4 10.1 V1.0: the C6 data lines run the other way round (schematic: GPIO14 = SD2_D0 ->
# C6 IO20 ... GPIO17 = SD2_D3 -> C6 IO23), and V1.0 has none of the series resistors / caps V1.1
# added on those lines. Elecrow's V1.0 Lesson17 runs the link 1-bit (D0 = GPIO14, D1 = GPIO15 as the
# interrupt line), so this does the same.
CONFIG_ESP_HOSTED_SDIO_1_BIT_BUS=y
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CLK_SLOT_1=18
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CMD_SLOT_1=19
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D0_SLOT_1=14
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_1BIT_BUS_SLOT_1=15
+10
View File
@@ -0,0 +1,10 @@
# CrowPanel P4 10.1 V1.1 and V1.2: C6 SDIO 4-bit, P4 GPIO17/16/15/14 = D0/D1/D2/D3.
# Source: V1.1/V1.2 schematics (net SD2_D0 = GPIO17 -> C6 IO20 ... SD2_D3 = GPIO14 -> C6 IO23) and
# Elecrow's V1.1/V1.2 Lesson17 sdkconfig (4-bit, same pins).
CONFIG_ESP_HOSTED_SDIO_4_BIT_BUS=y
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CLK_SLOT_1=18
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CMD_SLOT_1=19
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D0_SLOT_1=17
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_4BIT_BUS_SLOT_1=16
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D2_4BIT_BUS_SLOT_1=15
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D3_4BIT_BUS_SLOT_1=14
+1
View File
@@ -0,0 +1 @@
../../tanmatsu/sdkconfigs/wadamesh
+6
View File
@@ -0,0 +1,6 @@
{
"id": "airtime",
"name": "Airtime",
"ver": "1.5",
"desc": "Utilization bars with a % axis, rx/tx split, duty ceiling, budget. Reset button top-right. Keeps a Home widget with the channel load while it runs."
}
+105
View File
@@ -0,0 +1,105 @@
-- Airtime (Lua) — channel utilization over time: live + average bars with a %
-- axis, rx/tx air split, duty ceiling, tx budget and packet counters. While it
-- runs it also keeps its Home widget current (wada.widget, extended SDK boards).
local ui, sys, mesh, timer = wada.ui, wada.sys, wada.mesh, wada.timer
local widget = wada.widget -- nil on firmware or boards without it
-- Translate through the device's language table (wada.sys.tr, added for #257).
-- Older firmware has no sys.tr, so fall back to the English string rather than error.
local tr = sys.tr or function(x) return x end
local C = ui.colors
local app = {}
local W, narrow
local head, sub, legend, detail, chart
local base, t0, last
local peak_pct = 0
local spark, wlast = {}, 0 -- the Home widget's curve (one point per 5 s)
local function restart_window()
local st = mesh.stats()
base = { rx = st.rx_air_s, tx = st.tx_air_s }
t0 = sys.millis()
last = { ms = t0, rx = st.rx_air_s, tx = st.tx_air_s }
peak_pct = 0
chart:fill(1, 0)
chart:fill(2, 0)
head:set(tr("sampling..."))
head:color(C.accent)
sys.toast(tr("window restarted"), 900)
end
local function refresh()
local st = mesh.stats()
local now = sys.millis()
local wall = (now - t0) / 1000
if wall < 0.5 then return end
local rx_air, tx_air = st.rx_air_s - base.rx, st.tx_air_s - base.tx
local avg = math.min(100, (rx_air + tx_air) * 100 / wall)
local dw = (now - last.ms) / 1000
local live = 0
if dw > 0 then
live = math.min(100, ((st.rx_air_s - last.rx) + (st.tx_air_s - last.tx)) * 100 / dw)
end
last = { ms = now, rx = st.rx_air_s, tx = st.tx_air_s }
if live > peak_pct then peak_pct = live end
chart:push(1, math.floor(live))
chart:push(2, math.floor(avg))
head:set(string.format(tr("channel busy %.1f%%"), live))
head:color(live > 50 and C.bad or live > 20 and 0xE8A33D or C.good)
sub:set(string.format(tr("avg %.1f%% peak %.1f%% over %ds"), avg, peak_pct, math.floor(wall)))
if widget and now - wlast >= 5000 then
wlast = now
spark[#spark + 1] = math.floor(live * 10)
if #spark > 24 then table.remove(spark, 1) end
widget.set{ title = tr("Channel busy"), value = string.format("%.1f%%", live),
line = string.format(tr("avg %.1f%%, peak %.1f%%"), avg, peak_pct), spark = spark }
end
if narrow then
detail:set(string.format("air rx %ds / tx %ds\nduty %d%% budget %dms\ntx %d rx %d err %d",
rx_air, tx_air, st.duty_pct, st.tx_budget_ms, st.tx_pkts, st.rx_pkts, st.rx_err))
else
detail:set(string.format("air rx %ds / tx %ds duty %d%% budget %dms\ntx %d rx %d err %d drop %d SF%d",
rx_air, tx_air, st.duty_pct, st.tx_budget_ms, st.tx_pkts, st.rx_pkts, st.rx_err, st.rx_dropped, st.sf))
end
end
function app.on_open(w, h)
W, narrow = w, w < 280
local LH12, LH16 = ui.text_h(12), ui.text_h(16)
local y = 2
head = ui.label(tr("sampling..."), 4, y, 16, C.accent); head:width(w - 86)
ui.button(tr("Reset"), w - 76, y, 70, 30, function() restart_window() end) -- top-right
y = y + LH16 + 4
sub = ui.label("", 4, y, 12, C.sub); sub:width(w - 10)
y = y + LH12 + 5
local AX = 34
local det_lines = narrow and 3 or 2
local ch_h = h - y - (det_lines * LH12) - LH12 - 14
if ch_h < 48 then ch_h = 48 end
if ch_h > 110 then ch_h = 110 end
chart = ui.chart(w - AX - 8, ch_h, 60, C.accent, 0x4F9DF7, true)
chart:pos(AX, y)
chart:range(0, 100)
chart:axis(3, AX)
y = y + ch_h + 4
legend = ui.label(tr("teal = now blue = average"), 4, y, 12, C.sub); legend:width(w - 10)
y = y + LH12 + 2
detail = ui.label("", 4, y, 12, C.text); detail:width(w - 10)
local st = mesh.stats()
base = { rx = st.rx_air_s, tx = st.tx_air_s }
t0 = sys.millis()
last = { ms = t0, rx = st.rx_air_s, tx = st.tx_air_s }
timer.every(1000)
end
function app.on_tick(dt) refresh() end
return app
+1
View File
@@ -0,0 +1 @@
{"id":"airtime","name":"Airtime","ver":"1.6","desc":"How busy the channel is: the live load, average, peak, a filled chart with the duty ceiling and the rx/tx split. Keeps a Home widget with the channel load while it runs."}
+243
View File
@@ -0,0 +1,243 @@
-- Airtime (Lua): how busy the channel is. The live load as a big figure, tiles
-- for the average, the peak and this node's own time on the air, a filled
-- chart of the load with its average and the duty-cycle ceiling, and the
-- receive / transmit split as a bar. While it runs it also keeps its Home
-- widget current (wada.widget, extended SDK boards).
--
-- Looks best on firmware with wada.ui.panel (beta_95 and later); on older
-- firmware the same screen draws without the framed panels and the split bar.
local ui, sys, mesh, timer = wada.ui, wada.sys, wada.mesh, wada.timer
local widget = wada.widget -- nil on firmware or boards without it
-- Translate through the device's language table (wada.sys.tr, added for #257).
-- Older firmware has no sys.tr, so fall back to the English string rather than error.
local tr = sys.tr or function(x) return x end
local C = ui.colors
local WARN = C.warn or 0xE8A33D
local PANEL = C.panel or 0x15181B
local AVG = 0x4F9DF7
local app = {}
local function mix(a, b, t) -- colour a over b by t (0..1)
local function ch(s)
local x, y = (a >> s) & 0xFF, (b >> s) & 0xFF
return math.floor(y + (x - y) * t + 0.5) << s
end
return ch(16) | ch(8) | ch(0)
end
local AREA = { mix(C.accent, PANEL, 0.50), mix(C.accent, PANEL, 0.30), mix(C.accent, PANEL, 0.14) }
local GRID = mix(C.sub, PANEL, 0.30)
local W, H, narrow, LH12, LH16
local big, sub, tiles, cv, CW, CH, NPTS = nil, nil, {}, nil, 0, 0, 2
local grid_lbl = {}
local split_rx, split_tx, split_w, split_lbl, foot
local split_x0, split_y = 0, 0
local base, t0, last
local peak_pct = 0
local live_h, avg_h = {}, {} -- chart history, percent
local top = 10 -- chart ceiling, percent
local spark, wlast = {}, 0 -- the Home widget's curve (one point per 5 s)
local function panel(x, y, w, h, col)
if ui.panel then return ui.panel(x, y, w, h, col or PANEL, 6) end
end
local function load_col(p)
return p > 50 and C.bad or p > 20 and WARN or C.good
end
local function nice_top(v) -- the chart ceiling: 10, 20, 30, 50 or 100 percent
for _, t in ipairs({ 10, 20, 30, 50, 100 }) do if v <= t then return t end end
return 100
end
local function ypix(p)
if p < 0 then p = 0 elseif p > top then p = top end
return (CH - 1) - math.floor(p * (CH - 1) / top)
end
local function draw_chart(duty)
if not cv then return end
cv:fill(PANEL)
for g = 0, 2 do
local gy = g * (CH - 1) // 2
cv:line(0, gy, CW - 1, gy, GRID)
end
local n = #live_h
local b1, b2 = CH // 3, 2 * CH // 3
for i = 1, n do
local x = (NPTS - n + i - 1) * (CW - 1) // (NPTS - 1)
local x2 = (NPTS - n + i) * (CW - 1) // (NPTS - 1)
if x2 <= x then x2 = x + 1 end
local y = ypix(live_h[i])
if y < b1 then cv:rect(x, y, x2 - x, b1 - y, AREA[1]); y = b1 end
if y < b2 then cv:rect(x, y, x2 - x, b2 - y, AREA[2]); y = b2 end
if y < CH then cv:rect(x, y, x2 - x, CH - y, AREA[3]) end
end
local px, py, qx, qy
for i = 1, n do
local x = (NPTS - n + i - 1) * (CW - 1) // (NPTS - 1)
local y, ya = ypix(live_h[i]), ypix(avg_h[i])
if px then
cv:line(px, py, x, y, C.accent, 2)
cv:line(qx, qy, x, ya, AVG, 2)
end
px, py, qx, qy = x, y, x, ya
end
-- the duty-cycle ceiling, dashed, when the region has one and it is on the scale
if duty and duty > 0 and duty <= top then
local dy = ypix(duty)
for x = 0, CW - 1, 8 do cv:line(x, dy, math.min(x + 4, CW - 1), dy, WARN) end
end
end
local function set_split(rx, tx)
local tot = rx + tx
local rp = tot > 0 and math.floor(rx * 100 / tot + 0.5) or 0
local txt = tot > 0 and string.format(tr("rx %d%% tx %d%%"), rp, 100 - rp) or tr("no airtime yet")
if not split_rx then
if split_lbl then split_lbl:set(txt) end
return
end
local wr = tot > 0 and math.floor(split_w * rx / tot + 0.5) or 0
if tot > 0 and tx > 0 and wr >= split_w then wr = split_w - 1 end
split_rx:size(math.max(wr, 1), 8)
split_tx:pos(split_x0 + wr, split_y)
split_tx:size(math.max(split_w - wr, 1), 8)
split_tx:color(tot > 0 and tx > 0 and AVG or mix(C.sub, PANEL, 0.25))
if tot == 0 then split_rx:color(mix(C.sub, PANEL, 0.25)) else split_rx:color(C.accent) end
split_lbl:set(txt)
end
local function restart_window()
local st = mesh.stats()
base = { rx = st.rx_air_s, tx = st.tx_air_s }
t0 = sys.millis()
last = { ms = t0, rx = st.rx_air_s, tx = st.tx_air_s }
peak_pct = 0
live_h, avg_h = {}, {}
top = 10
big:set("--"); big:color(C.accent)
sys.toast(tr("window restarted"), 900)
end
local function refresh()
local st = mesh.stats()
local now = sys.millis()
local wall = (now - t0) / 1000
if wall < 0.5 then return end
local rx_air, tx_air = st.rx_air_s - base.rx, st.tx_air_s - base.tx
local avg = math.min(100, (rx_air + tx_air) * 100 / wall)
local dw = (now - last.ms) / 1000
local live = 0
if dw > 0 then
live = math.min(100, ((st.rx_air_s - last.rx) + (st.tx_air_s - last.tx)) * 100 / dw)
end
last = { ms = now, rx = st.rx_air_s, tx = st.tx_air_s }
if live > peak_pct then peak_pct = live end
live_h[#live_h + 1] = live; if #live_h > NPTS then table.remove(live_h, 1) end
avg_h[#avg_h + 1] = avg; if #avg_h > NPTS then table.remove(avg_h, 1) end
local hi = st.duty_pct or 0
for i = 1, #live_h do if live_h[i] > hi then hi = live_h[i] end end
local t = nice_top(hi * 1.15)
if t ~= top then
top = t
local v = { top, top // 2, 0 }
for g = 1, 3 do if grid_lbl[g] then grid_lbl[g]:set(v[g] .. "%") end end
end
draw_chart(st.duty_pct)
big:set(string.format("%.1f%%", live)); big:color(load_col(live))
local m = math.floor(wall)
sub:set(string.format(tr("over %d:%02d"), m // 60, m % 60))
tiles[1]:set(string.format("%.1f%%", avg))
tiles[2]:set(string.format("%.1f%%", peak_pct)); tiles[2]:color(load_col(peak_pct))
tiles[3]:set(string.format("%d s", tx_air))
tiles[4]:set(string.format("%d s", rx_air))
set_split(rx_air, tx_air)
foot:set(string.format(narrow and tr("budget %d s tx %d rx %d") or tr("budget %d s tx %d rx %d err %d duty %d%% SF%d"),
st.tx_budget_ms // 1000, st.tx_pkts, st.rx_pkts, st.rx_err, st.duty_pct, st.sf))
if widget and now - wlast >= 5000 then
wlast = now
spark[#spark + 1] = math.floor(live * 10)
if #spark > 24 then table.remove(spark, 1) end
widget.set{ title = tr("Channel busy"), value = string.format("%.1f%%", live),
line = string.format(tr("avg %.1f%%, peak %.1f%%"), avg, peak_pct), spark = spark }
end
end
function app.on_open(w, h)
W, H, narrow = w, h, w < 280
LH12, LH16 = ui.text_h(12), ui.text_h(16)
local LHB = ui.text_h(24)
-- the live figure, what it means, and Reset
local y = 4
big = ui.label("--", 6, y, 24, C.accent, true)
local bw = (ui.text_w and ui.text_w("100.0%", 24) or 70) + 10
local l1 = ui.label(tr("channel busy now"), 6 + bw, y + 2, 12, C.sub); l1:width(W - bw - 90)
sub = ui.label("", 6 + bw, y + 2 + LH12, 12, C.sub); sub:width(W - bw - 90)
ui.button(tr("Reset"), W - 76, y + 1, 70, math.max(28, LHB - 2), function() restart_window() end)
y = y + math.max(LHB, 2 * LH12 + 4, 30) + 6
local gap = 4
local labels = { tr("Average"), tr("Peak"), tr("TX"), tr("RX") }
local tw = (W - 8 - gap * 3) // 4
local th = LH12 + LH16 + 8
for i, k in ipairs(labels) do
local x = 4 + (i - 1) * (tw + gap)
panel(x, y, tw, th)
local kl = ui.label(k, x + 6, y + 3, 12, C.sub); kl:width(tw - 8)
tiles[i] = ui.label("--", x + 6, y + 3 + LH12, 16, C.text, true)
end
y = y + th + 6
-- the chart, with % labels in the gutter
local AX = (ui.text_w and ui.text_w("100%", 12) or 30) + 8
local foot_h = LH12 + 4
local split_h = LH12 + 6
CH = H - y - split_h - foot_h - 18
if CH > 120 then CH = 120 end
if CH < 40 then CH = 40 end
panel(AX, y, W - AX - 4, CH + 6)
CW = W - AX - 10
cv = ui.canvas(CW, CH); cv:pos(AX + 3, y + 3)
NPTS = math.max(CW // 2, 2)
local v = { top, top // 2, 0 }
for g = 0, 2 do
local l = ui.label(v[g + 1] .. "%", 0, y + 3 + g * (CH - 1) // 2 - LH12 // 2, 12, C.sub)
l:width(AX - 6, "right")
grid_lbl[g + 1] = l
end
y = y + CH + 6 + 6
-- the receive / transmit split
ui.label(tr("Air split"), 6, y, 12, C.sub)
local kw = (ui.text_w and ui.text_w(tr("Air split"), 12) or 60) + 10
local lw = (ui.text_w and ui.text_w("rx 100% tx 100%", 12) or 110) + 4
split_x0, split_y = 6 + kw, y + (LH12 - 8) // 2
split_w = W - split_x0 - lw - 12
if ui.panel and split_w > 20 then
ui.panel(split_x0, split_y, split_w, 8, mix(C.sub, PANEL, 0.25), 4)
split_rx = ui.panel(split_x0, split_y, 1, 8, C.accent, 4)
split_tx = ui.panel(split_x0 + split_w - 1, split_y, 1, 8, AVG, 4)
split_lbl = ui.label("", W - lw - 6, y, 12, C.text); split_lbl:width(lw, "right")
else
split_lbl = ui.label("", 6 + kw, y, 12, C.text); split_lbl:width(W - kw - 12)
end
y = y + split_h
foot = ui.label("", 6, y, 12, C.sub); foot:width(W - 12)
local st = mesh.stats()
base = { rx = st.rx_air_s, tx = st.tx_air_s }
t0 = sys.millis()
last = { ms = t0, rx = st.rx_air_s, tx = st.tx_air_s }
timer.every(1000)
end
function app.on_tick(dt) refresh() end
return app
+69 -6
View File
@@ -3,14 +3,14 @@
{
"id": "monitor",
"name": "RF Monitor",
"ver": "1.3",
"desc": "Scrollable: RSSI/noise chart with dBm axis, link margin, RX rate, radio params, heard feed."
"ver": "1.4",
"desc": "RSSI, noise, margin and packet-rate tiles, a filled signal chart and the recently heard packets. Tap a packet for who sent it, its route, margin, size and airtime."
},
{
"id": "airtime",
"name": "Airtime",
"ver": "1.4",
"desc": "Utilization bars with a % axis, rx/tx split, duty ceiling, budget. Reset button top-right."
"ver": "1.6",
"desc": "How busy the channel is: the live load, average, peak, a filled chart with the duty ceiling and the rx/tx split. Keeps a Home widget with the channel load while it runs."
},
{
"id": "snake",
@@ -18,17 +18,33 @@
"ver": "1.0",
"desc": "The classic, in Lua. Swipe to steer."
},
{
"id": "tetris",
"name": "Tetris",
"ver": "1.0",
"desc": "The classic falling-block puzzle. Swipe left/right to move, up to rotate, down to hard-drop. Contributed by samuelcoustet.",
"seed": false
},
{
"id": "protreck",
"name": "ProTreck",
"ver": "1.0",
"desc": "Outdoor watch: sunrise/sunset + moon phase (ASTRO), chronometer with laps (CHRONO), countdown timer with alarm (TIMER), altimeter with trend graph (ALTI). Swipe left/right to switch modes. Contributed by samuelcoustet.",
"seed": false
},
{
"id": "sdktest",
"name": "SDK Test",
"ver": "1.7",
"desc": "Developer tool. Checks the extended Lua SDK on this board: capabilities, clock, battery, GPS, private file access, audio API, read-only SD listing, crypto against published RFC vectors, channel discovery, and buttons that TRANSMIT test messages."
"desc": "Developer tool. Checks the extended Lua SDK on this board: capabilities, clock, battery, GPS, private file access, audio API, read-only SD listing, crypto against published RFC vectors, channel discovery, and buttons that TRANSMIT test messages.",
"requires": "sdk_ext"
},
{
"id": "2048",
"name": "2048",
"ver": "1.2",
"desc": "The classic sliding-tile puzzle. Swipe to move, tap to restart. Keeps your best score. Contributed by pisti87."
"desc": "The classic sliding-tile puzzle. Swipe to move, tap to restart. Keeps your best score. Contributed by pisti87.",
"seed": false
},
{
"id": "wardrive",
@@ -58,6 +74,53 @@
"desc": "Finds and removes Windows malware on the SD card, like the M9 worm.",
"requires": "sd",
"seed": false
},
{
"id": "tripodometer",
"name": "Trip Odometer",
"ver": "1.0",
"desc": "GPS trip computer: distance, time, speed, altitude. Start/Stop to track, Reset to clear. Saves your trip across restarts. Contributed by samuelcoustet.",
"requires": "gps",
"seed": false
},
{
"id": "breakout",
"name": "Breakout",
"ver": "1.0",
"desc": "Classic brick-breaker. Swipe left/right to move the paddle, swipe up to launch the ball. Clear all bricks to advance levels. Contributed by samuelcoustet.",
"seed": false
},
{
"id": "ping",
"name": "Ping",
"ver": "1.0",
"desc": "Send a mesh DM ping to any contact and measure round-trip time. Includes your GPS position so both sides see distance and bearing. Auto-replies to incoming pings. Contributed by samuelcoustet.",
"requires": "sdk_ext"
},
{
"id": "mecp",
"name": "MECP",
"ver": "1.0",
"desc": "Emergency messages in MECP code: pick severity and codes, add people and position, post to a channel. Decodes MECP lines into words. Code table: MECP by xiang-dev-1, CC BY 4.0.",
"requires": "sdk_ext",
"seed": false
},
{
"id": "music",
"name": "Music",
"ver": "1.0",
"desc": "MP3 and WAV from the SD card's Music folder, shuffle, repeat.",
"icon": "music",
"requires": "audio",
"seed": false
},
{
"id": "blackjack",
"name": "Blackjack",
"ver": "1.0",
"desc": "Casino Blackjack (21) with split, double down, 6-deck shoe, persistent bankroll, and T-Deck/T-Display P4 controls. Contributed by halflive-743.",
"icon": "game",
"seed": false
}
]
}
+1
View File
@@ -0,0 +1 @@
{"id":"blackjack","name":"Blackjack","ver":"1.0","desc":"Casino Blackjack (21) with split, double down, 6-deck shoe, persistent bankroll, and auto-scaling UI for T-Deck & T-Display P4 (AMOLED/LCD).","icon":"game"}
File diff suppressed because it is too large. Load diff
+1
View File
@@ -0,0 +1 @@
{"id":"breakout","name":"Breakout","ver":"1.0","desc":"Classic brick-breaker. Swipe left/right to move the paddle, swipe up to launch the ball. Clear all bricks to advance levels. Contributed by samuelcoustet."}
+188
View File
@@ -0,0 +1,188 @@
-- Breakout — wadamesh Lua app
-- Swipe left/right to move paddle | Swipe up to launch
-- Keyboard: arrows to move, Enter / Space to launch
-- Contributed by samuelcoustet
local ui, sys, store, timer = wada.ui, wada.sys, wada.store, wada.timer
local C = ui.colors
local app = {}
local COLS_B, ROWS_B = 8, 5
local BRICK_H, BRICK_GAP = 10, 2
local PADDLE_H, PADDLE_W = 7, 44
local BALL_R = 5
local TOP_H = 18 -- score strip height
local PAD_OFF = 22 -- paddle distance from bottom
local COLORS = { 0xff3355, 0xff7722, 0xffdd33, 0x22cc77, 0x3399ff }
local PTS = { 7, 5, 4, 3, 1 }
local cv, W, H, brick_w
local bricks, n_alive
local bx, by, bdx, bdy, speed
local paddle_x
local score, hiscore, lives, level
local launched, over
local function bk_x(c) return BRICK_GAP + (c-1)*(brick_w + BRICK_GAP) end
local function bk_y(r) return TOP_H + 4 + (r-1)*(BRICK_H + BRICK_GAP) end
local function pad_y() return H - PAD_OFF end
local function new_bricks()
bricks = {}; n_alive = 0
for r = 1, ROWS_B do
bricks[r] = {}
for c = 1, COLS_B do bricks[r][c] = true; n_alive = n_alive + 1 end
end
end
local function reset_ball()
launched = false
bx = paddle_x
by = pad_y() - BALL_R - 1
bdx = 0; bdy = 0
end
local function launch_ball()
local a = sys.random(-40, 40) * math.pi / 180.0
bdx = speed * math.sin(a)
bdy = -speed * math.cos(a)
launched = true
end
local function draw_all()
cv:fill(0x0a0e14)
cv:text(6, 3, string.format("Score:%d Best:%d Lv:%d", score, hiscore, level), C.text, 11)
for i = 1, lives do
cv:circle(W - 6 - (i-1)*13, TOP_H // 2, 4, C.good, true)
end
for r = 1, ROWS_B do
for c = 1, COLS_B do
if bricks[r][c] then
cv:rect(bk_x(c), bk_y(r), brick_w, BRICK_H, COLORS[r], true, 2)
end
end
end
cv:rect(paddle_x - PADDLE_W // 2, pad_y(), PADDLE_W, PADDLE_H, C.accent, true, 3)
cv:circle(math.floor(bx), math.floor(by), BALL_R, 0xffffff, true)
if not launched and not over then
cv:text(W // 2 - 40, pad_y() - 20, "swipe up to launch", C.sub, 11)
end
if over then
cv:rect(W // 2 - 62, H // 2 - 22, 124, 44, 0x141a22, true, 6)
cv:text(W // 2 - 44, H // 2 - 14, "GAME OVER", C.bad, 14)
cv:text(W // 2 - 34, H // 2 + 4, "tap to restart", C.sub, 11)
end
end
local function check_bricks()
for r = 1, ROWS_B do
for c = 1, COLS_B do
if bricks[r][c] then
local x1, y1 = bk_x(c), bk_y(r)
local ox = math.min(bx + BALL_R - x1, x1 + brick_w - (bx - BALL_R))
local oy = math.min(by + BALL_R - y1, y1 + BRICK_H - (by - BALL_R))
if ox > 0 and oy > 0 then
bricks[r][c] = false; n_alive = n_alive - 1
score = score + PTS[r] * level
if ox < oy then bdx = -bdx else bdy = -bdy end
return
end
end
end
end
end
local function step()
if over or not launched then return end
bx = bx + bdx
by = by + bdy
-- walls
if bx - BALL_R < 0 then bx = BALL_R; bdx = math.abs(bdx) end
if bx + BALL_R > W then bx = W - BALL_R; bdx = -math.abs(bdx) end
if by - BALL_R < TOP_H then by = TOP_H + BALL_R; bdy = math.abs(bdy) end
-- paddle
local py = pad_y()
if bdy > 0 and by + BALL_R >= py and by + BALL_R <= py + PADDLE_H + speed + 1 then
local half = PADDLE_W * 0.5
if bx >= paddle_x - half - BALL_R and bx <= paddle_x + half + BALL_R then
local rel = math.max(-1.0, math.min(1.0, (bx - paddle_x) / half))
bdx = speed * rel * 0.8
local sq = speed * speed - bdx * bdx
bdy = -(sq > 0 and math.sqrt(sq) or speed * 0.5)
by = py - BALL_R - 1.0
end
end
-- bricks
check_bricks()
-- ball lost
if by - BALL_R > H + 10 then
lives = lives - 1
if lives <= 0 then
if score > hiscore then hiscore = score; store.set("hiscore", hiscore) end
over = true; launched = false; return
end
paddle_x = W // 2; reset_ball()
end
-- level won
if n_alive == 0 then
level = level + 1
speed = math.min(speed + 0.6, 9.0)
new_bricks(); reset_ball()
end
end
local function move_paddle(dx)
paddle_x = math.max(PADDLE_W // 2 + 2, math.min(W - PADDLE_W // 2 - 2, paddle_x + dx))
if not launched then bx = paddle_x end
draw_all()
end
function app.on_open(w, h)
W, H = w, h
hiscore = store.get("hiscore", 0)
brick_w = math.floor((W - (COLS_B + 1) * BRICK_GAP) / COLS_B)
cv = ui.canvas(W, H); cv:pos(0, 0)
score = 0; lives = 3; level = 1; speed = 3.5; over = false
paddle_x = W // 2
new_bricks(); reset_ball()
draw_all()
timer.every(33)
end
function app.on_input(ev)
if over then
if ev.type == "down" or (ev.type == "key" and ev.key == "enter") then
over = false; score = 0; lives = 3; level = 1; speed = 3.5
paddle_x = W // 2; new_bricks(); reset_ball(); draw_all()
end
return
end
if ev.type == "swipe" then
local d = ev.dir
if d == "left" then move_paddle(-32)
elseif d == "right" then move_paddle( 32)
elseif d == "up" and not launched then launch_ball(); draw_all()
end
elseif ev.type == "key" then
local k = ev.key
if k == "left" then move_paddle(-20)
elseif k == "right" then move_paddle( 20)
elseif (k == "up" or k == "enter" or k == " ") and not launched then
launch_ball(); draw_all()
end
elseif ev.type == "down" then
if ev.x < W // 3 then move_paddle(-32)
elseif ev.x > (W * 2) // 3 then move_paddle( 32)
elseif not launched then launch_ball(); draw_all()
end
end
end
function app.on_tick(dt)
step(); draw_all()
end
function app.on_close() end
return app
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
Loaded 100 of 461 files, more files were not shown because too many files have changed in this diff. Show more