Compare commits

...
275 Commits
Author SHA1 Message Date
Kaj SchittecatandClaude Opus 4.8 4df391a283 touch: beta_42 - REMOTE fits the Heltec V4 + keyboard-less exit
Remote mode showed a black screen on 2 MB-PSRAM boards (Heltec V4): the
mirror ring/shadow were too big to allocate. Size the ring to one frame
and drop the remote render to 400x240 so the whole footprint is smaller
than VNC's (which already worked). Add a touch-and-hold (3 s) exit for
keyboard-less touch boards and an Exit button on the web page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 23:34:30 +02:00
Kaj SchittecatandClaude Opus 4.8 3f375f2fe4 touch: beta_41 - VNC + REMOTE web mirror (experimental)
Stream the live LVGL UI to a phone/desktop browser over the WS companion
server (VNC app) plus a headless REMOTE mode that renders the UI at a
browser-friendly resolution. Non-blocking per-client TX buffering, a
core-0 stream task guarded by a recursive mutex, an on-page Rotate
orientation toggle, and a small 480x288 render upscaled in the browser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 22:32:25 +02:00
Kaj Schittecat 9da367d0bc chore: refresh Mesh America catalog for beta_40 2026-07-11 12:28:49 +02:00
Kaj SchittecatandClaude Opus 4.8 01a5238662 touch: beta_40 stable — Tanmatsu About version fix + comprehensive release notes
- Tanmatsu (ESP32-P4) now bakes FIRMWARE_VERSION="wadamesh <tag>" like the S3
  release path, and the app-store publish passes the exact beta tag into the IDF
  build — so the About page shows the real beta number instead of the MeshCore
  default "v1.16.0-touch".
- beta_40 stable release notes: rolls up beta_36 through beta_40.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 12:27:00 +02:00
Kaj SchittecatandClaude Opus 4.8 946e73bb0b touch: beta_40 - card-less contacts-save stall fix + long-text auto-fit
Fixes:
- Card-less devices (Heltec V4) froze for seconds: a full contacts rewrite to
  SPIFFS on every advert refresh triggered a multi-second GC in the mesh loop.
  Coalesce advert-driven saves (a contact-set change still saves promptly); SD
  boards unchanged. Flush on reboot/power-off so nothing is lost.
- Long translations (FR/DE/...) overflowed fixed-width labels: new
  uiFitLabelWidth() shrinks the font to fit one line instead of clipping/wrapping
  - applied to the message menu, confirm dialogs, and app-drawer tiles.
- Home signal graph: "tap for details" hint moved to bottom-right so it no longer
  overlaps the "Sig" chip in longer languages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 18:31:26 +02:00
Kaj Schittecat 326d34aaec site: add BFGNeil community video (newest first) 2026-07-10 13:05:02 +02:00
Kaj Schittecat d2fe2ed728 release-notes: beta_39 2026-07-09 23:39:20 +02:00
Kaj SchittecatandClaude Opus 4.8 9547ce6ede touch: chat-history persistence fix + faster delete + emoji tofu fix
Persistence (Leon P / MrWeavis, beta_38):
- Deleting a message or clearing a chat now persists synchronously, so it
  survives an immediate hardware reset instead of "coming back" (the tombstone
  used to ride the 30 s lazy SD flush, which a reset beats).
- The history file is now written compactly — only the used records,
  oldest-first, instead of all ~5000 ring slots every time. A typical chat
  writes ~48 KB instead of ~1.2 MB, so the synchronous delete/clear/reboot
  flush no longer hitches the UI (and normal message flushes are lighter too).
  Old history files still load (the reader derives its slot count from size).

Emoji picker:
- Baked the 14 glyphs that were listed but never in the font (umbrella, coffee,
  pizza, cake, rocket, camera, phone, laptop, calendar, antenna, e-mail, house,
  car, watch); they rendered as the notdef box.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 23:38:37 +02:00
Kaj SchittecatandClaude Opus 4.8 49a8f31b8c touch(i18n): full untranslated-text audit + 307 new translation rows
Audited every user-visible string sink across the touch UI and closed the
gaps that had accumulated since the language table was last touched:

- TR() now strips a leading LVGL-symbol glyph (+ spaces) off the lookup key
  and rebuilds the label, so icon-prefixed menu/sheet rows ("<glyph>  Copy")
  translate instead of silently falling back to English.
- Wrapped 95 bare literal sinks (settingsRowLabel / showAlert / showConfirm /
  lv_label_set_text / dropdown / placeholder / checkbox) in TR(); unwrapped 29
  format-only strings that never should have been keys.
- Added 307 missing rows to kI18n[] covering all 11 non-English languages
  (NL DE FR ES IT RU UK BG SR EL PT-BR); keys byte-identical, printf
  placeholders and %-order preserved, Serbian in Cyrillic.

Builds green on T-Deck + Heltec V4; Tanmatsu app 3,169,296 B (107 KB under
the AppFS 50-page load ceiling).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 19:38:07 +02:00
Kaj SchittecatandClaude Fable 5 ded72bb91a docs: DEVICES.md support matrix + README board list refresh
One page with every supported board, its install path, release channel
and maturity, plus the open hardware requests and the channel model.
The README board list now covers all five targets and links there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 14:26:47 +02:00
Kaj SchittecatandClaude Fable 5 cb08116eae flasher/site: ThinkNode M9 + RAK WisMesh Tap V2 web-flash support
Both new S3 boards are installable from the browser now: release.sh
builds and ships their bins per channel, gen-flasher-meta emits their
esp-web-tools manifests, and the site gets install cards for both.
The cards are pinned to the beta feed (with a note) until the first
stable promote that includes their bins; the wmSetChannel comment says
how to unpin then. beta_38's beta feed and GitHub release carry the
new bins retroactively.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 13:55:35 +02:00
Kaj SchittecatandClaude Fable 5 f0a7265dec touch: beta_38 release notes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 13:15:59 +02:00
Kaj 814c8f5941 Merge pull request #130 from scratchdiver/steve/touch-chat-ui-pr
touch: virtualize chat scrolling (bubble + compact)
2026-07-09 13:14:44 +02:00
Kaj SchittecatandClaude Fable 5 76cd025bf5 tanmatsu: move identity + prefs to the SD card; open-probe the prefs load chain
The P4's internal FFat 'locfd' has a broken FAT metadata layer: open/
read/write work but f_stat/exists()/size() return garbage - and WHICH
garbage shifts with the build. At -Og the exists()-gated identity and
NodePrefs loads worked by luck; the -Os switch flipped the lie and the
device booted with a fresh identity and default name, and profile
changes never survived a reboot (the loads failed, not the saves).
Chat history was unaffected because it already lives on SD_MMC.

- DataStore::useSdMmcStorage(): full-store adoption of the card
  (/meshcomod root, identity store included), mirroring the T-Deck's
  useSdStorage(). The old card-root contacts3/channels2/adv_blobs from
  the secondary-FS era are renamed under /meshcomod.
- Tanmatsu boot: one-time rescue of identity + prefs off FFat by OPEN
  and READ probing (never exists()/size() on that FS), then adopt the
  card. FFat remains the no-card fallback only.
- DataStore::loadPrefs() probes candidates by opening and reading a
  byte instead of exists() - truthful on the P4, identical elsewhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 13:00:54 +02:00
Kaj SchittecatandClaude Fable 5 d20241627c touch: flush chat history on the companion CMD_REBOOT (fix messages lost on app-initiated reboot)
Since the beta_35 lazy history writer, message-ring flushes are spaced
up to 30 s apart on the deep SD ring. Every on-device reboot/power path
compensates by calling persistHistoryNow() first, but the companion
protocol's CMD_REBOOT (the phone app's reboot button) went straight to
board.reboot(), dropping everything since the last flush - the newest,
just-read messages, which reads as 'my channel and DM messages get
deleted after a manual reboot' (Leon P, T-Deck under Launcher, beta_35
through beta_37).

persistHistoryNow() is now an AbstractUITask hook (default no-op, so
non-touch companions are unchanged) and CMD_REBOOT calls it before
rebooting, matching the power-menu contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 12:48:53 +02:00
Kaj SchittecatandClaude Fable 5 0b886e4fee tanmatsu: build the P4 app at -Os instead of -Og (boot-verified)
The app has been compiling at debug optimization since the port began,
costing ~300 KB of flash-mapped footprint against the launcher's hard
50-page (3,276,800 B) app-load ceiling. At -Os current main is
2,973,520 B, ~296 KB under the ceiling. Boot-verified on the device:
app starts, C6 esp-hosted link up, Wi-Fi associates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 12:39:55 +02:00
Kaj SchittecatandClaude Fable 5 a49726d4ce touch(tdeck): migrate SPIFFS data to SD on adoption + recovery copy button (beta_36 lost-profile fix)
Users who flipped 'Store data on SD' before beta_36 ran with the toggle
IGNORED (the flag never survived a reboot), so their identity, prefs and
contacts kept living on SPIFFS. When beta_36 made the flag finally take
effect, useSdStorage() pointed the store at an EMPTY card, the identity
store minted a brand-new node, and the user 'lost' their profile. The
data was never gone - it was orphaned on internal flash.

Two-part fix:
- Boot: adopting the card while SPIFFS holds an identity the card lacks
  now copies every SPIFFS file into SD:/meshcomod first (kv files
  flatten to the card's SdNvsPrefs layout). If the identity file fails
  to land, the device stays on SPIFFS for the boot instead of adopting
  a card without it.
- Settings -> Storage gains 'Copy internal data to SD' for the users
  already bitten (their card now holds a fresh identity the boot-time
  no-clobber gate rightly won't touch): confirmation, overwrite-copy of
  everything from internal flash, force the SD pref on, reboot.

Workaround until this ships: toggle 'Store data on SD' OFF and reboot -
the device reads internal flash again and the old profile returns.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 12:29:54 +02:00
Kaj SchittecatandClaude Fable 5 6aa1147eb3 touch: message action menu lays out two buttons per row
With Delete the menu carries up to 7 actions; a single column no longer
fit the 240-px screens without scrolling. Two per row (wider 200-px card,
odd last button on its own row) keeps the whole menu on one screen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 12:06:52 +02:00
Kaj SchittecatandClaude Fable 5 d9f45405d5 touch: slim scroll-fade jump arrows + message delete + per-chat history wipe
- The chat jump buttons are bare arrows flush against the right edge now
  (28x36 transparent touch target, accent-colored glyph) instead of 40 px
  filled circles; they run at full opacity while the list scrolls and dim
  to 50 percent one second after the last scroll activity. The Tanmatsu
  keeps its F-key chip styling and geometry.
- The long-press message menu gains a Delete row: the message is
  tombstoned in the ring (thread tag blanked, so every walker skips the
  slot and live ring indexes stay valid) and both the chat and the thread
  list refresh.
- The chat/channel settings sheet (in-chat gear and list long-press)
  gains a 'Delete history' row for both DMs and channels: confirmation
  prompt, then every message of the thread is tombstoned. The thread row,
  channel membership and contact stay; only the conversation is wiped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 12:01:46 +02:00
Kaj SchittecatandClaude Fable 5 f04e01769d tanmatsu: build the P4 app at -Os instead of -Og
The app has been building at debug optimization since the port began.
At -Og the AppFS image sat 2.2 KB OVER the launcher's 50-page load
ceiling with the chat virtualization in; at -Os it is 2,980,688 bytes,
296 KB under the ceiling, retiring the size-locked-platform problem
(and likely speeding the UI up). Needs one on-device boot check before
anything ships with it - the P4 has never run an -Os build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 11:38:38 +02:00
Kaj SchittecatandClaude Fable 5 9785d8b7d1 fix(chat-virt): review fixes from the PR 130 deep review
- chatVirtReset no longer queues lv_obj_del_async on the divider: the
  empty-thread paths lv_obj_clean two lines later freed it first and the
  deferred lv_obj_del then ran on freed memory (confirmed UAF, plus a
  cross-panel variant when both panels are open). Downstream cleans own
  the widget; the reset only nulls the pointer now.
- Compact rows register bubbleLongPressMenuCb for LV_EVENT_LONG_PRESSED
  (the only event the callback acts on) instead of PRESSED/RELEASED/
  PRESS_LOST/DELETE, all of which it filtered out - the per-message menu
  was completely dead in compact mode.
- Bubble mode gets tap-to-resend back on failed sends (CLICKED handler
  plus the 'tap to resend' footer), matching the compact path and the
  pre-virtualization behavior.
- need_layout gains a content-generation guard (endpoint ring slots): at
  ring capacity n stays constant while content rotates, and equal-count
  thread switches reused stale offsets - rows rendered the next message's
  text at the previous message's position. The dead dirty_timeline rescue
  (cleared before its async consumer could read it) is removed.
- New chatVirtTryAppendLayout fast path: pure appends extend offsets for
  the new tail instead of re-measuring the whole thread (the full measure
  is ~100 ms of UI stall per received message on a 5000-message SD ring).
- lv_async_call failure no longer wedges the render busy flag; the nav
  focus fallback validates s_nav_objs[0] before focusing it (bubbles now
  recycle on scroll-end, so slot 0 can be one tick stale); dead
  chatVirtClearAllChildren/DeleteMsgsChild pair removed; stray
  FIRMWARE_BUILD_DATE bump reverted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 11:38:25 +02:00
Kaj bf96253308 Merge pull request #138 from 446564/finish-m9
m9 stable
2026-07-09 08:31:47 +02:00
Kaj SchittecatandClaude Fable 5 65a485d28c Merge main (RAK Tap V2 port) into finish-m9
Union of both PRs' landscape home-chart gate (TDECK || TANMATSU || RAK_TAP_V2 || THINKNODE_M9).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 08:26:49 +02:00
Kaj SchittecatandClaude Fable 5 b2950a7f88 fix(m9): restore the T-Deck/M9 control-center card arm + drop unreachable HOME leftover + lock-hint typo
The control-center sizing lost its '#elif HAS_TDECK_GT911' line, which
put the 200 px set_size inside the Tanmatsu arm (shrinking its card
from 384 to 200) and dropped the T-Deck into the portrait else-arm.
Restored as TDECK || M9. Also removed the unreachable duplicate lines
after the M9_KEY_HOME return and fixed 'hole the d-pad' on the lock
screen hint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 08:24:33 +02:00
Kaj SchittecatandClaude Fable 5 e95dd755e1 fix(rak_tap_v2): stray paren in the CAP_LARGE_SCREEN arm + keep power-menu height on existing boards
The bare ')' under #if CAP_LARGE_SCREEN broke the Tanmatsu (P4) build,
the only target that compiles that arm. The power-menu #else arm went
back to the original card_h = 206 so T-Deck/V4 keep their exact layout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 08:09:21 +02:00
Ethan-chen1234-zy 2532ddc7ba fix(rak_tap_v2): gate shared-path changes behind HAS_RAK_TAP_V2 2026-07-09 08:09:21 +02:00
Ethan-chen1234-zyandCursor 6309503060 fix(rak_tap_v2): align target with upstream ClockFloorRTC and PubSubClient dep
Signed-off-by: Ethan-chen1234-zy <ethan.chen@rakwireless.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 08:09:21 +02:00
Ethan-chen1234-zyandCursor b7bebd508b feat(rak_tap_v2): rebase onto upstream, add device_caps and README
Signed-off-by: Ethan-chen1234-zy <ethan.chen@rakwireless.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 08:09:21 +02:00
Ethan-chen1234-zy b470097534 feat(rak_tap_v2): HW CDC upload, tiles partition, hide download mode
Signed-off-by: Ethan-chen1234-zy <ethan.chen@rakwireless.com>
2026-07-09 08:09:21 +02:00
Ethan-chen1234-zy 1aab86b530 feat(rak_tap_v2): BOOT button support, USB CDC, upload fix
- PIN_USER_BTN: -1 -> 0 (enable BOOT key on GPIO0)
- Short press (<1s): toggle screen on/off (no hard-lock)
- Long press (>=1s): open power menu (Power off / Reboot / Cancel)
- Add ARDUINO_USB_CDC_ON_BOOT=1 (stable USB CDC COM port)
- boards/rak_tap_v2.json: use_1200bps_touch=false, wait_for_upload_port=false
  (manual download mode via BOOT+RESET, esptool upload on COM7)
- Remove unused boards/wiscore_rak3312.json
- Remove LovyanGFX from Heltec lib_deps (accidental regression in baseline)

Signed-off-by: Ethan-chen1234-zy <ethan.chen@rakwireless.com>
2026-07-09 08:09:21 +02:00
Ethan-chen1234-zy bc129b5ffb feat(rak_tap_v2): LovyanGFX HW FSPI display driver (30+ fps)
Replace USE_PIN_TFT bit-bang software SPI (~8-10 fps) with LovyanGFX
hardware FSPI (SPI2_HOST, 40 MHz, DMA) matching Meshtastic's architecture.

Files:
- variants/rak_tap_v2/LGFXDisplay.{h,cpp}: new DisplayDriver via LovyanGFX
- variants/rak_tap_v2/target.{h,cpp}: SPI.begin() in radio_init() for FSPI
- platformio.ini: DISPLAY_CLASS=LGFXDisplay, LGFX_INVERT_COLOR, LovyanGFX dep
- src/ui-touch/UITask.cpp: RAK path includes LGFXDisplay + 320x40 LVGL buffer
- NOTICE: LovyanGFX dependency

Signed-off-by: Ethan-chen1234-zy <ethan.chen@rakwireless.com>
2026-07-09 08:09:21 +02:00
Ethan-chen1234-zy 8133a47769 feat(rak_tap_v2): add RAK WisMesh Tap V2 board support (working baseline)
Display: USE_PIN_TFT software SPI, 8-10 fps, 40-line LVGL buffer
Touch: FT5x06 I2C 0x38, runtime rotation 3 + FLIP_X/Y
UI: landscape LV_DISP_ROT_270, mapRaw with RAW_W=240/RAW_H=320
Radio: LoRa SX1262 on HSPI (SPI3_HOST), 240MHz CPU
Signed-off-by: Ethan-chen1234-zy <ethan.chen@rakwireless.com>
2026-07-09 08:09:21 +02:00
Kaj SchittecatandClaude Fable 5 48e77933e2 touch: beta_37 release notes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 22:09:36 +02:00
Kaj SchittecatandClaude Fable 5 667436fdd2 touch(tdeck): GT911 phantom-press watchdogs + bounded init retry
A flash/reset landing mid-I2C-transaction can wedge the GT911 (the S3
resets, the touch controller keeps running) into repeating a stale
pressed frame. The driver's keep-prev-state-on-no-data poll then latched
an eternal press: real touches ignored, trackball clicks eaten (a live
touch outranks the click injection), idle timeout never firing, keyboard
lock/unlock still working - a dead-looking device that survived every
reboot and reflash because only a full power cycle resets the GT911.

Watchdogs in gt911Poll() now force a release when status reads fail
mid-press, when no frame arrives for 250 ms while pressed (a real held
finger produces a frame every GT911 scan cycle), or when one bit-exact
point stays pressed for 10 s (a human touch always jitters) - that last
point is then ignored until it moves or a real lift frame arrives. A
jitter-y phantom can still need a power cycle, but the common wedge
modes self-heal in under a second.

Init is no longer one-shot: the boot probe retries every 500 ms for
~6 s before caching failure, so a GT911 still resetting at the first
attempt no longer costs the session its touch AND keyboard (the kb
poll task only starts once touch inits).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 21:43:40 +02:00
Enot (ded) Skelly 381658ab2a Merge branch 'main' into finish-m9 2026-07-08 09:44:41 -07:00
Enot (ded) Skelly 4846d4fa51 fix: bug in batt logging, sd didn't check if directory exists
which would mean the user had not yet enabled loggin to sd but only
mounted a card
2026-07-08 09:40:02 -07:00
Enot (ded) Skelly c322b1fa3a chore: update m9 port doc 2026-07-08 09:31:05 -07:00
Steve GlennerandCursor d964c029d9 touch: virtualize chat scrolling (bubble + compact)
Viewport-based message virtualization for open chats: only the visible
window of LVGL widgets is materialised while layout offsets cover the
full thread (up to msgCap). Supports bubble and compact IRC modes, day
separators, jump to oldest/latest, and compressed LVGL scroll coords
for very long histories.

Finger drags advance layout scroll 1:1 even when coords are compressed,
so long histories scroll fluidly without micro-drags that trigger bubble
long-press menus.

Includes LVGL crash hardening from soak testing: defer refreshChatDetail
and bubble reflow off the indev/refresh path, sync-delete during async
render, size chatVirtEnsureMsgIdx to msgCap for the SD deep ring, and
open-to-bottom scroll that does not rely on premature scroll_bottom.

Fixes #126

Signed-off-by: Steve Glenner <steveglenner@hotmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 09:21:49 -07:00
Kaj SchittecatandClaude Fable 5 d2d3060051 touch: don't double-shift the layout indicator in the tall chat bar
It is align_to'd against the clock's final translated coords each tick; the
tall-bar top-row translate on top of that pushed its upper half out of the
bar (clipped) inside a chat.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 16:58:49 +02:00
Kaj SchittecatandClaude Fable 5 e2eb0d3118 touch: show the keyboard-layout indicator only inside a chat/channel
Typing (and thus the active layout) only matters in an open conversation;
everywhere else the language name was permanent bar clutter next to the clock.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 16:53:21 +02:00
Kaj SchittecatandClaude Fable 5 dd19cbe52d touch: park the keyboard-layout indicator relative to the clock on every board
The EN/DE indicator sat at a fixed -182 right offset while the clock's left
edge reaches past that in 12-hour mode ('7:45 PM'), overlapping on the T-Deck
bar. The Tanmatsu branch already parked it align_to the clock; un-gate that so
all boards track the clock wherever it lands (12/24h width, charging slide,
centred hide-name mode).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 16:43:15 +02:00
Yaz aa598d626b ui-touch: GPS position as the first quick-reply row
Adds the node's live GPS position as the first row of the chat quick-reply
picker: with a fix it shows the coordinates and a tap inserts them into the
composer at the cursor (same no-auto-send contract as the macro rows, so the
operator can add context before sending); without a live fix the row is
greyed out and disabled. Sharing where you are is one of the most common
things typed into a mesh chat, and hand-copying coordinates from the GPS
screen was the only way to do it.

Signed-off-by: Yaz <yazutsu@users.noreply.github.com>
2026-07-08 16:23:05 +02:00
Steve GlennerandCursor 862ec32f80 touch: fix SD message history flush reliability
Ensure the tile-fetch worker is running before arming a message-ring flush,
fall back to a synchronous write if spawn fails, write ui_msgs_v1.bin via a
.tmp rename for crash-safe replacement, and retry when the worker snapshot is
missing instead of treating the flush as successful.

Signed-off-by: Steve Glenner <steveglenner@hotmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 16:21:16 +02:00
Yaz 1eeeb267d2 ui-touch: make the Settings keyboard-backlight Off preset switch the mode off
Follow-up to the keyboard-backlight brightness control (issue #84). The Off
preset used to set the brightness percent to 0 while leaving the backlight
MODE untouched, inventing a second dark state: the keyboard went dark but the
control-center chip - which cycles and shows the mode - kept saying on/auto.

Off now switches the mode off, the same axis the CC chip shows, so the two
surfaces can never disagree. The brightness slider becomes 1-100: dark is
only expressible via the mode, and the on/auto modes always glow visibly at
the chosen level. The stored percent is left untouched by Off, so re-enabling
the backlight restores the previous brightness; a stored 0 from the previous
build is clamped to 1 at boot.

Signed-off-by: Yaz <yazutsu@users.noreply.github.com>
2026-07-08 16:18:16 +02:00
Kaj SchittecatandClaude Fable 5 2b30ca2a5a touch: Tanmatsu umlauts (#129) — ALT accent picker, double-space language cycle, compressed fonts
- ALT-accent picker on the Tanmatsu physical keyboard: hold ALT + letter
  opens the accent popup with the first variant highlighted, the letter
  cycles (auto-repeat walks the options), releasing ALT (its own scancode
  event) inserts the highlighted choice. Popup is nav-skipped/non-clickable
  so keypad-nav focus can't steal the cycle key, SC()-scaled, align-centred.
- Double-tap-space language cycle ported to the Tanmatsu typing path
  (navPump), and keyboardLayoutsApply now updates the active layout even
  with no on-screen keyboard object (was a permanent no-op there).
- Fonts: LV_USE_FONT_COMPRESSED on; extras_font_12/14/16 regenerated
  compressed (identical text glyphs; redundant mono emoji dropped — the
  colour imgfont covers emoji) freeing ~340 KB; new compressed Latin-accent
  extras_lat_20/24/28 spliced into the Large/Huge fallback chain
  (Tanmatsu-only) so umlauts size-match the scaled UI. Tanmatsu app.bin =
  3,269,440 B, UNDER the known-booting 3,271,856 B (50-page AppFS ceiling).
- TEMP: TDECK_KEYCODE_PROBE toast (build-flag gated, inert in releases)
  for mapping the T-Deck alt key.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 16:16:53 +02:00
Enot (ded) Skelly 2a34ad476e fix: m9 lock screen wake + bugfix lock paint
lockscreen was not being painted until after the display
wakes showing the previous screen, changed to paint lock
screen then wake
2026-07-07 15:16:18 -07:00
Enot (ded) Skelly 0ea242c81f feat: m9 long press enter key context menus 2026-07-07 14:55:56 -07:00
Enot (ded) Skelly a8f8a79d3f fix: show lock screen settings for m9 2026-07-07 14:47:25 -07:00
Enot (ded) Skelly 30ed2d77f6 fix: m9 control toggle layout 2026-07-07 14:06:23 -07:00
Enot (ded) Skelly c87cd3cc98 chore: update m9 port doc 2026-07-07 13:42:31 -07:00
Enot (ded) Skelly 996eac21fd feat: add buzzer for m9 2026-07-07 13:35:00 -07:00
Enot (ded) Skelly 3a3a37d193 feat: add m9 sd card 2026-07-07 13:23:20 -07:00
Enot (ded) Skelly dcf3958cc7 fix: m9 gps 2026-07-07 12:30:15 -07:00
Enot (ded) Skelly 6de8346e7f feat: add m9 mentions and advert hw button handlers 2026-07-07 10:34:57 -07:00
Enot (ded) Skelly be32315b98 chore: remove old m9 kb debug print 2026-07-07 10:14:39 -07:00
Enot (ded) Skelly 5da14d8bda fix: highlight selected textarea for hw nav 2026-07-07 09:21:29 -07:00
Enot (ded) Skelly 32e1668406 fix: bug in deep nested containers
they did not scroll because the parent was scrollable but they were not.
so scrolling with navigation through elements did not work
2026-07-07 09:15:25 -07:00
Enot (ded) Skelly 259aa36e40 fix: m9 correct home button behavior
was not working correctly on app drawer
2026-07-07 09:01:16 -07:00
Enot (ded) Skelly 51b513b181 fix: m9 backlight level 2026-07-07 08:42:03 -07:00
Kaj SchittecatandClaude Fable 5 6dc09b3185 touch: beta_36 — Launcher SD storage both ways, edit menu on all fields (#122), Wi-Fi full-screen sheets, lockscreen unread (#93), SD update download, AU Mid preset (#121)
- Edit menu (Cut/Copy/Paste/All/Sym) + double-tap select on every settings
  field via attachSettingsTaEvents; menu actions route through the keyboard
  mirror when bound (#122)
- Wi-Fi join/hidden/details sheets: full-screen settings-style pages riding
  the tall status bar title (s_apppage hook), on lv_scr_act so the bar's
  glass row isn't clipped
- Lock screen unread badge (envelope + count, anti-burn-in drift) (#93)
- About: Save update bin to SD (/BINS/wadamesh-<tag>-<channel>.bin) for
  Launcher installs; beta-channel toggle no longer gated on the OTA slot
- Australia (Mid) preset 915.075/125/SF9/CR5 (#121)
- M9: [M9KB] key debug print (bring-up)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:18:55 +02:00
Kaj SchittecatandClaude Fable 5 b3004b85eb fix(esp32): use_sd toggle follow-up — both directions on Launcher installs (PR #123 review)
Two edges from the #123 review:
- The raw-NVS mirror is best-effort now: on installs with unusable NVS
  (Launcher) the file write is the authoritative copy, so a failed mirror
  no longer fails the setter.
- When prefs actively live on the SD card, the toggle also rewrites the
  SPIFFS copy of touch.kv (record-preserving; new SdNvsPrefs::fileFs()
  accessor tells the setter which fs is active). Without this, boot's
  SPIFFS fallback kept reading the stale use_sd=true and SD storage could
  be turned on but never off again where NVS is broken.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:06:02 +02:00
Steve GlennerandCursor a7d435b03b fix(esp32): honor use_sd from touch prefs at boot
UI stores the SD toggle in /prefs/touch.kv via SdNvsPrefs; boot only read
raw NVS. Dual-write on change and read NVS with touch.kv fallback so SD
storage selection survives reboot.

Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Steve Glenner <steveglenner@hotmail.com>
2026-07-07 10:58:58 +02:00
Kaj Schittecat 8718e49827 chore: refresh Mesh America catalog for beta_35 2026-07-07 09:50:26 +02:00
Enot (ded) Skelly 38c0e7a016 correct m9 adc and add backlight handling 2026-07-06 15:45:29 -07:00
Enot (ded) Skelly 0be7c20600 fix m9 commander layout 2026-07-06 15:14:46 -07:00
Enot (ded) Skelly ad98f661cc fix m9 navigation 2026-07-06 15:02:15 -07:00
Kaj SchittecatandClaude Fable 5 a38ea1e3bb touch: Elecrow ThinkNode M9 port (ESP32-S3 + LR1110 + I2C QWERTY) — bring-up builds 1-9
New board: ThinkNode_M9_companion_radio_touch env, variants/thinknode_m9/
(board glue, LR1110 target, keyboard driver, partitions, M9_PORT.md pin
reference), boards/thinknode_m9.json, device_caps HAS_THINKNODE_M9 block,
HAS_M9_KEYBOARD input path in the touch UI (d-pad focus nav, tab jump keys,
QWERTY typing).

Based on a contributed bring-up patch by ded (SalishMesh), who also remote-
tested every build on a preproduction unit. Hard-won fixes on top of the
patch, each verified on hardware:

- LR1110 radio init -706: preprod chips run transceiver FW 0x0303, which
  rejects DriveDiosInSleepMode (0x012A, added in FW 0x0308) that RadioLib
  7.x sends unconditionally during begin(). New build-time patch
  scripts/build/patch_radiolib_lr11x0.py tolerates it on old FW;
  radio_init() prints the chip's device/FW/errors either way.
- TCXO: DIO3 at 3.3 V (the patch's active-oscillator/tcxo=0 theory gave
  -707 on hardware).
- Display: rotation 1 (not 3), plus the missing landscape s_ui_rotation
  override so LVGL renders 320x240 instead of portrait-into-landscape.
- Keyboard: the controller is a separate ESP32-S2 I2C slave (0x6C) with
  addressed registers - reads select register 0x01 first (bare reads
  return the HW-version register forever). Boot probe logs the
  controller's HW/FW; backlight duty register wired.
- USB pad release: the keyboard bus lives on GPIO20/21 = the S3's native
  USB D-/D+ pads, owned from reset by the ROM USB-Serial-JTAG (D+ pullup
  on SDA). M9Board::begin() releases them before any bus init.
- GPIO35-37 are octal-PSRAM pins on the S3R8 - never drive them (the
  patch's "SD CS = 36" misread package pin 36 = GPIO48; SD is on the
  shared SPI bus, support returns with PIN_SD_CS=48 later).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:49:25 +02:00
Kaj SchittecatandClaude Fable 5 01900296f3 touch: beta_35 — worker-thread history flush, radio memory guards, 30.5 KB DRAM freed, crash-safe prefs
- History flush runs on the core-0 worker (snapshot + storage-busy gate +
  bounded shutdown wait): kills the V4's multi-second ui:hist stalls and
  the refuses-to-wake-after-message symptom (SPIFFS GC off the UI thread).
- BLE + Wi-Fi runtime enables share the boot co-init heap guard (50 KB
  free + 20 KB block): refuse with a toast + revert the switch instead of
  panicking (BLE) or silently not starting while claiming on (Wi-Fi).
- Internal DRAM: static footprint 95,365 -> 64,908 B (30.5 KB freed):
  serial_interface -> PSRAM; 23 keyboard layout maps const'd -> flash;
  nine rings/tables -> psAlloc PSRAM (UITask/TouchPrefsStore/MyMesh);
  unused TinyUSB MSC + DFU-mode class drivers stubbed out of the link
  (usb_unused_class_stubs.c) — CDC + runtime-DFU untouched, double-flash
  verified over the stubbed stack.
- DataStore prefs writes are crash-safe (write .tmp, swap) with a
  self-healing loader (.tmp recovery) and a truthful save result up to
  the profile-name toast — fixes the boots-with-default-name-once report.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:34:24 +02:00
Kaj SchittecatandClaude Fable 5 4e1ac2ca6b touch: beta_34 — buffered LoRa receive ON by default (test channel) + core pin core-v1.16.5
Consumes the new core (buffered receive drain task + RX counters, #13
stock payload layout, GPS-date guard). Fork side:
- TouchPrefs v33 rx_queue, DEFAULT ON for the test channel (fresh
  installs + a one-time migrate for upgraders); opt-out toggle
  'Buffered receive (experimental)' in Radio & Mesh, applies live.
- About live tier shows LoRa RX heard/read/err/late-lost/qdrop, the
  field evidence for the missed-messages fix.
- Spectrum parks/resumes the drain task around raw radio use; MyMesh
  setParams sequences hold the radio mutex.
- TanmatsuLoraRadio: no-op API stubs (P4 keeps its own RX path).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 11:15:52 +02:00
Kaj Schittecat 5d9a184314 chore: refresh Mesh America catalog for beta_33 2026-07-05 23:50:32 +02:00
Kaj SchittecatandClaude Fable 5 8e17b1b071 touch: beta_33 — async MQTT connect (freeze fix) + About sketch-size cache + favorites-full toast
- MqttBridge: reconnect() (blocking DNS + TCP + CONNACK, 2-10 s vs an
  unreachable broker, every 15 s) now runs on a one-shot core-0 task;
  _connecting hands the client between threads. Broker socket writes get
  the same zero-timeout writability probe as the TCP/WS companion
  servers, so a wedged broker fails a publish instead of stalling loop().
- About: ESP.getSketchSize()/getFreeSketchSpace() esp_image_verify-walk
  the whole ~3 MB image (~190 ms each); the 1 Hz refresh cost ~390 ms/s
  and flooded the stall ring (the ui:sbar entries field testers
  photographed). Computed once and cached.
- Favorites: adding past the 16-cap now toasts 'Favorites full (max 16)'
  instead of the wrong 'Removed from favorites'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 23:34:22 +02:00
Kaj SchittecatandClaude Fable 5 b58cecab5c touch: beta_32 — non-blocking TCP/WS companion writes (Wi-Fi freeze fix) + chunked history flush + stall diagnostics
Root cause of the beta_31 periodic 1-2s freezes with Wi-Fi on: every
received mesh packet pushes an RX-log frame to all connected TCP/WS
companion clients from the loop thread, and writeAllBytes called
WiFiClient::write() unguarded — against a stale peer (phone asleep,
half-open socket) that blocks in the framework's 1s select() retries.
The 120ms app timeout only bounded attempts, not the first call.

- TCP+WS servers: probe writability (zero-timeout select on the client
  fd) before every write; lwIP reports writable only with ~2.8KB free,
  a whole frame is ~180B, so probed writes complete instantly.
- Reap clients whose writes fail continuously for 10s (half-open
  sockets previously stayed "connected" forever, eating the write
  budget on every pushed frame).
- Chat-history ring: chunked writer + wider flush spacing (same
  treatment saveContacts got in beta_30).
- Stall tracer: 16-entry ring of >=0.2s loop stalls, shown in About.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 22:55:42 +02:00
Kaj Schittecat 816045bb6b chore: refresh Mesh America catalog for beta_31 2026-07-05 21:13:27 +02:00
Kaj SchittecatandClaude Fable 5 11939a744e touch: beta_31 — WhatsApp-style chats tab (avatars + previews) with per-channel custom icons
- Default chat list: 56px rows with a round initials avatar in the thread's
  FNV-hue signature colour, last-message preview (getThreadLastMessage ring
  accessor), time top-right, unread pill + @ below; compact chat mode keeps
  the dense 34px contact-style rows (layout mode mixed into the rebuild sig)
- Per-channel avatar emoji: 'Chat icon' in the thread-settings sheet (tap =
  emoji sheet in new pick mode, long-press = reset to letters); persisted in
  a chmute-style keyed blob (touchPrefsGet/SetChannelEmoji, 24 x 48B)
- Fix: name/preview overlap (LONG_DOT labels need fixed one-line heights)
- Thread-sheet grid gap 6->4 so the 7th channel cell keeps one screen on T-Deck

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 21:07:42 +02:00
Kaj SchittecatandClaude Fable 5 f47d71a05b touch: beta_30 — big-mesh contacts perf (buffered save, in-place age labels, coalesced rebuilds) + live Spectrum
- DataStore::saveContacts packs byte-identical 152B records into ~5KB chunk
  writes (was ~12 tiny FS writes per contact: 2.6-3.0s at ~570 contacts on the
  loop thread — the #82 sluggishness); falls back to per-field writes on alloc failure
- Contacts 60s age tick updates the Heard labels in place (was a ~1.3s full
  teardown+rebuild); advert-driven rebuilds coalesced to one per 2.5s
- Spectrum: rolling waterfall row + live trace every quarter sweep (~250ms),
  8-bin/8ms sweep chunks (~20% faster full sweep); trace Y range now tracks the
  live min/max — instant expand, 3dB/sweep contract, 30dB minimum span

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 13:08:40 +02:00
Kaj SchittecatandClaude Fable 5 8eb63a7edd touch: beta_29 — clock floor for room-server replay guards, BLE repeats-heard, deep SD history, one-screen popups
- ClockFloorRTC on all boards: persisted monotonic send-timestamp floor (touchPrefs v32),
  garbage-set rejection, trusted-backstep cap; Join no-reply watchdog + server clock
  skew warning from the LOGIN_OK timestamp prefix (issue #89 follow-up)
- BLE repeats-heard: own-echo RX-log frames pass to BLE one-shot; the #46/#54
  congestion skip stays for everything else (issue #94)
- Mirror app-sent channel messages on-device (was Tanmatsu-only from birth)
- Chat delete: persist the ring purge; mesh-channel drop prefers the pinned slot
- Deep SD history (5000-msg ring) + date separators; chat-list rows restyled compact
- Popup unification: popupClose() + k_popup_registry drive dismiss/count/swipe-block
- Thread/channel sheet as one-screen 2-col grid (contact-sheet layout)
- Keypad nav: strip LVGL SCROLL_ON_FOCUS from collected widgets (About-page jump);
  update dot no longer focusable; verchk waits for an IP + retries with backoff
- Tanmatsu: FIRMWARE_RELEASE_TAG from git describe (drop the hand-bumped hardcode)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 10:07:16 +02:00
Kaj SchittecatandClaude Opus 4.8 5623237951 site: dismissible donate banner -> support the MeshCore founders
Sticky top banner asking would-be donors to help the MeshCore founders instead — links to the blog post (Learn more) and the givealittle donation page (Donate). Dismissible (x + localStorage), and it shifts the corner FAB rails down while shown so they don't collide with the close button.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 15:22:37 +02:00
Kaj SchittecatandClaude Fable 5 b90b8bac14 touch: beta_28 — room-server session fix (#89), ST7789 panel sleep, wyvern feedback batch
- Rooms: re-enable the keep-alive pinger (checkConnections), arm a 128s
  keep-alive on LOGIN_OK (servers send 0), blank-relogin self-heal while a
  room chat is open, 'Log in again' in the room sheet, read-only warning.
  Root-caused against simple_room_server: 3 unACKed pushes freeze a client
  until the server hears from it — and we never pinged (issue #89).
- Burn-in: ST7789 SLPIN/SLPOUT on screen-off (backlight-off alone kept the
  panel driving the static image), lit-lock dim guard on all boards, 10s
  notify-wake re-dim, lock clock pixel drift.
- wyvern.red batch: scroll-keys map zoom, backspace exits empty composer,
  Reset path in the chat sheet, compact chat toggle (TouchPrefs v31),
  clearer delivery glyphs + tap-to-resend on failed sends.
- OTA: hide Install-update when the running image's physical address falls
  outside the esp_ota running slot (mixed/legacy tables; beta_21 coredump).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 13:40:02 +02:00
Kaj Schittecat 3585b3a1e0 chore: refresh Mesh America catalog for beta_27 2026-07-04 08:45:41 +02:00
Kaj SchittecatandClaude Fable 5 a4d3715529 release-notes: beta_27 stable rollup (beta_26 + beta_27 since stable beta_25)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 08:45:29 +02:00
Kaj SchittecatandClaude Opus 4.8 ffc501d09d touch: hide on-device Install-update button on Launcher installs
touchHasOtaUpdateSlot() checked only that a spare OTA partition exists
(esp_ota_get_next_update_partition != NULL). Under bmorcelli's Launcher a
spare slot exists but is smaller than our image, so the check passed, the
Install-update button showed, and Update.begin() failed "won't fit: Bad Size
Given" (reported by Keith/KM1USR on beta_24 under Launcher). Require the target
slot be at least as large as the running slot; the whole OTA block (Install /
previous-version / beta toggle, already gated on this) then hides on Launcher
installs, where updates go through the Launcher. The update-available notice
still shows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 08:04:35 +02:00
Yaz 61666ac033 ui-touch: keyboard-backlight brightness control for the T-Deck (issue #84)
The T-Keyboard's C3 firmware takes a 2-byte I2C command [0x01, level 0-255],
but the level the UI drove it at was hardcoded 0xFF, so the backlight was
always 100% when lit. Adds a Keyboard backlight control at the top of
Settings -> Keyboard: a live slider plus Off/25/50/75/Max one-tap presets,
scaling the lit level for the on and auto modes (and the new-message flash).
The percent persists in the existing kbd_bl pref shared with the Tanmatsu
slider; adjusting it while the mode is off flips the mode to on, mirroring
the Tanmatsu control-centre behaviour.

Three latency lessons learned on the way:
- pct->duty is gamma-2 mapped: perceived LED brightness is roughly
  logarithmic in duty, so a linear map felt coarse at the dim end.
- the NVS save is debounced (one-shot timer re-armed per change): the pref
  setter rewrites the whole TouchCfg blob via cfgFlush(), which can block
  the UI thread for a good fraction of a second per tap.
- backlight requests now flush on every touch-poll tick (~8 ms) instead of
  riding the ~32 ms keyboard-scan cadence, and the Settings controls request
  the level directly instead of waiting for the next UITask::loop pass - so
  the control feels as immediate as the screen-brightness slider (whose
  ledcWrite hits the ESP32's own PWM peripheral synchronously).

Signed-off-by: Yaz <yazutsu@users.noreply.github.com>
2026-07-03 07:16:08 +02:00
Kaj SchittecatandClaude Opus 4.8 5f69a61c9c touch: beta_26 — fix SPIFFS-GC watchdog reboot loop (contacts->SD + shared guard)
Decoded a T-Deck coredump (Task WDT, loopTask stuck in spiffs_gc_clean under
DataStore::saveContacts): a contact save on a churned SPIFFS can trigger a
multi-second garbage-collection pass that starves core 0 and trips the task
watchdog, giving a reboot loop.

- Route contacts/channels to SD when a card is present, even on upgraded
  devices whose data is still on SPIFFS (main.cpp mounts SD every boot; if not
  fully adopting SD, store.setSecondaryFS(&SD)). DataStore::begin() migrates the
  existing /contacts3 + /channels2 to the card once; identity/prefs stay put.
- Move the ref-counted WdtHeavyGuard into a shared header so the core
  DataStore::saveContacts shares one count with the UI history/backup saves, and
  wrap saveContacts with it (guaranteed fix for card-less devices).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:11:19 +02:00
Kaj SchittecatandClaude Opus 4.8 ddc4a34d4d docs(site): add microSD-card storage tip to the overview
Recommend keeping a card inserted on SD-capable devices (T-Deck, Tanmatsu) so tiles,
contacts and chat history live on the card and avoid the limited/less-reliable internal flash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 11:08:02 +02:00
Kaj SchittecatandClaude Opus 4.8 df4a675cd3 deploy(meshamerica): match official MeshCore device names + add provider description
TJ Downes flagged that provider device names must match MeshCore's canonical names exactly
or they list as NEW devices instead of folding into the existing tiles. Rename to the official
'Heltec v4 + Expansion Kit (Touch)' and 'LilyGo T-Deck' (verified against the live config), and
add the new optional 'description' blurb shown on the provider badge tooltip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 09:40:15 +02:00
Kaj SchittecatandClaude Opus 4.8 8d068cfcc1 docs(site): add 'Bring your own tiles' offline-maps section to the Map page
Explains the microSD tile-pack layout (/maps/osm or /tiles, z/x/y, PNG/JPEG), the
'Tiles from SD card' toggle, making a pack, and per-board differences (T-Deck / V4 / Tanmatsu).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 08:58:16 +02:00
Kaj Schittecat c4f31e5a69 chore: refresh Mesh America catalog for beta_25 2026-07-01 23:44:43 +02:00
Kaj SchittecatandClaude Opus 4.8 0af47e5936 release: beta_25 stable notes — aggregate beta_22..25 over stable beta_21
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 23:44:40 +02:00
Kaj SchittecatandClaude Opus 4.8 492d0886ec touch: beta_25 (test) — contacts-list refresh, advertise-on-boot, Tanmatsu SD tiles + persistence
- Contacts list rebuilds on advert (force past the count-cache) + a count-delta safety
  net, so a node discovered off-tab or added by the phone app shows immediately (#73)
- New "Advertise on boot" toggle in the Advertise app, off by default (#76)
- Tanmatsu: cache Wi-Fi map tiles on microSD (the P4's internal FFat metadata layer is
  broken — f_getfree/f_stat lie); read tiles via open() not the misreporting exists()/size()
- Tanmatsu: persist contacts/channels/chat on microSD (DataStore secondary FS + chat
  history) so they survive reboot; identity/prefs stay on the (working) internal FFat
- Tanmatsu: boot fix (drop Serial.begin re-init of USB-Serial-JTAG), MQTT disabled, popups
  fit + scroll, keyboard-nav scoped to the open popup, 2000 contacts, Large UI default
- Emoji/quick-reply Esc/red-X closes the popup not the chat; lock-screen unlock after msg-wake

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 23:28:13 +02:00
Dan Vybiral 96dffaef58 touch: fix manual telemetry request reliability + async spinner blink
Two bugs in the manual telemetry request path (the Request button in the
telemetry window):

1. Usually only the second request succeeded. The guest LOGIN and the
   telemetry REQ were sent back-to-back, but a repeater drops a REQ from a
   sender it hasn't added to its ACL yet, and the ACL entry isn't committed
   by the time the first REQ is processed — so the first request got no reply
   and the user had to tap twice. Defer the REQ: uiSendRequestAfterGuestLogin()
   sends only the guest LOGIN and fires the REQ from onContactResponse once the
   LOGIN-OK arrives, by which point we're in the repeater's ACL and a direct
   out_path has been learned, so it lands on the first try. Also adds a
   single-flight guard on telemetryRequestNow(), holds auto-poll while a manual
   request is pending (so it can't clobber the pending reply tag), and disarms
   the deferred login on timeout so a late LOGIN-OK can't fire a stale REQ.

2. The status-bar async spinner only blinked for ~1.5s on a telemetry request
   (the markMeshRequest window) then froze, instead of blinking for the whole
   pending window like a status ping. Its active predicate watched the ping
   deadline but not the telemetry timers; OR in the telemetry pending state.

Auto-poll's best-effort chained send and the admin-console login path are
left unchanged.
2026-07-01 20:52:45 +02:00
Kaj SchittecatandClaude Opus 4.8 3a1495784e touch: beta_24 — signal probe uses standard node-discovery packet
The signal probe now sends a zero-hop NODE_DISCOVER_REQ control packet
(the standard MeshCore node-discovery that the Ultra / KiekR apps use)
instead of a trace, which repeaters do not answer. Repeaters reply
directly with a NODE_DISCOVER_RESP, captured in onControlDataRecv by
matching the probe tag into _ui_sig_* SNR/RSSI. Thanks to Tarmo for
decoding the exact packet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:46:33 +02:00
Kaj SchittecatandClaude Opus 4.8 26c9ccdc07 touch: beta_23 (test) — keep the per-packet RX log off BLE (#46, #54)
The device broadcast a PUSH_CODE_LOG_RX_DATA frame to every connected companion —
including BLE — for every LoRa packet it heard. BLE's ~16 frames/sec budget got
saturated on a busy mesh, delaying/dropping the frames that matter: chat messages
(#46) and repeater/room-server admin responses (#54, the "Streamsink" disconnects).
Skip the RX log on the BLE transport in writeFrameToAll; USB/TCP/WS still get it for
the on-device Monitor app. The MeshCore app never consumed it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:15:13 +02:00
Kaj SchittecatandClaude Opus 4.8 0d8e635848 site: document Stable vs Test channels + how to opt in
Landing page: a plain-language Stable vs Beta explainer under the install channel
toggle, covering both install paths (flash a channel here, or flip the on-device
"Get test builds" toggle). Userguide: a new "Updates & test builds" section (how
updates work, the two channels, how to turn on test builds) plus a Get-test-builds
line on the About settings card.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 09:19:23 +02:00
Kaj SchittecatandClaude Opus 4.8 0731b5ff67 touch: beta_22 (test) — trace-ping signal probe + companion contact-refresh (#73)
Signal probe: replace the beta_21 zero-hop advert (which nothing replies to, so the
signal never updated) with a directed TRACE ping to the nearest reachable repeater.
It retransmits — a real reply we measure — without flooding the mesh. New
MyMesh::uiSendSignalProbe() picks the shortest-path repeater; the reply is captured
silently in onTraceRecv via _ui_sig_probe_tag. Falls back to a zero-hop advert when
no repeater is known. Thanks to Tarmo for the insight.

Contacts (#73 part A): a contact discovered while a companion app is connected over
BLE now refreshes the device's own Contacts list + notification. onDiscoveredContact
fired the UI notify only in the standalone branch — move it out so it always fires,
and flag the list dirty for UITask::loop to rebuild on the LVGL thread.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 08:46:56 +02:00
b5e428734c release: credit Samuel Coustet (@samuelcoustet, #71) for the MQTT bridge
The beta_21 MQTT bridge is based on @samuelcoustet's PR #71, reworked for privacy
(consent gate, direct messages off by default, payload encryption). Record the
attribution in the release notes + the version.json "what's new".

Co-authored-by: Samuel Coustet <257299705+samuelcoustet@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 18:28:31 +02:00
Kaj SchittecatandClaude Opus 4.8 18904c9a5d deploy(nginx): serve the TEST/BETA channel paths on firmware.wadamesh.com
The two-channel rollout added releases/BETA + latest-beta, but the firmware
vhost only had exact-match handling for the old stable paths. Add:
- location = /releases/BETA  -> serves releases/BETA/index.json at the no-slash
  path the device requests when 'Get test builds (beta)' is on (mirrors TOUCH).
- location /latest-beta/      -> the beta web-flasher feed (was hitting the 404
  catch-all).

Stable paths and all bins under /releases/ were already correct; this only wires
up the new beta directory-index + feed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:47:46 +02:00
Kaj Schittecat 39a4db8f03 chore: refresh Mesh America catalog for beta_21 2026-06-30 17:36:58 +02:00
Kaj SchittecatandClaude Opus 4.8 03e50b0609 touch: beta_21 — first stable build + test channel, MQTT bridge, contacts & signal-probe fixes
New: stable/test update channels with an opt-in "Get test builds (beta)" toggle on
OTA devices (switches both the update check and what you install); experimental
opt-in MQTT bridge (consent-gated, payload encryption, direct messages off by
default); Portugal (Narrow) region preset (#74); saved-contacts counter in the
Contacts overflow menu (#72).

Fixed: the signal probe is now strictly zero-hop so a repeater can't re-flood it;
the contact list sorts before capping so the most relevant contacts show, with a
"+N more — search to narrow" footer (#73); the blocked-users list opened from
Contacts shows the two-line title bar with a back button; the Contacts overflow
popup fits the screen again.

Also lands the two-channel release tooling (scripts/release.sh stable/beta modes +
gen-flasher-meta channel arg), the docs site, and the MQTT reference decryptor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:34:11 +02:00
Kaj SchittecatandClaude Opus 4.8 d75db73fc8 release: credit Yazutsu (#66) for the WAV notification sounds in beta_20 notes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 23:50:46 +02:00
Kaj Schittecat 56cf91ef5b chore: refresh Mesh America catalog for beta_20 2026-06-28 23:36:03 +02:00
Kaj SchittecatandClaude Opus 4.8 b4c941f954 touch: beta_20 - spectrum analyzer, custom sounds, saved Wi-Fi, V4 high-gain RX, flood-scope + channel-send fixes
New: Spectrum analyzer app; custom notification sounds (SD WAV per event);
saved Wi-Fi networks with auto-join; unified chat settings screen; per-channel
mute; Heltec V4.3 high-gain receiver (FEM LNA) toggle; Apps button on the V4
home screen; Blocked list reachable from Contacts + chat settings; Portuguese
(BR) UI language.

Fixed: touch-screen flood adverts now region-scoped so region repeaters relay
them (#68); channel sends matched by name so a message can't go out on the
wrong channel's key; opt-in "scope direct messages to region" so room-server
logins work through a region-only path (#64); Wi-Fi scan no longer cancelled
by the reconnect retry (empty SSID list).

Under the hood: flood-scope rework (channel vs direct floods); Spectrum owns
the SX1262 while open and restores mesh RX on exit; wifiScanIsActive() gate;
FEM KCT8103L LNA wired; TouchPrefsStore migrations for the new prefs. Adds the
previously-untracked device_caps.h.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 23:33:44 +02:00
Kaj Schittecat 1e00ac3876 chore: refresh Mesh America catalog for beta_19 2026-06-27 15:09:40 +02:00
Kaj SchittecatandClaude Opus 4.8 2b45225e0b touch: beta_19 — scroll-popup crash fix, Radio airtime/telemetry, V4 Home fit
- Fix LVGL scroll-momentum use-after-free reboot when a scrollable overlay
  (emoji / quick-reply / add-channel / channel sheets, Wi-Fi scan, pickers,
  app drawer) is freed mid-throw: new closeScrollOverlay() helper
  (lv_indev_wait_release + lv_obj_del_async). Root-caused from field beta_18
  coredumps.
- Radio & Mesh settings: airtime / duty-cycle readout + "Answer telemetry
  requests" toggle, grouped into RADIO / MESH / SIGNAL sections.
- Accent popup dismissed on Enter-to-send (T-Deck physical keyboard).
- Heltec V4: Expansion-Kit Home env line + chart flow below the stats line
  so they fit the 240x320 screen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 15:08:03 +02:00
Kaj SchittecatandClaude Opus 4.8 dad7d295dc touch: airtime/duty-cycle readout + answer-telemetry toggle in Radio settings
Two firmware-only additions surfacing capability already in the stack:

- Airtime / duty-cycle readout: the radio modal exposed airtime_factor only as a raw
  'AF' number. Add a derived line — '≈ N% max duty cycle · ~M ms / 40-byte packet' —
  computing duty% from MeshCore's af=(100/duty)-1 and the per-packet time-on-air from
  the Semtech closed-form (new loraToaMs helper). Lets EU-868 users see compliance at
  a glance with no measurement.
- Answer telemetry requests toggle: MeshCore already gates telemetry responses on
  _prefs.telemetry_mode_base/env (0=DENY/2=ALLOW_ALL) but it wasn't in the touch UI.
  Add a switch (new UITask::setTelemetryAllow) that sets battery+environment modes;
  location telemetry keeps its own separate setting so a simple 'allow' can't leak
  position.

(RX boosted gain was already a switch in Experimental settings; per-packet RSSI/SNR
is already in the Monitor; region presets already bundle SF/BW/CR — so those research
items were already shipped and not re-added.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:21:12 +02:00
Kaj SchittecatandClaude Opus 4.8 2d49fb69ec site: add Tanmatsu support; Telegram -> Discord
- List the Tanmatsu (ESP32-P4) in the hero + meta description.
- Add a Tanmatsu box to the standalone installer: no USB flashing — install from the Tanmatsu app store on-device — plus a link to Nicolai Electronics' shop. Boards grid now auto-fits 3 cards.
- Replace the Telegram floating button with Discord (discord.gg/T8X2UaQ7Sm).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 20:45:47 +02:00
Kaj Schittecat 94ef98a12f chore: refresh Mesh America catalog for beta_18 2026-06-26 19:54:08 +02:00
Kaj Schittecat 2db46ec190 release: beta_18 notes 2026-06-26 19:51:31 +02:00
Kaj SchittecatandClaude Opus 4.8 11775ce5ac tanmatsu(lora): apply runtime radio-config changes to the C6 modem
The Tanmatsu's LoRa modem lives on the C6 coprocessor, shared with the Tanmatsu OS's own
LoRa settings. pushConfig() no-op'd once RX polling started (the remote link can block
under RX load), so a runtime frequency/SF/BW/CR/power change from the wadamesh radio
settings only updated _cfg and took effect on the NEXT REBOOT — the radio stayed on
whatever the OS or boot last set. That is the 'change the wadamesh frequency but receive
nothing on it' report, and why wadamesh's settings never overrode the Tanmatsu OS's.

Now a runtime change marks _cfg dirty and recvRaw() applies it on the RX-poll thread,
between polls: drop to STANDBY (so the link isn't under active-RX load and lora_set_config
can't block), push, re-arm continuous RX. wadamesh's settings now reach the modem live
and win over the OS config. The '0.00001 MHz' the OS page showed is the C6 frequency
corrupted by the two configs fighting; wadamesh consistently owning the config should
clear it — needs on-device confirmation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:44:54 +02:00
Kaj SchittecatandClaude Opus 4.8 7d3b4a4336 tanmatsu: the_mesh in PSRAM on the P4 too (build fix for b986f49)
b986f49 moved the_mesh to PSRAM and changed MyMesh.h's extern to 'MyMesh&', but the
Tanmatsu has its OWN main.cpp (tanmatsu/main/main.cpp, separate from src/main.cpp) that
still declared a plain 'MyMesh the_mesh' global — a conflicting declaration that broke
the P4 build (only the S3 envs were built for b986f49). Apply the same placement-new-
into-PSRAM + reference pattern here; the P4's 32 MB PSRAM gets the same DRAM win.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:44:54 +02:00
Kaj SchittecatandClaude Opus 4.8 f78e3ed493 touch: settings category labels fit on one line (no mid-word split)
The Settings landing tiles centred their label in card_w - 80 px (40 px margins each
side). On the T-Deck's 2-column landscape layout that left only ~64 px, so a single
word ('Bluetooth') split mid-word and 'Radio & Mesh' / 'Quick replies' wrapped to two
lines. Lay the label out to the RIGHT of the left accent icon and centre within that
region (~100 px usable) instead of symmetric centring — every English category name
now fits on one line, unsplit. Wide cards (V4 portrait) are unaffected; long
translations still wrap at word boundaries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:24:53 +02:00
Kaj SchittecatandClaude Opus 4.8 b986f49e1a core: move the_mesh (~42 KB) to PSRAM — reclaim internal DRAM
the_mesh was the single biggest static internal-DRAM consumer at 42 KB (dominated
by the MAX_CONTACTS=100 ContactInfo array inside it). Internal DRAM (~320 KB) is the
scarce resource on the ESP32-S3 — Wi-Fi/BLE DMA + heap fight over it, and the V4 was
hitting ~99% on map-tile load (issue #47b). PSRAM (8 MB) is plentiful.

Place the whole the_mesh object in PSRAM via placement-new into a heap_caps_malloc
(MALLOC_CAP_SPIRAM) block and bind a reference, so every the_mesh.foo() call site is
unchanged. The constructor still runs at the SAME static-init point as the old direct
global (PSRAM is already up then), so timing and behaviour are identical; only the
address moves off internal DRAM. Falls back to internal RAM if PSRAM is absent.
ESP32-only (STM32/RP2040 keep the plain global). The contacts array rides along
inside the object — no vendored-core change.

Measured on heltec_v4 (.dram0.bss): 124160 -> 82000 B = ~41 KB internal DRAM freed.
Verified on the T-Deck: clean boot, [BOOT] mesh ok, reason=0, zero panic markers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:11:13 +02:00
f6192aa6e5 touch+deploy: opt-in OpenTopoMap style + persist map link-lines (open PRs)
Implements the three open PRs, with OpenStreetMap kept as the shipping default and
OpenTopoMap added as a user toggle (the deploy PRs flipped the default to topo —
this keeps OSM default and makes topo opt-in).

PR #61 (@Yazutsu) — persist the map 'Show link lines' toggle across reboots:
TouchCfg gains map_show_links (cfg v20, default 1); saved in mapOptLinesCb, loaded
in begin().

PR #52/#53 (@samuelcoustet) — OpenTopoMap tile proxy, adapted:
- deploy: tiles.wadamesh.com.conf keeps the default /{z}/{x}/{y}.jpg on OSM and
  adds /opentopo/ (OpenTopoMap) + an explicit /osm/ alias; tile-transcode.py grows
  _fetch_tile_png/_tile_response_from_png helpers feeding /osm + /opentopo while the
  root route stays OSM.
- firmware: new map_style pref (cfg v21, 0=OSM default, 1=OpenTopoMap) + a
  'Topographic map' switch in Map → Options. Topo tiles are namespaced on disk
  (/tiles/topo) and fetched via the proxy's /opentopo route, so the OSM and topo
  caches never collide and toggling is instant for already-cached tiles. Topo
  bypasses the OSM-only microSD packs and always fetches online; the shared
  (z,x,y) fetch-dedup ring is cleared on toggle so the new style re-queues.

Legal: OpenTopoMap map tiles are © OpenTopoMap (CC-BY-SA) over © OpenStreetMap
contributors (ODbL) + SRTM. The on-map © attribution and the Options → Info credits
sheet switch to that text when topo is active; the proxy keeps a contactable
User-Agent + 14-day cache per OpenTopoMap's tile-usage policy.

Topo needs the VPS deploy (restart wadamesh-tile-transcode + reload nginx) before
the /opentopo route serves; until then topo tiles 404 (handled as a permanent miss,
no retry hammer). OSM default is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Yazutsu <andrzej@gruziel.pl>
2026-06-26 18:49:41 +02:00
Kaj SchittecatandClaude Opus 4.8 cff58bbaa2 touch: move Discovered sort to a header icon (always visible)
The inline sort button sat after the empty-list early-return, so a Discovered
list with no entries (the common case when auto-add is on) never showed it, and
even when populated it didn't read as a control. Move it to a ⇄ icon button in the
modal header next to the cog — always present, same proven render path as the
cog/purge, opens the same sort sheet. Purge shifts one slot left to make room.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:25:31 +02:00
Kaj SchittecatandClaude Opus 4.8 b31e37cbbe touch: contacts Sort sheet closes on a choice + the close-X works
Regression from the previous commit: making the Sort sheet stay open (for the
asc/desc flip) meant picking a sort no longer dismissed it, and the close-X was a
no-op because it reused the backdrop callback whose target==current_target guard
(meant to ignore child taps on the backdrop) rejected the tap on the badge.

- ctSortOptCb applies the choice and closes the sheet again; flip a mode's
  direction by re-opening and tapping the active row (the ▲/▼ arrow shows it).
- The option-sheet close-X now uses an unconditional close handler instead of the
  guarded backdrop one, so it always dismisses (also fixes the Filter + Discovered
  sort sheets, which share the shell).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:16:11 +02:00
Kaj SchittecatandClaude Opus 4.8 76f0754d0d touch: emoji additions, Chats-header button reorder + QR icon, distance sort
Emoji: bake 4 more Noto colour glyphs (soccer, american football, moai,
transgender flag) and add them to the picker. The trans flag is a ZWJ sequence
the single-codepoint imgfont lookup can't render directly, so its combined Noto
image is keyed on the lead codepoint (U+1F3F3) with the trailing symbol mapped to
the zero-width glyph — the whole sequence renders as the one flag. Only the 4 new
glyphs are baked + spliced (scripts/build/add-emoji.py); the existing 252 stay
byte-identical (no Noto-main art drift).

Chats header: group the three actions at the LEFT edge in the order + (add) /
✓ (mark all read) / QR (share), and draw a real baked QR glyph (qr_icon.h, from
scripts/build/gen-qr-icon.py, recoloured to the text colour) instead of the old
LV_SYMBOL_IMAGE 'picture' stand-in.

Contacts sort: every sort option now toggles ascending/descending — re-tapping the
active row in the Sort sheet flips it and shows a ▲/▼ arrow. New 'Nearest first'
(distance) sort orders by distance from our GPS and excludes contacts that have
shared no location.

Discovered list: gains the same sort sheet (Recently heard / Name / Nearest, each
asc/desc) via a sort button at the top of the list; distance likewise excludes
no-location entries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:06:24 +02:00
Kaj SchittecatandClaude Opus 4.8 87cb1c3880 touch: retry transiently-failed map tiles instead of pinning them blank (#20)
renderMapTiles already queues every visible tile (edges + corners) on pan,
and queueTileForFetch marks each one 'seen' the instant it is queued. But the
fetch worker only cleared that mark on success — so a tile whose download hit a
TRANSIENT failure stayed flagged forever and was never re-queued. The heap-
safety gate is the common trigger: during a pan the render thread is decoding a
burst of tiles (heavy internal-DRAM use), so the worker's <12 KB-free skip trips
for several of the freshly-queued edge/corner tiles, which then sit blank until
the user forces a full 'reload tiles'. Wi-Fi blips, a full tile cache and proxy
5xx/timeouts had the same one-shot-and-stuck behaviour.

Add tileFetchForget() and drop the 'seen' mark on every transient failure
(heap gate, Wi-Fi down, cache full, negative/5xx/408/429 HTTP, disk write
hiccup) so the next render miss re-queues the tile automatically. A permanent
4xx rejection still stays marked so we don't re-hammer genuinely-absent tiles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:28:41 +02:00
Kaj SchittecatandClaude Opus 4.8 f39e072bd7 touch: stop debug prints corrupting the binary companion stream (#25, #54, #23)
The companion link is a BINARY protocol over Serial (USB-CDC). Unconditional debug
prints in operational paths — [ROOM] login send/resp, [TILE] fetch, [JPG] decode —
interleaved text into that stream and corrupted it. The MeshCore app then throws
'Bad state: Streamsink is bound to a stream' (#54) and the PC-app connect (#25) and
member-room login (#23) fail. Route them through a gated WIRE_DBG macro
(COMPANION_WIRE_DEBUG, off in release) like the existing SYNC_DEBUG_PRINTLN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:18:07 +02:00
Kaj SchittecatandClaude Opus 4.8 bed04417ed touch(v4): narrow the status-bar name window so a long name clears the clock (#47a)
On the 240px V4 bar the node-name window (74px) left only ~4px before the clock, so a
long name visually ran into the time. Shrink it to 66px for clearance. (Best-effort
without a V4 on the bench; clock position may need a follow-up once verified on-device.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:02:38 +02:00
Kaj SchittecatandClaude Opus 4.8 b1adef7364 touch: keyboard-nav toggle no longer snaps the settings list to the top (#45)
beta_15 preserved scroll on close, but turning keyboard-nav ON still jumped to the
top: the focus group was empty (cleared while nav was off) so navMaybeRebuild had no
focus to keep and fell to the first item, scrolling there. Add a one-shot focus hint
that pins focus to the switch you just toggled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 16:49:09 +02:00
Kaj SchittecatandClaude Opus 4.8 36e53d9a1c crash report: only prompt on a crash-boot, not every reset
'The device restarted after a crash' re-appeared after a manual reset / power-cycle:
the prompt gated only on a coredump EXISTING in flash, and the dump persists across
resets until exported. Gate crashReportMaybePrompt() on this boot's reset reason
(panic / task-WDT / int-WDT) instead. The dump stays exportable from About + esptool.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 16:37:50 +02:00
Kaj SchittecatandClaude Opus 4.8 7498c502cc touch: widen task-watchdog grace to 20s to stop flash-op WDT reboots (#56)
Decoded coredumps (device + the beta_16 ELFs) are all task-watchdog panics:
CPU0=IDLE0 CPU1=IDLE1, core 0 in spi_flash_op_block_func. A legitimate-but-slow
flash burst (SPIFFS GC, or a bulk DataStore save = thousands of writes) parks both
cores with the cache disabled, starving both IDLE tasks past the ~5s default so the
TWDT panics. The burst can't be chunked under the limit easily and the per-core
WdtHeavyGuard only covers core 0, so re-init the TWDT with a 20s grace (still
catches a genuine multi-second hang) instead. esp_task_wdt_init re-init reconfigures
the running TWDT (old API on this arduino-esp32).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 16:30:33 +02:00
Kaj SchittecatandClaude Opus 4.8 bbb7d31975 touch(tdeck): fix kbd backlight stuck-on after flash (#33) + lock-screen burn-in (#55)
#33: tdeckKeyboardSetBacklight only marked the I2C write dirty on a value CHANGE, and
the cached default is 0. A reflash resets the ESP32 but not the keyboard's C3, which
keeps its lit backlight, so the boot 'off' request (0==0) was never sent. Force the
first write so the C3's retained state is always overwritten.

#55: a hard-locked LIT panel never dimmed (clock burning in) because reveals/taps kept
_last_input_ms fresh. Add a burn-in guard: arm _lock_lit_ms only on a real off->lit
transition and dim the locked panel after a bounded window regardless of the screen
timeout or held/ghost touches.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 08:39:40 +02:00
Kaj SchittecatandClaude Opus 4.8 83cf0202f7 tanmatsu app-store: pre-flight validate name/description vs store regex
Catch a comma/em-dash/over-length description locally and abort, instead of
discovering it as a failed verify_metadata check on the upstream PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:56:33 +02:00
Kaj SchittecatandClaude Opus 4.8 142ea87064 tanmatsu app-store: description without comma/em-dash (verify_metadata regex)
The launcher store's verify_metadata check restricts description to
^[A-Za-z0-9_\- .!@#$%^&*()/\\]+$ (no commas, no em-dash). Reword so the
template passes on its own instead of needing a manual PR edit each release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:53:36 +02:00
Kaj SchittecatandClaude Opus 4.8 1ca2f4e3e6 scripts: mark tanmatsu-appstore-publish.sh executable
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:32:21 +02:00
Kaj SchittecatandClaude Opus 4.8 59b1b2ab09 touch(tanmatsu): beta_17 — Vol± gestures, message LED, microSD, backups on FFat, status-bar fixes
- Vol+ short = top-bar dropdown toggle; long hold = sound master toggle
- Message-notification LED: flash on new message, soft green glow on unread; toggle in Display settings
- Backups use the internal FFat partition (export/scan/import) + Import button on the Backups page
- microSD browsable in the file manager (P4 SDMMC slot 0; the C6 radio's slot 1 untouched)
- Status-bar icon overlaps fixed (charging-slide + language indicator now SC-scaled with the cluster)
- Vol- screen sleep/wake/lock (screen-lock work)
- Companion link: TCP server start + BLE bond NVS persistence + app-sent channel-message mirror

All Tanmatsu changes gated #if defined(HAS_TANMATSU); T-Deck/V4 bins stay byte-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:30:55 +02:00
Kaj SchittecatandClaude Opus 4.8 c62107c28a tanmatsu: untrack esp-idf/esp-idf-tools (were self-referential symlinks)
main committed tanmatsu/esp-idf + esp-idf-tools as symlinks pointing at their
own absolute path (mode 120000). A fresh 'git checkout main' materialises those
self-loops over the real local ESP-IDF install — breaking the build ('too many
levels of symbolic links' at source export.sh) and deleting the gitignored IDF.
Untrack + gitignore them like the build setup expects; the project-local
ESP-IDF is installed locally, never committed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 16:45:04 +02:00
Kaj Schittecat eb0f3cf0b0 chore: refresh Mesh America catalog for beta_16 2026-06-24 15:45:04 +02:00
Kaj SchittecatandClaude Opus 4.8 36a328a4f7 release: beta_16 notes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:42:41 +02:00
Kaj SchittecatandClaude Opus 4.8 13fca6369f touch: OTA previous-version — one button + version popup + confirm
Reworks the earlier-version OTA UI from three inline buttons into a single
'Install a previous version' button that opens a popup of the available older
betas; picking one shows an are-you-sure confirm (reusing showConfirm) before it
closes and hands off to the existing download/verify/reboot flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:21:37 +02:00
Kaj SchittecatandClaude Opus 4.8 abdb878006 touch: OTA — install a previous version (up to 3 back)
Adds a 'Reinstall an earlier version' section to the Wi-Fi OTA UI (native
dual-OTA boards only): up to three buttons for beta_(latest-1..3), each flashing
that exact published bin via the same download/verify/reboot path. The worker URL
is parameterised on a target version; the existing 'Install update' button now
routes through the same otaStartInstall(latest) helper. Buttons auto-hide when
there aren't that many earlier releases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:08:18 +02:00
Kaj SchittecatandClaude Opus 4.8 2b4f2bc29e touch: fix status-bar clock overlapping the node name on the V4
The clock's runtime placement used the wide-bar -142 offset on every board,
overriding the V4's intended -126 build slot. On the narrow V4 portrait bar that
pulled the clock left into the width-capped node-name window — the long-standing
'clock overlaps the name' issue. Make the placement bar-width-aware: <300 px bars
(V4) use -126/-94, wide bars (T-Deck/Tanmatsu) keep -142/-110.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:47:15 +02:00
Kaj SchittecatandClaude Opus 4.8 79b4f10c25 touch: V4 Sensors tab + Home env widget only when sensors present
Refines the Expansion-Kit UI (#29) so it doesn't intrude on a bare V4: the
Sensors tab and the Home environment widget now appear only when an actual
environment sensor (BME280 / GXHTV3) is detected at boot — battery and GPS no
longer count, so a V4 without the Expansion Kit board shows the normal 5-tab
layout and a clean Home (no empty "Batt ----" / "Temp ...." lines). Adds a
"Show Sensors tab" toggle in Display settings to hide it even when sensors are
present (applies on restart). Tab indices resolve at boot so Settings shifts
back to slot 4 when the Sensors tab is absent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:34:29 +02:00
42746ef387 touch: Heltec V4 Expansion Kit UI + mesh console (issue #29)
Heltec V4 Expansion Kit local-sensor UI, board-gated behind HAS_EXPANSION_KIT
(V4 touch env only — T-Deck/Tanmatsu unaffected): a Sensors bottom tab
(atmosphere / GPS / battery + history charts), a Home env summary widget, an
Expansion Kit card in Device settings, and a Local Sensors detail page. Reads
on-board MeshCore sensors (BME280, SHT4x, battery, GPS, buzzer) via the standard
sensor/telemetry API — no new hardware drivers.

Mesh console: enable the existing Terminal on the V4 (HAS_TOUCH_UI), renamed
"Mesh console", with wifi/tcp/ble on-off + ota status quick commands. The
SD-backed file-manager pieces stay T-Deck-only (V4/Tanmatsu get a no-op path).

Tab indices are conditional so non-Expansion boards keep 5 tabs (Settings=4) and
the V4 gets 6 (Sensors=4, Settings=5). The PR's GPS-nav/waypoint and weather
pressure-alarm bundles were intentionally left out.

Ported from PR #29 by @samuelcoustet, re-applied against current main.

Co-Authored-By: samuelcoustet <257299705+samuelcoustet@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 13:57:40 +02:00
300bf89e1a scripts: add companion-protocol serial test harness
Standalone dev tool (pyserial) that drives the companion binary protocol over a
serial port — DEVICE_QUERY/APP_START, battery/storage, stats, telemetry with a
Cayenne-LPP decoder, and meshcomod CLI commands. No firmware coupling; dev-only.

Ported verbatim from PR #29 by @samuelcoustet.

Co-Authored-By: samuelcoustet <257299705+samuelcoustet@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:41:50 +02:00
45e46cd3ab touch: EU keyboard layouts (FR/NL/DE/ES/IT) + auto-select by UI language
Adds French (AZERTY), Dutch, German (QWERTZ), Spanish and Italian layouts — both
the on-screen keyboards and the T-Deck physical-keyboard maps (KeyboardLayoutId
FR..IT). Picking a UI language now auto-enables and selects the matching keyboard
layout, so choosing French gives AZERTY without a separate trip to keyboard
settings. Accented letters still come from the long-press accent popup; no new
fonts required. Extended the settings layout-enable list to match the larger enum
(was a latent out-of-bounds once the layout count grew).

Ported from PR #29 by @samuelcoustet — the layout data applied cleanly; the
UITask wiring was re-applied by hand against current main (the PR was cut from
beta_12 and no longer merges).

Co-Authored-By: samuelcoustet <257299705+samuelcoustet@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:41:50 +02:00
Kaj SchittecatandClaude Opus 4.8 4bb7c08bf0 site: standalone — 'install a previous version' picker (latest 4 betas + notes)
Collapsible version picker in the standalone install guide: a dropdown of the latest 4 betas (from the GitHub releases API — the firmware-host listing has no CORS), rendered release notes per version, and selecting a version re-points both Install buttons at that build (client-built manifest -> the CORS-enabled firmware-host -merged.bin). Defaults to latest; switching back restores the /latest/ manifest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 12:35:20 +02:00
Kaj Schittecat 97b24c4268 Merge PR #48: per-channel scope shows default region as placeholder (tun0) 2026-06-24 12:11:23 +02:00
Kaj Schittecat 981fcba226 Merge PR #49: contact action-sheet landscape overflow fix (tun0) 2026-06-24 12:11:23 +02:00
Kaj Schittecat 50f73f41b0 Merge PR #50: edge-scroll — modals block background scroll (tun0) 2026-06-24 12:11:23 +02:00
Kaj SchittecatandClaude Opus 4.8 c5c0a04484 touch: restore Wi-Fi OTA self-update on native dual-OTA boards
Re-enables the over-the-air update that #74 stubbed out — the block then was
undersized 1.5 MB OTA slots, and both touch tables are now 3.875 MB A/B. Wi-Fi
self-update runs again on natively-flashed boards (every V4 + standalone T-Deck);
Launcher single-slot installs and the Tanmatsu (AppFS) keep the manual path.

- Gated on touchHasOtaUpdateSlot() (a spare A/B slot exists) — already drives the
  update badge, so Launcher/Tanmatsu never reach the install path.
- Self-contained Arduino Update flow in the touch app (the vendored core's OTA
  allow-list is meshcomod-only, so we don't route through board.startHttpOtaFromUrl).
  Fetches the immutable releases/TOUCH/beta_<N>/<board>.bin over plain HTTP from
  firmware.wadamesh.com on the tile worker (off the UI thread); progress polled by
  a UI timer.
- Update.begin(len) does the spare-slot select AND the size check, so a legacy
  too-small slot fails cleanly and falls back to the manual flasher message.
- Verify (MD5) + set-bootable via Update.end(true), then rebootDevice() (saves
  chat history first). A corrupt/short download leaves the running slot intact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 11:56:04 +02:00
Ruben Laban 10ea0d8399 touch: fix contact action sheet overflow on landscape display
The grid_items count missed the conditional "Show on map" button,
causing the card height to be calculated short when the contact has a
GPS fix. Additionally, on landscape displays (sh=240) the backdrop is
only 218px, which repeater cards with many buttons already exceeded.

Fix both: pre-check the GPS condition and include it in grid_items,
and cap the card's displayed height to the available backdrop space
with vertical scroll enabled when the content overflows. The close
badge is FLOATING so it stays anchored regardless of scroll position.
2026-06-24 11:11:56 +02:00
Kaj Schittecat 475245c90e chore: refresh Mesh America catalog for beta_15 2026-06-24 11:01:48 +02:00
Kaj SchittecatandClaude Opus 4.8 1f43a8ba37 release: beta_15 notes — amber power-save battery, @-mention picker (#42), overlay scroll-keep (#45), app-send sync (#46), kbd-nav off by default
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 10:58:45 +02:00
Kaj SchittecatandClaude Opus 4.8 d491065719 touch: power-save shows as an amber battery, not a moon icon (T-Deck)
Idle power-save (light-sleep) advertised itself with a separate crescent-moon
glyph in the status bar. Replace it with an iPhone-Low-Power-Mode-style amber
tint on the battery icon (iOS systemYellow): clearer, and frees the bar slot.
The moon stays in the tree but hidden, keeping the layout and its tap handler.

A single applyBattColor() helper owns the battery colour so the map-chrome
restyle (black/white over light tiles) and the per-tick refresh never fight —
the amber overlays whatever base colour the context wants when power-save is on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 10:54:06 +02:00
Ruben Laban ab52385f98 touch: edge-scroll — don't fall through to background when dialog is at scroll limit
If lv_layer_top() has a visible widget at the search coordinate (i.e. a
modal or dialog is open), stop the layer search there regardless of whether
a scrollable was found. Prevents the content behind the dialog from being
scrolled when the dialog itself has no more room to scroll.
2026-06-24 10:49:40 +02:00
Kaj SchittecatandClaude Opus 4.8 d11a5bb6b2 touch: keep list scroll position when closing an overlay (issue #45)
Closing a settings item / modal / chat with keyboard-nav on snapped the
underlying list back to the top. navMaybeRebuild rebuilds the focus group when
the overlay closes, but the previously-focused element lived inside the overlay
and is gone, so focus fell to the first list item and the list scrolled up to
show it. Affected every screen because every overlay closes through this path.

Stash the page item that had focus when leaving for an overlay/chat, and restore
it on return — so focus, and the scroll position, land back where they were.
Falls back to the prior behaviour if that item is genuinely gone. Most visible on
beta_14 (keyboard-nav defaulted on); new installs now default it off.

Reported by ZorkaTheDog (#45).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 10:31:47 +02:00
Ruben Laban 80af33e7f9 touch: show default region scope as placeholder in per-channel scope field
When a global region scope is configured, the per-channel override input
now shows it as greyed-out placeholder text instead of the generic "#region",
so users can see at a glance what scope an empty field will inherit.
Falls back to "(no default)" when no global scope is set.
2026-06-24 10:29:35 +02:00
Kaj SchittecatandClaude Opus 4.8 c8849916df touch: show companion-app-sent DMs on the device (issue #46, half 1)
A DM sent from the MeshCore companion app went out over the mesh but never
appeared on the T-Deck: the wire protocol deliberately doesn't echo sent
messages back to clients, so the on-device UI (a separate consumer) never saw
app-originated sends. Add an appSentMsgToContact() UI hook (default no-op for
non-touch UIs); the touch UI mirrors the message as a local outgoing bubble in
the recipient's thread. Called from the CMD_SEND_TXT_MSG handler on first
handling only (a client/transport retry was already mirrored).

Still open: the receive direction (T-Deck-received messages reaching the app) —
queueMessage already pushes RESP_CODE_CONTACT_MSG_RECV_V3 to all transports, so
that half needs a frame capture against the real app to pin down.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 09:54:38 +02:00
Kaj SchittecatandClaude Opus 4.8 278b31cc92 touch: keyboard navigation OFF by default (Tanmatsu stays on)
Reverses the v15 force-on. New installs and existing T-Deck/V4 devices now
default to keyboard-nav OFF (a v18 one-time migration flips existing installs
to off); the user's later explicit on/off then persists. The Tanmatsu is exempt
— it has no touchscreen, so keyboard nav is its only input and stays on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 09:39:28 +02:00
Kaj SchittecatandClaude Opus 4.8 24f338438e touch: tapping an already-selected field enters edit mode
The touch-to-edit path keyed off navFocusCb (a focus CHANGE), so a field that was
already the selected one — e.g. the Profile node-name field, auto-selected on
entry — couldn't be made typable by tapping it (the tap doesn't re-focus, so
navFocusCb never fires). settingsFieldFocusCb already runs on CLICKED; now a real
tap also sets edit mode, so a selected field becomes typable on tap. Keyboard-nav
onto a field still starts in navigate mode (press Enter).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 09:29:05 +02:00
Kaj SchittecatandClaude Opus 4.8 3e1b45504f touch: navTreeSig must not COUNT NAV_SKIP children (complete the accent-box fix)
The previous fix returned 0 from the recursion for NAV_SKIP/HIDDEN subtrees, but
the parent loop still iterated over the added/removed child (h = h*prime + 0),
so the child COUNT still moved the signature. The accent box + @-mention picker
are created/deleted on every keystroke, so the count flipped each time → rebuild
→ edit-mode reset → typing broke mid-word (and Esc then read as navigate-back,
exiting the screen). Now skip those children in the loop entirely so they never
affect the signature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 09:22:18 +02:00
Kaj SchittecatandClaude Opus 4.8 12c34a2b35 touch: accent/mention box no longer breaks typing in non-chat fields
Non-chat text fields (add-channel, Wi-Fi, room login, settings modals) live on
the top UI layer, so keyboard nav is rooted there (useTop). The accent box and
@-mention picker are ALSO on the top layer, and they were counting toward
navTreeSig() — so each keystroke that showed/hid the box flipped the signature,
triggered a navMaybeRebuild(), and that rebuild's remove-all + re-focus churn
reset edit mode, dropping you out of typing mid-word.

- navTreeSig() now skips NAV_SKIP_FLAG subtrees (accent box, mention picker), so
  those passive overlays never trigger a rebuild.
- navFocusCb() ignores the transient f==null a rebuild's remove-all emits, so any
  other rebuild can't reset edit mode either.

(The T-Deck never used the keyboard mirror — kbMirrorActive() is false there; the
mirror is V4-only for the on-screen keyboard.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 09:05:25 +02:00
Kaj SchittecatandClaude Opus 4.8 0a3f9fe82f touch: @-mention picker (#42), edit-mode cursor cue, robust edit-state
- @-mention picker (issue #42): typing "@" in a chat composer pops a touch list
  of matching contacts (prefix-filtered, up to 6); tap one to insert "@Name ".
  Passive NAV_SKIP_FLAG overlay like the accent box; touch-select for now. Takes
  priority over the accent box while a mention is active (composerSuggestRefresh).
- Cursor now reflects edit mode: shown only while editing, hidden while a focused
  field is just the keyboard-nav target — a reliable "ready to type" cue
  (navSyncCursor() each nav tick).
- Edit mode is decided only on a genuine move to a different field, so a rebuild
  that re-focuses the same field (e.g. while the accent box is up) no longer
  kicks you out of typing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 08:20:40 +02:00
Kaj SchittecatandClaude Opus 4.8 02cff3389d touch: tap-to-edit on touch focus + map-render watchdog yield
- Keyboard nav: tapping a text field by touch now enters edit mode immediately
  (focus came from the pointer indev) instead of requiring Enter. Reaching a
  field with the letter-nav keys still starts in navigate mode (press Enter).
- Map: yield to IDLE after each tile in renderMapTiles() so a slow multi-tile
  render on loopTask can't starve the task watchdog and panic — fixes a beta_14
  TWDT reboot seen during map use (CPU1=loopTask). The download worker already
  does the same vTaskDelay(1) on its side.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 08:08:16 +02:00
Kaj Schittecat 4e3c212a89 chore: refresh Mesh America catalog for beta_14 2026-06-24 00:43:49 +02:00
Kaj SchittecatandClaude Opus 4.8 a804d709cf release: beta_14 notes — hotfix (kbd-nav-off typing regression)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 00:41:55 +02:00
Kaj SchittecatandClaude Opus 4.8 7111789fa7 touch: fix typing dead when keyboard-nav is OFF (beta_13 regression)
The keyboard-nav rework gated the physical-keyboard typing target on
s_nav_ta_editing (edit mode). That flag is only ever set while keyboard
navigation is ON (navFocusCb, driven by the nav focus group). With keyboard
navigation turned OFF the nav group isn't maintained, so the flag stayed false
and `ta` was always null — keys never reached the focused field and typing was
completely dead on the T-Deck (and Tanmatsu).

Fix: when keyboard-nav is OFF there is no navigation to protect, so a focused
field is always the typing target (ta = ta_focused). Edit-mode gating still
applies only when keyboard-nav is ON. Affects handleHwKey (T-Deck) + navPump
(Tanmatsu); the V4 (no keyboard nav) was never affected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 00:39:38 +02:00
Kaj SchittecatandClaude Opus 4.8 fbc015cf5c site: Community Videos section + YouTube/Docs/Map quick-link buttons
- New "Community videos" section under the install/flasher area: a JS-array-driven grid with a click-to-play facade (youtube-nocookie, lazy iframe). First entry is KDHD Stuff's review; add more by appending to the VIDEOS array.
- Float the quick-links into stacked left/right rails: left = Telegram + a YouTube button that jumps to the videos section; right = bug report + a Documentation button (placeholder — toasts "coming soon") + a Map button to t413.com's MapTileDl downloader.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 00:34:44 +02:00
Kaj Schittecat 3ae9582431 chore: refresh Mesh America catalog for beta_13 2026-06-24 00:02:48 +02:00
Kaj SchittecatandClaude Opus 4.8 36e5ed4c36 release: beta_13 notes — community drop (#39 #40 #22 #38 #29 + keyboard/typing/12h/V4 fixes)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 23:59:56 +02:00
Kaj SchittecatandClaude Opus 4.8 f817fca41c touch: disable crash-report UI on Heltec V4 (no way to retrieve/send the dump)
The boot crash-report prompt + the About-page export button are only useful
where the saved coredump file can actually be retrieved and sent to the devs:
the T-Deck (SD card) and Tanmatsu (FFat, via the launcher file manager). On the
Heltec V4 there's no SD and no user-reachable internal storage, so the prompt
was a dead end. Gate crashDumpCheck() to those boards only — on the V4 the dump
is never detected, so the whole crash-report UI stays off (everything downstream
gates on s_crash_dump_size). The panic coredump still sits in its flash
partition for a USB/esptool pull if ever needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 23:49:41 +02:00
SamandClaude Sonnet 4.6 5fcf6ddac3 fix: block commands while transport-off confirmation is pending
Any command arriving while s_meshcomod_pending_action is non-NONE and
the timeout hasn't expired previously fell through the confirmation
block and was processed normally, letting a second 'tcp off' / 'ble off'
silently overwrite the pending action and lose the original confirmation.

Add a final else clause that blocks the new command and reminds the user
to 'ok' or 'cancel' the in-flight request.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 23:25:02 +02:00
SamandClaude Sonnet 4.6 5e6aaa8852 fix: restart TCP immediately on enable, guard OTA slot, fix toggleTcp alert race, re-enable warnings
- enableTcp() now calls startTcpServer(false) directly instead of relying
  on the main loop — fixes TCP not restarting when wifi_started is false
  (BLE-only mode, radio toggled off, or cold re-enable from USB).

- Add touchHasOtaUpdateSlot() guard to the OTA button creation block so
  the update affordances stay hidden under Launcher (single app slot),
  where ota url would fail silently.

- Fix toggleTcpCb alert race: snapshot isTcpEnabled() before the toggle
  so the alert text reflects the transition, not a post-action re-read
  that could mismatch during OTA suspension.

- Replace global -w with -Wno-deprecated-declarations -Wno-unused-parameter
  in both touch environments to keep third-party lib noise down while
  surfacing real regressions in OTA, network, and companion paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 23:25:02 +02:00
Kaj Schittecat cd2dc8b3f8 Merge PR #38: touch — optional trackball edge-scroll (tun0) 2026-06-23 23:22:34 +02:00
Kaj Schittecat f4fb7171e5 Merge PR #22: touch — beer emoji, status-bar unread tap, new-contact toast toggle, Czech accents (Vybo)
# Conflicts:
#	src/helpers/esp32/TouchPrefsStore.cpp
2026-06-23 23:22:23 +02:00
Kaj Schittecat 8322b5361d Merge PR #40: ui-touch — room-server posts show author, not room name (Yazutsu, closes #32) 2026-06-23 23:19:14 +02:00
Kaj Schittecat 266f5c8adc Merge PR #39: touch — Direct (0-hop) contact + map filter (tun0) 2026-06-23 23:19:01 +02:00
Kaj SchittecatandClaude Opus 4.8 069aeb8908 touch: keyboard-nav typing fixes, dedicated symbol picker, 12h everywhere
Post-beta_12 touch-UI fixes (T-Deck / Tanmatsu keyboard nav + composer):

Keyboard navigation
- Chat composer auto-edits on open (cursor shows, type immediately); every
  OTHER field stays in navigate mode so the letter-nav keys keep working —
  press select/Enter to edit it. Enter (or Esc/X on Tanmatsu) on an empty
  composer drops back to navigate mode.
- Menu-bar tab-hotkey letters no longer fire while a text field is the focused
  element (fixes a stray tab-jump while typing); directional nav still moves.
- New "Show menu-bar letters" setting (off by default) for the tab hotkeys.

Accent box
- The tap-to-pick accent box no longer steals the keyboard-nav focus group
  (NAV_SKIP_FLAG) — it's a passive hint, so typing continues and a keybind no
  longer navigates. Ready to wire arrow-key selection later.

Composer
- Dedicated "Special characters" picker (% $ @ # & ... currency/math/accents),
  separate from the emoji picker.
- Long-press a sent message -> Resend.

Misc
- 12-hour clock honoured everywhere (top bar, chat bubbles, lock screen), not
  just the chat overview.
- GPS UART RX buffer enlarged only when GPS is enabled (restores V4 RAM).
- Derive time-of-day from received mesh message timestamps when Wi-Fi/GPS off.
- Self-record the task-watchdog context on a TWDT reset for the crash report.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 23:18:34 +02:00
Yazutsu 61a914240f ui-touch: show post author instead of room name for room-server messages
Room posts arrive as TXT_TYPE_SIGNED_PLAIN with the author in the 4-byte sender_prefix and a bare body. The display path passed the room contact's name, so every post showed the room name. Resolve the author from the prefix and pass it as the per-message sender via a new newRoomMsgFromPubWithMeta hook; the room stays the thread. Tested on T-Deck Plus.

Signed-off-by: Yazutsu <andrzej@gruziel.pl>
2026-06-23 22:45:39 +02:00
Ruben Laban 29204dd33f touch: add Direct (0-hop) filter to contact list and map
Contact list gains a "Direct (0-hop)" option in the Filter sheet (value 5)
that shows only nodes heard without any repeater hops (out_path_len == 0).
Header chip shows "Dir" when active. Selecting a filter now auto-dismisses
the sheet (matching sort behaviour).

Map options panel gets a "Direct (0-hop) only" toggle that hides both
contact markers and link lines for any multi-hop or path-unknown node.
2026-06-23 14:36:57 +02:00
Ruben Laban bb86a32b04 touch: add optional edge-scroll for trackball cursor mode
When enabled in Settings → Keyboard, pushing the cursor past any screen
edge routes the overflow into the nearest scrollable widget rather than
absorbing it.  Works on regular tab content (lv_scr_act children) and on
lv_layer_top() overlays such as the app drawer.

Implementation:
- tbFindScrollableAt() hit-tests at the vertical centre of the content
  area (avoids status/tab-bar chrome and per-tab headers), searches
  lv_layer_top() before lv_scr_act(), then walks ancestors to the first
  scrollable with room in the overflow direction.
- scroll_by direction is negated relative to raw overflow (scroll_by(+dy)
  moves children down = reveals earlier content = scroll up).
- Scroll amount is capped to lv_obj_get_scroll_top/bottom/left/right so
  we never push past the widget's content bounds.
- Preference persisted as NVS key "tb_edgesc" (default off).
- Map, Snake, and emoji-picker modes are unaffected (they return before
  cursor mode runs).
2026-06-23 14:07:57 +02:00
Kaj Schittecat f985458363 chore: refresh Mesh America catalog for beta_12 2026-06-23 08:51:17 +02:00
Kaj SchittecatandClaude Opus 4.8 30083efdd7 touch: beta_12 — GPS hotfix (enlarge GPS UART RX buffer)
The GPS NMEA UART used Arduino's default 256-byte RX ring (~66 ms of slack at
38400 baud). On the busy touch UI a single long LVGL/map frame stalls the loop
past that window, dropping bytes and corrupting NMEA ephemeris subframes — each
loss costs the receiver ~30 s, turning a ~1-minute fix into several minutes or,
with frequent stalls, never acquiring. Confirmed on-device: peak UART backlog
hit 448 B at idle, well past the 256 B default. Bump the ring to 4096 B (~1 s of
slack) before the core opens Serial1 in sensors.begin(). App-side (no core
re-tag); gated to ENV_INCLUDE_GPS so both touch boards get it. A -DGPS_BUF_DEBUG
backlog probe is included (compiled out of releases).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 08:49:00 +02:00
Dan Vybiral 4a0d7f5247 touch: beer emoji, status-bar unread tap, new-contact toast toggle, Czech accents
- emoji: bake the beer mug U+1F37A and clinking beers U+1F37B as colour glyphs
  into emoji_data.c (now 252) and add the beer mug to the composer picker; the
  clinking pair was already listed but only rendered monochrome, now colour.
- status bar: tapping the unread badge (envelope + count) on the left now jumps
  to the Chats inbox. The CLICKABLE flag is toggled per-tick so only the badge
  state intercepts the tap — every other left-zone state (Back chevron, profile
  name, OSM credit, blank) keeps the bar's control-center / Back tap.
- contacts: add a "Notify on new contact" toggle to the Auto-add page
  (TouchCfg v8, default ON). When off it suppresses the auto-discovery toast
  chip in UITask::notify(); the diag-log line and tab badge are unaffected.
- keyboard: add the Czech accents c-caron, e-caron, t-caron, z-caron, r-caron
  and u-ring (plus uppercase) to the long-press / tap accent popup. Glyphs are
  already baked in the extras fallback fonts, so no font regeneration needed.

Builds verified on both LilyGo_TDeck_companion_radio_touch and
heltec_v4_tft_companion_radio_usb_tcp_touch.

Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-22 20:37:24 +02:00
Kaj SchittecatandClaude Opus 4.8 d22390aeb8 flasher: point manifest at immutable per-tag bin, not latest/
The web-flasher manifest referenced https://firmware.wadamesh.com/latest/<board>-merged.bin
— a stable filename cached 4h (Cache-Control max-age=14400) and overwritten in place each
release. version.json (max-age=300) advertised the new tag within minutes, but the latest/
bin could keep serving the PREVIOUS beta's bytes per-POP for up to 4h, so a user flashing
right after a release could install the old version (reported: flashed beta_11, device showed
beta_10). Point the manifest at the immutable releases/TOUCH/<tag>/ bin instead — that URL is
never overwritten so its cache is always correct, and the 5-min manifest TTL propagates the
new path fast.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 20:10:44 +02:00
Kaj Schittecat 3130a1198e chore: refresh Mesh America catalog for beta_11 2026-06-22 19:17:00 +02:00
Kaj SchittecatandClaude Opus 4.8 1f755ec3b8 touch: beta_11 — bindable keyboard navigation, crash-report prompt, map zoom buttons, app-drawer-as-home; publish Tanmatsu port source
Headline: full keyboard navigation (on by default) — WASDZ spatial focus move, S select,
Q back, per-tab hotkeys (E/R/T/U/I) shown on the icons, plus scroll up/down (F/C). Every
key is remappable in Settings -> Keyboard. Foundation for keyboard-first devices like the
Tanmatsu T-Pager (no touchscreen) and a reliable keyboard workflow on the T-Deck today.
Also: #27 receive/flood reboot fix, on-boot crash-report prompt, map zoom +/- buttons (#26),
route-replay wide overview, 'App drawer as home' option. The in-progress Tanmatsu (ESP32-P4)
port source is now public. Mesh America catalog + release.sh kept from main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 19:10:40 +02:00
Kaj SchittecatandClaude Opus 4.8 c931df6054 touch: beta_10 — trackball D-pad mode, microSD tile-cache fill, Battery group
- T-Deck: trackball mouse/D-pad navigation toggle (Settings -> Keyboard).
- Map (#20): microSD-tile mode now caches Wi-Fi-fetched tiles onto the SD card so
  the offline library fills in + grows instead of going blank past its edge.
- Power: PR #19 idle power-save (Dan Vybiral / @Vybo) relocated into a new
  'Battery' group under Settings -> System, alongside a button that opens the
  battery / power-history screen.
- TouchCfg v10 (ui_scale, tb_keypad, sleep_idle). Mesh America catalog -> beta_10.

Curated touch overlay onto main (the WIP Tanmatsu ESP32-P4 build system stays on
the tanmatsu-port branch). Co-Authored-By: Dan Vybiral <dan.vybiral@greencode.cz>

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 00:08:13 +02:00
Dan Vybiral 11b31e1cdd touch: idle power-save — loop-throttle + downclock when parked (#19)
Opt-in Idle power-save for the T-Deck: when parked (screen off, on battery, no
companion client, Wi-Fi+BLE off, mesh idle) the main loop is throttled with
vTaskDelay so the FreeRTOS idle task can halt the CPU between ticks, on top of the
existing 80 MHz screen-off downclock — without ever stopping mesh RX. Default OFF
(Settings -> Lock, persisted TouchCfg v8 sleep_idle); status-bar moon indicator +
idle stats. Heltec V4 compiles (gate never passes there). A plain scheduler throttle
rather than esp_light_sleep_start(), which trips INT_WDT in this build.

Co-authored-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-21 22:39:59 +02:00
Kaj SchittecatandClaude Opus 4.8 c16b5230a6 release: auto-refresh + push the Mesh America catalog each beta
release.sh now regenerates deploy/meshamerica-catalog.json for the tag and, when
run on main, commits + pushes that one file so apps.meshamerica.com (the flasher
Cascadia Mesh uses) always points at the newest immutable bins instead of going
stale on the previous beta. Idempotent (no-op when already current); non-fatal if
the push fails; also mirrors the catalog into the VPS publish tree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 22:27:28 +02:00
Kaj SchittecatandClaude Opus 4.8 f5362b1cb3 deploy: add Mesh America Configurator provider catalog
Catalog JSON (Mesh America provider schema) + a reusable generator so the two
wadamesh touch boards appear in the Mesh America Device Configurator
(apps.meshamerica.com — the flasher Cascadia Mesh uses). Points at the immutable
per-tag bins on firmware.wadamesh.com (HTTPS + CORS). Served live over
raw.githubusercontent.com.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 22:20:29 +02:00
Kaj SchittecatandClaude Opus 4.8 7a63ffe86c touch: beta_9 — crash/Wi-Fi reliability, crash-report export, room fixes, app-drawer icon size
- Stability: Wi-Fi auto-reconnect; corrupt chat-history quarantine (fixes reboot-on-
  receive / inaccessible messages); draw-buffer + flush NULL-guards (boot-panic).
- Crash-report export to SD/SPIFFS (Settings -> About).
- Map: reload purges on-disk tiles offline + visible zoom band; corrupt-tile self-heal
  on load; z15 dedup-key overflow fixed.
- Contacts: favourites always listed (#17).
- Chat: room message send-time (#26) + sender labels; QR share uses the official
  meshcore://contact/add format (#16); unread badge capped at 99+.
- App drawer: icon-size setting (Compact/Large) + smoother scrolling.
- ~5 KB internal DRAM reclaimed (backup arrays + msg_idx -> PSRAM).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:16:04 +02:00
Kaj SchittecatandClaude Opus 4.8 2aa8f59410 touch: beta_8 — Ack reply, map visibility toggles + auto-refresh, RAM trim
- Chat: quick replies insert at the cursor (no longer replace typed text);
  long-press 'Ack' drops a signal-strength + route reply into the composer
  (@mentions the sender on channels); fixed the action-menu overflowing the screen.
- Map: +/- zoom glyph; per-line on-map visibility toggles (coords / tile xyz /
  contacts); microSD-tiles regrouped under a heading; auto-refresh follow button
  with on/off toast; close-X overlap fix when scrolled.
- Perf: ~18.9 KB internal DRAM reclaimed (6 scratch buffers -> on-demand PSRAM).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 14:36:46 +02:00
Kaj SchittecatandClaude Opus 4.8 c8b451f14f release: beta_7 notes — credit contributors prominently (Vybo #12, Ruben Laban #15, marcelverdult #13)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 12:32:03 +02:00
Kaj SchittecatandClaude Opus 4.8 9e567e1789 touch: beta_7 — app-drawer fit + visible scroll, Snake touch steering & icon
- App drawer: size tiles to fit so the bottom row's labels aren't clipped, and
  show a persistent scrollbar when the grid overflows (discoverable scroll).
- Snake: steer via the hardware swipe detector (works on the V4 cap-touch and
  the T-Deck), stop touch/swipe leaking to the screen behind the overlay, and
  gate the status bar while playing.
- Snake app-drawer icon drawn from shapes — the U+1F40D emoji isn't in the baked
  colour-emoji set, so it rendered as a tofu box.
- Add beta_7 release notes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 12:21:49 +02:00
Kaj Schittecat bbcafa3088 Merge PR #12 (Vybo): status-bar tools, battery history, map night mode, image viewer, Snake
# Conflicts:
#	src/ui-touch/UITask.cpp
2026-06-18 11:04:08 +02:00
Kaj Schittecat b3af35666b Merge PR #15 (tun0): fix inaccurate unread badges + split history persistence
- closeChatPanel marks-read + flags dirty immediately (no stale badge)
- post-load reconciliation clears phantom unread for evicted threads
- split thread-meta (200ms flush) vs message-ring (lazy 2s) persistence
- ring 96 -> 500
2026-06-18 10:58:08 +02:00
Kaj SchittecatandClaude Opus 4.8 eca5281f22 touch: post-beta_6 batch — BLE PIN, wallpaper-from-files, storage & memory fixes
- BLE: user-settable 6-digit pairing code (Settings -> Bluetooth), MyMesh::setBLEPin.
- Lock wallpaper: pick via the file manager (fast, any folder) + 'Set as wallpaper'
  in the image viewer; downscaling JPEG decoder accepts images > 1024 px and keeps
  the held buffer small; clear message for progressive JPEGs.
- Map tiles: validate JPEG magic before caching + re-fetch bad cached tiles
  (fixes black quadrants).
- Channel @reply: quick-reply now appends at the cursor instead of wiping the
  mention.
- Audio: heap pre-flight before i2s_driver_install (fixes the esp_timer NO_MEM
  abort when toggling sound under BLE+Wi-Fi memory pressure).
- App drawer: squircle tiles. Log hygiene: CORE_DEBUG_LEVEL 1->0 (mute benign
  vfs/Preferences spam off the USB-CDC companion stream).
- Core bump to core-v1.16.4 (drops the 4-byte chat-payload trailer, issue #13).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 10:57:50 +02:00
Ruben LabanandClaude Sonnet 4.6 3a70a8ab0f touch: fix legacy history migration when ring size grew
loadLegacyHistoryFromStorage() tried to read MAX_UI_MESSAGES (now 500)
records from a file that only contained 96 (old ring size). It read the
thread metadata fine — populating unread counts — then hit EOF on the
messages and returned false. The reconciliation pass that resets phantom
unread counts never ran, so the first boot after the ring bump showed
high unread counters against empty channels.

Fix: derive the actual slot count from the remaining file size and only
read that many message records, zero-filling the rest. The phantom-badge
reconciliation in loadHistoryFromStorage() then clears any thread whose
messages were already evicted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 10:45:30 +02:00
Ruben LabanandClaude Sonnet 4.6 2894776a11 touch: split history save into threads+msgs files; bump ring to 500
Thread metadata (~4 KB, contains unread counts) and the message ring
(now ~115 KB at 500 slots) are now persisted to separate files
(/ui_threads_v1.bin and /ui_msgs_v1.bin).

Thread metadata flushes on the original 200 ms delay so unread counts
survive a hard reset. The message ring flushes lazily every 2 s,
batching burst arrivals into a single write instead of hammering SPIFFS
on every message — reducing flash wear and WdtHeavyGuard exposure as
the ring grows.

MAX_UI_MESSAGES 96 → 500 (PSRAM cost: 26 KB → 133 KB, well within
budget). Ring overflow within a session is now extremely unlikely on
LoRa; the cross-session phantom-badge fix (post-load reconciliation)
from the previous commit handles the remaining edge case.

Migration: on first boot the loader falls back to the legacy combined
file (/ui_chat_history_v1.bin), then schedules writes of the new split
files. Old file is left in place and ignored on subsequent boots.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 10:31:33 +02:00
Ruben LabanandClaude Sonnet 4.6 88a9d386f1 touch: reset phantom unread on load when ring has no messages for thread
The display ring is a fixed-size cache independent of the per-thread
unread counter. After a reboot the ring may not contain the messages
that were counted as unread (they were evicted before the save), leaving
a non-zero badge on a thread that opens to an empty chat.

Fix: after loading history, zero the unread counter for any thread whose
messages are absent from the ring. The unread counter and the ring are
independent — the ring is a display buffer only — so opening the thread
via the first fix (markThreadRead on close) handles the mid-session case;
this one-shot check handles the cross-reboot case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 09:05:36 +02:00
Ruben Laban a6647dcf55 Revert "touch: fix phantom unread badge when ring evicts channel messages"
This reverts commit 28879f55c4.
2026-06-18 09:04:37 +02:00
Ruben LabanandClaude Sonnet 4.6 28879f55c4 touch: fix phantom unread badge when ring evicts channel messages
The 96-slot message ring can overflow when there is sustained traffic
across many threads. When ring wraparound silently overwrites a channel's
messages, thread.unread stays non-zero but getThreadMessageIndexes
returns 0 — producing a badge with a count but an empty chat on open.
The same phantom persists across reboots because unread is saved to
disk independently of the ring contents.

Fix has three parts:

1. counted_as_unread per UIMessage (RAM-only): set true when a message
   is the reason a thread's unread counter was incremented. Cleared when
   the thread is marked read, so future evictions of those slots don't
   double-decrement.

2. Eviction hook in appendMessage: before overwriting a ring slot,
   decrement its thread's unread if the slot was counted_as_unread.
   Keeps the counter in sync as the ring wraps.

3. Post-load reconciliation in loadHistoryFromStorage: after restoring
   thread+message state from disk, any thread whose persisted unread
   count exceeds its actual incoming messages in the ring gets clamped
   (or reset to 0 if the ring has nothing for it). Also reconstructs
   the counted_as_unread flags so evictions after boot are tracked.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 08:34:37 +02:00
Ruben LabanandClaude Sonnet 4.6 d16c833171 touch: fix stale unread badge on per-channel/DM inbox rows
Two related causes:

1. Messages arriving while a chat is open increment unread (appendMessage
   always passes mark_unread=true). closeChatPanel never called
   markThreadRead, so those in-session messages left a phantom badge in
   the inbox after the user closed the chat.

2. Neither threadSelectCb (open) nor closeChatPanel (close) set
   dirty_threads, so the inbox list could lag up to 4 s (the periodic
   refreshThreadsFromMesh tick) before reflecting a cleared count.

Fix: call markThreadRead on the active thread in closeChatPanel and set
dirty_threads so the inbox rebuilds immediately on return.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 08:19:32 +02:00
Dan Vybiral 031cb03bd4 battery chart on V4 (SPIFFS fallback); telemetry log under /meshcomod/telemetry
Battery log/chart now compile on both builds: a small FS selector prefers the SD
card when present (T-Deck, /meshcomod/battery.log) and falls back to internal
SPIFFS (/battery.log) otherwise, so the 24h chart + battery tap work on the
Heltec V4 (no SD slot) too. The 5-min logger and the battery tap-wiring are no
longer gated behind HAS_TDECK_GT911.

Telemetry per-node logs + poll.cfg move from /telemetry to /meshcomod/telemetry
to sit with the other app files. Telemetry logging/window/auto-poll stay
T-Deck-only (V4 keeps the request/reply toast).

Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 14:59:30 +02:00
Dan Vybiral 5ec9176906 file browser: fix stale image when viewing several in a row
s_fm_img_dsc is a reused static, so LVGL's image cache (keyed on the src pointer)
returned the previous image's decoded geometry/data — the 'half old, half new'
render. Invalidate the cache entry when building the dsc and on close.

Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 14:42:50 +02:00
Dan Vybiral ed9cf619ec map: invert status bar + tab bar chrome under night mode
On the map tab the immersive chrome forces black text/icons for legibility over
the light tiles. With night mode on the tiles are dark, so flip the status-bar
text and the bottom tab-bar icons to off-white instead. Only affects the map tab;
re-applied immediately when night mode is toggled.

Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 14:32:18 +02:00
Dan Vybiral 460b82f9bc file browser: view PNG and BMP images, not just JPEG
The viewer already routed image taps to fmOpenImage but only ran the JPEG
decoder and bailed on PNG with a stale 'not supported' note. Sniff the magic
bytes and route PNG -> decodePngToRgb565 (lodepng, already used for map tiles)
and BMP -> a small 16/24-bpp reader (so screenshots saved by the status-bar hold
are viewable). fmIsImage now also accepts .bmp.

Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 14:30:36 +02:00
Dan Vybiral 0a4d8d5b2f map: live zoom-level readout above the zoom slider
A 'zoom <level>' chip, centred above the slider, appears with it and updates
live while dragging so the target level is visible before release.

Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 14:23:21 +02:00
Dan Vybiral e72a1b1bd5 map: real magnifier glyph on the zoom button
Add a one-glyph FontAwesome font (zoom_font.c, U+F002 magnifying-glass, 16 px bpp4,
generated with lv_font_conv from fa-solid-900.ttf) and splice it into the g_font_16
fallback chain alongside person_font. The map zoom button now shows a magnifier
(TOUCH_SYM_ZOOM) instead of the borrowed + glyph.

Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 13:56:37 +02:00
Dan Vybiral 30fe6fbd6f map/telemetry/battery/snake: visibility, zoom slider, night mode, screenshot
Battery chart: stats line spans the full width so the CPU MHz figure no longer
wraps; the CPU series + chip now use the RF-monitor noise-floor grey (0x4A5256).

Telemetry: the window no longer auto-requests on open — it shows history and a
Request button (async refresh glyph, left of the gear) sends a poll. Legend text
recoloured to the axis-note grey instead of near-black.

Map: the + / - overlay buttons are replaced by a single zoom button that toggles
a full-width zoom slider along the bottom of the map (tap again to hide); the
chosen level is persisted across reboots and wins over the first-open auto-snap.
New 'Night mode (invert)' toggle in Map options inverts tile colours at render
time only (cache untouched), persisted.

Snake: pause/resume button next to the close X.

Status bar: hold for 3 s to capture a full-screen screenshot to
/screenshots/<time>.bmp on the SD card (16-bit BMP — fastest viewable format;
the hold suppresses the control-center tap).

Prefs: cfg v5 adds map_night + map_zoom.
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 13:47:38 +02:00
Dan Vybiral 5e8dd55847 battery chart: make the window scrollable; lay the bottom block out top-down
The card was a fixed 246 px — taller than the screen — and not scrollable, so its
bottom-anchored estimate/stats/points labels were clipped and overlapped (the
narrower text width also let the estimate wrap onto the stats line). Switch the
card to content height with a max-height cap + vertical scrolling, and place the
chart, the three text lines and the CPU/trash buttons with a running y so they
stack cleanly and the window scrolls when it doesn't fit.

Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 13:19:39 +02:00
Dan Vybiral 26ae08101c telemetry/battery: data-visibility toggles + telemetry settings panel
Battery chart: a CPU chip beside the trash button shows/hides the CPU-MHz
series; the secondary axis, series and bottom-line CPU figure are dropped when
hidden and the chart redraws immediately.

Telemetry window: a gear opens a settings panel spawned on top with checkboxes
for the battery / temperature / humidity series (the chart underneath redraws
live as they toggle), a manual auto-poll interval field (minutes, 0 = off,
replacing the cycling button) and the clear-history button. The main window now
holds only the reading, chart and time-range inputs; its layout was reworked so
the chart clears the wrapped reading line and the time-range fields no longer
overlap their labels.

Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 13:05:07 +02:00
Dan Vybiral aa89572631 chore: gitignore custom build outputs and local notes
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 13:05:07 +02:00
Dan Vybiral ff5818f7f7 battery chart: stack bottom text clear of the trash button; dim the CPU line
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 12:48:14 +02:00
Dan Vybiral be7fb32f23 battery chart: estimated life left (d/h/m) from the discharge slope, skipping charge
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 12:43:02 +02:00
Dan Vybiral 17981029a7 telemetry: per-node window with battery/temp/humidity chart, auto-poll, clear
- Manual telemetry opens a per-node window immediately (request-in-progress),
  then updates to the reading on reply or "last request failed" on timeout — the
  window shows history either way (issue: see history even on failure).
- Chart from /telemetry/<id>.log: battery V (left) + temperature/humidity (right),
  RF-monitor-style axis ticks + grid, point markers.
- Display window chosen with two fields (now-X h .. now-Y h ago); "Show" reloads.
- Auto-poll per node (toggle + 1/5/15/30/120 min), persisted to /telemetry/poll.cfg;
  due nodes are polled from UITask::loop — emits the async spinner, logs the reply,
  does NOT open the window.
- Clear-history buttons (with confirmation) on both the telemetry and battery windows.

Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 11:51:04 +02:00
Dan Vybiral 9bc9822111 telemetry: log each reply (battery/temp/humidity) to /telemetry/<id>.log per node
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 11:38:19 +02:00
Dan Vybiral 799a3754d0 battery chart: clamp charging spikes to top; RF-monitor-style axis ticks + grid
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 11:35:06 +02:00
Dan Vybiral c9e3de15d4 status bar: park the async spinner left of the clock so it never overlaps it
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 10:27:34 +02:00
Dan Vybiral be1c72fbb1 status bar: SD LED also covers file-manager reads + cross-volume copies
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 10:21:55 +02:00
Dan Vybiral b478851a21 status bar: async-request spinner (telemetry/admin/ping/trace/range/sightline)
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 10:18:42 +02:00
Dan Vybiral 8ce6baf786 battery chart: blue series, calibrated range, visible points, per-point value labels
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 10:14:08 +02:00
Dan Vybiral b82f4a2363 apps: floor the drawer tile height so a full grid scrolls instead of shrinking
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 10:04:16 +02:00
Dan Vybiral 6e62d35328 contacts: add "Show on map" to the action sheet (centre the map on the node)
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:45 +02:00
Dan Vybiral fe8925f91d apps: Snake review fixes — trackball, New-game gate, full field, tab-bar lock
- trackball now steers Snake (UITask routes motion via SnakeGame::isOpen()/steer()
  and hides the soft cursor on that page).
- a "New game" button gates the start so it doesn't move on open; tap restarts
  after a game over.
- slower tick (220 -> 300 ms).
- playfield fills the unoccupied screen (dynamic cols x rows at kCellPx instead of
  a small fixed square).
- the tab bar is hard-locked while Snake is open so a stray swipe/tap can't slip
  through to the app switcher (e.g. into the map).

Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:44 +02:00
Dan Vybiral 7463530f63 ui: review fixes — contacts sort, battery chart, close buttons, clock centring
- contacts: the "?" (unknown last-heard) sink-to-end now mirrors formatAgeBadge's
  exact condition (also future/garbage/>400-day timestamps, not just ==0), using
  the clock captured before the sort. Fixes "no change in behaviour".
- battery chart: add a universal close X (addCloseXBadge); lighter grid + time
  (vertical) division lines; axis notes (V left, MHz right, time on X); point
  dots so a single/sparse sample renders; second ORANGE CPU line on a secondary
  Y axis. battery.log gains a CPU-MHz column (5th field); pre-upgrade 4-col lines
  read as a CPU gap.
- telemetry window: add the same universal close X.
- hide device name: park the clock CENTRED on every screen (not just Home),
  clear of the left-zone title (chat / Files / map credit) and the right icons.

Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:44 +02:00
Dan Vybiral d7151dbbf8 apps: extract Snake into a self-contained SnakeGame class (own files)
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:44 +02:00
Dan Vybiral dd67336fdb apps: add a Snake game (canvas + swipe to steer)
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:43 +02:00
Dan Vybiral 13b240062a telemetry: show the response in a dismissible window instead of a toast
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:04 +02:00
Dan Vybiral 9ddb08ba93 status bar: tap battery for a 24h history chart, logged to SD every 5 min
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:03 +02:00
Dan Vybiral 373be3e546 settings: option to hide the device name and park the clock left
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:03 +02:00
Dan Vybiral d2e776afe9 status bar: fix charging-shift offsets to match the SD-LED layout
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:03 +02:00
Dan Vybiral a5968afb70 status bar: light the SD LED on file-manager microSD access too
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:03 +02:00
Dan Vybiral 7aeee1f456 map: add a zoom + tile-path line under the OSM attribution
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:02 +02:00
Dan Vybiral ed72f15a81 about: tag this fork's builds with an "unofficial fork build" note
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:02 +02:00
Dan Vybiral 59896b6bdb about: show microSD card size + free space in the storage section
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:02 +02:00
Dan Vybiral 5ff7590168 chat: show hop count on received flood messages (next to the time)
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:01 +02:00
Dan Vybiral 00f2f70800 contacts: sort unknown last-heard ("?") to the end of the list
Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
2026-06-17 09:54:01 +02:00
Dan Vybiral fdaf34575e status bar: microSD read/write activity LED left of Wi-Fi
A small amber dot in the status bar (just left of the Wi-Fi glyph) lights
for ~180 ms after any microSD access: SD-backed tile cache, SD tile packs,
and card mount. markSdIo() stamps a timestamp on the I/O path (one 32-bit
store, safe from either core); UITask::loop toggles the dot edge-triggered,
so an idle device pays a single comparison per loop and never relayouts.
The ble / clock / keyboard-layout labels shift left to free the slot.
2026-06-17 09:54:01 +02:00
Kaj SchittecatandClaude Opus 4.8 bb95aed673 touch: beta_6 — RF Monitor sniffer app + chat icons, popup & scroll fixes
- New Monitor app (app drawer): live RF sniffer — recently-heard feed
  (type / RSSI / SNR / hops, colour-coded), link-margin grade, packet rate,
  and an RSSI + noise-floor scope with an on-screen dBm scale; portrait /
  landscape aware. Backed by a recent-RX ring buffer in MyMesh (logRxRaw).
- Chat: DM person icon / channel group icon; block non-contact senders by
  name; keep scroll position when opening a thread; contacts sort popup
  closes on select; long-press popup close-X no longer overlaps buttons.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 00:37:41 +02:00
Kaj SchittecatandClaude Opus 4.8 e9ece7bf6b touch: tester feedback batch + Launcher/RTC/BLE fixes + timezone picker
Core bump: lib_deps -> meshcomod core-v1.16.3 (BLE advertises the node
name not "NimBLE"; GPS won't push a pre-fix ~1902 date).

Tim's quick-wins: Enter-key-sends toggle, 12-hour clock, lock-when-screen-off,
reverse scrollball.

Launcher / map: fix Wi-Fi tiles never downloading under Launcher (the SD-fallback
cache made tilesFsLowSpace() read the unmounted LittleFS as "full"); plot contact
markers before the tile decode so dots aren't ~2s late.

RTC / time: anchored-time guard re-applies UTC when the clock reads garbage
(GPS clobber); blank pre-2020 timestamps so empty channels don't show 1969/1970;
timezone picker (named zones with correct DST) replacing the CET-only base, with
a v2->v3 config migration mapping prior manual offsets onto a Custom zone.

BLE: persist the random pairing PIN (was re-rolled every boot).

Chat: don't auto-jump to latest on a new message when scrolled up; block-by-name
for non-contact room/channel senders (un-blockable room bots).

Power: drop CPU to 80 MHz when the screen is off; enable Wi-Fi modem-sleep only
once associated (enabling it on the unassociated STA broke the setup-wizard scan).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:40:55 +02:00
Dan Vybiral 976bf4022c map: fix server + SD tile rendering, add fetch diagnostics (#11)
Community PR by @Vybo (Dan Vybiral). Fixes Launcher SD-fallback tile caching (tilesFsLowSpace read the unmounted LittleFS partition as full and dropped every download), plus write-verification, read/write cap match, SD PNG-pack support, and on-screen fetch diagnostics.
2026-06-16 22:39:35 +02:00
Kaj SchittecatandClaude Opus 4.8 b85e0a4849 site: centre the BY MESHCOMOD byline on the full lockup (mark + word)
Position the byline relative to .brand (the whole mark+word lockup) instead of just the word, so it centres on the combined logo+title (x=640) rather than under the word alone (x=694). The mark stays aligned with the word.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 23:58:43 +02:00
Kaj SchittecatandClaude Opus 4.8 e7d6c73d16 site: align mark with the WADAMESH word; byline floats beneath
The mark was vertically centring against the word+byline column, so it sat ~13px below the wordmark. Position the byline absolutely under the word so the mark centres on the word itself. ("Free — as it should be" was already centred on the mark+word lockup.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 23:49:49 +02:00
Kaj SchittecatandClaude Opus 4.8 48ef76fd88 site: Free byline, Telegram + bug buttons, BY MESHCOMOD byline
- BY MESHCOMOD byline under the WADAMESH wordmark.
- Plain "Free — as it should be" line under the open-source badge.
- Two fixed floating buttons: Telegram (top-left -> the meshcomod group) and a bug report (top-right -> the GitHub issues page).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 22:59:34 +02:00
Kaj SchittecatandClaude Opus 4.8 11bd8365d6 beta_5: official-app region scope + contacts/discovered/sound/map overhaul
Companion / core (ported from meshcomod):
- Region/default scope from the official MeshCore app now works: handle
  CMD_SET/GET_DEFAULT_FLOOD_SCOPE 63/64 (companion-v1.16.0.3, issue #31) —
  previously fell through with no reply ("no_event_received").
- US/Canada radio preset corrected to 910.525 MHz / 62.5 kHz / SF7.
- T-Deck +22 dBm TX fix (SX1262 DIO2-as-RF-switch, issue #6) retained.

Touch UI (since beta_4):
- Contacts: person/antenna + red-blocked icons, block/unblock in the popup,
  table with sort/filter + multi-select delete (favourites protected),
  compact time/distance columns, long-name marquee, instant fav-star toggle.
- Discovered: persists across reboots, holds 48, cogwheel settings
  (auto-delete oldest + auto-delete above N hops), "48!" full badge.
- Sound: +/- volume, master toggle, separate Messages/@mention switches.
- Chats: per-row last-message time, per-channel mute, @mention jump-to-message.
- Map: remembers zoom + pan; compact contact popup (no Block button).
- Auto-add on its own page; full-screen settings; refined tab bar; drawer polish.
- T-Deck keyboard backspace at the caret; quick-replies scroll.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:34:04 +02:00
Kaj SchittecatandClaude Opus 4.8 537148802e fix(t-deck): enable SX1262 DIO2-as-RF-switch — restores full +22 dBm TX (issue #6)
The T-Deck env had SX126X_DIO2_AS_RF_SWITCH=false, so std_init() called
setDio2AsRfSwitch(false). On the T-Deck the SX1262's antenna TX/RX switch is
driven ONLY by DIO2 (no external PA, no TXEN/RXEN pins), so disabling it strands
the +22 dBm PA off the antenna — ~16 dB loss, ~4 mW radiated. That matches the
reported measurement and likely why some sends weren't propagating. Flip to true.

Cross-checked: Meshtastic's T-Deck variant enables it, as does the Heltec V4 here
and every other SX1262 variant in the MeshCore lib — the T-Deck (copied from
upstream MeshCore's identically-buggy lilygo_tdeck variant) was the lone outlier.
LORA_TX_POWER=22, CURRENT_LIMIT=140 and the 1.8 V TCXO were already correct.

Needs an on-air re-measure (no RF meter here); the reporter offered to test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 09:20:31 +02:00
Kaj Schittecat 7b7ea17031 release-notes: beta_4 2026-06-14 17:23:19 +02:00
Kaj SchittecatandClaude Opus 4.8 1fe92362be splash: add MESHCOMOD product line; power menu: Download-mode button
- Boot splash now shows MESHCOMOD (small, same size as TOUCH BETA) between the
  WADAMESH wordmark and the TOUCH BETA channel line, with its own fade-in.
- Control-center power menu gains a 'Download mode' button: flushes chat, then
  sets RTC_CNTL_FORCE_DOWNLOAD_BOOT + restarts so the device drops into the USB
  serial bootloader for reflashing — no need to hold BOOT + tap RST. Card resized
  to fit the 4th button on both boards.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:00:16 +02:00
Kaj SchittecatandClaude Opus 4.8 9b1fd82eb6 map: keep the route-replay HUD clear of the right-edge button column
The HUD pill (hop name + replay button) was full-width top-center, so its replay
button landed under the map's settings gear / zoom column (right edge, x~sw-36).
Anchor it top-LEFT and cap its width to stop before that column.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:48:23 +02:00
Kaj SchittecatandClaude Opus 4.8 07353263c0 home: cap profile-name width so it can't overlap the clock + slower marquee
The topbar reorder moved the clock left, and the name's short-name path used
LV_SIZE_CONTENT with no width cap — a name <=11 chars but with wide glyphs (W/M)
overran into the clock on the narrow V4. Always cap the label to a fixed window
(100 px T-Deck / 74 px V4); LONG_SCROLL_CIRCULAR stays static when the name fits
and only marquees when it overflows, so no char-count threshold is needed. Also
slow the marquee (anim_speed 14) for readability.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:44:03 +02:00
Kaj SchittecatandClaude Opus 4.8 beb85bd391 setup: first-boot is Wi-Fi-only — Bluetooth off, opt-in afterwards
ble_en defaults ON, so a fresh device co-inits BLE at boot and the first-boot
setup ran with both radios up. On the fresh-flash path only (setupWizardMaybeOpen,
which returns early once setup is done) force BLE off + persist ble_en=0, before
the UI is interactive — so first-boot setup is Wi-Fi-only and the user opts BLE in
later via the control center. Already-set-up devices return early, so their BLE
state is untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:26:23 +02:00
Kaj SchittecatandClaude Opus 4.8 2a93779d75 map: stop tile writes before the LittleFS cache fills (fix reboot)
Coredump from a reboot during a route replay: the replay re-centred the map onto
an un-cached area, the fetch task wrote new z16 tiles, and FS::mkdir faulted
inside lfs_alloc — a full/fragmented LittleFS faults rather than returning NOSPC.
The tile cache had no free-space guard or eviction, so heavy map use fills the
4.75 MB partition until the next mkdir reboots the device.

Add tilesFsLowSpace() (cached ~5s, fetch-task-only) and skip tile writes when the
cache has < 320 KB free. Reads keep working so cached areas still display; only
new tiles stop caching once full. The crashing mkdir is never reached.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:03:07 +02:00
Kaj SchittecatandClaude Opus 4.8 19c856127f map replay: instant popup close + replay button + hop name/id banner
- Tapping 'Replay route on map' now hides the Info popup immediately (was
  lingering over the map transition while the async delete caught up).
- Route-replay HUD: a top banner shows the current hop's repeater name +
  identifier (hash) as each node is revealed, and a replay button (↻) re-runs the
  reveal animation from the first hop. HUD lives on lv_layer_top while the route
  is shown and is torn down when leaving the map.
- RouteNode now carries the resolved name + hex id (routeFillId); 'You' for the
  ME endpoint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 15:51:13 +02:00
Kaj SchittecatandClaude Opus 4.8 8562384f50 perf: stop the 4Hz refresh tick from re-laying-out unchanged labels
Audit of the per-tick UI refresh (runs 4x/s on the UI thread) found ~8 labels
re-written every tick unconditionally — lv_label_set_text always invalidates +
re-lays-out (it never compares), so byte-identical text was redrawn 4x/s on Home
and the control center. Add setLabelIfChanged() (compares against the label's
current text, so it's safe across home/tab rebuilds) and route the status/home/CC
labels through it: CC sysinfo + GPS, home state/IP, RAM/PSRAM, unread, signal
chip, traffic legend, settings footer.

Also coalesce the Wi-Fi tile-arrived re-render: the dirty flag is set per
delivered tile, so a download burst drove back-to-back full-grid tile read+decode
passes on the UI thread — cap to ~2.5x/s (flag stays set; next tick catches up).

Structural levers (DMA flush, double buffer, bigger draw buffer) were left alone:
the audit confirmed they were already tried and reverted for Wi-Fi/BLE RAM, so
they are NOT no-risk. CPU is already 240MHz, tick is esp_timer, touch is 125Hz
async — all already optimal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 15:34:32 +02:00
Kaj SchittecatandClaude Opus 4.8 a10abe1487 fix theme persistence; control-center back-to-dropdown + themed slider/chips
- Theme reverting on reboot: the boot wordmark reads a pref (UI rotation) BEFORE
  SdNvsPrefs::useFile() switches the backend to files, so the settings blob got
  cached from legacy NVS and file-saved values (accent, brightness, language…)
  were ignored every boot. Add touchPrefsReload(), called right after useFile(),
  to re-read from the file backend. (main.cpp now quote-includes TouchPrefsStore.h
  so it sees the new decl, not the monorepo lib's stale copy.)
- Control center: a settings page opened via a chip long-press now returns to the
  dropdown (not Home) when you tap the bar to go Back — tracked with
  s_settings_from_cc.
- Control center now follows the theme accent: brightness slider indicator/knob
  and the toggle chips use COLOR_ACCENT (chips: solid on / faint off) instead of
  a fixed green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 15:16:05 +02:00
Kaj SchittecatandClaude Opus 4.8 5f6398b845 home: tappable unread row w/ mail icon; control-center chips long-press to settings
- Home: split the unread count onto its own row with an envelope icon; tapping it
  jumps to the Chats inbox. Memory (RAM/PSRAM) drops to the line below.
- Control center: long-pressing a chip opens that feature's full settings page
  (Wi-Fi, Bluetooth, GPS->Radio & Mesh, Theme->Display, Keyboard, Lock, Sound).
  A short tap still toggles; lv_indev_wait_release swallows the release click so
  a long-press doesn't also flip the radio.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:55:51 +02:00
Kaj SchittecatandClaude Opus 4.8 f25c9921af msg info: list every route hop, name-only, drop the confusing trailing hash
The route list appended each resolved hop's raw hash hex after its name. With
long repeater names the line wrapped, so a hop's trailing hash glued onto the
next hop's number (hop 2 hash '31' + hop 3 '3.' rendered as '313.'). Now show
just the name when the hash resolves to a known repeater; the hash is a fallback
only for unknown hops. Also drop the 6-hop '+N more' cap and grow the body
buffer so the whole path lists — the popup already scrolls. Same name-only
cleanup applied to the outgoing 'Repeats heard' list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:35:26 +02:00
Kaj SchittecatandClaude Opus 4.8 90f8975f45 topbar: close the gap left by the hidden % while charging
When charging the battery shows the bolt only (no % text), but the % label still
reserved its slot, leaving a %-wide gap between the signal bars and the bolt.
Slide the signal/Wi-Fi/Bluetooth/clock cluster right by the reclaimed width on
the charging transition so it stays snug against the bolt; slide back on unplug.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:16:34 +02:00
Kaj SchittecatandClaude Opus 4.8 fe7a3ed66d topbar: lightning-only when charging + reorder battery/signal/wifi/bt/time
- Charging now shows just the lightning-bolt glyph (batteryGlyphForMv already
  returns it); the 'CHG' text is dropped.
- Right-to-left order is now battery%, signal bars, Wi-Fi, Bluetooth, time.
  Wi-Fi and Bluetooth are split into independent glyphs (were one combined
  label) so each sits in its own slot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:09:24 +02:00
Kaj SchittecatandClaude Opus 4.8 d07945c2c1 topbar: clear chat name + cog when leaving the inbox by any route
Starting a route replay inside a channel jumps to the map via goToTab(), which
hides the chat overlay but didn't clear s_chat_title — so the channel name + its
settings cog lingered in the status bar afterwards (e.g. on Home). Clear the chat
title in onTabChanged's leaving-inbox path, so a tab tap, swipe, or replay jump
all drop it (was only cleared by the explicit chat-close button).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 13:17:48 +02:00
Kaj SchittecatandClaude Opus 4.8 c8fbc5571f home: drop the list icon from the graph's 'tap for details' cue
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 13:08:49 +02:00
Kaj SchittecatandClaude Opus 4.8 79f47dd576 home: move graph tap-cue to top-right + acquire signal fast after boot
- The 'tap for details' cue moves to the graph's top-right corner (was bottom).
- Signal now acquires quickly: the discover probe is a FLOOD advert (repeaters
  re-broadcast it -> we measure the echo's SNR), fired ~4 s after boot and re-
  evaluated every 60 s but only actually sent when we have no recent signal
  (no RX, or older than 100 s). Zero-hop adverts produced nothing to measure
  since they aren't re-broadcast -> the icon sat on 'Sig --'. A chatty mesh that
  already feeds us traffic keeps it quiet; a quiet one refreshes via the echo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 13:03:32 +02:00
Kaj SchittecatandClaude Opus 4.8 7c145df7dd home: in-graph signal chip + a clear 'tap for details' cue
Overlay two chips inside the RX/TX graph box: the live signal ('Sig -3 dB',
top-left) and an accent-coloured 'list  tap for details' cue (bottom-right) so
it's obvious the graph is tappable. Both ride a faint chip to stay readable over
the lines and are non-clickable, so taps fall through to the chart's handler. The
legend above goes back to plain TX/RX traffic counts (signal now lives in the box).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:57:18 +02:00
Kaj SchittecatandClaude Opus 4.8 75fe3b9e5b home: signal in the RX/TX graph legend + a tap-for-detail popup
The graph legend now leads with the live signal ('Sig -3dB  TX n  RX n  (tap)').
Tapping the graph (or the legend) opens a Signal & traffic popup: SNR, RSSI,
bars 0-4, how long ago we last heard the mesh, the flood/direct TX+RX totals
since boot, and the auto-discover interval. logRxRaw now also records RSSI for it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:50:47 +02:00
Kaj SchittecatandClaude Opus 4.8 dee2037310 topbar: mesh signal-strength bars driven by a periodic discover probe
Add a 4-bar signal indicator to the status row, left of the connection icon.
MyMesh::logRxRaw now records the SNR (+ timestamp) of every received packet;
updateGlobalStatusBar maps that to 0-4 lit bars and dims them when nothing's been
heard for 5 min. To keep it fresh when idle, UITask::loop sends a light zero-hop
'discover' advert every 60 s (neighbours only, not flooded -> minimal airtime) so
nearby repeaters/nodes are prompted and their SNR shows up. Home-name cap shrinks
on the narrow V4 to leave room for the bars.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:40:45 +02:00
Kaj SchittecatandClaude Opus 4.8 6a1770cac4 topbar: show the profile name on the home tab (marquee if long)
The home tab's left status-zone showed the brand 'WADAMESH'; show the user's
profile name instead. Capped to a ~11-char-wide window; longer names
marquee-scroll (LV_LABEL_LONG_SCROLL_CIRCULAR) so they never push into the status
icons. Reconfigured only on a name change (re-setting the scroll mode each tick
would restart the marquee) and reset to content-width when the left zone is
reused by another tab. Empty name falls back to 'WADAMESH'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:32:22 +02:00
Kaj SchittecatandClaude Opus 4.8 86c3c122f7 about: stop the per-second sysinfo refresh from stuttering the scroll
The About sheet re-set its ~800-char System-info label every second; re-wrapping
+ repainting that label mid-scroll collided with the scroll redraw and made it
jittery. Skip the refresh while any pointer is actively scrolling/throwing
(lv_indev_get_scroll_dir != NONE) and re-check at 120 ms so live uptime/heap
resumes the moment the user lets go. (General scroll smoothness is a separate
render-path matter — single-buffered, blocking SPI flush.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:13:10 +02:00
Kaj SchittecatandClaude Opus 4.8 072253355b prefs: FIX bootloop — keep SdNvsPrefs layout identical to the lib's stale header
My previous commit added an fs::FS* _fs member (and grew _path) to SdNvsPrefs.h.
But the monorepo lib ships a STALE copy of this header that UITask.cpp picks up
via an angle include, so that translation unit saw the OLD layout while the .cpp
saw the new one — an ODR/layout mismatch. The inserted member shifted _sd's
offset, so the stale-header destructor (buildUiTree, on a local SdNvsPrefs) freed
the _sd vector at the wrong offset -> free() of a garbage pointer -> assert ->
panic -> bootloop. Confirmed by coredump (heap_caps_free 'outside heap areas',
~SdNvsPrefs -> ~vector<Kv>, header at .pio/libdeps/.../MeshCore/.../SdNvsPrefs.h).

Fix: keep the DATA LAYOUT byte-identical to the stale header (no added/reordered
members) so mixed headers can't corrupt anything. The file backend now uses
file-static state (s_file_fs/dir/mode) + free helpers instead of an _fs member;
_path stays [40]; only the layout-neutral static useFile() is added (fs::FS
forward-declared, not included). All file-mode behaviour (read-through NVS
migration, file-only writes) is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:05:52 +02:00
Kaj SchittecatandClaude Opus 4.8 3624540002 prefs: store touch settings in files (SD/SPIFFS), off NVS
Make the touch app's own prefs filesystem-backed instead of NVS, so it no longer
contributes to the tiny 20 KB NVS partition whose exhaustion (esp. under
Launcher) triggered boot loops. main.cpp calls SdNvsPrefs::useFile() once after
the storage decision: SD (/meshcomod) when a card is the active data store, else
SPIFFS (/prefs); V4 always SPIFFS (no SD). The radio stack + Launcher still use
NVS (not ours to remove).

Backward compatible / in-place update:
- Read-through migration: in file mode each getter falls back to the old NVS
  value (read-only), so existing settings + Wi-Fi creds still load and move to
  the file on their next save — nothing is lost, no re-entry needed. Gated by a
  one-time 'does NVS have our data' probe so fresh devices don't log-spam.
- New writes only ever touch the file; NVS is never written in file mode.
- On-disk .kv format unchanged, so existing /meshcomod/<ns>.kv files keep working.
- Legacy NVS-or-SD path kept intact for before useFile() runs (early boot read)
  and as a fallback, so the device stays byte-compatible if it's never reached.
- factory-reset (clear) also wipes the legacy NVS copy so it's permanent.

main.cpp includes SdNvsPrefs.h quoted to pick wadamesh's src/ copy over the
monorepo lib's stale one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:52:32 +02:00
Kaj SchittecatandClaude Opus 4.8 2b224f4d8c about: show NVS partition usage in System info
Add an 'NVS (settings store)' line to the About sub-tab's live System info via
nvs_get_stats() on the whole 'nvs' partition: used/total entries + percent, free
entries, namespace count. NVS is shared by Wi-Fi creds, the Launcher and our
settings, and filling it is what triggers the boot loop — so surfacing how full
it is makes that diagnosable at a glance (refreshes ~1 Hz like the rest).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:16:37 +02:00
Kaj SchittecatandClaude Opus 4.8 dd919190e7 map: don't poison the tile dedup ring on a dropped/failed fetch
Two fixes for 'downloading forever, nothing happens':
- queueTileForFetch marked a tile 'seen' BEFORE the queue send, so a tile dropped
  by a full queue (or one whose fetch later failed) was never retried. Split the
  ring into check (tileFetchSeen) + mark (tileFetchMarkSeen) and only mark after a
  successful xQueueSend.
- Clear the dedup ring on map-tab open so a level that failed to fetch once gets
  another chance on re-open (the fetch task still skips on-disk tiles).

(The underlying cause was the tiles.wadamesh.com proxy 404ing .jpg; fixed
server-side. These make the client resilient to any future fetch failure.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:01:44 +02:00
Kaj SchittecatandClaude Opus 4.8 5efaefbf7f deploy: add tiles.wadamesh.com PNG->JPEG transcode service
The firmware fetches map tiles as .jpg, but the tiles.wadamesh.com proxy was a
plain PNG passthrough (no .jpg handler) -> every Wi-Fi tile fetch 404'd. Add the
transcode service (tile-transcode.py on 127.0.0.1:5005, fetches OSM PNG and
re-encodes JPEG with Pillow) + its systemd unit, and update the nginx vhost to
proxy /{z}/{x}/{y}.jpg and /elev to it while keeping the legacy .png passthrough.
Same design as the meshcomod tile proxy. HTTP-only by design (device can't TLS).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:54:45 +02:00
Kaj SchittecatandClaude Opus 4.8 6b56ffb091 map: fix task-watchdog reboot from the zoomed-out tile prefetch
Coredump (loopTask stuck in tileCacheExists via queueZoomPackForCenter ->
renderMapTiles) showed the prefetch doing ~58 synchronous LittleFS stat calls
plus a usedBytes() traversal ON THE UI THREAD every render. Zooming out into an
un-cached area fires renders back-to-back as tiles stream in, so this ran in a
tight loop and starved core-0 IDLE past the 5 s task watchdog.

The prefetch now does zero filesystem work on the UI thread: it just enqueues
the pyramid tiles (queueTileForFetch is in-memory: dedup ring + queue send) and
the background fetch task already skips anything already on disk. Dropped the
usedBytes() free-space guard (a full partition fails the write gracefully).
Throttled to re-enqueue only when the center crosses a new z9 tile (~50 km).
Also yield once per dequeued item in the fetch task so a burst of on-disk skips
(now handled there) can't starve IDLE either.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:22:05 +02:00
Kaj SchittecatandClaude Opus 4.8 2f95304e7b ui: Info popup — buttons above the route, repeaters tinted by bubble colour
- Move the Replay/Trace action row to just below the title (above the route
  info) instead of pinned at the bottom; the metadata scrolls beneath it.
- Colour each repeater's identifier in the Route / Repeats-heard lists with the
  same signature colour it carries as a chat bubble (nodeSigColorHex reuses
  usernameBubbleColors' FNV-1a hue), via LVGL recolor markers on the body label.
  Nameless hops get a stable tint from their hash. Long-press-copy stays plain —
  copyLabelLongPressCb now strips recolor tokens. Body buffer grown to fit them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:08:55 +02:00
Kaj SchittecatandClaude Opus 4.8 73fd690149 ui: make the message Info popup body scrollable
Wrap the metadata body in a vertical scroll container with the title and the
action row pinned, and cap the card to the screen height. A long route (many
named hops) now scrolls inside the card instead of overflowing or pushing the
buttons off-screen — also fixes the pre-existing top/bottom clipping of the
oversized card on the short 320x240 T-Deck. Thin visible scrollbar; long-press
to copy still works (drag scrolls, hold copies).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 09:56:24 +02:00
Kaj SchittecatandClaude Opus 4.8 af27ebd0a4 map: replay a message's repeater route on the map (Info popup button)
The message Info popup gains a 'Replay' button that draws the repeaters the
message travelled through onto the map and animates the path one hop at a time.

- Received floods: a chain from the first known repeater through to ME.
- Our own sent floods: a star from ME to each repeater that echoed it back.

Repeater hops are resolved to their advertised GPS via a new read-only
MyMesh::uiHopPos(); only repeaters that have shared a position are plotted, and
the button only appears when >=2 nodes are mappable. Tapping it switches to the
map, fits center+zoom to the route, and reveals the amber path/nodes on a timer.
The overlay rides the pan layer (pans/zooms with the map), is freed by
freeMapMarkers each render, and is cleared on leaving the map tab.
onMapTabActivated skips its self-recenter while a replay fit is pending.

The Info popup's bottom row now holds Replay and/or Trace (side-by-side when
both apply). Pairs naturally with the wider offline tile cache, so a
country-scale route has tiles to draw on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 09:50:48 +02:00
Kaj SchittecatandClaude Opus 4.8 1994f6daba map: prefetch a zoomed-out pyramid over Wi-Fi (province -> country -> borders)
The Wi-Fi tile prefetch cached only two single tiles (z12 + z16), both close
range, so zooming out to see a province/country had nothing cached and dead-ended
offline. Cache a small grid around the center at the zoomed-out levels instead:
3x3 at z7 (~590 km, country + all borders), z8 (~295 km, whole country) and z9
(~145 km, a province), plus the existing z12/z16 detail tiles. Goes through the
same rate-capped background fetch path, runs after the visible tiles (so the
current view still downloads first), self-skips anything already cached, and a
512 KB free-space guard keeps it from thrashing a near-full partition. Bumped the
fetch queue (32->64) and dedup ring (16->48) so a full pyramid pass fits at once.
Now zoom-out is reachable down to country+border scale even offline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 09:25:06 +02:00
Kaj SchittecatandClaude Opus 4.8 47e98f186b fix: don't truncate repeater names in message Info (issue #30)
The name buffer was nm[21]; contact names are up to 32 bytes and emoji cost 4
bytes each, so names like the screenshot's emoji repeater got cut mid-name. Size
the buffer to the full 32-byte name (nm[33]) in both the received-route and
sent-repeater renderers, and widen the body[] stop-margin to match the longer
lines. Label is LV_LABEL_LONG_WRAP so long names wrap rather than clip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 09:11:08 +02:00
Kaj SchittecatandClaude Opus 4.8 9e07a5768a feat: name the repeaters behind 'Repeats heard' on sent messages (issue #30)
Sent messages have no inbound route, so #30's repeater-names had nothing to show
there. Now when we hear an echo of our own flood, capture the re-flooding
repeater (the echo's last path hop) into the echo ring (deduped, bounded to 3),
and list those repeaters by name under 'Repeats heard' in the message Info.
Zero added risk: fixed-size store (no growth), hard buffer guards in the
renderer, read-only contact lookup. Covers #30 in both directions now.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 08:57:21 +02:00
Kaj SchittecatandClaude Opus 4.8 1134e49c79 feat: repeater names in the message Info route (issue #30)
The inbound-flood route in the message Info popup showed bare hop hashes
(24E0>6401>...). Resolve each hop to its repeater contact name when known, shown
as a numbered 'N. Name  HASH' list (falls back to the bare hash for unknown
repeaters). Implemented with zero added risk: a read-only, null-safe, bounded
name lookup (MyMesh::uiHopName) + a hard buffer guard and a hop cap in the
renderer, so it can never overflow body[] or grow past the card.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 08:34:43 +02:00
Kaj SchittecatandClaude Opus 4.8 3104b6eaf6 site: add 512px PNG app icon (for app catalogs / Launcher)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 02:20:56 +02:00
Kaj SchittecatandClaude Opus 4.8 5410290c17 fix(wdt): stop the heavy-flash guard from arming the core-1 watchdog (reboot)
A user on beta_2 hit a task-watchdog reboot (task_wdt: IDLE1, loopTask on CPU1).
Decoding the backtrace showed the network task idle, so the UI thread stalled on
a local op. Root cause: in the Arduino S3 build only core 0's idle task is
WDT-subscribed (CHECK_IDLE_TASK_CPU1 is off). The heavy-flash guard called
disableCore1WDT() (which fails) then enableCore1WDT(), and the latter
*newly subscribes* IDLE1 via esp_task_wdt_add — arming a core-1 watchdog that
didn't exist before. The next multi-second SPIFFS-GC burst on the UI thread
(history save) then starves IDLE1 and reboots. So the guard CAUSED the very
reboot it existed to prevent. Fix: only touch core 0's WDT; leave core 1 at the
Arduino default (unwatched), so a slow write briefly stalls the UI instead of
rebooting. Built + flashed to the T-Deck.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 23:34:08 +02:00
215 changed files with 63510 additions and 29310 deletions

No files matched your search

+6
View File
@@ -28,3 +28,9 @@ scripts/build/wifi_secrets.h
# Local working notes + agent config (contain ops/infra details — never publish)
CLAUDE.md
.claude/
# Custom timestamped build outputs
/builds/
# Local reconstruction notes (scratch, never publish)
/.notes/
+51
View File
@@ -0,0 +1,51 @@
# Supported devices
Current hardware support, install paths and maturity. "Stable" means the board
ships on the stable release channel; "Beta" means it is new and lives on the
test channel until the next stable promote. Install links:
[flasher.wadamesh.com](https://flasher.wadamesh.com) (browser, Chrome/Edge over
USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases).
| Device | MCU / radio | Display and input | Install | Channel | Status |
|---|---|---|---|---|---|
| LilyGo T-Deck / T-Deck Plus | ESP32-S3, SX1262 | 2.8" 320x240 touch, QWERTY, trackball | Web flasher (standalone) or Launcher app image | Stable | Fully supported, reference device |
| 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 |
| 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 |
## Feature notes per board
- **T-Deck**: the everything device: touch, physical keyboard, trackball cursor
or d-pad navigation, microSD (deep 5000-message chat history, map tile packs,
data storage), GPS on the Plus, notification sounds through the I2S speaker.
- **Heltec V4 + TFT**: touch UI with the on-screen keyboard; the optional
Expansion Kit adds environment sensors (home-screen chart) and a piezo
buzzer. V4.3 boards get the switchable high-gain receive LNA toggle.
- **Tanmatsu**: keyboard-driven UI (no touchscreen) with the coloured function
keys mapped to tabs, ALT accent picker, UI scaling (Normal/Large/Huge),
microSD for all persistent data. Ships through the Tanmatsu launcher store,
updates arrive as store updates.
- **ThinkNode M9**: keyboard plus d-pad navigation (no touch), same feature set
as the other boards where the hardware allows. New in beta_38; expect rough
edges and report them.
- **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.
## Requested boards
Open hardware requests, roughly in demand order. Ports are welcome, see
[CONTRIBUTING.md](CONTRIBUTING.md); the M9 and Tap V2 both started as community
PRs.
- LilyGo T-Deck Pro Max: [#62](https://github.com/ALLFATHER-BV/wadamesh/issues/62)
- LilyGo T-pager: [#60](https://github.com/ALLFATHER-BV/wadamesh/issues/60)
- SenseCAP Indicator D1L: [#14](https://github.com/ALLFATHER-BV/wadamesh/issues/14)
## Channels
- **Stable**: the tested default. The flasher installs it unless you pick Beta.
- **Beta**: new features and fixes earlier; on-device opt-in via Settings,
About, "Get test builds (beta)". New boards debut here and move to Stable
with the next promote.
+3
View File
@@ -60,5 +60,8 @@ Adafruit GFX / ST7735_ST7789 (T-Deck display backend)
chsc6x (Heltec V4 CHSC6x cap-touch driver)
Quency-D — https://github.com/Quency-D/chsc6x
LovyanGFX (RAK WisMesh Tap V2 display backend)
Copyright (c) lovyan03 — FreeBSD License — https://github.com/lovyan03/LovyanGFX
This list is not exhaustive and is expanded as dependencies are vendored or
added.
+8 -2
View File
@@ -17,8 +17,14 @@ MeshCore fork via PlatformIO `lib_deps`.
## Boards
- LilyGo T-Deck / T-Deck Plus — env `LilyGo_TDeck_companion_radio_touch`
- Heltec V4 + TFT + CHSC6x touch — env `heltec_v4_tft_companion_radio_usb_tcp_touch`
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
- Elecrow ThinkNode M9 — env `ThinkNode_M9_companion_radio_touch` (beta)
- RAK WisMesh Tap V2 (RAK3312) — env `rak_tap_v2_companion_radio_touch` (beta)
## Architecture
+94
View File
@@ -0,0 +1,94 @@
# WadaMesh release channels — Stable + Test
Goal: ship **test** builds to the community fast, let them shake out, then **promote**
a proven test build to **stable**. What ships to stable is exactly what was tested
(promote the binary, don't rebuild).
## Two concepts, kept separate
- **Branches** = where code lives.
- **Channels** = what a device / the flasher pulls (Stable vs Beta).
## Branches
- **`main`** — active line. All work lands here; every *test* release is cut from `main`. (Unchanged from today.)
- **`stable`** — promoted line. Moves only when a test build is blessed. Starts at the **beta_20** commit.
- Promote = `git branch -f stable <blessed beta commit>` + push, then run the stable-promotion publish.
## Versioning / tags
- **Test builds:** `beta_N` (immutable, incrementing), marked **GitHub pre-release**. Also mirrored to a rolling **`beta-latest`** pre-release (overwritten each time) for the Launcher beta sync.
- **Stable:** the blessed `beta_N`, marked **GitHub Latest**. Keep the `beta_N` id for now; friendly `vX.Y` names are an optional later polish (would need a rebuild-with-tag per promotion).
- First stable = **beta_20**.
## Distribution matrix
| Target | Stable | Test / Beta |
|---|---|---|
| GitHub release | blessed `beta_N`, marked **Latest** | every `beta_N`, **pre-release** + rolling `beta-latest` |
| VPS archive | `releases/TOUCH/<beta_N>/` (only blessed lands here) | `releases/BETA/<beta_N>/` |
| VPS channel feed | `stable/` (version.json + bins) | `beta/` (version.json + bins) |
| Flasher (flasher.wadamesh.com) | default | "Beta / testing" toggle |
| On-device update check | stable feed (legacy firmware reads `releases/TOUCH`) | beta feed (Phase 2 setting) |
| LauncherHub (bmorcelli, T-Deck) | existing entry (follows GitHub Latest) | new "WadaMesh (Beta)" entry (Phase 3) |
| Tanmatsu app-store | promote on stable only | (skipped for tests) |
## The linchpin: keeping field devices on stable
Legacy firmware's update check scans `releases/TOUCH/` for the highest `beta_N`. So:
- **Test builds archive under `releases/BETA/`** — invisible to legacy firmware.
- **Promotion copies the blessed build into `releases/TOUCH/`** — legacy devices then see it and show "update available," landing everyone on stable.
Net effect: existing devices quietly settle on **stable (beta_20)** and stop chasing
every beta; beta testers opt in via the flasher Beta toggle (or, later, the on-device
Beta channel).
## Two workflows
### Cut a TEST release (fast, frequent) — from `main`
1. `beta_N` = next number.
2. Build both touch envs with the tag embedded.
3. GitHub: create `beta_N` release **`--prerelease`** with the 7 assets; also refresh the rolling `beta-latest` pre-release.
4. VPS: rsync to `releases/BETA/<beta_N>/` + refresh the `beta/` feed.
5. Flasher Beta toggle now serves it. Skip LauncherHub / Tanmatsu / site. (~minutes)
### Promote to STABLE (deliberate, when proven)
1. `git branch -f stable <beta_N commit>` + push.
2. GitHub: mark that `beta_N` release **Latest** (clear pre-release).
3. VPS: copy `releases/BETA/<beta_N>/` → `releases/TOUCH/<beta_N>/`; refresh `stable/` + `latest/` (flasher stable) feeds.
4. Full fan-out: LauncherHub follows automatically; push the Tanmatsu store; bump the site "what's new."
## Phases
### Phase 1 — channel split, no firmware change (do now)
- [ ] Create + push `stable` branch at beta_20.
- [ ] VPS: stand up `stable/`, `beta/`, `releases/BETA/`; point `latest/` (flasher stable) at beta_20; freeze `releases/TOUCH/` at beta_20.
- [ ] GitHub: ensure beta_20 is **Latest**; future betas → `--prerelease`.
- [ ] Flasher: Stable (default) / Beta toggle + the beta manifest.
- [ ] `release.sh` / `gen-flasher-meta.py`: add `--channel beta|stable` producing the right feed/assets + the rolling `beta-latest`.
- [ ] Rewrite the `wadamesh-release` skill into the two-workflow form.
### Phase 2 — on-device channel selector (ships in a beta, then promoted)
- [ ] Settings → **Update channel: Stable / Beta**, default Stable (persisted).
- [ ] `verchkFetchLatest` reads the `stable/` or `beta/` feed per the setting.
- [ ] Beta devices get the new-beta banner automatically; stable devices never see betas.
### Phase 3 — LauncherHub beta listing (PR to bmorcelli, when wanted)
- [ ] Rolling `beta-latest` already produced in Phase 1.
- [ ] PR `bmorcelli/M5Stack-json-fw`: add a `wadamesh_beta.py` (mirror `bruce_beta.py`, track `beta-latest`) + a `t-deck.json` (Beta) entry.
- [ ] Result: T-Deck Launcher shows **WadaMesh** + **WadaMesh (Beta)**, both auto-synced.
- Until then: T-Deck Launcher beta testers side-load the beta `.bin` manually (it is a T-Deck-only gap; V4 uses the flasher).
## Where current work lands
- The uncommitted post-beta_20 work (auto-advert GUI, message-flash, docs, RAM pass)
plus the hardened **MQTT-privacy** branch → commit to `main` → cut as **beta_21**,
the first TEST release in this model. Community tests it; promote when proven.
- This is exactly the model that handles PR #71: land the reviewed/hardened pieces on
`main`, ship as a beta, promote later.
## Decisions
- **Locked:** `main` + `stable`; keep `beta_N` ids with a Stable/Beta channel label; Phase 1 now, Phase 2 in beta_21, Phase 3 as a fast-follow.
- **Open (reversible):** whether stable later gets friendly `vX.Y` names (needs a rebuild-with-tag per promotion).
+235
View File
@@ -0,0 +1,235 @@
# Tanmatsu port — working tracker
Goal: run the **full** wadamesh UI + functionality on the **Tanmatsu** (Nicolai
Electronics), kept as **one codebase** with the existing S3 boards so a UI change
ships everywhere at once. This file is the running plan/status — update it as we go.
Status: **device IN HAND (2026-06-20); ENTIRE APP COMPILES + LINKS on P4** (`f04dba6`) · branch
`tanmatsu-port` on `beta_9` · the **full shared src/ — MyMesh + the 28k-line LVGL UITask + all
helpers + variant glue — compiles, and the project builds `application.bin`** · LVGL 8.4 + the
core + ~10 Arduino libs all build · **next = the real `app_main` bring-up (the spike main still
ships, so the bin isn't the real app yet), storage-on-FAT, the 800×480 layout, the bsp_input→LVGL
indev, then on-device install via BadgeLink/AppFS.**
---
## Decision (architecture) — UPDATED 2026-06-20: wadamesh is an APP on the launcher
On the Tanmatsu **everything is an app** (badge.team OS + GUI launcher + **AppFS** app
store). So wadamesh ships as a **launcher app, NOT a firmware that replaces the OS.**
AppFS apps are **standard ESP-IDF app binaries** installed into an AppFS flash partition
(via `appfs_add_file.py` / BadgeLink / the badge.team app store) and started from the
launcher; a running app owns the screen + input via `badge-bsp` and returns to the
launcher on exit (docs.tanmatsu.cloud → AppFS).
⇒ The Tanmatsu target is an **ESP-IDF app**, NOT the pioarduino/Arduino *standalone*
flash (that would overwrite the launcher). The PlatformIO route in the old plan is
**dropped**; the "IDF subproject" that was the fallback is now the **primary path** —
because the whole app model is IDF-native.
We still keep **one repo, one `src/`**: the shared app (UITask, MyMesh, i18n, fonts…)
compiles inside the IDF app via **arduino-esp32 as an ESP-IDF component**, alongside
`badge-bsp` (display/keyboard/power), `tanmatsu-lora` (LoRa — bridge already written in
`variants/tanmatsu/`) and `esp_wifi_remote` (C6 Wi-Fi/BLE). Our **LVGL 8.3** flushes to
the badge-bsp `esp_lcd` panel (not PAX; not the LVGL-9.3 `badge-bsp-lvgl`). **Distribution
= the badge.team app store** — the Tanmatsu analog of LauncherHub. `boards/tanmatsu.json`
+ the pioarduino env in the old plan are no longer the build; the LoRa bridge + variant
wiring carry over.
## Compile spike — RESULTS (2026-06-20) ✅ arduino-as-component PROVEN on P4
IDF-app build lives in **`tanmatsu/`** (badge.team Makefile + project-local IDF **v5.5.1**
in `tanmatsu/esp-idf`, gitignored). Build via **`./tanmatsu/build.sh <target>`** (wraps
`idf.py -DDEVICE=tanmatsu -DSDKCONFIG_DEFAULTS="sdkconfigs/general;sdkconfigs/tanmatsu;sdkconfigs/wadamesh" -DIDF_TARGET=esp32p4`).
Toolchain: PlatformIO already had the riscv32-esp P4 toolchain; IDF cloned via the Makefile
+ `idf_tools.py install cmake ninja` (the targeted `install.sh esp32p4` skips those).
Proven end-to-end:
- ✅ Stock badge.team template builds for P4 (baseline) — toolchain + component manager OK.
- ✅ **arduino-esp32 3.3.10** compiles + links **alongside badge-bsp** (241 arduino objs).
esp32p4 first-class in arduino since 3.1.3. ⇒ the "one shared `src/` via arduino-on-P4"
architecture is **validated** — this was the make-or-break risk.
**Fixes (all in `tanmatsu/sdkconfigs/wadamesh`, our overlay — badge.team files kept pristine):**
1. `CONFIG_FREERTOS_HZ=1000` — arduino-esp32 hard-requires it (badge.team defaults 100).
2. `CONFIG_BT_ENABLED=n` (+ `BT_NIMBLE_ENABLED=n`, `ESP_HOSTED_ENABLE_BT_NIMBLE=n`) — arduino's
bundled BLE won't compile vs IDF 5.5.1 NimBLE (`ble_gap_read_local_irk` renamed). P4 has no
native BLE + we use the separate NimBLE-Arduino lib, so BT-off compiles arduino
BLE/SimpleBLE/BluetoothSerial out via their `#if` guards. **Revisit hosted BLE (phone
companion over the C6) later.** NB: arduino's `CONFIG_ARDUINO_SELECTIVE_COMPILATION` does
NOT filter in this setup (verified) — use feature disables, not the selective knob.
**Gotchas found (carry into the real src/ wiring):**
- **badge-bsp headers have NO `extern "C"` guards** → wrap every `#include "bsp/*.h"` in
`extern "C" { }` from C++ or the bsp symbols mangle and fail to link.
- **Two esp-hosted stacks collide:** arduino + `espressif/esp_wifi_remote` pull
`espressif/esp_hosted`, whose whole-archive C6 SDIO driver duplicates badge.team's
`nicolaielectronics/esp-hosted-tanmatsu` → "multiple definition". **Fix:
`tanmatsu/components/esp_hosted/` (empty forwarder, `REQUIRES esp-hosted-tanmatsu`) shadows
the registry component by name** (local components win) → one esp-hosted, the hardware fork,
and arduino's `WiFi.h` rides on it. Runtime TODO: the `ESP_HOSTED_GPIO_SLAVE_RESET_SLAVE`
config warning (C6 reset pin) — set it when WiFi is brought up live.
## Spike C1 — MeshCore core compiles on P4 (2026-06-20) ✅
The core is vendored by **`tanmatsu/fetch-deps.sh`** from `.pio/libdeps/.../MeshCore` into
`components/meshcore/core/` (gitignored) under a committed `components/meshcore/CMakeLists.txt`.
The Arduino libs it needs (Crypto, RTClib, Adafruit BusIO, Adafruit Unified Sensor, Melopero
RV3028, CayenneLPP, ArduinoJson) all go in ONE committed **`ardlibs`** component (so cross-lib
`#include`s resolve). Build: `./build.sh build`. What it took:
- vendored lib dir names must be **space-free** (spaces break IDF's component-req matching → `BUG:`).
- the core's bundled **C ed25519** (`lib/ed25519/`) must be added explicitly (PlatformIO
auto-compiles it as a nested lib; IDF doesn't).
- `ardlibs` needs `nvs_flash` (Crypto's RNG seeds from NVS); `meshcore` needs `nvs_flash`+`efuse`.
- **strip IDF's `-Werror=all`** from `COMPILE_OPTIONS` — vendored code trips reorder/format/
class-memaccess, and a bare `-Wno-error` does NOT cancel `-Werror=all` (done in main/CMakeLists).
- exclude over-globbed files vs `build_as_lib.py`: non-ESP32 platform helpers (nrf52/stm32/rp2040),
`helpers/radiolib/*` (→ LoRa bridge), the S3 display drivers, `ESPNOWRadio`, `SerialBLEInterface`
(BT off), and the `MC_VENDORED_TOUCH_APP` files (our own src/ ships them).
**Remaining for a running app — ordered worklist (next session):**
1. **Tanmatsu define set** (`main/CMakeLists.txt` build flags) — a *curated subset* of the S3 touch
`build_flags`: KEEP `ESP32_PLATFORM`, `MC_VENDORED_TOUCH_APP`, `UI_LVGL=1`, `HAS_TOUCH_UI=1`,
`MULTI_TRANSPORT_COMPANION`, `LORA_FREQ/BW/SF`, `LV_CONF_PATH=lv_conf.h`+`LV_CONF_INCLUDE_SIMPLE=1`
+`LV_INDEV_DEF_SCROLL_THROW=7`, `MAX_CONTACTS`, `ENABLE_PRIVATE_KEY_*`, `ENV_INCLUDE_GPS`,
`-I src -I src/ui-touch -I include`. DROP every S3-board define (`HELTEC_LORA_V4*`, `USE_SX1262`,
`RADIO_CLASS`/`WRAPPER_CLASS`, all `P_LORA_*`/`PIN_*`/TFT/CHSC6x, `ST7789_*`, `RADIOLIB_*`). Give
the Tanmatsu its own board/display identity (badge-bsp, NOT `DISPLAY_CLASS=ST7789`).
2. **`wadamesh_app` component** — glob `src/` MINUS `main.cpp` (own app_main) + the S3 input helpers
(`HeltecV4CapTouch`, `TDeckKeyboard/Touch/Trackball` → Tanmatsu input is `bsp_input`); add
`ui-touch/` once LVGL is in.
3. **LVGL component** — vendor lvgl 8.3 + the repo `lv_conf.h`.
4. **Transports** — re-add AsyncTCP+ESPAsyncWebServer with a WDT-config fix (`CONFIG_ESP_TASK_WDT_*`)
or the ESP32Async fork; OR ship companion over USB-serial first (off the critical path).
5. **`variants/tanmatsu/` glue** — `TanmatsuBoard` (board API over bsp/power), the LVGL→badge-bsp
display driver (`flush_cb`→`bsp_display_blit`, 800×480 RGB565), a `bsp_input`→`lv_indev`
keyboard/pointer, `radio_init`→the `TanmatsuLoraRadio` bridge, storage→the FAT `locfd` partition.
6. **`app_main`** — replicate setup()/loop(): board.begin → radio_init → storage → the_mesh.begin →
ui_task.begin(disp) → loop{ui_task.loop; the_mesh.loop}.
7. **Install** — `make build` → `application.bin` → BadgeLink/AppFS → the badge.team app store.
Biggest chunks: UITask (28k lines) vs the Tanmatsu defines + LVGL, and the 800×480 display flush
(768 KB/frame → PSRAM). All methodical integration on the proven foundation.
Dep status: vendored + compiling = MeshCore core, ed25519, Crypto, RTClib, Adafruit BusIO, Adafruit
Unified Sensor, Melopero RV3028, CayenneLPP, ArduinoJson, MicroNMEA. Still to add = LVGL, AsyncTCP +
ESPAsyncWebServer (WDT fix), and (only if telemetry wanted) the Adafruit sensor libs.
## Why this is feasible (verified 2026-06-18)
- **Arduino-ESP32 supports the ESP32-P4** — core **3.3.10** (2026-06-05) has P4
boards incl. MIPI-display variants (`ESP32P4_4DS_MIPI`).
- **PlatformIO can target it** via the **pioarduino** platform (rel `55.03.39`),
whose board list already includes **`m5stack-tab5-p4`** — a P4 + MIPI display +
C6-coprocessor-radio handheld, i.e. a structural near-twin of the Tanmatsu. Crib
display/WiFi bring-up from that board.
- **Radio maps cleanly:** `mesh::Radio` (7 pure virtuals) ↔ `tanmatsu-lora`'s
`lora.h` (raw 256-byte tx/rx, settable freq/bw/sf/cr/sync/preamble/power,
RSSI/SNR). Same SX1262/1268 family the S3 boards use. Bridge written (below).
- **No driver writing:** display/keyboard/power/wifi/lora all exist as IDF
components (`badge-bsp`, `tanmatsu-wifi`, `tanmatsu-lora`).
## Hardware / platform facts
| | | (confirmed from docs.tanmatsu.cloud + hackster/CNX) |
|---|---|---|
| Main SoC | ESP32-**P4** dual-core RISC-V | **@ 400 MHz, 32 MB PSRAM, 16 MB flash**, microSD (SDIO) |
| Display | **3.97" MIPI-DSI, 800×480, RGB565** | `esp_lcd` panel from `badge-bsp`; we flush **LVGL 8.3** to it (not PAX) |
| Input | **69-key QWERTY** + 6 fn keys | events via `bsp_input_get_queue()` |
| Coprocessors | **WCH CH32V203** (keyboard + power) · **ESP32-C6** (WiFi/BLE/802.15.4 via `tanmatsu-wifi`) · **SX1262** LoRa 433/868-915 (via `tanmatsu-lora`, "remote") | the P4 has no native radio |
| Native SDK | ESP-IDF ≥ 5.5, CMake, C, PAX graphics | template: `Nicolai-Electronics/tanmatsu-template` |
## Research findings (2026-06-18, no hardware)
**Radio bridge — fully validated against `tanmatsu-lora/lora.c`; the code was already correct.**
- `lora_packet_stats_t = {rssi_pkt_raw u8, snr_pkt_raw i8, signal_rssi_pkt_raw u8}`; `rssi_dbm = -rssi_pkt_raw/2`, `snr_db = snr_pkt_raw/4`. ✓ matches the bridge.
- `lora_send_packet()` is **blocking** (waits ≤2 s on the transaction semaphore) → `isSendComplete()` latch is correct. ✓
- `config.bandwidth` = kHz, valid {7,10,15,20,31,41,62,125,250,500} (62.5→62); `config.coding_rate` = 5..8 → 4/5..4/8. `setParams()` is correct. ✓
- Only remaining radio tuning (small, MeshCore-side): `sync_word` / `preamble_length` / `power` to match `RadioLibWrappers.cpp` so it interoperates with the existing mesh.
**Display/input — DECISION: keep wadamesh's LVGL 8.3 and write our own flush; do NOT adopt `badge-bsp-lvgl`.**
- `badge-bsp` exposes the screen as an `esp_lcd` panel (`bsp_display_get_panel` / `_get_panel_io` / `_get_parameters` → h_res/v_res/color_format) and the keyboard as a FreeRTOS event queue (`bsp_input_get_queue`). Reference wiring: `badgeteam/konsool-template-lvgl`.
- BUT the badge LVGL component (`badgeteam/esp32-component-badge-bsp-lvgl`) targets **LVGL 9.3**, and wadamesh's UITask is **LVGL 8.3**. Adopting it forces an 8→9 migration of ~30k lines — not worth it. So: bring up our own LVGL 8.3 (esp_lvgl_port supports 8.x, or a manual `flush_cb`) flushing to the badge-bsp `esp_lcd` panel, + an `lv_indev` that reads the `bsp_input` queue.
- **UI layout target: 800×480 landscape** — make UITask responsive for it (it already branches on `hor_res`).
**Repos:** `badgeteam/esp32-component-badge-bsp` (BSP) · `badgeteam/konsool-template-lvgl` (LVGL-on-bsp reference) · `Nicolai-Electronics/esp32-component-tanmatsu-lora` (lora.c).
## Plan / status
- [x] Confirm framework gap is bridgeable (Arduino-P4 + pioarduino + Tab5 precedent)
- [x] Map `mesh::Radio` ↔ `lora.h`; write `TanmatsuLoraRadio` (full drop-in for `radio_driver`: setParams/setTxPower/setRxBoostedGainMode/idle/getPacketsRecvErrors/RSSI/SNR)
- [x] `idf_component.yml` for the BSP/wifi/lora components
- [x] `boards/tanmatsu.json` (based on pioarduino `m5stack-tab5-p4`)
- [x] `variants/tanmatsu/{target.h,target.cpp,TanmatsuBoard.h}` — wires `radio_driver`=bridge, `board`, `radio_init()`, `radio_new_identity()` (StdRNG)
- [ ] **Compile spike** — get `src/` to build for P4 under pioarduino (no HW needed; see below)
- [ ] `[env:tanmatsu_companion_radio_touch]` in `platformio.ini` — port the shared touch flags, drop S3 LoRa pins; **resolve the RadioLib build issue (next item)**
- [x] RadioLib build question — RESOLVED: keep RadioLib in `lib_deps` + the same `RADIOLIB_*` flags as the S3 envs. `RadioLibWrappers.cpp` still compiles, but since the Tanmatsu `target.cpp` never instantiates `WRAPPER_CLASS`/`RADIO_CLASS`, the unused code is linker-stripped. Net: the env mirrors the T-Deck's RadioLib deps/flags; only the `radio_driver` *type* differs (set in `variants/tanmatsu/target.h`). No `build_src_filter` gymnastics needed.
- [ ] Wire the IDF components into the pio build (component manager) ⚠️ main risk
- [ ] **Headless bring-up:** call `radio_init()` → `TanmatsuLoraRadio` joins the mesh (serial log)
- [ ] WiFi/BLE via `esp_wifi_remote`/`tanmatsu-wifi` (likely close to free in Arduino)
- [ ] LVGL display driver: flush → `bsp_display_blit`; LVGL indev ← `bsp/input` (keyboard)
- [ ] Make UITask responsive for the larger screen (it already branches on `hor_res`)
- [ ] Tune the `TanmatsuLoraRadio` TODO(device) items
- [ ] Release path (separate bin; not covered by the `wadamesh-release` skill yet)
## What's done this session (no hardware)
A structurally-complete variant skeleton — the radio + board wiring is real (mirrors
the T-Deck variant); display/input/wifi glue + the env are the device/spike work left.
- `variants/tanmatsu/TanmatsuLoraRadio.{h,cpp}` — the LoRa bridge, real code against
the actual `lora.h` v0.3.0 API + `mesh::Radio` contract, reusing MeshCore's own
airtime/packetScore math. A full drop-in for `radio_driver` (every method the app
calls in `MyMesh.cpp`). Compiles-ready *pending* the `TODO(device)` items.
- `variants/tanmatsu/target.{h,cpp}` — wires `radio_driver` (the bridge), `board`,
`radio_init()` (→ `lora_init_remote`), `radio_new_identity()` (seeded via StdRNG).
- `variants/tanmatsu/TanmatsuBoard.h` — `: public ESP32Board`; battery/power via
badge-bsp left as TODO(device).
- `variants/tanmatsu/idf_component.yml` — the managed-component deps.
- `boards/tanmatsu.json` — P4/16MB/PSRAM board def (based on `m5stack-tab5-p4`;
confirm flash size + f_cpu on the real unit).
- This tracker + the `tanmatsu-port` memory.
### `TanmatsuLoraRadio` — TODO(device) (confirm/tune on hardware)
- `lora_packet_stats_t` field names + raw→real RSSI/SNR conversion (assumed `-raw/2`, `raw/4`).
- `bandwidth` units (assumed kHz) and `coding_rate` encoding (assumed 4/x, x=5..8).
- `sync_word` / `preamble_length` — match `RadioLibWrappers.cpp` so it interoperates with the existing mesh.
- Whether `lora_send_packet()` is blocking (assumed yes → `isSendComplete()` latch is fine).
- Where the variant creates the radio object + calls `begin()`/`setParams()` (mirror the S3 radio glue in the variant target).
## Compile spike (run first, needs only the toolchain)
```bash
# add the env (below) to platformio.ini + a stub boards/tanmatsu.json, then:
PIO=/Users/kaj/Library/Python/3.9/bin/pio
$PIO run -e tanmatsu_companion_radio_touch # expect: platform/toolchain resolves, src/ starts compiling
```
Outcome tells us: does pioarduino-P4 + Arduino build the shared `src/`? Compiles →
proceed with display/radio glue. Fails on the IDF-component wiring → that's the known
risk; fall back to the IDF-subproject layout.
### env block to add to `platformio.ini` (draft)
```ini
[env:tanmatsu_companion_radio_touch]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.39/platform-espressif32.zip
framework = arduino
board = tanmatsu ; boards/tanmatsu.json — model on pioarduino m5stack-tab5-p4
build_flags =
${env.build_flags} ; reuse the shared touch flags
-D HAS_TANMATSU=1
-I variants/tanmatsu
; board_build.partitions = variants/tanmatsu/partitions_tanmatsu.csv ; TODO
; the BSP/wifi/lora IDF components are pulled via variants/tanmatsu/idf_component.yml
```
## Risks / open questions
1. **IDF components inside PlatformIO** (`badge-bsp`/`tanmatsu-lora`) — the fiddliest part; pioarduino supports the component manager but expect friction. **Still the #1 risk.**
2. **Display = our own LVGL 8.3 flush onto the badge-bsp `esp_lcd` panel** (decided; don't use the LVGL-9.3 `badge-bsp-lvgl`). Work = the flush_cb + the `bsp_input`→`lv_indev` glue + an 800×480 layout pass on UITask. Bounded now that the BSP API + konsool reference are known.
3. **WiFi/BLE via the C6** (`esp_wifi_remote` + `esp_hosted` over SDIO). Arduino `WiFi.h` on the P4 *is* backed by `esp_wifi_remote` in arduino-esp32 3.x, so wadamesh's WiFi (tile fetch, setup wizard, network tab) can likely run **mostly unchanged** — but it needs the C6 on the right `esp_hosted` slave firmware + the components wired in, and it's a **known-finicky area** in practice (cf. esphome#10956). The template uses IDF wifi helpers (`wifi_remote.h`), not Arduino `WiFi.h`, so confirm Arduino WiFi coexists with `tanmatsu-wifi`'s C6 bring-up during the spike. A real task + risk, not free — but not a rewrite.
4. **Heap/PSRAM** — fine: **32 MB PSRAM**, and an 800×480 RGB565 buffer is only ~768 KB. Lots of headroom vs the S3 boards.
## References
- Template: https://github.com/Nicolai-Electronics/tanmatsu-template (ESP-IDF, P4, PAX, badge-bsp)
- LoRa component: https://github.com/Nicolai-Electronics/esp32-component-tanmatsu-lora (`include/lora.h`, v0.3.0)
- Precedent board: pioarduino `m5stack-tab5-p4` (P4 + MIPI + C6)
- `mesh::Radio` base: monorepo `src/Dispatcher.h`; example backend `src/helpers/esp32/ESPNOWRadio.h`
+43
View File
@@ -0,0 +1,43 @@
{
"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=1",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "dio",
"psram_type": "qspi",
"hwids": [["0x303A", "0x1001"]],
"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": "RAK WisMesh Tap V2 (16 MB FLASH, 8 MB Octal PSRAM)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 8388608,
"maximum_size": 16777216,
"use_1200bps_touch": false,
"wait_for_upload_port": false,
"require_upload_port": true,
"speed": 460800
},
"url": "https://store.rakwireless.com/products/wismesh-tap-v2",
"vendor": "RAKwireless"
}
+29
View File
@@ -0,0 +1,29 @@
{
"build": {
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=1"
],
"f_cpu": "400000000L",
"f_flash": "80000000L",
"f_psram": "200000000L",
"flash_mode": "qio",
"mcu": "esp32p4",
"variant": "esp32p4"
},
"connectivity": ["wifi", "bluetooth"],
"debug": { "openocd_target": "esp32p4.cfg" },
"frameworks": ["arduino", "espidf"],
"name": "Tanmatsu (ESP32-P4 + C6)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 512000,
"maximum_size": 16777216,
"require_upload_port": true,
"speed": 1500000
},
"url": "https://nicolaielectronics.nl/",
"vendor": "Nicolai Electronics"
}
+40
View File
@@ -0,0 +1,40 @@
{
"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_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"hwids": [["0x303A", "0x1001"]],
"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 ThinkNode M9 (16 MB FLASH, 8 MB PSRAM)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"require_upload_port": true,
"speed": 921600
},
"url": "https://www.elecrow.com/",
"vendor": "Elecrow"
}
+14 -1
View File
@@ -3,12 +3,25 @@
Distribution stack for wadamesh: a **VPS nginx origin behind Cloudflare**.
```
tiles.wadamesh.com →CF (HTTP, edge-cached) → nginx → tile.openstreetmap.org
tiles.wadamesh.com →CF (HTTP, edge-cached) → nginx → OpenStreetMap / OpenTopoMap
firmware.wadamesh.com →CF (cache bins) → nginx → /srv/wadamesh/firmware
flasher.wadamesh.com →CF (HTTPS) → web flasher (TODO — see below)
wadamesh.com →CF (HTTPS) → landing page (later)
```
**Map tile styles.** The default `/{z}/{x}/{y}.jpg` route serves **OpenStreetMap**
(the firmware default). An opt-in **OpenTopoMap** topographic style is served from
`/opentopo/{z}/{x}/{y}.jpg` (explicit OSM alias at `/osm/...`); the device requests
it only when the user enables *Map → Options → Topographic map*. Legal: OpenTopoMap
map tiles are **© OpenTopoMap (CC-BY-SA)** over **© OpenStreetMap contributors
(ODbL) + SRTM** — the touch UI shows that attribution when topo is active, and the
14-day disk cache keeps each tile hitting OpenTopoMap at most once per fortnight
(their tile-usage policy asks for a contactable UA + caching, both of which the
transcode service provides). Deploying the topo routes = update
`tiles.wadamesh.com.conf` + `tile-transcode.py`, then
`systemctl restart wadamesh-tile-transcode && nginx -t && systemctl reload nginx`
and purge the Cloudflare cache for `tiles.wadamesh.com/opentopo/*`.
The firmware fetches **tiles + the update-check over plain HTTP** (on-device HTTPS
isn't viable — mbedTLS needs ~30 KB heap, only ~5 KB is free post-Wi-Fi), so the
tile + firmware hosts must stay reachable over HTTP. Cloudflare provides the edge
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Generate the Mesh America Configurator catalog JSON for wadamesh.
Mesh America (apps.meshamerica.com — the flasher Cascadia Mesh uses) merges a
provider "catalog" JSON into its device list. Spec: third-party hosts ONE static
JSON (this file's output) at a stable HTTPS+CORS URL, plus the firmware bins over
HTTPS+CORS; their app reads it live. See provider-api-spec.md from TJ Downes.
We host:
- the bins on firmware.wadamesh.com/releases/TOUCH/<TAG>/ (Cloudflare HTTPS +
`Access-Control-Allow-Origin: *`, immutable per-tag dir — see deploy/nginx/
firmware.wadamesh.com.conf), and
- this catalog at a stable HTTPS+CORS URL (deployed alongside the firmware).
Usage: python3 deploy/gen-meshamerica-catalog.py <TAG> > meshamerica-catalog.json
(TAG defaults to the newest beta_N dir; notes come from release-notes/<TAG>.txt)
Re-run it each release (or wire it into scripts/release.sh) so the catalog always
points at the newest immutable bins — the spec wants firmware URLs treated as
immutable, so each release adds a fresh versioned URL.
"""
import sys, json, os, glob, re
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def newest_tag():
tags = []
for p in glob.glob(os.path.join(REPO, "release-notes", "beta_*.txt")):
m = re.search(r"beta_(\d+)", os.path.basename(p))
if m:
tags.append(int(m.group(1)))
return "beta_%d" % max(tags) if tags else "beta_9"
TAG = sys.argv[1] if len(sys.argv) > 1 else newest_tag()
BASE = "https://firmware.wadamesh.com/releases/TOUCH/%s" % TAG
ICON = "https://wadamesh.com/wadamesh-icon-512.png"
# "What's new" — one user-facing line per non-blank, non-# line of the notes file.
notes = ""
nf = os.path.join(REPO, "release-notes", "%s.txt" % TAG)
if os.path.exists(nf):
notes = "\n".join(l.strip() for l in open(nf, encoding="utf-8")
if l.strip() and not l.strip().startswith("#"))
# Both touch boards are ESP32-S3, single touch-UI ("gui") role. ota_0 sits at
# 0x10000 on both (partitions_t*_touch.csv), so flash-update@0x10000 is correct.
# `name` MUST match the official MeshCore device name character-for-character, or Mesh America lists it
# as a NEW device instead of folding our firmware into the existing tile (TJ Downes, 2026-06). Canonical
# names verified from apps.meshamerica.com/proxy/flasher/config.json. The V4 entry is the Expansion Kit
# (Touch) variant only, per Kaj. `slug` still maps to our bin filenames on firmware.wadamesh.com.
BOARDS = [
{"name": "Heltec v4 + Expansion Kit (Touch)", "slug": "heltec-v4-tft"},
{"name": "LilyGo T-Deck", "slug": "tdeck"},
]
def device(b):
merged, appbin = "wadamesh-%s-merged.bin" % b["slug"], "wadamesh-%s.bin" % b["slug"]
return {
"maker": "wadamesh",
"class": "wadamesh",
"name": b["name"],
"type": "esp32",
"tooltip": "<img class='device' src='%s'>" % ICON,
"firmware": [{
"role": "gui",
"title": "Touch UI",
"version": {TAG: {
"notes": notes,
"files": [
{"type": "flash-wipe", "name": merged,
"url": "%s/%s" % (BASE, merged),
"title": "Full install (bootloader + firmware)"},
{"type": "flash-update", "name": appbin,
"url": "%s/%s" % (BASE, appbin),
"title": "Update (app only)"},
],
}},
}],
}
# Optional "what's this?" blurb Mesh America shows on a hover tooltip next to our provider badge
# (spec section 3, added by TJ 2026-06-30). Plain text only — rendered escaped, so no markup/links.
DESCRIPTION = (
"WADAMESH is a full touch-screen interface for MeshCore: on-device chat, contacts, a live map "
"with offline tiles, and complete radio settings. Same MeshCore protocol and phone-app companion "
"as stock MeshCore, just a richer on-device front-end."
)
catalog = {
"description": DESCRIPTION,
"maker": {"wadamesh": {"name": "wadamesh"}},
"device": [device(b) for b in BOARDS],
}
print(json.dumps(catalog, indent=2, ensure_ascii=False))
+74
View File
@@ -0,0 +1,74 @@
{
"description": "WADAMESH is a full touch-screen interface for MeshCore: on-device chat, contacts, a live map with offline tiles, and complete radio settings. Same MeshCore protocol and phone-app companion as stock MeshCore, just a richer on-device front-end.",
"maker": {
"wadamesh": {
"name": "wadamesh"
}
},
"device": [
{
"maker": "wadamesh",
"class": "wadamesh",
"name": "Heltec v4 + Expansion Kit (Touch)",
"type": "esp32",
"tooltip": "<img class='device' src='https://wadamesh.com/wadamesh-icon-512.png'>",
"firmware": [
{
"role": "gui",
"title": "Touch UI",
"version": {
"beta_40": {
"notes": "Big translation pass: hundreds of previously-English strings across menus, settings, dialogs and the setup flow are now translated into all 11 non-English languages (Dutch, German, French, Spanish, Italian, Russian, Ukrainian, Bulgarian, Serbian, Greek, Brazilian Portuguese). Menu rows that carry an icon translate too. Switch language under Settings.\nChat scrolling is fully virtualized (contributed by scratchdiver, PR #130): a chat now scrolls through its ENTIRE stored history, up to 5000 messages on SD-card devices, smoothly in both bubble and compact mode, instead of only the most recent rows.\nLong-press any message for a Delete option that removes just that message; the message menu lays its buttons two per row so it fits on one screen.\nThe chat and channel settings sheet gains \"Delete history\": wipes the whole conversation after a confirmation, while the channel membership or contact stays.\nThe in-chat jump buttons are slim arrows tucked against the right edge: fully visible while you scroll, fading to half opacity a second after you stop.\nThinkNode M9 hardware support is complete (contributed by ded, PR #138): GPS, microSD, buzzer, backlight control, lock screen, long-press context menus, and a d-pad-driven setup wizard. It can be installed straight from the web flasher.\nNew board: RAK WisMesh Tap V2 (contributed by Ethac.chen, PR #136), with a 30+ fps display driver, also installable from the web flasher.\nThe Wi-Fi network screens (join a network, hidden network, saved-network details) are full-screen pages now instead of small popups, matching the other settings pages and scrolling properly with the keyboard open.\nThe lock screen shows an envelope with the number of unread messages under the clock, so you can check for news without unlocking (idea from issue #93).\nPasswords with special characters can be entered everywhere: long-press any settings field, including the Wi-Fi password, for a Cut / Copy / Paste / All / Sym menu. Sym opens a picker with the characters the on-screen keyboard lacks (issue #122). Double-tap selects a word.\nFor Launcher installs: a \"Save update bin to SD\" button on the About page downloads the latest firmware of your selected channel into a BINS folder on the SD card, ready to flash from the Launcher. The test-builds toggle is visible on Launcher installs too.\nNew region preset \"Australia (Mid)\": 915.075 MHz, 125 kHz, SF9, CR5 (requested in issue #121).\nYour GPS position is now the first row in the quick-reply picker, so you can send your current coordinates with one tap (contributed by Yazutsu, PR #125).\nTanmatsu: hold ALT and press a letter to open the accent picker; keep ALT held and press again to cycle, release to insert (requested by jochentj, issue #129). Double-tap the space bar to switch between your enabled keyboard languages, same as the T-Deck.\nWhile in a chat, the top bar shows the active keyboard language next to the clock when more than one layout is enabled.\nThe \"Store data on SD\" toggle is now actually honored after a reboot (fix contributed by scratchdiver, PR #123). On Launcher installs it silently reverted to internal storage every boot, which also kept the slow internal-flash writes. Turning it off works reliably now too.\nTurning on \"Store data on SD\" migrates your existing profile, settings and identity to the card FIRST, so upgrading no longer risks adopting an empty card and booting as a brand-new node. If you were already hit by that, Settings -> General -> \"Copy internal data to SD\" restores the profile from internal flash.\nCard-less devices (Heltec V4, and any board without an SD card) no longer freeze for several seconds at a time. A busy mesh re-writes the whole contacts file to internal flash on every advert refresh, and that flash garbage-collection stalled the whole device; contact saves are now coalesced so only real changes (a new or removed node) are written promptly.\nDeleting a message, or clearing a whole chat, now sticks: it survives a reboot or a hardware reset instead of coming back (reported by Leon P and MrWeavis). Deleting is also much faster - chat history is written compactly, only the messages you actually have, so a delete, a history wipe, or a reboot no longer pauses the screen while it saves.\nMessages no longer vanish when you reboot the device from the phone app (reported by Leon P): the app's reboot command now saves chat history first, like every on-device reboot path already did.\nChat history saves to the SD card are more robust: files are written atomically with a fallback save path, so a badly timed reboot can no longer truncate your message history (contributed by scratchdiver, PR #135).\nLonger translated labels (French, German, etc.) that used to clip or wrap now shrink to fit: the message action menu, confirmation dialogs, and the app-drawer tiles all keep their text on one line. The signal card's \"tap for details\" hint no longer overlaps in other languages.\Line truncated
"files": [
{
"type": "flash-wipe",
"name": "wadamesh-heltec-v4-tft-merged.bin",
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_40/wadamesh-heltec-v4-tft-merged.bin",
"title": "Full install (bootloader + firmware)"
},
{
"type": "flash-update",
"name": "wadamesh-heltec-v4-tft.bin",
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_40/wadamesh-heltec-v4-tft.bin",
"title": "Update (app only)"
}
]
}
}
}
]
},
{
"maker": "wadamesh",
"class": "wadamesh",
"name": "LilyGo T-Deck",
"type": "esp32",
"tooltip": "<img class='device' src='https://wadamesh.com/wadamesh-icon-512.png'>",
"firmware": [
{
"role": "gui",
"title": "Touch UI",
"version": {
"beta_40": {
"notes": "Big translation pass: hundreds of previously-English strings across menus, settings, dialogs and the setup flow are now translated into all 11 non-English languages (Dutch, German, French, Spanish, Italian, Russian, Ukrainian, Bulgarian, Serbian, Greek, Brazilian Portuguese). Menu rows that carry an icon translate too. Switch language under Settings.\nChat scrolling is fully virtualized (contributed by scratchdiver, PR #130): a chat now scrolls through its ENTIRE stored history, up to 5000 messages on SD-card devices, smoothly in both bubble and compact mode, instead of only the most recent rows.\nLong-press any message for a Delete option that removes just that message; the message menu lays its buttons two per row so it fits on one screen.\nThe chat and channel settings sheet gains \"Delete history\": wipes the whole conversation after a confirmation, while the channel membership or contact stays.\nThe in-chat jump buttons are slim arrows tucked against the right edge: fully visible while you scroll, fading to half opacity a second after you stop.\nThinkNode M9 hardware support is complete (contributed by ded, PR #138): GPS, microSD, buzzer, backlight control, lock screen, long-press context menus, and a d-pad-driven setup wizard. It can be installed straight from the web flasher.\nNew board: RAK WisMesh Tap V2 (contributed by Ethac.chen, PR #136), with a 30+ fps display driver, also installable from the web flasher.\nThe Wi-Fi network screens (join a network, hidden network, saved-network details) are full-screen pages now instead of small popups, matching the other settings pages and scrolling properly with the keyboard open.\nThe lock screen shows an envelope with the number of unread messages under the clock, so you can check for news without unlocking (idea from issue #93).\nPasswords with special characters can be entered everywhere: long-press any settings field, including the Wi-Fi password, for a Cut / Copy / Paste / All / Sym menu. Sym opens a picker with the characters the on-screen keyboard lacks (issue #122). Double-tap selects a word.\nFor Launcher installs: a \"Save update bin to SD\" button on the About page downloads the latest firmware of your selected channel into a BINS folder on the SD card, ready to flash from the Launcher. The test-builds toggle is visible on Launcher installs too.\nNew region preset \"Australia (Mid)\": 915.075 MHz, 125 kHz, SF9, CR5 (requested in issue #121).\nYour GPS position is now the first row in the quick-reply picker, so you can send your current coordinates with one tap (contributed by Yazutsu, PR #125).\nTanmatsu: hold ALT and press a letter to open the accent picker; keep ALT held and press again to cycle, release to insert (requested by jochentj, issue #129). Double-tap the space bar to switch between your enabled keyboard languages, same as the T-Deck.\nWhile in a chat, the top bar shows the active keyboard language next to the clock when more than one layout is enabled.\nThe \"Store data on SD\" toggle is now actually honored after a reboot (fix contributed by scratchdiver, PR #123). On Launcher installs it silently reverted to internal storage every boot, which also kept the slow internal-flash writes. Turning it off works reliably now too.\nTurning on \"Store data on SD\" migrates your existing profile, settings and identity to the card FIRST, so upgrading no longer risks adopting an empty card and booting as a brand-new node. If you were already hit by that, Settings -> General -> \"Copy internal data to SD\" restores the profile from internal flash.\nCard-less devices (Heltec V4, and any board without an SD card) no longer freeze for several seconds at a time. A busy mesh re-writes the whole contacts file to internal flash on every advert refresh, and that flash garbage-collection stalled the whole device; contact saves are now coalesced so only real changes (a new or removed node) are written promptly.\nDeleting a message, or clearing a whole chat, now sticks: it survives a reboot or a hardware reset instead of coming back (reported by Leon P and MrWeavis). Deleting is also much faster - chat history is written compactly, only the messages you actually have, so a delete, a history wipe, or a reboot no longer pauses the screen while it saves.\nMessages no longer vanish when you reboot the device from the phone app (reported by Leon P): the app's reboot command now saves chat history first, like every on-device reboot path already did.\nChat history saves to the SD card are more robust: files are written atomically with a fallback save path, so a badly timed reboot can no longer truncate your message history (contributed by scratchdiver, PR #135).\nLonger translated labels (French, German, etc.) that used to clip or wrap now shrink to fit: the message action menu, confirmation dialogs, and the app-drawer tiles all keep their text on one line. The signal card's \"tap for details\" hint no longer overlaps in other languages.\Line truncated
"files": [
{
"type": "flash-wipe",
"name": "wadamesh-tdeck-merged.bin",
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_40/wadamesh-tdeck-merged.bin",
"title": "Full install (bootloader + firmware)"
},
{
"type": "flash-update",
"name": "wadamesh-tdeck.bin",
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_40/wadamesh-tdeck.bin",
"title": "Update (app only)"
}
]
}
}
}
]
}
]
}
+21 -1
View File
@@ -25,12 +25,22 @@ server {
root /srv/wadamesh/firmware;
# The update-check feed — served at the no-slash path the firmware requests.
# STABLE channel (the default on-device update check).
location = /releases/TOUCH {
default_type application/json;
alias /srv/wadamesh/firmware/releases/TOUCH/index.json;
add_header Cache-Control "public, max-age=60";
}
# Same, for the opt-in TEST/BETA channel — what a device requests when the
# "Get test builds (beta)" switch is on. Without this exact-match the no-slash
# path 301-redirects to a dir with no index and the beta check comes back empty.
location = /releases/BETA {
default_type application/json;
alias /srv/wadamesh/firmware/releases/BETA/index.json;
add_header Cache-Control "public, max-age=60";
}
# Bins + everything else under /releases/. CORS so the browser flasher
# (flasher.wadamesh.com) can fetch the images cross-origin.
location /releases/ {
@@ -41,7 +51,7 @@ server {
}
# Rolling "latest" merged images — the web flasher's manifests point here so
# they always install the newest build (release.sh refreshes this dir).
# they always install the newest build (release.sh refreshes this dir). STABLE.
location /latest/ {
autoindex off;
types { application/octet-stream bin; }
@@ -49,5 +59,15 @@ server {
add_header Access-Control-Allow-Origin "*";
}
# Rolling "latest" for the TEST/BETA channel — the beta web-flasher feed
# (version.json + manifests + rolling bins). Without this it falls to the
# catch-all 404 and the flasher's beta toggle can't load metadata.
location /latest-beta/ {
autoindex off;
types { application/octet-stream bin; }
add_header Cache-Control "public, max-age=300";
add_header Access-Control-Allow-Origin "*";
}
location / { return 404; }
}
+114 -14
View File
@@ -1,15 +1,31 @@
# tiles.wadamesh.com — OSM tile proxy for the wadamesh map tab.
# tiles.wadamesh.com — map-tile proxy for the wadamesh map tab. Default style is
# OpenStreetMap; an opt-in OpenTopoMap topographic style is served from /opentopo/.
#
# The firmware fetches plain HTTP (on-device HTTPS isn't workable: mbedTLS wants
# ~30 KB heap for a handshake and only ~5 KB is free after Wi-Fi associates), so
# this vhost is HTTP-only by design. Cloudflare sits in front and caches the
# .png tiles at the edge; this origin keeps a second 14-day disk cache.
# this vhost is HTTP-only by design. Cloudflare sits in front and caches results
# at the edge; this origin keeps a second 14-day disk cache.
#
# Deploy: copy to /etc/nginx/sites-available/, symlink into sites-enabled/, then
# nginx -t && systemctl reload nginx
# Cloudflare: tiles.wadamesh.com proxied (orange cloud), SSL/TLS mode "Flexible"
# (CF<->origin HTTP), and a Cache Rule: Eligible for cache + Edge TTL ~14d for
# path "/*". Do NOT enable "Always Use HTTPS" on this hostname.
# The device also can only decode JPEG cheaply (SJPG/TJpgDec -> RGB565 in
# stripes); PNG decode (lodepng) needs a 256 KB ARGB8888 buffer per tile. OSM
# only serves PNG, so the device requests `.jpg` and a local transcode service
# (tile-transcode.py on 127.0.0.1:5005) fetches the OSM PNG and re-encodes it as
# JPEG. The legacy `.png` passthrough (used by the website / older callers) is
# kept alongside it.
#
# device --HTTP--> CF --HTTP--> nginx (this) --> 127.0.0.1:5005 --HTTPS--> OSM
# PNG -> JPEG (Pillow)
#
# Deploy:
# sudo systemctl enable --now wadamesh-tile-transcode # the :5005 service
# sudo cp deploy/nginx/tiles.wadamesh.com.conf /etc/nginx/sites-available/
# sudo ln -sf /etc/nginx/sites-available/tiles.wadamesh.com.conf /etc/nginx/sites-enabled/
# sudo mkdir -p /var/cache/nginx/wadamesh-tiles && sudo chown www-data: /var/cache/nginx/wadamesh-tiles
# sudo nginx -t && sudo systemctl reload nginx
# Cloudflare: tiles.wadamesh.com proxied (orange cloud), SSL/TLS "Flexible",
# Cache Rule: Eligible for cache + Edge TTL ~14d for "/*". Do NOT enable
# "Always Use HTTPS" on this hostname. Purge the CF cache after first deploy
# so any negatively-cached `.jpg` 404s clear.
proxy_cache_path /var/cache/nginx/wadamesh-tiles levels=1:2
keys_zone=wadamesh_tiles:10m max_size=4g inactive=14d use_temp_path=off;
@@ -19,20 +35,98 @@ server {
listen [::]:80;
server_name tiles.wadamesh.com;
# Re-resolve the upstream at request time. A literal hostname in proxy_pass
# fails with "no resolver defined" on a fresh box; a variable + resolver works.
# Re-resolve the OSM upstream (for the .png passthrough) at request time. A
# literal hostname in proxy_pass fails with "no resolver defined" on a fresh
# box; a variable + resolver works.
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
# The location regex is DOUBLE-QUOTED on purpose: nginx's lexer otherwise
# treats { and } (quantifiers) as block delimiters and refuses to start.
# Primary path the firmware uses: /{z}/{x}/{y}.jpg -> local transcode service
# (fetches the OSM PNG, re-encodes as JPEG). The regex is DOUBLE-QUOTED on
# purpose: nginx's lexer otherwise treats { and } (quantifiers) as block
# delimiters and refuses to start.
location ~ "^/(?<z>[0-9]{1,2})/(?<x>[0-9]{1,7})/(?<y>[0-9]{1,7})\.jpg$" {
proxy_pass http://127.0.0.1:5005/$z/$x/$y.jpg;
proxy_set_header Host "tiles.wadamesh.com";
proxy_cache wadamesh_tiles;
proxy_cache_key "jpg:$z/$x/$y";
proxy_cache_valid 200 14d;
proxy_cache_valid 404 1d;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
add_header X-Cache-Status $upstream_cache_status always;
add_header Cache-Control "public, max-age=2592000" always;
proxy_buffering on;
proxy_read_timeout 20s;
proxy_connect_timeout 5s;
}
# OpenTopoMap (topographic) — OPT-IN alternate style. The firmware requests
# this path only when the user enables "Topographic map"; the default
# /{z}/{x}/{y}.jpg above stays OpenStreetMap. Legal: map © OpenTopoMap
# (CC-BY-SA), data © OpenStreetMap contributors + SRTM — the touch UI shows
# that attribution when topo is selected. The 14-day disk cache means each
# tile hits OpenTopoMap at most once per fortnight (their tile policy asks it).
location ~ "^/opentopo/(?<z>[0-9]{1,2})/(?<x>[0-9]{1,7})/(?<y>[0-9]{1,7})\.jpg$" {
proxy_pass http://127.0.0.1:5005/opentopo/$z/$x/$y.jpg;
proxy_set_header Host "tiles.wadamesh.com";
proxy_cache wadamesh_tiles;
proxy_cache_key "opentopo-jpg:$z/$x/$y";
proxy_cache_valid 200 14d;
proxy_cache_valid 404 1d;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
add_header X-Cache-Status $upstream_cache_status always;
add_header Cache-Control "public, max-age=2592000" always;
proxy_buffering on;
proxy_read_timeout 20s;
proxy_connect_timeout 5s;
}
# Explicit OpenStreetMap alias (identical to the default root path above).
location ~ "^/osm/(?<z>[0-9]{1,2})/(?<x>[0-9]{1,7})/(?<y>[0-9]{1,7})\.jpg$" {
proxy_pass http://127.0.0.1:5005/osm/$z/$x/$y.jpg;
proxy_set_header Host "tiles.wadamesh.com";
proxy_cache wadamesh_tiles;
proxy_cache_key "osm-jpg:$z/$x/$y";
proxy_cache_valid 200 14d;
proxy_cache_valid 404 1d;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
add_header X-Cache-Status $upstream_cache_status always;
add_header Cache-Control "public, max-age=2592000" always;
proxy_buffering on;
proxy_read_timeout 20s;
proxy_connect_timeout 5s;
}
# Elevation profile for the line-of-sight analyzer (same local service).
location = /elev {
proxy_pass http://127.0.0.1:5005;
proxy_set_header Host "tiles.wadamesh.com";
proxy_cache wadamesh_tiles;
proxy_cache_key "elev:$args";
proxy_cache_valid 200 30d;
proxy_cache_valid 400 404 1h;
proxy_cache_lock on;
add_header X-Cache-Status $upstream_cache_status always;
proxy_buffering on;
proxy_read_timeout 20s;
proxy_connect_timeout 5s;
}
# Legacy direct PNG passthrough -> OSM (kept for the website / older callers).
location ~ "^/(\d{1,2})/(\d{1,7})/(\d{1,7})\.png$" {
set $osm_upstream "tile.openstreetmap.org";
proxy_pass https://$osm_upstream/$1/$2/$3.png;
proxy_ssl_server_name on;
proxy_set_header Host tile.openstreetmap.org;
# OSM tile usage policy requires a valid, identifying User-Agent.
proxy_set_header User-Agent "wadamesh-tile-proxy/1.0 (+https://wadamesh.com)";
proxy_set_header Referer "https://wadamesh.com/";
@@ -45,5 +139,11 @@ server {
expires 14d;
}
# Tiny landing so a human hitting the bare hostname isn't met with 404.
location = / {
default_type text/plain;
return 200 "wadamesh tile proxy (HTTP only)\n\n /{z}/{x}/{y}.jpg -> OpenStreetMap (default)\n /osm/{z}/{x}/{y}.jpg -> OpenStreetMap\n /opentopo/{z}/{x}/{y}.jpg -> OpenTopoMap (CC-BY-SA)\n\nUpstream PNG transcoded to JPEG.\n";
}
location / { return 404; }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+534
View File
@@ -0,0 +1,534 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WADAMESH — Documentation</title>
<meta name="description" content="User guide for the WADAMESH touch firmware on the LilyGo T-Deck: home, chat, contacts, map, settings and the built-in apps. Real device screenshots.">
<link rel="icon" href="wadamesh-badge.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@600;700&display=swap" rel="stylesheet">
<style>
:root{
--bg:#ffffff; --bg2:#f4f6f2; --ink:#15181e; --mut:#5d6770;
--line:#e5e8e2; --teal:#15b6a6; --teal-ink:#0e8d80;
color-scheme:light;
}
*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
body{font:16px/1.65 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Inter,sans-serif; background:var(--bg); color:var(--ink); -webkit-font-smoothing:antialiased}
a{color:var(--teal-ink); text-decoration:none}
.top{position:sticky; top:0; z-index:30; display:flex; align-items:center; gap:14px; padding:12px 22px;
background:rgba(255,255,255,.88); backdrop-filter:saturate(1.2) blur(8px); border-bottom:1px solid var(--line)}
.top .word{font-weight:700; letter-spacing:2px; font-size:1.05rem; color:var(--ink)}
.top .word .t{color:var(--teal)}
.top .tag{font-family:'JetBrains Mono',monospace; font-size:.66rem; letter-spacing:.18em; text-transform:uppercase; color:var(--mut); border:1px solid var(--line); border-radius:100px; padding:4px 9px}
.top .sp{flex:1}
.top .home{display:inline-flex; align-items:center; gap:7px; color:var(--mut); font-size:.86rem; border:1px solid var(--line); border-radius:100px; padding:6px 13px; background:#fff; transition:background .15s,color .15s,border-color .15s}
.top .home:hover{color:var(--teal-ink); background:var(--bg2); border-color:#bfe6e0}
.top .home svg{width:16px;height:16px}
.shell{max-width:1200px; margin:0 auto; display:grid; grid-template-columns:220px 1fr; gap:34px; padding:0 22px}
nav.toc{position:sticky; top:64px; align-self:start; height:calc(100vh - 64px); overflow:auto; padding:26px 0 40px}
nav.toc .grp{font-family:'JetBrains Mono',monospace; font-size:.68rem; letter-spacing:.13em; text-transform:uppercase; color:var(--mut); margin:20px 0 7px}
nav.toc .grp:first-child{margin-top:0}
nav.toc a{display:block; padding:6px 12px; border-radius:9px; color:#46525c; font-size:.92rem; border-left:2px solid transparent}
nav.toc a:hover{background:var(--bg2); color:var(--ink)}
nav.toc a.on{color:var(--teal-ink); background:rgba(21,182,166,.09); border-left-color:var(--teal); font-weight:600}
main{padding:30px 0 90px; min-width:0}
.hero h1{font-size:clamp(1.9rem,4.4vw,2.6rem); letter-spacing:-.5px; font-weight:750}
.hero p{color:var(--mut); max-width:64ch; margin-top:10px; font-size:1.05rem}
.hero .meta{display:flex; gap:8px; flex-wrap:wrap; margin-top:16px}
.chip{display:inline-flex; align-items:center; gap:6px; font-size:.8rem; color:#3a444d; border:1px solid var(--line); border-radius:100px; padding:5px 11px; background:#fff}
.chip svg{width:14px;height:14px;color:var(--teal)}
section.doc{padding:36px 0; border-top:1px solid var(--line); scroll-margin-top:74px}
.doc h2{font-size:1.5rem; font-weight:730; letter-spacing:-.3px; display:flex; align-items:center; gap:10px}
.doc h2 .ico{width:30px;height:30px;border-radius:9px;display:grid;place-items:center;flex:none; background:rgba(21,182,166,.12);color:var(--teal)}
.doc h2 .ico svg{width:18px;height:18px}
.row{display:grid; grid-template-columns:minmax(0,1fr) 372px; gap:34px; align-items:start; margin-top:16px}
@media(max-width:980px){.row{grid-template-columns:1fr}}
.prose{color:#3c454d; min-width:0}
.prose p{margin:0 0 12px}
.prose h3{font-size:1.02rem; margin:18px 0 6px; color:var(--ink)}
.prose ul{margin:4px 0 12px; padding-left:18px}
.prose li{margin:4px 0}
.prose b{color:var(--ink)}
.prose code{font-family:'JetBrains Mono',monospace; font-size:.85em; background:var(--bg2); border:1px solid var(--line); border-radius:6px; padding:1px 6px}
.shot{position:sticky; top:74px}
.cap{text-align:center; font-family:'JetBrains Mono',monospace; font-size:.68rem; color:var(--mut); margin-top:10px; letter-spacing:.04em}
/* T-Deck frame: the real screenshot + a generated QWERTY keyboard (added in JS) */
.device{width:344px; margin:0 auto; background:#16191d; border-radius:16px; padding:12px 12px 9px; border:1px solid #e9ece7; box-shadow:0 24px 60px -30px rgba(21,24,30,.5)}
.device img.screen{display:block; width:320px; height:240px; border-radius:8px}
.kbd{margin:9px auto 2px; display:flex; flex-direction:column; gap:4px; width:312px}
.kr{display:flex; gap:4px; justify-content:center}
.key{flex:1; max-width:28px; height:15px; background:#23282e; border-radius:4px; display:grid; place-items:center; font:8px/1 system-ui; color:#7d8893}
.key.wide{max-width:140px; flex:4}
.gallery{display:grid; grid-template-columns:repeat(auto-fill,minmax(232px,1fr)); gap:20px; margin-top:18px}
.mini{background:#16191d; border-radius:12px; padding:7px; border:1px solid #e9ece7}
.mini img{display:block; width:100%; height:auto; border-radius:6px}
.mini .cap{margin-top:6px}
/* Settings-page detail cards: real screenshot + what the page does + its controls */
.sgrp{font-family:'JetBrains Mono',monospace; font-size:.7rem; letter-spacing:.14em; text-transform:uppercase; color:var(--mut); margin:30px 0 0; padding-top:4px}
.scards{display:grid; grid-template-columns:repeat(auto-fill,minmax(290px,1fr)); gap:20px; margin-top:13px}
.scard{border:1px solid var(--line); border-radius:14px; background:#fff; padding:13px; display:flex; flex-direction:column}
.scard .thumb{background:#16191d; border-radius:9px; padding:6px}
.scard .thumb img{display:block; width:100%; height:auto; border-radius:5px}
.scard .thumb.ph{aspect-ratio:4/3; display:grid; place-items:center; border:1px dashed #39424b; padding:0}
.scard .thumb.ph span{font-family:'JetBrains Mono',monospace; font-size:.6rem; letter-spacing:.12em; color:#7d8893; text-transform:uppercase}
.scard h3{font-size:1.04rem; margin:12px 0 2px; color:var(--ink); display:flex; align-items:center; gap:8px; flex-wrap:wrap}
.scard h3 .tag{font-family:'JetBrains Mono',monospace; font-size:.58rem; letter-spacing:.05em; text-transform:uppercase; color:var(--teal-ink); background:rgba(21,182,166,.1); border-radius:5px; padding:2px 6px; font-weight:600}
.scard .lede{color:var(--mut); font-size:.9rem; margin:2px 0 9px}
.scard ul{margin:0; padding-left:17px}
.scard li{margin:4px 0; font-size:.89rem; color:#3c454d; line-height:1.5}
.scard li b{color:var(--ink); font-weight:600}
/* Caution callout (amber) */
.callout{display:flex; gap:11px; margin-top:18px; padding:13px 15px; border-radius:11px; background:#fff7ed; border:1px solid #f6dcb8}
.callout .cico{flex:none; width:20px; height:20px; color:#c2740b; margin-top:1px}
.callout .cico svg{width:20px; height:20px}
.callout b{display:block; color:#7c4a03; margin-bottom:3px; font-size:.94rem}
.callout p{margin:0; color:#7a5320; font-size:.9rem; line-height:1.55}
footer{border-top:1px solid var(--line); padding:26px 22px; text-align:center; color:var(--mut); font-size:.85rem}
@media(max-width:980px){
.shell{grid-template-columns:1fr; gap:0}
nav.toc{position:static; height:auto; border-bottom:1px solid var(--line); padding:14px 0; display:flex; gap:6px; overflow:auto; white-space:nowrap}
nav.toc .grp{display:none} nav.toc a{border-left:0; flex:none}
.shot{position:static; margin:0 auto 6px}
}
</style>
</head>
<body>
<div class="top">
<a class="word" href="index.html">WADA<span class="t">MESH</span></a>
<span class="tag">Docs</span>
<span class="sp"></span>
<a class="home" href="index.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg> Back to site</a>
</div>
<div class="shell">
<nav class="toc" id="toc">
<div class="grp">Start here</div>
<a href="#overview">Overview</a>
<div class="grp">The five tabs</div>
<a href="#home">Home</a>
<a href="#chat">Chat</a>
<a href="#contacts">Contacts</a>
<a href="#map">Map</a>
<a href="#settings">Settings</a>
<div class="grp">With your phone</div>
<a href="#companion">Companion mode</a>
<div class="grp">Reference</div>
<a href="#updates">Updates &amp; test builds</a>
<a href="#settings-pages">Settings pages</a>
<a href="#apps">Apps</a>
</nav>
<main>
<div class="hero" id="overview">
<h1>WADAMESH user guide</h1>
<p>WADAMESH turns a LilyGo T-Deck — its landscape screen, BlackBerry-style keyboard and trackball — into a complete front-end for your MeshCore radio: chat, channels, contacts, a live map and deep radio settings, all on the device. (It also runs on the Heltec V4 and Tanmatsu.) Every screen below is a real screenshot from the device.</p>
<div class="meta">
<span class="chip"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg> No phone required</span>
<span class="chip"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12.55a11 11 0 0 1 14 0"/><path d="M8.5 16.4a5 5 0 0 1 7 0"/><line x1="12" y1="20" x2="12" y2="20"/></svg> LoRa + Wi-Fi + Bluetooth</span>
<span class="chip"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/></svg> Keyboard + touch</span>
</div>
<div class="callout">
<span class="cico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h11l5 5v11a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4z"/><path d="M9 4v4M13 4v4"/></svg></span>
<div>
<b>Got a microSD slot? Keep a card inserted.</b>
<p>On devices with a microSD slot (the LilyGo T-Deck and Tanmatsu), wadamesh is best used with a card in place. It keeps map tiles, your contacts and your chat history on the card, which is roomier and far more reliable than the small internal flash, so a card prevents memory issues and stops that data being lost on reboot. The Heltec V4 has no card slot and manages fine on its internal storage.</p>
</div>
</div>
</div>
<section class="doc" id="home">
<h2><span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></svg></span> Home</h2>
<div class="row">
<div class="prose">
<p>The home tab is your <b>app launcher</b> — a grid of everything on the device:</p>
<ul>
<li><b>Cmdr</b> — the command centre overview.</li>
<li><b>Chats</b>, <b>Contacts</b>, <b>Map</b>, <b>Mentions</b> — jump straight to those.</li>
<li><b>Advertise</b> — announce yourself to the mesh (manual or on a timer).</li>
<li><b>Signal</b> and <b>Monitor</b> — live RX signal and a repeater-style RF activity view.</li>
<li><b>Spectrum</b> — sweep the LoRa band for interference.</li>
<li><b>Settings</b>, <b>Terminal</b> (MeshCore CLI), <b>Files</b> (microSD).</li>
</ul>
<p>The bar along the bottom is always there — it switches the five main tabs (Chat, Contacts, Home, Map, Settings) from anywhere. The top bar shows unread count, time, signal and battery.</p>
</div>
<div class="shot"><div class="device"><img class="screen" src="docs-img/home.png" alt="Home app launcher"></div><div class="cap">Home — app launcher</div></div>
</div>
</section>
<section class="doc" id="chat">
<h2><span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></span> Chat</h2>
<div class="row">
<div class="prose">
<p>Every conversation in one list, newest first — both <b>channels</b> (group chats like <code>#wadamesh</code>, <code>Public</code>) and <b>direct messages</b>. An unread badge shows the count.</p>
<h3>On each row</h3>
<ul>
<li><b>Tap</b> a row to open the conversation, then type on the keyboard to reply.</li>
<li>The <b>gear</b> on the right opens that chat's settings — mute, region &amp; scope, share secret, blocked users, remove — the same screen as the cog inside a chat.</li>
</ul>
<p>The top-right buttons add a contact, mark everything read, and show your QR code.</p>
</div>
<div class="shot"><div class="device"><img class="screen" src="docs-img/chat.png" alt="Chat channel list"></div><div class="cap">Chat — channels &amp; DMs</div></div>
</div>
</section>
<section class="doc" id="contacts">
<h2><span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg></span> Contacts</h2>
<div class="row">
<div class="prose">
<p>Everyone your node knows — name, type and how recently you heard them. Tap a contact to message it or open its details: signal history, a route trace, telemetry, and share/favourite options.</p>
<p>The <b>overflow menu</b> (top right) holds Search, the <b>Discovered</b> list (nodes heard but not yet added), Add contact, Auto-add settings, and the Blocked list. Auto-add can pull new nodes in from their adverts, or you can add them by hand.</p>
</div>
<div class="shot"><div class="device"><img class="screen" src="docs-img/contacts.png" alt="Contacts list"></div><div class="cap">Contacts</div></div>
</div>
</section>
<section class="doc" id="map">
<h2><span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l-6 3V6l6-3 6 3 6-3v15l-6 3-6-3z"/><path d="M9 3v15"/><path d="M15 6v15"/></svg></span> Map</h2>
<div class="row">
<div class="prose">
<p>An OpenStreetMap / OpenTopoMap view with your position and your contacts plotted as markers, plus link lines from you to each node. Tiles are fetched over Wi-Fi through the wadamesh proxy and cached to flash (or microSD), so areas you've viewed work offline.</p>
<p>It also has a line-of-sight analyzer: pick a contact and it uses SRTM elevation to show whether terrain blocks the path.</p>
</div>
<div class="shot"><div class="device"><img class="screen" src="docs-img/map.png" alt="Map"></div><div class="cap">Map</div></div>
</div>
<div class="prose">
<h3>Bring your own tiles (offline maps)</h3>
<p>You don't need Wi-Fi to see the map. Copy a standard OpenStreetMap tile pack onto the T-Deck's microSD card and the map reads straight off the card, fully offline.</p>
<p><b>Where they go.</b> Use the usual slippy-map <code>z/x/y</code> folder layout, in either of these locations on the card:</p>
<ul>
<li><code>/maps/osm/&lt;z&gt;/&lt;x&gt;/&lt;y&gt;.png</code> &mdash; the Meshtastic / MeshCore standard layout (recommended, so packs are interchangeable between apps).</li>
<li><code>/tiles/&lt;z&gt;/&lt;x&gt;/&lt;y&gt;.png</code> &mdash; also read. This is the same folder the Wi-Fi download cache uses, so your own tiles and downloaded ones live side by side.</li>
</ul>
<p><b>Format.</b> 256&times;256 <code>.png</code> tiles (<code>.jpg</code> also works). <code>z</code> is the zoom level and <code>x</code>/<code>y</code> the tile column/row at that zoom &mdash; the same scheme OSM's own tile server and every XYZ tile downloader use, so most ready-made packs drop in unchanged.</p>
<p><b>Turn it on.</b> Open the map's options (the gear button on the map) and switch on <b>Tiles from SD card</b>; the status line confirms <code>Map tiles: microSD</code>. Turn it back off to fetch live tiles over Wi-Fi again.</p>
<p><b>Making a pack.</b> Any XYZ / slippy-map tile downloader works &mdash; point it at OpenStreetMap for your area and a sensible zoom range, then copy the resulting <code>z/x/y</code> folders onto the card. Each extra zoom level roughly quadruples the tile count, so grab only the levels you'll actually use.</p>
<div class="callout">
<span class="cico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>
<div>
<b>Board differences</b>
<p>The "Tiles from SD card" toggle is a T-Deck feature. The Heltec V4 has no microSD slot, so it caches downloaded tiles to a small internal partition (no offline packs there). On the Tanmatsu, tiles &mdash; your own or downloaded &mdash; live at <code>/tiles/&lt;z&gt;/&lt;x&gt;/&lt;y&gt;.jpg</code> on the microSD card; drop JPEG tiles there and they show alongside the downloaded ones.</p>
</div>
</div>
</div>
</section>
<section class="doc" id="settings">
<h2><span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-2.4.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 8 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-2.4 1.65 1.65 0 0 0-1.51-1H1a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 3 8z"/></svg></span> Settings</h2>
<div class="row">
<div class="prose">
<p>A tile for each category. Tap one to open its page; the title bar carries a back chevron. The full set is in <a href="#settings-pages">Settings pages</a> below.</p>
<ul>
<li><b>Profile</b> — your name and node identity.</li>
<li><b>Radio &amp; Mesh</b> — frequency, spreading factor, region scope, high-gain receiver, signal probe, auto-advert.</li>
<li><b>Auto-add, Wi-Fi, Bluetooth, GPS</b> — discovery and connectivity.</li>
<li><b>Clock, Battery, Sensors, Display, Keyboard, Sound, Quick replies, Lock screen</b> — the device experience.</li>
<li><b>Language</b> (twelve UI languages), <b>Backups</b>, and <b>About</b> (firmware version, update check, diagnostics).</li>
</ul>
</div>
<div class="shot"><div class="device"><img class="screen" src="docs-img/settings.png" alt="Settings categories"></div><div class="cap">Settings categories</div></div>
</div>
</section>
<section class="doc" id="companion">
<h2><span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="2" width="14" height="20" rx="2"/><line x1="11" y1="18" x2="13" y2="18"/></svg></span> Companion mode</h2>
<div class="row">
<div class="prose">
<p>WADAMESH runs everything on its own, but it can also be the radio for the <b>official MeshCore app</b> on your phone (iOS and Android). Your phone brings the big screen and full keyboard; the device does the LoRa. Best of all it works <b>alongside</b> the on-device UI — you can keep using the touch screen while your phone is connected, and your contacts, channels and messages stay in sync between them.</p>
<h3>Three ways to connect</h3>
<ul>
<li><b>USB</b> — plug the device into an Android phone or a computer; the app connects over the cable. Nothing to set up on the device.</li>
<li><b>Bluetooth</b> — turn on Bluetooth in <b>Settings &rarr; Bluetooth</b>. The device appears in the app as <code>MeshCore-&lt;your node name&gt;</code>; pair with the 6-digit code (default <code>123456</code>, changeable on the same page).</li>
<li><b>Wi-Fi</b> — put the device on your network in <b>Settings &rarr; Wi-Fi</b>. On the same Wi-Fi, the app connects to the device's IP address on port <code>5000</code> (the Wi-Fi page shows the IP).</li>
</ul>
<div class="callout">
<span class="cico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></span>
<div>
<b>Run Bluetooth and Wi-Fi together only when you need to</b>
<p>Bluetooth and Wi-Fi can be on at the same time, but each one reserves a good chunk of RAM. On the Heltec V4 especially — it has the least free memory of the three boards — keeping both on at once leaves little headroom and can lead to instability or odd behaviour. If you only want the phone app, pick one transport (Bluetooth or Wi-Fi) and leave the other off; if you mostly use the device on its own, keep both off.</p>
</div>
</div>
</div>
<div class="shot"><div class="device"><img class="screen" src="docs-img/set_bluetooth.png" alt="Bluetooth pairing page"></div><div class="cap">Bluetooth &mdash; pairing code</div></div>
</div>
</section>
<section class="doc" id="updates">
<h2><span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg></span> Updates &amp; test builds</h2>
<div class="row">
<div class="prose">
<p>On the boards that update over Wi-Fi (the Heltec V4 and the standalone T-Deck), WADAMESH checks for new firmware on its own. When one is available, <b>Settings &rarr; About</b> shows it and you can flash it straight from the device with <b>Install update</b>, no computer needed. (Under Launcher and on the Tanmatsu you update through Launcher or the app store instead.)</p>
<h3>Two channels: Stable and Test</h3>
<ul>
<li><b>Stable</b> is the default. It is the build that has been tested and is recommended for everyday use. Leave your device on this unless you want to help test.</li>
<li><b>Test (beta)</b> gets new features and fixes earlier, before they are proven. It can be rough, so it is meant for trying things early and reporting bugs, not for a device you rely on.</li>
</ul>
<h3>Turn on test builds</h3>
<p>Flip <b>Settings &rarr; About &rarr; "Get test builds (beta)"</b> on. From then on, both the update check and <b>Install update</b> follow the test channel, so the device offers the newest test build over Wi-Fi. Turn the switch back off to return to stable. You can switch either way at any time.</p>
<p>Prefer to start on a test build from scratch? Pick <b>Beta &middot; testing</b> when you flash from <a href="https://flasher.wadamesh.com" target="_blank" rel="noopener">flasher.wadamesh.com</a>.</p>
</div>
<div class="shot"><div class="device"><img class="screen" src="docs-img/set_about.png" alt="About page"></div><div class="cap">The About page</div></div>
</div>
</section>
<section class="doc" id="settings-pages">
<h2><span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="20" y2="18"/></svg></span> Settings pages</h2>
<div class="prose" style="max-width:68ch"><p>WADAMESH splits its settings into eighteen pages. Here is what each one does and the controls you will find on it. Open any page from the Settings tab; a back chevron in the title bar returns you to the grid.</p></div>
<div class="sgrp">Identity &amp; radio</div>
<div class="scards">
<div class="scard">
<div class="thumb"><img src="docs-img/set_profile.png" alt="Profile settings"></div>
<h3>Profile</h3>
<div class="lede">Who you are on the mesh.</div>
<ul>
<li><b>Node name</b> — your callsign on the network</li>
<li><b>Advert location</b> plus a share-in-advert toggle</li>
<li><b>Identity key</b> and <b>Share QR</b> to hand out your contact</li>
<li><b>Export / Import</b> your whole setup as a JSON backup</li>
</ul>
</div>
<div class="scard">
<div class="thumb"><img src="docs-img/set_radio___mesh.png" alt="Radio and Mesh settings"></div>
<h3>Radio &amp; Mesh</h3>
<div class="lede">Every LoRa and mesh parameter, split into RADIO / MESH / SIGNAL.</div>
<ul>
<li><b>Radio</b> — preset, frequency, bandwidth, spreading factor, coding rate, TX power, with a live duty-cycle and airtime-per-packet readout</li>
<li><b>Mesh</b> — answer-telemetry, path-hash size, region scope tag, scope direct messages</li>
<li><b>Signal</b> — high-gain receiver (Heltec V4.3), signal probe, and a shortcut to the Advertise app</li>
<li><b>Experimental</b> — multi-ACKs, client repeat, RX boost, duty meter</li>
</ul>
</div>
<div class="scard">
<div class="thumb"><img src="docs-img/set_auto_add.png" alt="Auto-add settings"></div>
<h3>Auto-add</h3>
<div class="lede">Which nodes join Contacts automatically from their adverts.</div>
<ul>
<li>Per-type toggles: <b>chat</b>, <b>repeater</b>, <b>room</b>, <b>sensor</b></li>
<li><b>Overwrite oldest</b> when the contact list is full</li>
<li><b>Notify</b> when a new contact is added</li>
<li><b>Max hops</b> filter for how far away a node can be</li>
</ul>
</div>
</div>
<div class="sgrp">Connectivity</div>
<div class="scards">
<div class="scard">
<div class="thumb"><img src="docs-img/set_wi_fi.png" alt="Wi-Fi settings"></div>
<h3>Wi-Fi</h3>
<div class="lede">For map tiles, clock sync and update checks.</div>
<ul>
<li>On / off with a live status line (IP, signal)</li>
<li><b>Scan</b> and tap an SSID, or type one in by hand</li>
<li><b>Save &amp; reconnect</b> applies live — no reboot</li>
<li>Saved network slots for one-tap quick-connect</li>
</ul>
</div>
<div class="scard">
<div class="thumb"><img src="docs-img/set_bluetooth.png" alt="Bluetooth settings"></div>
<h3>Bluetooth</h3>
<div class="lede">Pair with the MeshCore phone app over BLE.</div>
<ul>
<li><b>Enable Bluetooth</b> with a live mode status line</li>
<li>Set a 6-digit <b>pairing code</b> (applies after a reboot)</li>
</ul>
</div>
<div class="scard">
<div class="thumb"><img src="docs-img/set_gps.png" alt="GPS settings"></div>
<h3>GPS</h3>
<div class="lede">The on-board GPS receiver.</div>
<ul>
<li>On / off with a live <b>fix status</b> (acquiring, 2D/3D, age)</li>
<li><b>Serial baud</b> rate (applies after a reboot)</li>
<li>Reads "no GPS module" on devices without one</li>
</ul>
</div>
</div>
<div class="sgrp">Time &amp; power</div>
<div class="scards">
<div class="scard">
<div class="thumb"><img src="docs-img/set_clock___time.png" alt="Clock and time settings"></div>
<h3>Clock &amp; time</h3>
<div class="lede">Keep the clock right.</div>
<ul>
<li><b>Sync clock</b> from the system / NTP</li>
<li><b>Time zone</b> picker, or a custom UTC offset with steppers</li>
<li><b>12-hour clock</b> toggle</li>
</ul>
</div>
<div class="scard">
<div class="thumb"><img src="docs-img/set_battery.png" alt="Battery settings"></div>
<h3>Battery</h3>
<div class="lede">Power readouts and tuning.</div>
<ul>
<li><b>Battery &amp; power history</b> chart</li>
<li><b>Calibrate</b> — tap to set 100% at the current voltage</li>
<li><b>Battery saver</b> throttles the CPU when idle (T-Deck)</li>
</ul>
</div>
<div class="scard">
<div class="thumb"><img src="docs-img/set_sensors.png" alt="Sensors settings"></div>
<h3>Sensors <span class="tag">Heltec V4</span></h3>
<div class="lede">For the V4 Expansion Kit.</div>
<ul>
<li><b>Expansion Kit</b> configuration</li>
<li><b>Show Sensors tab</b> in the bottom bar (applies after restart)</li>
</ul>
</div>
</div>
<div class="sgrp">Experience</div>
<div class="scards">
<div class="scard">
<div class="thumb"><img src="docs-img/set_display.png" alt="Display settings"></div>
<h3>Display</h3>
<div class="lede">Look and feel.</div>
<ul>
<li><b>Screen timeout</b> before it sleeps</li>
<li><b>UI size</b> — Normal / Large / Huge</li>
<li><b>Colourful chat bubbles</b>, distance in miles, hide device name</li>
<li><b>Theme colour</b> picker</li>
</ul>
</div>
<div class="scard">
<div class="thumb"><img src="docs-img/set_keyboard.png" alt="Keyboard settings"></div>
<h3>Keyboard</h3>
<div class="lede">Typing and on-device input.</div>
<ul>
<li><b>Secondary layouts</b> — Cyrillic, Greek, Arabic, French, German and more</li>
<li><b>Accent popups</b> — long-press for accented letters</li>
<li><b>Enter sends</b> the message (T-Deck)</li>
<li><b>Flash</b> the screen and keys on a new message (T-Deck)</li>
</ul>
</div>
<div class="scard">
<div class="thumb"><img src="docs-img/set_sound.png" alt="Sound settings"></div>
<h3>Sound</h3>
<div class="lede">Notification audio.</div>
<ul>
<li>Master sound plus per-type toggles: <b>message</b>, <b>DM</b>, <b>@mention</b></li>
<li>Custom <b>sound files</b> from microSD where supported</li>
<li><b>Volume</b> control (T-Deck / Tanmatsu)</li>
</ul>
</div>
<div class="scard">
<div class="thumb"><img src="docs-img/set_quick_replies.png" alt="Quick replies settings"></div>
<h3>Quick replies</h3>
<div class="lede">One-tap canned messages.</div>
<ul>
<li>Edit <b>six</b> short replies</li>
<li>Pick them from the composer to send instantly</li>
</ul>
</div>
<div class="scard">
<div class="thumb"><img src="docs-img/set_lock_screen.png" alt="Lock screen settings"></div>
<h3>Lock screen <span class="tag">T-Deck</span></h3>
<div class="lede">A simple lock when idle.</div>
<ul>
<li><b>Wallpaper</b> picker and <b>lock text colour</b></li>
<li><b>Lock when screen off</b>, then hold to unlock</li>
</ul>
</div>
</div>
<div class="sgrp">System</div>
<div class="scards">
<div class="scard">
<div class="thumb ph"><span>Screenshot pending</span></div>
<h3>General</h3>
<div class="lede">Device housekeeping.</div>
<ul>
<li><b>Send advert now</b></li>
<li><b>Store data on SD</b> (reboot)</li>
<li><b>Run setup again</b> from scratch</li>
<li><b>Reboot device</b></li>
</ul>
</div>
<div class="scard">
<div class="thumb"><img src="docs-img/set_backups.png" alt="Backups settings"></div>
<h3>Backups</h3>
<div class="lede">Save and restore your whole config.</div>
<ul>
<li><b>Export</b> a new JSON backup</li>
<li>Restore from the <b>saved list</b></li>
<li><b>Factory reset</b> in the danger zone</li>
</ul>
</div>
<div class="scard">
<div class="thumb"><img src="docs-img/set_language.png" alt="Language settings"></div>
<h3>Language</h3>
<div class="lede">Twelve UI languages.</div>
<ul>
<li>Tap a language; the device reboots to apply</li>
</ul>
</div>
<div class="scard">
<div class="thumb"><img src="docs-img/set_about.png" alt="About settings"></div>
<h3>About</h3>
<div class="lede">Version, updates and diagnostics.</div>
<ul>
<li><b>Firmware version</b> plus update check / install</li>
<li><b>Get test builds (beta)</b> switch: opt into the <a href="#updates">test channel</a></li>
<li>Full <b>system diagnostics</b> — uptime, chip, memory, flash, storage</li>
<li><b>Device ID</b> and crash-report export</li>
</ul>
</div>
</div>
</section>
<section class="doc" id="apps">
<h2><span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg></span> Apps</h2>
<div class="prose" style="max-width:64ch">
<p>Tools from the home launcher. <b>RF Monitor</b> is a repeater-style view of recent packets and radio stats; <b>Spectrum</b> sweeps the band so you can spot interference or find a clear frequency.</p>
</div>
<div class="gallery">
<div class="mini"><img src="docs-img/app_rfmonitor.png" alt="RF Monitor"><div class="cap">RF Monitor</div></div>
<div class="mini"><img src="docs-img/app_spectrum.png" alt="Spectrum analyzer"><div class="cap">Spectrum analyzer</div></div>
</div>
</section>
</main>
</div>
<footer>WADAMESH — open-source MeshCore touch firmware · <a href="index.html">wadamesh.com</a></footer>
<script>
var KB = ['QWERTYUIOP', 'ASDFGHJKL', 'ZXCVBNM'];
document.querySelectorAll('.device').forEach(function(dev){
var kb = document.createElement('div'); kb.className = 'kbd';
KB.forEach(function(rowStr){
var r = document.createElement('div'); r.className = 'kr';
rowStr.split('').forEach(function(ch){ var k = document.createElement('span'); k.className = 'key'; k.textContent = ch; r.appendChild(k); });
kb.appendChild(r);
});
var sp = document.createElement('div'); sp.className = 'kr';
sp.innerHTML = '<span class="key">sym</span><span class="key wide"></span><span class="key">&#9166;</span>';
kb.appendChild(sp);
dev.appendChild(kb);
});
var links = Array.prototype.slice.call(document.querySelectorAll('nav.toc a'));
var obs = new IntersectionObserver(function(es){
es.forEach(function(e){ if (e.isIntersecting) {
links.forEach(function(l){ l.classList.remove('on'); });
var a = document.querySelector('nav.toc a[href="#' + e.target.id + '"]');
if (a) a.classList.add('on');
}});
}, { rootMargin: '-25% 0px -65% 0px' });
['overview','home','chat','contacts','map','settings','companion','settings-pages','apps'].forEach(function(id){ var el = document.getElementById(id); if (el) obs.observe(el); });
</script>
</body>
</html>
+254 -21
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WADAMESH — a touchscreen for your mesh radio</title>
<meta name="description" content="Open-source MeshCore touch-UI firmware for the LilyGo T-Deck and Heltec V4. Map, chat, channels, contacts — install from your browser.">
<meta name="description" content="Open-source MeshCore touch-UI firmware for the LilyGo T-Deck, Heltec V4 and Tanmatsu. Map, chat, channels, contacts — install from your browser.">
<link rel="icon" href="wadamesh-badge.svg">
<meta name="theme-color" content="#ffffff">
<link rel="preconnect" href="https://fonts.googleapis.com">
@@ -37,14 +37,33 @@
.wrap{max-width:1040px; margin:0 auto; padding:0 22px}
header{padding:74px 0 30px; text-align:center}
.kicker{display:inline-block; margin-bottom:24px; padding:6px 13px; border:1px solid var(--line);
.kicker{display:inline-block; margin-bottom:10px; padding:6px 13px; border:1px solid var(--line);
border-radius:100px; color:var(--mut); font-size:.78rem; letter-spacing:.05em; text-transform:uppercase;
font-family:'JetBrains Mono',monospace; background:#fff; transition:border-color .15s, color .15s}
.kicker:hover{border-color:var(--teal); color:var(--ink)}
.brand{display:flex; align-items:center; justify-content:center; gap:clamp(12px,2.6vw,20px); flex-wrap:wrap}
.freeline{margin:0 auto 24px; color:var(--mut); font-size:.92rem}
.fab-rail{position:fixed; top:var(--fab-top,16px); z-index:45; display:flex; flex-direction:column; gap:12px}
/* dismissible donate banner */
.donate-bar{position:sticky; top:0; z-index:50; display:flex; align-items:center; justify-content:center; gap:7px 16px; flex-wrap:wrap;
padding:9px 44px; background:var(--ink); color:#fff; font-size:.9rem; line-height:1.4; text-align:center}
.donate-bar a{color:#fff; text-decoration:underline; text-underline-offset:2px}
.donate-bar .donate-cta{background:var(--teal); color:#fff; text-decoration:none; padding:5px 14px; border-radius:100px; font-weight:650; white-space:nowrap}
.donate-bar .donate-cta:hover{background:var(--cyan)}
.donate-bar .x{position:absolute; right:12px; top:50%; transform:translateY(-50%); background:none; border:0; color:#fff; font-size:1.5rem; line-height:1; cursor:pointer; opacity:.75; padding:0 4px}
.donate-bar .x:hover{opacity:1}
.fab-rail.left{left:16px} .fab-rail.right{right:16px}
.fab{width:44px; height:44px; border-radius:50%; display:grid; place-items:center; cursor:pointer;
background:#fff; border:1px solid var(--line); color:#46525c; box-shadow:0 10px 26px -14px rgba(21,24,30,.45);
transition:border-color .15s, color .15s, transform .15s, box-shadow .15s}
.fab:hover{border-color:var(--teal); color:var(--teal-ink); transform:translateY(-2px); box-shadow:0 14px 30px -14px rgba(21,182,166,.5)}
.fab svg{width:21px; height:21px}
.brand{display:flex; align-items:center; justify-content:center; gap:clamp(12px,2.6vw,20px); flex-wrap:wrap; position:relative}
.brand .mark{width:clamp(60px,13vw,88px); height:auto; flex:none}
.brand .word{font-weight:700; font-size:clamp(2rem,7.5vw,3.35rem); letter-spacing:3px; color:var(--ink); line-height:1}
.brand .word .t{color:var(--teal)}
/* byline floats beneath the whole lockup, centred on mark+word combined (mark still centres on the word) */
.byline{position:absolute; top:100%; left:0; width:100%; margin-top:7px; text-align:center;
font-family:'JetBrains Mono',monospace; font-size:clamp(.62rem,1.7vw,.8rem); letter-spacing:.2em; text-transform:uppercase; color:var(--mut); font-weight:600}
/* animated mesh mark — repeaters broadcast pings; signal packets ride the trails */
.brand .mark{overflow:visible}
.wm-flow{display:none}
@@ -84,7 +103,7 @@
.h2{font-size:clamp(1.55rem,4vw,2.15rem); font-weight:750; letter-spacing:-.5px; text-align:center}
.p{color:var(--mut); text-align:center; max-width:560px; margin:10px auto 0}
/* lift hero + section text off the animated net so it stays readable */
.brand .word, .lede, .sub, .cue, .h2, .p{
.brand .word, .byline, .freeline, .lede, .sub, .cue, .h2, .p{
text-shadow: 0 1px 3px #fff, 0 0 14px #fff, 0 0 14px #fff, 0 0 26px #fff, 0 0 26px #fff, 0 0 42px #fff;
}
@@ -124,7 +143,7 @@
.step p{color:var(--mut); font-size:.92rem}
.step code{background:#fff; border:1px solid var(--line); border-radius:6px; padding:1px 6px; font-size:.85em; font-family:'JetBrains Mono',monospace}
.boards{display:grid; gap:14px; grid-template-columns:1fr 1fr; max-width:760px; margin:18px auto 0}
.boards{display:grid; gap:14px; grid-template-columns:repeat(auto-fit,minmax(210px,1fr)); max-width:760px; margin:18px auto 0}
@media(max-width:560px){.boards{grid-template-columns:1fr}}
.board{border:1px solid var(--line); background:var(--card); border-radius:14px; padding:18px; display:flex;
flex-direction:column; gap:10px; align-items:flex-start; box-shadow:0 8px 26px -22px rgba(21,24,30,.5)}
@@ -136,9 +155,46 @@
.dl:hover{background:var(--teal-ink)}
.dl svg{width:15px;height:15px}
.hint{max-width:760px;margin:18px auto 0;color:var(--mut);font-size:.86rem;text-align:center}
/* "install a previous version" picker */
.verpick{max-width:760px; margin:16px auto 0; border:1px solid var(--line); border-radius:12px; background:var(--bg2); text-align:left}
.verpick summary{cursor:pointer; padding:12px 16px; font-weight:600; font-size:.92rem; color:var(--ink); list-style:none; user-select:none}
.verpick summary::-webkit-details-marker{display:none}
.verpick summary::before{content:"\25B8"; color:var(--teal); margin-right:9px; display:inline-block; transition:transform .15s}
.verpick[open] summary::before{transform:rotate(90deg)}
.verpick-body{padding:2px 16px 16px}
.verpick-row{display:flex; align-items:center; gap:10px}
.verpick-row label{font-size:.9rem; color:var(--mut)}
#verSel{font:inherit; font-size:.92rem; padding:7px 11px; border:1px solid var(--line); border-radius:8px; background:#fff; color:var(--ink); cursor:pointer}
.verpick-note{margin:9px 0 0; font-size:.86rem; color:var(--teal-ink)}
.rn{margin-top:12px; max-height:280px; overflow:auto; font-size:.88rem; color:var(--mut); line-height:1.55}
.rn .rn-h{font-size:.92rem; color:var(--ink); font-weight:700; margin:13px 0 5px} .rn .rn-h:first-child{margin-top:0}
.rn ul{margin:5px 0 0; padding-left:18px} .rn li{margin:3px 0}
.rn p{margin:6px 0} .rn code{background:#fff; border:1px solid var(--line); border-radius:5px; padding:1px 5px; font-size:.86em}
.rn a{color:var(--teal-ink)}
.noserial{display:none;color:#b4631a;text-align:center;margin-top:14px}
body.no-serial .noserial{display:block}
/* community videos */
.vids{display:grid; gap:18px; grid-template-columns:repeat(auto-fill,minmax(300px,1fr)); margin-top:34px}
.vid{border:1px solid var(--line); border-radius:16px; overflow:hidden; background:var(--card); box-shadow:0 12px 30px -22px rgba(21,24,30,.5)}
.vid-thumb{position:relative; aspect-ratio:16/9; width:100%; border:0; padding:0; cursor:pointer; display:block; background-size:cover; background-position:center}
.vid-thumb::after{content:""; position:absolute; inset:0; background:rgba(21,24,30,.12); transition:background .15s}
.vid-thumb:hover::after{background:rgba(21,24,30,0)}
.vid-thumb .play{position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); width:62px; height:44px; border-radius:12px;
background:rgba(229,57,53,.94); display:grid; place-items:center; z-index:1; transition:transform .15s, background .15s}
.vid-thumb:hover .play{transform:translate(-50%,-50%) scale(1.07); background:#e53935}
.vid-thumb .play svg{width:24px; height:24px; color:#fff}
.vid-embed{aspect-ratio:16/9; width:100%; border:0; display:block; background:#000}
.vid-cap{padding:13px 15px; font-size:.92rem; color:var(--ink); font-weight:600}
.vid-cap span{display:block; color:var(--mut); font-size:.82rem; font-weight:400; margin-top:2px}
/* toast (docs placeholder etc.) */
.toast{position:fixed; left:50%; bottom:26px; transform:translateX(-50%) translateY(12px); z-index:70;
background:var(--ink); color:#fff; padding:11px 20px; border-radius:100px; font-size:.92rem; font-weight:600;
box-shadow:0 16px 36px -14px rgba(21,24,30,.55); opacity:0; visibility:hidden; pointer-events:none;
transition:opacity .2s ease, transform .2s ease}
.toast.show{opacity:1; visibility:visible; transform:translateX(-50%) translateY(0)}
footer{border-top:1px solid var(--line); margin-top:48px; padding:30px 0 56px; text-align:center; color:var(--mut); font-size:.86rem}
footer a{color:var(--teal-ink)}
.legal-links{margin-top:12px}
@@ -162,11 +218,39 @@
</style>
</head>
<body>
<div class="donate-bar" id="donateBar">
<span>Like the project and want to donate? Help out the <b>MeshCore</b> founders instead. <a href="https://blog.meshcore.io/2026/07/04/help-us-save-meshcore" target="_blank" rel="noopener">Learn more</a></span>
<a class="donate-cta" href="https://givealittle.co.nz/cause/help-us-save-meshcore" target="_blank" rel="noopener">Donate</a>
<button class="x" id="donateX" type="button" aria-label="Dismiss">&times;</button>
</div>
<div id="vanta-bg"></div>
<!-- floating quick links -->
<div class="fab-rail left">
<a class="fab" href="https://discord.gg/T8X2UaQ7Sm" target="_blank" rel="noopener" aria-label="Join us on Discord" title="Join us on Discord">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.317 4.369a19.79 19.79 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.249a18.27 18.27 0 0 0-5.487 0 12.6 12.6 0 0 0-.617-1.249.077.077 0 0 0-.079-.037A19.74 19.74 0 0 0 3.677 4.369a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.2 14.2 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.1 13.1 0 0 1-1.872-.892.077.077 0 0 1-.008-.128c.126-.094.252-.192.372-.291a.074.074 0 0 1 .078-.01c3.927 1.793 8.18 1.793 12.061 0a.074.074 0 0 1 .079.009c.12.099.246.198.373.292a.077.077 0 0 1-.006.127 12.3 12.3 0 0 1-1.873.891.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.84 19.84 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.06.06 0 0 0-.031-.029zM8.02 15.331c-1.182 0-2.157-1.086-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.332-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.086-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.332-.946 2.418-2.157 2.418z"/></svg>
</a>
<a class="fab" href="#videos" aria-label="Community videos" title="Community videos">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>
</a>
</div>
<div class="fab-rail right">
<a class="fab" href="https://github.com/ALLFATHER-BV/wadamesh/issues" target="_blank" rel="noopener" aria-label="Report a bug on GitHub" title="Report a bug">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="8" height="14" x="8" y="6" rx="4"/><path d="m19 7-3 2"/><path d="m5 7 3 2"/><path d="m19 19-3-2"/><path d="m5 19 3-2"/><path d="M20 13h-4"/><path d="M4 13h4"/><path d="m10 4 1 2"/><path d="m14 4-1 2"/></svg>
</a>
<a class="fab" id="docsBtn" href="docs.html" aria-label="Documentation" title="Documentation">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/><path d="M14 2v5h5"/><path d="M16 13H8"/><path d="M16 17H8"/><path d="M10 9H8"/></svg>
</a>
<a class="fab" href="https://t413.com/p/projects/MapTileDl/" target="_blank" rel="noopener" aria-label="Map tile downloader" title="Map tile downloader">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 18l-6 3V6l6-3 6 3 6-3v15l-6 3-6-3z"/><path d="M9 3v15"/><path d="M15 6v15"/></svg>
</a>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<header>
<div class="wrap">
<a class="kicker" href="https://github.com/ALLFATHER-BV/wadamesh" target="_blank" rel="noopener">open source · GPL-3.0</a>
<p class="freeline">Free — as it should be</p>
<!-- wadamesh brand: mark + wordmark, flex-centered -->
<div class="brand">
<svg class="mark" viewBox="0 0 180 120" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="wadamesh">
@@ -198,9 +282,10 @@
<g class="wm-nodes" fill="#15B6A6"><circle cx="10" cy="60" r="7"/><circle cx="90" cy="60" r="7"/><circle cx="170" cy="60" r="7"/><circle cx="50" cy="12" r="7"/><circle cx="130" cy="12" r="7"/><circle cx="50" cy="108" r="7"/><circle cx="130" cy="108" r="7"/></g>
</svg>
<span class="word mono">WADA<span class="t">MESH</span></span>
<span class="byline">by meshcomod</span>
</div>
<p class="lede">A real touchscreen UI for your mesh radio.</p>
<p class="sub">MeshCore MESHCOMOD firmware for the <b>LilyGo&nbsp;T-Deck</b> and <b>Heltec&nbsp;V4</b> — map, chat, channels and contacts on the device itself, <b>and</b> a companion radio for the <b>MeshCore app</b> at the same time.</p>
<p class="sub">MeshCore MESHCOMOD firmware for the <b>LilyGo&nbsp;T-Deck</b>, <b>Heltec&nbsp;V4</b> and <b>Tanmatsu</b> — map, chat, channels and contacts on the device itself, <b>and</b> a companion radio for the <b>MeshCore app</b> at the same time.</p>
<div class="pills">
<span class="pill" tabindex="0"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 20l-6-3V4l6 3 6-3 6 3v13l-6-3-6 3z"/><path d="M9 7v13M15 4v13"/></svg>Offline map
<span class="pop"><b>Offline map</b>A real OpenStreetMap you can pan and zoom on the device. Pre-load tile packs to an SD card for fully offline use, or fetch over Wi-Fi — and see your contacts' GPS positions plotted on it.</span></span>
@@ -245,6 +330,12 @@
<button class="back" data-back>← choose a different path</button>
<div class="guide-head"><h3>Standalone install</h3></div>
<p class="p">Plug the device in over USB and flash it right here. Works in <b>Chrome</b> or <b>Edge</b> on desktop.</p>
<div class="chtoggle" role="group" aria-label="Release channel" style="display:inline-flex;margin:14px 0 0;border:1px solid var(--line);border-radius:10px;overflow:hidden">
<button type="button" data-wm-ch="stable" style="font:inherit;font-size:.85rem;font-weight:600;padding:7px 16px;border:0;background:#fff;color:var(--mut);cursor:pointer">Stable</button>
<button type="button" data-wm-ch="beta" style="font:inherit;font-size:.85rem;font-weight:600;padding:7px 16px;border:0;border-left:1px solid var(--line);background:#fff;color:var(--mut);cursor:pointer">Beta · testing</button>
</div>
<p style="margin:12px auto 0;max-width:600px;text-align:left;font-size:.88rem;line-height:1.6"><b>Stable</b> is the tested default. Pick it unless you want to help test. <b>Beta &middot; testing</b> gets new features and fixes earlier, before they are proven, so it can be rough. It is for trying things early and reporting bugs.<br>Already running WADAMESH? You do not have to reflash to switch channels: turn on <b>Settings &rarr; About &rarr; "Get test builds (beta)"</b> on the device and it updates to test builds over Wi-Fi. Switch it off to go back to stable.</p>
<p data-wm-betahint hidden style="margin:8px auto 0;font-size:.84rem;color:#b4631a;max-width:560px">Beta builds are newer and less tested — for trying things early and reporting bugs. On a T-Deck under Launcher you download the .bin and side-load it.</p>
<div data-whatsnew hidden style="margin:18px auto 0;max-width:560px;background:rgba(21,182,166,.07);border:1px solid rgba(21,182,166,.28);border-radius:12px;padding:13px 16px;text-align:left">
<div style="font-weight:600">What's new in <span data-wn-tag style="color:var(--teal)">the latest build</span></div>
<ul data-wn-notes style="margin:7px 0 0;padding-left:18px;font-size:.9rem;color:var(--mut);line-height:1.55"></ul>
@@ -256,10 +347,27 @@
</div>
<div class="boards">
<div class="board"><h4>Heltec V4 + TFT</h4><p>ESP32-S3 with CHSC6x touch.</p>
<esp-web-install-button manifest="https://firmware.wadamesh.com/latest/manifest-heltec-v4-tft.json"></esp-web-install-button></div>
<esp-web-install-button id="inst-heltec" manifest="https://firmware.wadamesh.com/latest/manifest-heltec-v4-tft.json"></esp-web-install-button></div>
<div class="board"><h4>LilyGo T-Deck / Plus</h4><p>ESP32-S3 keyboard handheld.</p>
<esp-web-install-button manifest="https://firmware.wadamesh.com/latest/manifest-tdeck.json"></esp-web-install-button></div>
<esp-web-install-button id="inst-tdeck" manifest="https://firmware.wadamesh.com/latest/manifest-tdeck.json"></esp-web-install-button></div>
<div class="board"><h4>ThinkNode M9</h4><p>ESP32-S3 QWERTY handheld (LR1110).</p>
<esp-web-install-button id="inst-m9" manifest="https://firmware.wadamesh.com/latest-beta/manifest-thinknode-m9.json"></esp-web-install-button>
<p data-wm-betaonly style="color:var(--mut);font-size:.78rem;margin:6px 0 0">New board: beta channel only until the next stable.</p></div>
<div class="board"><h4>RAK WisMesh Tap V2</h4><p>ESP32-S3 touch node.</p>
<esp-web-install-button id="inst-rak" manifest="https://firmware.wadamesh.com/latest-beta/manifest-rak-tap-v2.json"></esp-web-install-button>
<p data-wm-betaonly style="color:var(--mut);font-size:.78rem;margin:6px 0 0">New board: beta channel only until the next stable.</p></div>
<div class="board"><h4>Tanmatsu</h4><p>ESP32-P4 keyboard handheld.</p>
<p style="color:var(--mut);font-size:.84rem;margin:0;line-height:1.5">Already have one? No USB flashing needed — install WADAMESH straight from the <b>Tanmatsu app store</b> on the device.</p>
<a class="dl" href="https://shop.nicolaielectronics.nl/" target="_blank" rel="noopener"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>Get a Tanmatsu</a></div>
</div>
<details class="verpick">
<summary>Install a previous version</summary>
<div class="verpick-body">
<div class="verpick-row"><label for="verSel">Version</label><select id="verSel" aria-label="Firmware version"></select></div>
<p class="verpick-note" id="verActive" hidden></p>
<div id="verNotes" class="rn"></div>
</div>
</details>
<div class="noserial">Your browser can't flash here — use <b>Chrome</b> or <b>Edge</b> on a desktop.</div>
<p class="hint">T-Deck not detected? Hold the trackball while tapping reset to force download mode.</p>
</div>
@@ -276,13 +384,21 @@
</div>
<div class="boards" style="grid-template-columns:1fr; max-width:380px">
<div class="board"><h4>LilyGo T-Deck / Plus</h4><p>app image (no bootloader)</p>
<a class="dl" href="https://firmware.wadamesh.com/latest/wadamesh-tdeck.bin"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v12M7 11l5 5 5-5M5 21h14"/></svg>Download .bin</a></div>
<a class="dl" id="dl-tdeck" href="https://firmware.wadamesh.com/latest/wadamesh-tdeck.bin"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v12M7 11l5 5 5-5M5 21h14"/></svg>Download .bin</a></div>
</div>
<p class="hint">New to Launcher? It's the easiest way to try WADAMESH without giving up your other firmware — and you can fully switch to Standalone any time.</p>
</div>
</div>
</section>
<section id="videos">
<div class="wrap">
<h2 class="h2">Community videos</h2>
<p class="p">Walkthroughs, demos and reviews from the community.</p>
<div class="vids" id="vids"></div>
</div>
</section>
<footer>
<div class="wrap">
Built for the mesh · open source under GPL-3.0 ·
@@ -383,21 +499,138 @@ We use no tracking, analytics, or advertising cookies. Our CDN and security prov
try { if (!localStorage.getItem('wm-cookie-ack')) cbar.classList.add('show'); } catch(_) {}
document.getElementById('cookieok').addEventListener('click', () => { cbar.classList.remove('show'); try { localStorage.setItem('wm-cookie-ack','1'); } catch(_) {} });
// ---- donate banner: dismissible; shifts the corner FABs down while shown ----
(function(){
const bar = document.getElementById('donateBar'); if (!bar) return;
function setFabTop(){ document.documentElement.style.setProperty('--fab-top', (getComputedStyle(bar).display === 'none' ? 16 : bar.offsetHeight + 12) + 'px'); }
try { if (localStorage.getItem('wm-donate-dismiss') === '1') bar.style.display = 'none'; } catch(_) {}
setFabTop();
addEventListener('resize', setFabTop);
document.getElementById('donateX').addEventListener('click', () => { bar.style.display = 'none'; setFabTop(); try { localStorage.setItem('wm-donate-dismiss','1'); } catch(_) {} });
})();
// Live release version + "what's new" — read from the firmware host so the
// flasher always reflects the current build (the release pipeline updates
// version.json; no site redeploy needed per release).
fetch('https://firmware.wadamesh.com/latest/version.json', {cache:'no-store'})
.then(r => r.ok ? r.json() : null)
.then(v => {
if (!v || !v.tag) return;
document.querySelectorAll('[data-wn-tag]').forEach(e => e.textContent = v.tag);
const notes = Array.isArray(v.notes) ? v.notes : [];
document.querySelectorAll('[data-wn-notes]').forEach(ul => {
ul.innerHTML = '';
notes.forEach(n => { const li = document.createElement('li'); li.textContent = n; ul.appendChild(li); });
});
if (notes.length) document.querySelectorAll('[data-whatsnew]').forEach(b => b.hidden = false);
}).catch(()=>{});
// Release channel (Stable default / Beta testing). The toggle swaps the feed dir
// on the firmware host — latest/ (stable) <-> latest-beta/ (beta) — and the install
// manifests, "what's new", the Launcher .bin link and the version picker follow.
const WM = (window.WM = { feed:'latest', arch:'TOUCH' });
// ThinkNode M9 + RAK Tap V2 exist on the BETA feed only until the first stable
// promote that includes them: their install buttons are pinned to latest-beta in
// the HTML and deliberately NOT switched by wmSetChannel. When a stable ships
// with their bins, add them here + to wmSetChannel and drop the pinned notes.
const WM_MAN = { heltec:'manifest-heltec-v4-tft.json', tdeck:'manifest-tdeck.json' };
window.wmManifest = board => 'https://firmware.wadamesh.com/' + WM.feed + '/' + WM_MAN[board];
function wmWhatsNew(){
const wn = document.querySelectorAll('[data-whatsnew]');
fetch('https://firmware.wadamesh.com/' + WM.feed + '/version.json', {cache:'no-store'})
.then(r => r.ok ? r.json() : null)
.then(v => {
if (!v || !v.tag) { wn.forEach(b => b.hidden = true); return; }
document.querySelectorAll('[data-wn-tag]').forEach(e => e.textContent = v.tag);
const notes = Array.isArray(v.notes) ? v.notes : [];
document.querySelectorAll('[data-wn-notes]').forEach(ul => {
ul.innerHTML = '';
notes.forEach(n => { const li = document.createElement('li'); li.textContent = n; ul.appendChild(li); });
});
wn.forEach(b => b.hidden = !notes.length);
}).catch(() => { wn.forEach(b => b.hidden = true); });
}
window.wmSetChannel = function(ch){
WM.feed = ch === 'beta' ? 'latest-beta' : 'latest';
WM.arch = ch === 'beta' ? 'BETA' : 'TOUCH';
const bH = document.getElementById('inst-heltec'), bT = document.getElementById('inst-tdeck');
if (bH) bH.setAttribute('manifest', wmManifest('heltec'));
if (bT) bT.setAttribute('manifest', wmManifest('tdeck'));
const dl = document.getElementById('dl-tdeck');
if (dl) dl.href = 'https://firmware.wadamesh.com/' + WM.feed + '/wadamesh-tdeck.bin';
document.querySelectorAll('[data-wm-ch]').forEach(b => { const on = b.getAttribute('data-wm-ch') === ch;
b.style.background = on ? 'var(--teal)' : '#fff'; b.style.color = on ? '#fff' : 'var(--mut)'; });
document.querySelectorAll('[data-wm-betahint]').forEach(e => e.hidden = ch !== 'beta');
const vs = document.getElementById('verSel');
if (vs && vs.options.length) { vs.selectedIndex = 0; vs.dispatchEvent(new Event('change')); }
wmWhatsNew();
};
document.querySelectorAll('[data-wm-ch]').forEach(b => b.addEventListener('click', () => wmSetChannel(b.getAttribute('data-wm-ch'))));
wmSetChannel('stable');
// ---- community videos — add an entry here to add a video ----
const VIDEOS = [
// newest first
{ id: 's-8pRsC0TYA', title: 'WadaMesh: The Feature That Makes T-Deck Plus Stand Out', by: 'BFGNeil' },
{ id: 'V19od8jbFRc', title: 'WadaMesh: The Best T-Deck Firmware So Far', by: 'KDHD Stuff' },
// { id: '<youtube-id>', title: '<title>', by: '<channel>' },
];
const vidsEl = document.getElementById('vids');
if (vidsEl) VIDEOS.forEach(v => {
const card = document.createElement('div'); card.className = 'vid';
const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'vid-thumb';
btn.setAttribute('aria-label', 'Play: ' + (v.title || 'community video'));
btn.style.backgroundImage = 'url(https://i.ytimg.com/vi/' + v.id + '/hqdefault.jpg)';
btn.innerHTML = '<span class="play"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></span>';
btn.addEventListener('click', () => {
const f = document.createElement('iframe'); f.className = 'vid-embed';
f.src = 'https://www.youtube-nocookie.com/embed/' + v.id + '?autoplay=1&rel=0';
f.title = v.title || 'Community video';
f.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';
f.allowFullscreen = true; f.loading = 'lazy';
card.replaceChild(f, btn);
});
card.appendChild(btn);
if (v.title) {
const cap = document.createElement('div'); cap.className = 'vid-cap'; cap.textContent = v.title;
if (v.by) { const s = document.createElement('span'); s.textContent = 'by ' + v.by; cap.appendChild(s); }
card.appendChild(cap);
}
vidsEl.appendChild(card);
});
// ---- toast + docs placeholder ----
function toast(msg){ const t = document.getElementById('toast'); if (!t) return; t.textContent = msg; t.classList.add('show'); clearTimeout(t._t); t._t = setTimeout(() => t.classList.remove('show'), 2400); }
// The documentation button is a plain link to docs.html (no handler needed).
// ---- standalone: install a previous version (latest 4 betas + release notes) ----
(function(){
const sel = document.getElementById('verSel'); if (!sel) return;
const wrap = sel.closest('.verpick');
const notesEl = document.getElementById('verNotes'), activeEl = document.getElementById('verActive');
const btnH = document.getElementById('inst-heltec'), btnT = document.getElementById('inst-tdeck');
const latestH = () => window.wmManifest('heltec'), latestT = () => window.wmManifest('tdeck');
const num = t => parseInt((String(t).match(/beta_(\d+)/) || [])[1] || '-1', 10);
let notes = {};
const esc = s => s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
const inl = s => esc(s).replace(/\*\*(.+?)\*\*/g,'<b>$1</b>').replace(/`([^`]+?)`/g,'<code>$1</code>')
.replace(/\[(.+?)\]\((https?:\/\/[^)\s]+)\)/g,'<a href="$2" target="_blank" rel="noopener">$1</a>');
function md(t){ let out='', ul=false; const close=()=>{ if(ul){ out+='</ul>'; ul=false; } };
(t||'').split(/\r?\n/).forEach(line=>{ line=line.trim(); let m;
if(!line) close();
else if(m=line.match(/^#{1,6}\s+(.*)/)){ close(); out+='<div class="rn-h">'+inl(m[1])+'</div>'; }
else if(m=line.match(/^[-*]\s+(.*)/)){ if(!ul){ out+='<ul>'; ul=true; } out+='<li>'+inl(m[1])+'</li>'; }
else { close(); out+='<p>'+inl(line)+'</p>'; } });
close(); return out; }
function showNotes(tag){ notesEl.innerHTML = notes[tag] ? md(notes[tag])
: '<p>Release notes on <a href="https://github.com/ALLFATHER-BV/wadamesh/releases/tag/'+tag+'" target="_blank" rel="noopener">GitHub</a>.</p>'; }
function blobManifest(tag, board){ const m = { name:'wadamesh '+tag, version:tag, new_install_prompt_erase:true,
builds:[{ chipFamily:'ESP32-S3', parts:[{ path:'https://firmware.wadamesh.com/releases/'+((window.WM&&window.WM.arch)||'TOUCH')+'/'+tag+'/wadamesh-'+board+'-merged.bin', offset:0 }] }] };
return URL.createObjectURL(new Blob([JSON.stringify(m)], {type:'application/json'})); }
function apply(){ const tag = sel.value, isLatest = sel.selectedIndex === 0;
if (isLatest){ if(btnH) btnH.setAttribute('manifest', latestH()); if(btnT) btnT.setAttribute('manifest', latestT()); activeEl.hidden = true; }
else { if(btnH) btnH.setAttribute('manifest', blobManifest(tag,'heltec-v4-tft')); if(btnT) btnT.setAttribute('manifest', blobManifest(tag,'tdeck'));
activeEl.hidden = false; activeEl.innerHTML = 'The <b>Install</b> buttons above now flash <b>'+tag+'</b>.'; }
showNotes(tag); }
// GitHub API gives version list + notes in one CORS-enabled call (the firmware
// host listing has no CORS); bins are still pulled from the firmware host.
fetch('https://api.github.com/repos/ALLFATHER-BV/wadamesh/releases?per_page=12').then(r=>r.ok?r.json():[]).then(rs=>{
const rels = (Array.isArray(rs)?rs:[]).filter(r=>r&&/^beta_\d+$/.test(r.tag_name||''));
rels.forEach(r=>{ notes[r.tag_name]=r.body||''; });
const tags = rels.map(r=>r.tag_name).sort((a,b)=>num(b)-num(a)).slice(0,4);
if (!tags.length){ if(wrap) wrap.hidden = true; return; }
sel.innerHTML = tags.map((t,i)=>'<option value="'+t+'">'+t+(i===0?' — latest':'')+'</option>').join('');
showNotes(tags[0]);
sel.addEventListener('change', apply);
}).catch(()=>{ if(wrap) wrap.hidden = true; });
})();
</script>
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

@@ -0,0 +1,44 @@
# WadaMesh
A full **[MeshCore](https://github.com/meshcore-dev/MeshCore)** client for the Tanmatsu.
It runs the LoRa mesh, Wi-Fi and Bluetooth radios **all at the same time**, so the device
works as a complete standalone communicator *and* as a phone companion at once — no
mode-switching, no rebooting to swap radios.
MeshCore is an encrypted, infrastructure-free LoRa mesh network — messages hop from device
to device with no cell tower, Wi-Fi or internet in between. WadaMesh gives it a full
graphical front-end on the Tanmatsu's own screen and keyboard.
## Why WadaMesh
- **Multi-radio, simultaneously.** LoRa, Wi-Fi and Bluetooth all live at once. Chat on the
mesh while Wi-Fi streams map tiles and a phone stays paired over Bluetooth — concurrently.
- **Standalone *and* companion at the same time.** A complete client on its own screen and
keyboard, while a phone app can connect over Bluetooth or Wi-Fi in parallel. You don't
have to pick a role.
- **Live mesh map.** Plot yourself and your contacts on a real OpenStreetMap from their
shared GPS positions (tiles fetched and cached over Wi-Fi).
- **Built for the keyboard.** Arrow-key navigation, type-to-edit and function-key shortcuts —
designed around the Tanmatsu's hardware, not bolted on from a touchscreen.
- **Polished and localised.** Full-colour emoji and 11 interface languages.
## Features
- **Messaging** — direct messages and channels with unread markers, a "new messages"
divider and quick replies.
- **Contacts & map** — contact list with live signal and distance, plus the mesh map.
- **Radio control** — frequency / spreading factor / bandwidth / TX power, and GPS.
- **Connectivity** — LoRa, Wi-Fi and Bluetooth at once; use it standalone or as a Wi-Fi +
Bluetooth companion for a phone.
## Links
- Source & releases: <https://github.com/ALLFATHER-BV/wadamesh>
- Web flasher & project info: <https://wadamesh.com>
## Licence
WadaMesh is distributed under **GPL-3.0** — see [LICENSE](LICENSE). It incorporates
[MeshCore](https://github.com/meshcore-dev/MeshCore) (MIT, © Scott Powell /
rippleradios.com); MeshCore-derived files keep their MIT notices, and the combined
work is GPL (MIT is GPL-compatible). See [NOTICE](NOTICE) for third-party licenses.
Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

@@ -0,0 +1,30 @@
{
"name": "WadaMesh",
"description": "MeshCore client running LoRa Wi-Fi & Bluetooth at once. A standalone unit and phone companion in one.",
"categories": [
"tools",
"utils",
"hardware"
],
"version": "1.0.0",
"icon": {
"16x16": "icon16.png",
"32x32": "icon32.png",
"64x64": "icon64.png"
},
"author": "Kaj Schittecat - ALLFATHER BV",
"license_type": "GPL-3.0",
"license_file": "LICENSE",
"application": [
{
"targets": [
"tanmatsu",
"konsool"
],
"revision": 1,
"type": "appfs",
"executable": "wadamesh.bin",
"assets": []
}
]
}
+198
View File
@@ -0,0 +1,198 @@
#!/usr/bin/env python3
"""
wadamesh tile transcode service.
Why this exists
---------------
The touch firmware fetches map tiles over plain HTTP (on-device HTTPS is not
workable — mbedTLS needs ~30 KB of internal heap for a handshake and only
~5 KB survives Wi-Fi association). It also can only decode JPEG cheaply: the
device's SJPG/TJpgDec path decodes straight to RGB565 in small stripes, while
PNG decode (lodepng) needs a full 256 KB ARGB8888 buffer per tile and bogged
the UI down / rendered as noise.
OpenStreetMap only serves PNG. So this service sits between nginx and OSM:
it fetches the PNG from OSM (HTTPS, with the policy-required identifying
User-Agent) and re-encodes it as JPEG, which is what the device asks for.
nginx (see tiles.wadamesh.com.conf) reverse-proxies tiles.wadamesh.com
to this service on 127.0.0.1:5005 and caches the JPEG result on disk, so OSM
is only hit on a cache miss and transcoding only happens once per tile.
Run
---
pip install flask pillow requests
python3 tile-transcode.py # listens on 127.0.0.1:5005
Production: use the systemd unit (wadamesh-tile-transcode.service).
"""
import io
import sys
import time
try:
import requests
from flask import Flask, Response, abort, request
from PIL import Image
except ImportError:
sys.exit("ERROR: pip install flask pillow requests")
OSM_URL = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
# OpenTopoMap (topographic / terrain-relief style) — an OPT-IN alternate style.
# The firmware default stays OpenStreetMap; the device only requests /opentopo/
# when the user enables it. Like OSM it serves PNG, so it goes through the same
# fetch+transcode path. Legal: map style © OpenTopoMap (CC-BY-SA), underlying
# data © OpenStreetMap contributors (ODbL) + SRTM — the touch UI shows that
# attribution whenever topo is the selected style. OpenTopoMap's tile-usage
# policy asks for a contactable User-Agent + caching; nginx caches results 14
# days so each unique tile hits OpenTopoMap at most once per fortnight.
OPENTOPO_URL = "https://tile.opentopomap.org/{z}/{x}/{y}.png"
# Tile policy: identify yourself with a contactable UA. This is what the upstream
# tile servers see — the device never talks to them directly.
OSM_UA = "wadamesh-tile-proxy/1.0 (+https://wadamesh.com)"
JPEG_QUALITY = 80 # slippy tiles compress well; 80 ≈ visually lossless
MAX_TILE_BYTES = 256 * 1024
REQUEST_TIMEOUT = 12 # seconds for the OSM fetch
# Elevation backend for the line-of-sight analyzer. opentopodata's public
# SRTM 30 m dataset: max 100 locations/request, ~1 req/sec, 1000/day. Fine
# for a personal device. Self-host opentopodata if you outgrow the cap.
ELEV_URL = "https://api.opentopodata.org/v1/srtm30m"
ELEV_MAX_POINTS = 100
app = Flask(__name__)
_session = requests.Session()
_session.headers.update({"User-Agent": OSM_UA, "Accept": "image/png,image/*"})
def _fetch_tile_png(upstream_url: str, z: int, x: int, y: int) -> bytes:
# Range-check before touching the upstream so we can't be turned into an open proxy.
if not (0 <= z <= 19):
abort(404)
n = 1 << z
if not (0 <= x < n and 0 <= y < n):
abort(404)
try:
r = _session.get(upstream_url.format(z=z, x=x, y=y), timeout=REQUEST_TIMEOUT,
stream=True)
except requests.RequestException:
abort(502)
if r.status_code != 200:
# Pass the upstream status through (404 = empty/sea tile — nginx caches it).
abort(r.status_code if r.status_code in (404, 429) else 502)
raw = r.raw.read(MAX_TILE_BYTES + 1, decode_content=True)
if len(raw) > MAX_TILE_BYTES:
abort(502)
return raw
def _tile_response_from_png(raw: bytes) -> Response:
try:
img = Image.open(io.BytesIO(raw)).convert("RGB")
out = io.BytesIO()
img.save(out, "JPEG", quality=JPEG_QUALITY, optimize=True)
except Exception:
abort(502)
return Response(out.getvalue(), mimetype="image/jpeg",
headers={"Cache-Control": "public, max-age=2592000"})
# Default path the firmware uses → OpenStreetMap (the shipping default style).
@app.get("/<int:z>/<int:x>/<int:y>.jpg")
def tile(z: int, x: int, y: int):
return _tile_response_from_png(_fetch_tile_png(OSM_URL, z, x, y))
# Explicit OpenStreetMap alias (identical to the root path).
@app.get("/osm/<int:z>/<int:x>/<int:y>.jpg")
def tile_osm(z: int, x: int, y: int):
return _tile_response_from_png(_fetch_tile_png(OSM_URL, z, x, y))
# OpenTopoMap (topographic) — opt-in alternate style selected on the device.
@app.get("/opentopo/<int:z>/<int:x>/<int:y>.jpg")
def tile_opentopo(z: int, x: int, y: int):
return _tile_response_from_png(_fetch_tile_png(OPENTOPO_URL, z, x, y))
@app.get("/elev")
def elev():
"""Elevation profile for the line-of-sight analyzer.
Request: GET /elev?locations=lat,lon|lat,lon|... (up to 100 points)
Response: compact CSV of integer metres, one per point, in order:
"12,15,40,38,..." ('null' for any point with no data).
The device computes the great-circle sample points itself; this just
proxies them to the SRTM backend and strips the JSON down to a tiny
body the firmware can parse without a JSON library.
"""
locations = request.args.get("locations", "").strip()
if not locations:
abort(400)
pts = locations.split("|")
if len(pts) < 2 or len(pts) > ELEV_MAX_POINTS:
abort(400)
# Validate each "lat,lon" before forwarding so we can't be used to hit
# arbitrary query strings upstream.
for p in pts:
try:
lat_s, lon_s = p.split(",")
lat, lon = float(lat_s), float(lon_s)
except ValueError:
abort(400)
if not (-90.0 <= lat <= 90.0 and -180.0 <= lon <= 180.0):
abort(400)
# opentopodata rate-limits to ~1 req/sec and occasionally 5xx's. Retry a
# couple of times with a >1 s gap so a transient limit/blip doesn't bubble
# up to the device as a failure.
# 2 attempts max so the proxy's worst case (~2 × (timeout + 1.2 s) ≈ 26 s)
# stays under the device's read timeout (30 s) — otherwise the device
# gives up mid-retry and the work is wasted.
r = None
for attempt in range(2):
if attempt:
time.sleep(1.2)
try:
r = _session.get(ELEV_URL, params={"locations": locations},
timeout=REQUEST_TIMEOUT)
except requests.RequestException:
r = None
continue
if r.status_code == 200:
break
if r.status_code in (429, 500, 502, 503, 504):
continue # transient — retry
break # other 4xx — don't bother retrying
if r is None:
abort(502)
if r.status_code != 200:
abort(502 if r.status_code >= 500 or r.status_code == 429 else 400)
try:
data = r.json()
except ValueError:
abort(502)
if data.get("status") != "OK":
abort(502)
out = []
for res in data.get("results", []):
e = res.get("elevation")
out.append(str(int(round(e))) if e is not None else "null")
if not out:
abort(502)
return Response(",".join(out), mimetype="text/plain",
headers={"Cache-Control": "public, max-age=2592000"})
@app.get("/healthz")
def healthz():
return "ok\n", 200
if __name__ == "__main__":
# threaded=True so concurrent device fetches don't serialize behind one
# another. Bind to localhost only — nginx is the public face.
app.run(host="127.0.0.1", port=5005, threaded=True)
+18
View File
@@ -0,0 +1,18 @@
[Unit]
Description=wadamesh tile transcode service (OSM PNG -> JPEG for touch firmware)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/wadamesh-tiles
# Runs from a venv so it doesn't depend on the system Python's package set.
# Falls back to /usr/bin/python3 if the venv isn't present (see deploy notes).
ExecStart=/opt/wadamesh-tiles/venv/bin/python /opt/wadamesh-tiles/tile-transcode.py
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
+13
View File
@@ -3,6 +3,10 @@
#define LV_COLOR_DEPTH 16
#define LV_COLOR_16_SWAP 0
// NOTE: LV_USE_LARGE_COORD is intentionally OFF — enabling it caused a boot-time
// LoadProhibited in LVGL on the T-Deck. Chat scroll heights >8191 px are handled
// in UITask via int32 layout offsets + compressed LVGL scroll coords.
// Route LVGL's general allocator through PSRAM-preferred wrappers (defined in
// LvglPsramAlloc.cpp). The board has 8 MB of PSRAM and the ~60 KB of widget
// state LVGL grows during buildUiTree would otherwise come out of the 320 KB
@@ -48,6 +52,14 @@
#define LV_FONT_MONTSERRAT_12 1
#define LV_FONT_MONTSERRAT_14 1
#define LV_FONT_MONTSERRAT_16 1
/* Larger Montserrat sizes for the Tanmatsu's crisp "UI size" (Large/Huge) — render the UI bigger
* at native resolution instead of upscaling a low-res frame. Gated to the Tanmatsu so the
* flash-tighter S3 touch builds don't pay for fonts they never use. */
#if defined(HAS_TANMATSU)
#define LV_FONT_MONTSERRAT_18 1
#define LV_FONT_MONTSERRAT_20 1
#define LV_FONT_MONTSERRAT_24 1
#endif
/* 28 px Montserrat for the boot splash title — keeps the rest of the UI on
* the smaller fonts so the .data cost stays modest. */
#define LV_FONT_MONTSERRAT_28 1
@@ -100,6 +112,7 @@
* render full-colour emoji inline with text. The emoji set is pre-baked to
* RGB565+alpha lv_img_dsc_t C-arrays (emoji_data.c) and wired as the tail of the
* font fallback chain in UITask (montserrat -> extras -> emoji). */
#define LV_USE_FONT_COMPRESSED 1 /* decode RLE-compressed extras/accent fonts (Tanmatsu size budget) */
#define LV_USE_IMGFONT 1
/* Bi-directional text + Arabic/Persian contextual shaping. Lets Arabic (and
+470 -6
View File
@@ -7,6 +7,7 @@
default_envs =
heltec_v4_tft_companion_radio_usb_tcp_touch
LilyGo_TDeck_companion_radio_touch
ThinkNode_M9_companion_radio_touch
[env:heltec_v4_tft_companion_radio_usb_tcp_touch]
platform = platformio/espressif32@6.11.0
@@ -17,8 +18,13 @@ board_build.partitions = partitions_tft_touch.csv
extra_scripts = pre:scripts/inject_wifi_env.py, merge-bin.py
build_flags =
-w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1
-Wno-deprecated-declarations -Wno-unused-parameter -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1
-D MC_VENDORED_TOUCH_APP
; NOTE: tried -D ARDUINO_USB_MODE=1 (HW-CDC, drops unused TinyUSB MSC/DFU ≈ 12 KB internal
; DRAM) — but on the real V4 the binary companion link regressed: small per-message frames
; arrived yet the larger device-profile frame (node name + keys) dropped bytes over HW-CDC's
; smaller TX FIFO. Reverted to the board JSON's TinyUSB CDC. Revisit only with an HW-CDC TX
; buffer/DTR-wait fix verified on hardware. (T-Deck ships MODE=1 and is fine — board-specific.)
-D LORA_FREQ=869.618
-D LORA_BW=62.5
-D LORA_SF=8
@@ -122,10 +128,16 @@ build_flags =
-I src/ui-touch
-D UI_LVGL=1
-D HAS_TOUCH_UI=1
; Heltec V4 Expansion Kit: local environment sensors UI (Sensors tab, Home env
; widget, Local-Sensors page). Board-gated — only this env compiles it.
-D HAS_EXPANSION_KIT=1
-D FIRMWARE_OTA_ENV='"heltec_v4_tft_companion_radio_usb_tcp_touch"'
-D ENABLE_ADVERT_ON_BOOT=0
-D LV_CONF_PATH=lv_conf.h
-D LV_CONF_INCLUDE_SIMPLE=1
; Smoother scroll: lower deceleration than LVGL's default (10) so flicks glide
; longer instead of stopping abruptly. lv_conf leaves this unset, so this -D wins.
-D LV_INDEV_DEF_SCROLL_THROW=7
-D USER_SETUP_LOADED=1
-D ST7789_DRIVER=1
-D TFT_WIDTH=240
@@ -146,6 +158,7 @@ build_flags =
-D DISPLAY_CLASS=ST7789LCDDisplay
-D ADMIN_PASSWORD='"password"'
-D MULTI_TRANSPORT_COMPANION=1
-D WADAMESH_FORK_BUILD=1
-D TCP_PORT=5000
-D WS_PORT=8765
-D BLE_PIN_CODE=123456
@@ -155,6 +168,7 @@ build_flags =
build_src_filter =
+<*.cpp>
+<*.c>
+<helpers/*.cpp>
+<helpers/esp32/*.cpp>
+<helpers/input/*.cpp>
@@ -166,7 +180,7 @@ lib_deps =
; MeshCore core = the meshcomod monorepo built as a lib, pinned by tag.
; Touch-app files (TouchPrefsStore, WifiRuntimeStore, transports, ...) are
; vendored in this app's src/ and dropped from the lib via -DMC_VENDORED_TOUCH_APP.
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.16.2
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.16.5
SPI
Wire
jgromes/RadioLib @ ^7.6.0
@@ -197,6 +211,7 @@ lib_deps =
lvgl/lvgl @ ^8.3.11
bodmer/TFT_eSPI @ ^2.5.43
densaugeo/base64 @ ~1.4.0
knolleary/PubSubClient @ ^2.8.0
; ======================================================================
; LilyGo T-Deck / T-Deck Plus — flattened equivalent of meshcomod's
@@ -212,7 +227,7 @@ board_build.partitions = variants/lilygo_tdeck/partitions_tdeck_touch.csv
extra_scripts = pre:scripts/inject_wifi_env.py, merge-bin.py
build_flags =
-w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1
-Wno-deprecated-declarations -Wno-unused-parameter -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1
-D MC_VENDORED_TOUCH_APP
-D LORA_FREQ=869.618
-D LORA_BW=62.5
@@ -238,7 +253,10 @@ build_flags =
-I variants/lilygo_tdeck
-D LILYGO_TDECK
-D BOARD_HAS_PSRAM=1
-D CORE_DEBUG_LEVEL=1
; 0 = silence ARDUHAL [E] spam (benign vfs_api cache-miss "no permits for
; creation" + Preferences nvs_open NOT_FOUND on first-namespace-access) that
; also pollutes the USB-CDC companion stream. Our own Serial.printf logs stay.
-D CORE_DEBUG_LEVEL=0
-D ARDUINO_USB_CDC_ON_BOOT=1
-D PIN_USER_BTN=0
-D PIN_PERF_POWERON=10
@@ -246,7 +264,12 @@ build_flags =
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
-D SX126X_DIO2_AS_RF_SWITCH=false
; The T-Deck's SX1262 antenna (TX/RX) switch is driven by DIO2 — this MUST be
; true. With it false, setDio2AsRfSwitch(false) leaves the switch uncontrolled,
; so on TX the +22 dBm PA is never routed to the antenna (~16 dB loss -> ~4 mW).
; That was issue #6. (Meshtastic's T-Deck + the Heltec V4 here + every other
; MeshCore SX1262 variant use true; the T-Deck was the lone outlier.)
-D SX126X_DIO2_AS_RF_SWITCH=true
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D SX126X_DIO3_TCXO_VOLTAGE=1.8f
@@ -302,11 +325,15 @@ build_flags =
-D PIN_TB_RIGHT=1
-D LV_CONF_PATH=lv_conf.h
-D LV_CONF_INCLUDE_SIMPLE=1
; Smoother scroll: lower deceleration than LVGL's default (10) so flicks glide
; longer instead of stopping abruptly. lv_conf leaves this unset, so this -D wins.
-D LV_INDEV_DEF_SCROLL_THROW=7
-D MAX_CONTACTS=2000
-D MAX_GROUP_CHANNELS=40
-D DISPLAY_CLASS=ST7789LCDDisplay
-D ADMIN_PASSWORD='"password"'
-D MULTI_TRANSPORT_COMPANION=1
-D WADAMESH_FORK_BUILD=1
-D TCP_PORT=5000
-D WS_PORT=8765
-D BLE_PIN_CODE=123456
@@ -316,6 +343,7 @@ build_flags =
build_src_filter =
+<*.cpp>
+<*.c>
+<helpers/*.cpp>
+<helpers/esp32/*.cpp>
+<helpers/input/*.cpp>
@@ -324,7 +352,7 @@ build_src_filter =
+<../variants/lilygo_tdeck/*.cpp>
lib_deps =
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.16.2
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.16.5
SPI
Wire
jgromes/RadioLib @ ^7.6.0
@@ -353,3 +381,439 @@ lib_deps =
file://arch/esp32/AsyncElegantOTA
lvgl/lvgl @ ^8.3.11
densaugeo/base64 @ ~1.4.0
knolleary/PubSubClient @ ^2.8.0
; ======================================================================
; Elecrow ThinkNode M9 — ESP32-S3R8 / LR1110 / 2.4" 240x320 ST7789 / QWERTY
; keyboard + d-pad (no touchscreen). Pin map verified against the V1.0
; schematic + Meshtastic boot log during Specter (ESP-IDF) bring-up; the
; panel IC, TCXO voltage, battery divider, and LR1110 RF-switch DIO mapping
; below are all schematic/manufacturer-confirmed (NOT the earlier ILI9341 /
; 2:1-divider / unverified-RF-switch state from a prior attempt). See
; variants/thinknode_m9/M9_PORT.md for the full pin table and the remaining
; hardware-verify checklist (GPS EN polarity/baud, display rotation, RF-switch
; truth-table polarity, SD mount-timing margins) — those items are
; deliberately left unset/unconfirmed below.
; ======================================================================
[env:ThinkNode_M9_companion_radio_touch]
platform = platformio/espressif32@6.11.0
framework = arduino
board = thinknode_m9
monitor_speed = 115200
board_build.partitions = variants/thinknode_m9/partitions_m9_touch.csv
; patch_radiolib_lr11x0: preprod M9 LR1110s run old transceiver FW that rejects
; DriveDiosInSleepMode (0x012A, FW 0x0308+) with CMD_PERR -> begin() = -706.
; The pre-script makes RadioLib's config() skip it on old FW (idempotent,
; per-env libdeps copy). See scripts/build/patch_radiolib_lr11x0.py.
extra_scripts = pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_radiolib_lr11x0.py, merge-bin.py
build_flags =
-Wno-deprecated-declarations -Wno-unused-parameter -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1
-D MC_VENDORED_TOUCH_APP
-D LORA_FREQ=869.618
-D LORA_BW=62.5
-D LORA_SF=8
-D ENABLE_PRIVATE_KEY_IMPORT=1
-D ENABLE_PRIVATE_KEY_EXPORT=1
-D RADIOLIB_EXCLUDE_CC1101=1
-D RADIOLIB_EXCLUDE_RF69=1
-D RADIOLIB_EXCLUDE_SX1231=1
-D RADIOLIB_EXCLUDE_SI443X=1
-D RADIOLIB_EXCLUDE_RFM2X=1
-D RADIOLIB_EXCLUDE_SX128X=1
-D RADIOLIB_EXCLUDE_AFSK=1
-D RADIOLIB_EXCLUDE_AX25=1
-D RADIOLIB_EXCLUDE_HELLSCHREIBER=1
-D RADIOLIB_EXCLUDE_MORSE=1
-D RADIOLIB_EXCLUDE_APRS=1
-D RADIOLIB_EXCLUDE_BELL=1
-D RADIOLIB_EXCLUDE_RTTY=1
-D RADIOLIB_EXCLUDE_SSTV=1
-D ESP32_PLATFORM
; --- ThinkNode M9 board ---
-I variants/thinknode_m9
-D HAS_THINKNODE_M9
-D BOARD_HAS_PSRAM=1
; M9 uses the UART bridge (ttyUSB0), not native USB-CDC — confirmed during
; Specter bring-up. With ARDUINO_USB_CDC_ON_BOOT=1 (the T-Deck's setting)
; app Serial logs go to a ttyACM* port this board doesn't expose and
; appear completely silent.
-D ARDUINO_USB_CDC_ON_BOOT=0
-D PIN_USER_BTN=0
-D RADIO_CLASS=CustomLR1110
-D WRAPPER_CLASS=CustomLR1110Wrapper
-D LORA_TX_POWER=22
; LR1110 clock: TCXO mode at 3.3 V — EMPIRICALLY CONFIRMED on the tester's
; preproduction unit: Meshtastic's thinknode_m9 variant logs "using DIO3 as
; TCXO reference voltage at 3.300000 V" followed by "LR1110 init result 0".
; The earlier theory here (Y1 = self-powered active oscillator -> tcxo must
; be 0) was disproven on hardware: with 0 the chip boots on its internal RC
; (SPI alive, no -2) but the first command needing the real 32 MHz clock is
; rejected -> radio init failed -707 (bring-up build #1, 2026-07-06). The
; schematic's "VTCXO rail" feeding Y1 is evidently the LR1110's own
; TCXO-supply output, so the chip must drive it.
-D LR11X0_DIO3_TCXO_VOLTAGE=3.3
; Bring-up only: RadioLib's own boot prints (found-chip line with the
; transceiver/WiFi/GNSS FW versions, calibration errors). Goes to Serial =
; UART0 on this board. Low volume; drop once the radio is proven.
-D RADIOLIB_DEBUG_BASIC=1
-D RX_BOOSTED_GAIN=true
; DIO5/DIO6 -> RFSW0_V1/RFSW1_V2 confirmed against the V1.0 schematic (LR1110
; block, U7 pins 19/20 -> R19/R20 -> U8 V1/V2) — see target.cpp for the table
; and M9_PORT.md for the crop. The per-mode polarity is the conventional
; 2-pin table every other MeshCore LR1110 board uses, not independently
; re-derived from U8's datasheet (part number isn't printed on the schematic).
-D RF_SWITCH_TABLE
; --- LR1110 SPI / control pins (verified: V1.0 schematic + boot log) ---
-D P_LORA_NSS=39
-D P_LORA_DIO_1=42
-D P_LORA_RESET=45
-D P_LORA_BUSY=41
-D P_LORA_SCLK=40
-D P_LORA_MISO=38
-D P_LORA_MOSI=47
; --- Peripheral I2C bus 0 (Wire): RTC PCF8563 0x51 / IMU QMI8658 0x6b /
; compass QMC6309 0x7c. Brought up by ESP32Board::begin(). ---
-D PIN_BOARD_SDA=7
-D PIN_BOARD_SCL=6
-D PIN_VBAT_READ=13
; --- GPS (CC1167Q, UART1) ---
-D ENV_INCLUDE_GPS=1
-D PIN_GPS_RX=3
-D PIN_GPS_TX=2
-D PIN_GPS_EN=11
-D PIN_GPS_RESET=5
-D PIN_GPS_RESET_ACTIVE=HIGH
; GPS_RST1 -> R46 -> NPN Q16 base: GPIO HIGH turns Q16 on, pulling the module's
; reset pin to GND — so HIGH asserts reset, LOW releases it (inverted vs. library default)
-D PIN_GPS_EN_ACTIVE=LOW
; GPIO10 (GPS ON_OFF) and GPIO4 (1PPS) exist on the schematic but have no
; consumer in this driver yet — reserved, not wired.
-D GPS_BAUD_RATE=115200
-D THINKNODE_M9_BUZZER_PIN=9
-D ENV_INCLUDE_AHTX0=0
-D ENV_INCLUDE_BME280=0
-D ENV_INCLUDE_BMP280=0
-D ENV_INCLUDE_SHTC3=0
-D ENV_INCLUDE_SHT4X=0
-D ENV_INCLUDE_LPS22HB=0
-D ENV_INCLUDE_INA3221=0
-D ENV_INCLUDE_INA219=0
-D ENV_INCLUDE_INA226=0
-D ENV_INCLUDE_INA260=0
-D ENV_INCLUDE_MLX90614=0
-D ENV_INCLUDE_VL53L0X=0
-D ENV_INCLUDE_BME680=0
-D ENV_INCLUDE_BMP085=0
; --- ST7789 panel (manufacturer-confirmed; NOT ILI9341) ---
; No LILYGO_TDECK / HELTEC_LORA_V4_TFT macro is defined, so ST7789LCDDisplay
; uses its default branch (Adafruit_ST7789 over the global `SPI`, the same
; instance the radio uses) — matches the LR1110 reference variants, which
; also share one SPI peripheral across radio + display + (on the M9) SD.
-D DISPLAY_SCALE_X=1.0
-D DISPLAY_SCALE_Y=1.0
; HARDWARE-VERIFIED (bring-up #6, 2026-07-06): rotation 3 rendered the UI
; 180 degrees off on the tester's unit; 1 is the correct landscape
; orientation (keyboard below screen). Adafruit rotations 1/3 are the two
; landscape options, 180 degrees apart.
-D DISPLAY_ROTATION=1
; PIN_TFT_LEDA_CTL is set to -1, NOT left undefined: the installed ST7789LCDDisplay
; references it unconditionally (`if (PIN_TFT_LEDA_CTL != -1)`, no #ifdef guard around
; the macro itself — leaving it undefined is a compile error, not a no-op). -1 makes
; that check skip the pin entirely, since the vendored class always drives it
; active-HIGH, but the M9's backlight is a PNP transistor (active-LOW) — so
; ThinkNodeM9Board owns BL_EN (GPIO17) directly instead (see M9Board.h/.cpp).
; PIN_PERIPH_POWER is the OTHER (also active-low) rail, gating the LCD/GPS/sensors,
; and IS passed through ST7789LCDDisplay's RefCountedDigitalPin constructor arg in
; target.cpp.
-D PIN_TFT_LEDA_CTL=-1
-D PIN_PERIPH_POWER=18
-D PIN_TFT_BL_EN=17
-D PIN_TFT_RST=14
-D PIN_TFT_DC=15
-D PIN_TFT_CS=16
; PIN_TFT_SCL / PIN_TFT_SDA intentionally NOT defined: only the
; USE_PIN_TFT / LILYGO_TDECK / HELTEC_LORA_V4_TFT branches of
; ST7789LCDDisplay's constructor consume them, and M9 uses none of those —
; it takes the default `display(&SPI, PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RST)`
; branch (global SPI, same instance the radio uses).
;
; NOTE: a software-SPI (USE_PIN_TFT) diagnostic build was tried here to test
; whether the class's hardcoded 80MHz hardware SPI clock was corrupting the
; boot-splash write. It wasn't — software SPI reproduced the EXACT same
; freeze-then-TG1WDT_SYS_RST at the exact same point, just slower, which
; rules out the SPI clock/signal-integrity theory entirely. See
; M9_PORT.md / git history if revisiting that path.
; microSD: on the shared LoRa SPI bus, same as T-Deck. CS is GPIO48 — the
; schematic net SPICLK_N, which is NOT one of the GPIO33-37 octal-PSRAM
; lines: SPICLK_N/GPIO48 (and SPICLK_P/GPIO47) are only reserved on the
; R8V/R16V 1.8V-differential-clock variants. This board is a plain R8 (see
; the env comment above), which uses the ordinary single-ended SPICLK
; instead, so GPIO48 is genuinely free. Confirmed via schematic: GPS_RST1
; and this net were the ones directly identified, not inferred.
-D PIN_SD_CS=48
-D HAS_M9_KEYBOARD=1
; Keyboard bus SCHEMATIC-VERIFIED (V1.0 sheet, bring-up #9): S3 GPIO20 =
; net ESP32-2_SDA, GPIO21 = ESP32-2_SCL, to the keyboard ESP32-S2 (its
; GPIO7/6) through R2/R1 series resistors, pullups R73/R72, S2 powered from
; always-on 3V3 via R3. GPIO19/20 are also the S3's native USB D-/D+ pads —
; M9Board::begin() must release the USB-Serial-JTAG pad claim first.
-D PIN_KB_SDA=20
-D PIN_KB_SCL=21
-D PIN_KB_ADDR=0x6c
; --- touch UI / companion stack (M9 has no touch — keyboard + d-pad text
; entry only; keypad-nav focus traversal is NOT wired yet, see
; M9_PORT.md) ---
-I include
-I src
-I src/ui-touch
-D UI_LVGL=1
-D HAS_TOUCH_UI=1
-D FIRMWARE_OTA_ENV='"ThinkNode_M9_companion_radio_touch"'
-D ENABLE_ADVERT_ON_BOOT=0
-D LV_CONF_PATH=lv_conf.h
-D LV_CONF_INCLUDE_SIMPLE=1
-D LV_INDEV_DEF_SCROLL_THROW=7
-D MAX_CONTACTS=2000
-D MAX_GROUP_CHANNELS=40
-D DISPLAY_CLASS=ST7789LCDDisplay
-D ADMIN_PASSWORD='"password"'
-D MULTI_TRANSPORT_COMPANION=1
-D WADAMESH_FORK_BUILD=1
-D TCP_PORT=5000
-D WS_PORT=8765
-D BLE_PIN_CODE=123456
-D CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=1
-D CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384
-D CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096
build_src_filter =
+<*.cpp>
+<*.c>
+<helpers/*.cpp>
+<helpers/esp32/*.cpp>
+<helpers/input/*.cpp>
+<ui-touch/*.cpp>
+<ui-touch/*.c>
+<../variants/thinknode_m9/*.cpp>
lib_deps =
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.16.5
SPI
Wire
jgromes/RadioLib @ ^7.6.0
rweather/Crypto @ ^0.4.0
adafruit/RTClib @ ^2.1.3
melopero/Melopero RV3028 @ ^1.1.0
electroniccats/CayenneLPP @ 1.6.1
h2zero/NimBLE-Arduino @ 1.4.3
adafruit/Adafruit INA3221 Library @ ^1.0.1
adafruit/Adafruit INA219 @ ^1.2.3
robtillaart/INA226 @ ^0.6.4
adafruit/Adafruit INA260 Library @ ^1.5.3
adafruit/Adafruit AHTX0 @ ^2.0.5
adafruit/Adafruit BME280 Library @ ^2.3.0
adafruit/Adafruit BMP280 Library @ ^2.6.8
adafruit/Adafruit SHTC3 Library @ ^1.0.1
sensirion/Sensirion I2C SHT4x @ ^1.1.2
arduino-libraries/Arduino_LPS22HB @ ^1.0.2
adafruit/Adafruit MLX90614 Library @ ^2.1.5
adafruit/Adafruit_VL53L0X @ ^1.2.4
stevemarple/MicroNMEA @ ^2.0.6
adafruit/Adafruit BME680 Library @ ^2.0.4
adafruit/Adafruit BMP085 Library @ ^1.2.4
adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0
ESP32Async/ESPAsyncWebServer @ 3.10.3
file://arch/esp32/AsyncElegantOTA
lvgl/lvgl @ ^8.3.11
densaugeo/base64 @ ~1.4.0
knolleary/PubSubClient @ ^2.8.0
; ======================================================================
; RAK WisMesh Tap V2 — ST7789 TFT + FT5x06 capacitive touch + SX1262.
; RAK3312 module (ESP32-S3, 16 MB Flash DIO, 8 MB Octal PSRAM).
; ======================================================================
[env:rak_tap_v2_companion_radio_touch]
platform = platformio/espressif32@6.11.0
framework = arduino
board = rak_tap_v2
monitor_speed = 115200
board_build.flash_mode = dio ; RAK3312 requires DIO (board JSON defaults to qio)
board_build.partitions = variants/rak_tap_v2/partitions_rak_tap_v2_touch.csv
extra_scripts = pre:scripts/inject_wifi_env.py, merge-bin.py
; Round-1b USB: MODE=1 HW CDC (single COM7). No 1200bps touch — use DTR/RTS reset.
build_unflags =
-D ARDUINO_USB_MODE=0
upload_speed = 460800
upload_flags =
--before=default_reset
--after=hard_reset
build_flags =
-Wno-deprecated-declarations -Wno-unused-parameter -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1
-D MC_VENDORED_TOUCH_APP
-D LORA_FREQ=869.618
-D LORA_BW=62.5
-D LORA_SF=8
-D ENABLE_PRIVATE_KEY_IMPORT=1
-D ENABLE_PRIVATE_KEY_EXPORT=1
-D RADIOLIB_EXCLUDE_CC1101=1
-D RADIOLIB_EXCLUDE_RF69=1
-D RADIOLIB_EXCLUDE_SX1231=1
-D RADIOLIB_EXCLUDE_SI443X=1
-D RADIOLIB_EXCLUDE_RFM2X=1
-D RADIOLIB_EXCLUDE_SX128X=1
-D RADIOLIB_EXCLUDE_AFSK=1
-D RADIOLIB_EXCLUDE_AX25=1
-D RADIOLIB_EXCLUDE_HELLSCHREIBER=1
-D RADIOLIB_EXCLUDE_MORSE=1
-D RADIOLIB_EXCLUDE_APRS=1
-D RADIOLIB_EXCLUDE_BELL=1
-D RADIOLIB_EXCLUDE_RTTY=1
-D RADIOLIB_EXCLUDE_SSTV=1
-D ESP32_PLATFORM
; --- sensors (GPS + AHTx0 + BME680 enabled) ---
-D ENV_INCLUDE_GPS=1
-D ENV_INCLUDE_AHTX0=1
-D ENV_INCLUDE_BME280=0
-D ENV_INCLUDE_BMP280=0
-D ENV_INCLUDE_SHTC3=0
-D ENV_INCLUDE_SHT4X=0
-D ENV_INCLUDE_LPS22HB=0
-D ENV_INCLUDE_INA3221=0
-D ENV_INCLUDE_INA219=0
-D ENV_INCLUDE_INA226=0
-D ENV_INCLUDE_INA260=0
-D ENV_INCLUDE_MLX90614=0
-D ENV_INCLUDE_VL53L0X=0
-D ENV_INCLUDE_BME680=1
-D ENV_INCLUDE_BMP085=0
; --- LoRa SX1262 (RAK3312 module, same pins as RAK3112) ---
-I variants/rak_tap_v2
-D RAK_WISMESH_TAP_V2=1
-D USE_SX1262
-D ESP32_CPU_FREQ=240
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
-D P_LORA_TX_LED=46
-D P_LORA_DIO_1=47
-D P_LORA_NSS=7
-D P_LORA_RESET=8
-D P_LORA_BUSY=48
-D P_LORA_SCLK=5
-D P_LORA_MISO=3
-D P_LORA_MOSI=6
-D SX126X_DIO2_AS_RF_SWITCH=true
-D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D PIN_GPS_RX=43
-D PIN_GPS_TX=44
-D PIN_USER_BTN=0
-D PIN_VEXT_EN=14
-D PIN_BOARD_SDA=9
-D PIN_BOARD_SCL=40
; --- TFT (ST7789 240x320, LovyanGFX hardware SPI on FSPI/SPI2_HOST SCL=13/SDA=11) ---
; USB: ARDUINO_USB_* from boards/rak_tap_v2.json (MODE=1 HW CDC, Meshtastic-aligned)
-D DISPLAY_CLASS=LGFXDisplay
-D LGFX_INVERT_COLOR=true ; false=white bg, true=black bg (ST7789 INVON)
-D DISPLAY_ROTATION=3
-D DISPLAY_SCALE_X=1.0
-D DISPLAY_SCALE_Y=1.0
-D PIN_TFT_RST=-1
-D PIN_TFT_LEDA_CTL=41
-D PIN_TFT_LEDA_CTL_ACTIVE=HIGH
-D PIN_TFT_CS=12
-D PIN_TFT_DC=42
-D PIN_TFT_SCL=13
-D PIN_TFT_SDA=11
; --- FT5x06 capacitive touch (I2C0 addr 0x38, SDA=9/SCL=40) ---
-D HAS_RAK_TAP_V2=1
-D RAK_TS_FLIP_X=1
-D RAK_TS_FLIP_Y=1
-D PIN_TOUCH_SDA=9
-D PIN_TOUCH_SCL=40
-D PIN_TOUCH_INT=39
; --- touch UI / companion stack ---
-I include
-I src
-I src/ui-touch
-D UI_LVGL=1
-D HAS_TOUCH_UI=1
-D FIRMWARE_OTA_ENV='"rak_tap_v2_companion_radio_touch"'
-D ENABLE_ADVERT_ON_BOOT=0
-D LV_CONF_PATH=lv_conf.h
-D LV_CONF_INCLUDE_SIMPLE=1
-D LV_INDEV_DEF_SCROLL_THROW=7
-D USER_SETUP_LOADED=1
-D ST7789_DRIVER=1
-D TFT_WIDTH=320
-D TFT_HEIGHT=240
-D TFT_MOSI=11
-D TFT_SCLK=13
-D TFT_CS=12
-D TFT_DC=42
-D TFT_RST=-1
-D TFT_MISO=-1
-D SPI_FREQUENCY=40000000
-D TFT_RGB_ORDER=TFT_RGB
-D TFT_INVERSION_ON=1
-D TFT_BL=41
-D TFT_BACKLIGHT_ON=1
-D MAX_CONTACTS=2000
-D MAX_GROUP_CHANNELS=40
-D ADMIN_PASSWORD='"password"'
-D MULTI_TRANSPORT_COMPANION=1
-D WADAMESH_FORK_BUILD=1
-D TCP_PORT=5000
-D WS_PORT=8765
-D BLE_PIN_CODE=123456
-D CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=1
-D CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384
-D CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096
build_src_filter =
+<*.cpp>
+<helpers/*.cpp>
+<helpers/esp32/*.cpp>
+<helpers/input/*.cpp>
+<ui-touch/*.cpp>
+<ui-touch/*.c>
+<../variants/rak_tap_v2/*.cpp>
lib_deps =
https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.16.5
SPI
Wire
jgromes/RadioLib @ ^7.6.0
rweather/Crypto @ ^0.4.0
adafruit/RTClib @ ^2.1.3
melopero/Melopero RV3028 @ ^1.1.0
electroniccats/CayenneLPP @ 1.6.1
h2zero/NimBLE-Arduino @ 1.4.3
adafruit/Adafruit INA3221 Library @ ^1.0.1
adafruit/Adafruit INA219 @ ^1.2.3
robtillaart/INA226 @ ^0.6.4
adafruit/Adafruit INA260 Library @ ^1.5.3
adafruit/Adafruit AHTX0 @ ^2.0.5
adafruit/Adafruit BME280 Library @ ^2.3.0
adafruit/Adafruit BMP280 Library @ ^2.6.8
adafruit/Adafruit SHTC3 Library @ ^1.0.1
sensirion/Sensirion I2C SHT4x @ ^1.1.2
arduino-libraries/Arduino_LPS22HB @ ^1.0.2
adafruit/Adafruit MLX90614 Library @ ^2.1.5
adafruit/Adafruit_VL53L0X @ ^1.2.4
stevemarple/MicroNMEA @ ^2.0.6
adafruit/Adafruit BME680 Library @ ^2.0.4
adafruit/Adafruit BMP085 Library @ ^1.2.4
adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0
ESP32Async/ESPAsyncWebServer @ 3.10.3
file://arch/esp32/AsyncElegantOTA
lvgl/lvgl @ ^8.3.11
lovyan03/LovyanGFX@1.2.21
bodmer/TFT_eSPI @ ^2.5.43
densaugeo/base64 @ ~1.4.0
knolleary/PubSubClient @ ^2.8.0
+13
View File
@@ -0,0 +1,13 @@
# beta_10 — what's new (one user-facing note per non-blank, non-# line)
# --- T-Deck input ---
New trackball mode — switch the T-Deck trackball between a mouse cursor (the default) and D-pad navigation (roll to move the selection, click to choose), under Settings → Keyboard. Handy if you prefer button-style navigation.
# --- Map ---
microSD map tiles now fill in as you explore: with "Use microSD for tiles" on, the device also downloads any missing tiles over Wi-Fi and saves them straight onto the SD card, so your offline library grows instead of showing blank areas past its edge (thanks 1nerdherder, #20)
# --- Power ---
Experimental battery saver (idle power-save): throttles the CPU when the device is parked — screen off, on battery, no app connected and the mesh quiet — to trim idle power use, without ever missing a received packet. Off by default; enable it under Settings → System → Battery. (Dan Vybiral / @Vybo, #19)
# --- Settings ---
New "Battery" section under Settings → System — opens the battery & power-history screen (the same one you get by tapping the battery icon up top) and is where the experimental battery saver now lives (it moved out of the Lock settings).
+22
View File
@@ -0,0 +1,22 @@
# beta_11 — what's new (one user-facing note per non-blank, non-# line; # lines are section comments)
# --- Keyboard navigation (headline) ---
New keyboard navigation — drive the whole UI from the keyboard, no touchscreen needed: the WASDZ cluster moves the on-screen selection in the actual direction you press (W up, A left, Z down, D right), S selects, Q goes back, and each bottom tab has its own hotkey (E Messages, R Contacts, T Home, U Map, I Settings) shown as a small letter on its icon. On by default; toggle it any time in Settings → Keyboard.
Every navigation key is hotkey-bindable — the directions, select, back, the per-tab hotkeys, and the new scroll-up / scroll-down keys (default F / C) — remap them all under Settings → Keyboard.
This lays the groundwork for upcoming keyboard-first devices like the Tanmatsu T-Pager that have no touchscreen, and it already gives every T-Deck a fast, reliable way to use wadamesh entirely from the keyboard.
The selected item now shows as clean reverse-video (a bright outline for switches) instead of a coloured border, and the highlight only appears while you're actively navigating with the keys.
The Home hotkey mirrors tapping the Home tab — press it again on Home to flip between the Commander screen and the app drawer.
# --- Stability ---
Fixed a reboot that could occur when receiving a message, especially during advert/message floods — a use-after-free while a list rebuilt mid-scroll (#27).
After a crash, the device now detects it on the next boot and offers to save the crash report so you can send it in, instead of you having to dig through Settings.
# --- Map ---
Map options can now switch the zoom control between the slider and a +/- button pair, like older builds (#26).
Route replay now opens on a wide overview so you can see where the hops start and the whole path at a glance — and you can pan and zoom the map freely while it plays.
# --- Home ---
New "App drawer as home" option (in the app drawer's cog) makes the launcher your home screen instead of the Commander view, for people who prefer the app grid.
# --- Open source ---
The in-progress Tanmatsu (ESP32-P4) port source is now part of the public tree.
+8
View File
@@ -0,0 +1,8 @@
# beta_12 — GPS hotfix (one user-facing note per non-blank, non-# line; # lines are section comments)
# --- GPS fix (headline) ---
Fixed GPS being slow to get a fix — or sometimes never acquiring at all — on the T-Deck. The GPS receives its data over a small serial buffer, and on the busy touch UI a heavy screen redraw could stall long enough to overflow it and drop the satellite data mid-stream, which forced the receiver to start over. The buffer is now 16x larger, so those redraws no longer cost you the fix — acquisition is back to normal (a fix in about a minute instead of several, or none).
# --- A note from Kaj ---
More and more people are picking up wadamesh lately — which is genuinely awesome — and it's surfacing feature requests and bug reports fast, in big numbers.
I can't get back to every post in every channel, but I'm on it: I'm prioritising the fixes and quality-of-life improvements, and I'll be working through them later today. Thanks for all the reports and for the patience!
+29
View File
@@ -0,0 +1,29 @@
# beta_13 — community drop (one user-facing note per non-blank, non-# line; # lines are section comments)
# --- A note from Kaj (headline) ---
This one's a community release — thank you! A bunch of the features below came from contributor pull requests, and the rest are the keyboard, typing and quality-of-life fixes I promised after the last drop. Huge thanks to everyone sending PRs, opening issues and reporting bugs: Yazutsu, tun0, Vybo and samuelcoustet all have their work in this build. Keep it coming.
# --- Typing & keyboard navigation (T-Deck + Tanmatsu) ---
Open a chat or channel and the cursor is ready — you can type straight away. Every other text field now waits for Enter before it captures the keyboard, so the letter-navigation keys keep working until you actually want to type.
Pressing Enter (or Esc / the back button) on an empty message box drops the cursor and re-selects the field, so you can navigate away with the buttons again.
The menu-bar letter shortcuts no longer jump you to another page while you're typing in a field.
New "Show menu-bar letters" setting under Settings -> Keyboard (off by default) — turn it on to see the tab hotkey letters on the bar.
The accent pop-up no longer grabs the keyboard when it appears, so you can keep typing right through it.
# --- Composer ---
New dedicated "Special characters" picker for symbols that aren't on the keyboard (% $ @ # & =, currency, maths, accents) — separate from the emoji picker.
Long-press one of your sent messages to Resend it.
# --- Clock ---
The 12-hour clock setting now applies everywhere — the top bar, the chat bubbles and the lock screen — not just the chat list.
# --- From contributors ---
Room-server posts now show the actual author's name on each message instead of the room's own name. (Yazutsu, #40)
New "Direct (0-hop)" filter for the contact list and the map — show only nodes you're hearing directly, with no repeater hops in between. (tun0, #39)
Beer emoji in colour, tap the unread badge in the status bar to jump to your Chats, a new "notify on new contact" toggle, and Czech accents in the accent pop-up. (Vybo, #22)
Optional trackball "edge scroll" — push the cursor past a screen edge to scroll the list underneath it (off by default, under Settings -> Keyboard). (tun0, #38)
TCP now restarts immediately after you turn it off and on again, the OTA action is hidden when there's no update slot to use, and a TCP-toggle alert glitch is fixed. (samuelcoustet, #29)
# --- Fixes ---
Heltec V4: removed the after-crash report prompt — that board has no SD card or reachable storage to save the report to, so it was a dead end. (The T-Deck still offers it.)
When both Wi-Fi and GPS are off, the clock now sets itself from the timestamps on messages received over the mesh.
+7
View File
@@ -0,0 +1,7 @@
# beta_14 — hotfix (one user-facing note per non-blank, non-# line; # lines are section comments)
# --- Fix (headline) ---
Fixed a bug introduced in beta_13: if you had turned keyboard navigation OFF (Settings -> Keyboard), the physical keyboard stopped typing into text fields entirely on the T-Deck and Tanmatsu. Typing now works again with keyboard navigation off; with it on, nothing changed. (The Heltec V4 was never affected.)
# --- Also in this release ---
Includes everything from beta_13: room-server posts show the author's name, the Direct (0-hop) contact + map filter, beer emoji with a status-bar unread tap and a new-contact toast toggle and Czech accents, optional trackball edge-scroll, the dedicated special-characters picker, the 12-hour clock applied everywhere, and the keyboard navigation + typing improvements.
+18
View File
@@ -0,0 +1,18 @@
# beta_15 (one user-facing note per non-blank, non-# line; # lines are section comments)
# --- New ---
Power-saving now shows as an amber battery icon in the status bar — like iPhone's Low Power Mode — instead of a separate moon icon (T-Deck).
New @-mention picker: type @ in the message box to pick a contact from a list and drop their name in (thanks robinshizuyo-dev, #42).
# --- Fixed ---
Closing a settings item, menu or chat no longer snaps the list back to the top — it stays where you left it (thanks ZorkaTheDog, #45).
Messages you send from the MeshCore companion app now also show up on the device, in the right conversation (thanks HonzaDandy, #46).
Typing is more reliable on the T-Deck and Tanmatsu: tapping a text field — even one that's already highlighted — starts typing right away, and the accent pop-up no longer interrupts typing outside of chats.
The text cursor now only appears when a field is actually ready for input, so it's a dependable "you can type now" cue.
Smoother map panning: tile rendering no longer trips the watchdog and reboots during heavy scrolling.
# --- Changed ---
Keyboard navigation is now OFF by default. If you prefer driving the UI with the trackball / arrow keys, switch it back on in Settings -> Keyboard (your choice sticks until you change it). The Tanmatsu keeps it on, since it has no touchscreen.
# --- Also in this release ---
Includes everything from beta_14 and the earlier betas.
+16
View File
@@ -0,0 +1,16 @@
# beta_16 (one user-facing note per non-blank, non-# line; # lines are section comments)
# --- New ---
Heltec V4 Expansion Kit support: a dedicated Sensors tab (temperature, humidity, pressure, GPS and battery, with small history charts), a Home-screen environment readout, and an on-device "mesh console" for poking at the radio. It only turns up when a sensor board is actually attached, so a bare V4 looks exactly as it did before. This one was a real grind to land — it came in as a huge pull request built on an old beta, and getting it in cleanly meant re-doing most of it by hand against the current code and carefully fencing it all off so it can't bloat or disturb the T-Deck and Tanmatsu builds. Thanks to Samuel Coustet for the original work (#29).
French, Dutch, German, Spanish and Italian keyboards, on-screen and on the T-Deck's physical keys. Choosing one of those as your interface language now auto-selects the matching layout. Also from Samuel Coustet (#29).
Wi-Fi firmware updates are back: on a normally-flashed device (not the Launcher) you can update straight from Settings -> About, no cable needed.
You can also roll back now — "Install a previous version" lets you reinstall one of the last few releases over Wi-Fi, with a confirmation first since it's a downgrade.
# --- Fixed ---
The status-bar clock no longer overlaps the node name on the V4 (it had been crammed against it for a good while).
Closing a dialog no longer lets a scroll carry through to the page behind it (thanks tun0, #50).
The contact action sheet no longer overflows the screen in landscape (thanks tun0, #49).
The per-channel region field now shows your default region as the hint, instead of a generic placeholder (thanks tun0, #48).
# --- Also ---
Everything from beta_15 is included.
+23
View File
@@ -0,0 +1,23 @@
# beta_18 (one user-facing note per non-blank, non-# line; # lines are section comments)
# --- New ---
A topographic map style: turn on Map -> Options -> "Topographic map" for terrain-relief tiles (OpenStreetMap stays the default). Thanks to Samuel Coustet for the proxy work (#52).
New emoji in the picker: soccer ball, american football, moai, and the transgender flag.
Contacts can now sort ascending or descending, and there's a new "Nearest first" sort that orders contacts by distance from you (contacts without a shared location are left out).
The Discovered list gets the same sorting — by most-recently-heard, name, or distance — from a new sort button in its header.
The Chats header is tidied up: the add / mark-all-read / share-contact buttons are grouped together on the left, and sharing your contact now uses a proper QR icon.
# --- Fixed ---
The USB/PC companion connection no longer drops or throws "bad state" errors mid-session. The same fix also clears up repeater admin mode disconnecting and not being able to log in to member rooms.
Map tiles along the edges and corners of the view now fill in automatically as you pan, instead of needing a manual "reload tiles".
Reclaimed about 41 KB of memory by moving a large internal buffer to PSRAM — this fixes the Heltec V4 running out of RAM and rebooting when it loaded map tiles, and leaves more headroom on every board.
The map's "Show link lines" toggle is now remembered across reboots. Thanks to Yazutsu (#61).
Settings category names (Bluetooth, Radio & Mesh, Quick replies, ...) now fit on one line instead of wrapping or splitting mid-word.
Turning keyboard navigation on or off no longer snaps the settings list back to the top.
Heltec V4: the node name in the status bar no longer covers the clock.
T-Deck: the keyboard backlight no longer gets stuck on after flashing when it's set to Off.
Crash reporting is more reliable (it only prompts after an actual crash, not on every restart), and there are fewer stray reboots while saving settings.
Tanmatsu: changing the LoRa radio settings (frequency and so on) now takes effect immediately, without needing a reboot, and the app's settings now drive the radio.
# --- Also ---
Everything from beta_16 (and the Tanmatsu's beta_17) is included.
+14
View File
@@ -0,0 +1,14 @@
# beta_19 (one user-facing note per non-blank, non-# line; # lines are section comments)
# --- New ---
Radio & Mesh settings now show an airtime / duty-cycle readout: roughly how much of the airtime budget your current spreading factor, bandwidth and coding rate use, plus the time-on-air per packet — handy for staying inside regional limits like EU 868.
A new "Answer telemetry requests" toggle in Radio & Mesh settings controls whether other nodes can pull your battery and environment telemetry.
The Radio & Mesh settings page is now split into clear RADIO / MESH / SIGNAL sections so the options are easier to find.
# --- Fixed ---
Fixed a reboot that could happen when you closed a scrollable popup — the emoji picker, quick replies, the Wi-Fi scan list, the app drawer and similar — while it was still coasting from a flick. Thanks to everyone who sent in crash reports.
The accent popup no longer lingers on screen after you send a message on the T-Deck keyboard.
Heltec V4: the Expansion Kit environment line and mini chart on the Home screen no longer overlap the status text or run off the bottom of the display.
# --- Also ---
Everything from beta_18 is included.
+33
View File
@@ -0,0 +1,33 @@
# beta_20 (one user-facing note per non-blank, non-# line; # lines are section comments)
# --- New ---
Spectrum analyzer: a new app in the app drawer that sweeps the LoRa band and draws a live spectrum with a waterfall under it, a colour scale on the side and a peak-frequency readout. Handy for spotting interference or finding a quiet part of the band before you settle on a frequency.
Custom notification sounds: you can set your own notification sound from a 16-bit WAV file on the SD card, per event, instead of only the built-in chime. There is also a separate switch for the direct-message chime. Contributed by Yazutsu (#66).
Saved Wi-Fi networks: the device remembers more than one network now and can auto-join them. Each saved network has its own auto-join switch and can be forgotten on its own.
One chat settings screen: the gear on a chat or channel row, a long-press on that row, and the cog inside an open chat now all open the same screen. Everything is in one place: mark as read, mute messages, mute mentions, region and scope, share secret, blocked users, and remove or delete.
Per-channel mute: silence the notification sound for a single channel while the rest keep chiming.
High-gain receiver for the Heltec V4.3: the V4.3 front-end module has a roughly 17 dB receive amplifier that the firmware never switched on, so every V4.3 shipped with it bypassed. There is now a "High-gain receiver (FEM LNA)" toggle under Radio and Mesh, in the Signal section. Turning it on can noticeably improve weak-signal reception in quiet or remote spots; in a noisy area it can overload the front end, so it is off by default. V4.2 boards do not have this amplifier and do not show the toggle. Thanks to Wireless Rocks for the report and the measurements.
Apps button on the Heltec V4 home screen: the V4 home screen now has an Apps button next to Send advert, so the app drawer (Spectrum, RF Monitor, Snake and the rest) is one tap away. The T-Deck and Tanmatsu already had this.
Blocked list is easier to reach: open it from Contacts (the overflow menu) and from the chat settings screen, not only from where it used to live.
Portuguese (Brazil) is now a UI language, which brings the total to twelve. Community translation.
# --- Fixed ---
Adverts sent from the touch screen are now relayed by region-scoped repeaters. The advert went out without a region tag, so a repeater set to a single region dropped it, while the same advert sent from the phone app got through. The device now tags its flood advert the same way the phone companion does. (Issue #68, reported by EricMW61.)
A message could be transmitted on the wrong channel. After you added or removed channels the cached channel index could drift, so a message typed in one channel was sent on another channel's key. It looked correct on your own screen but arrived in the other channel. The send path now matches the target channel by name at the moment of sending, so this can no longer happen.
Room server logins could time out when the only route to the server was through a region-scoped repeater. A new opt-in toggle, "Scope direct messages to region" under Radio and Mesh, in the Signal section, tags your direct, login and admin messages with your region so a region-only repeater will relay them. It is off by default, so cross-region messaging is unchanged for anyone who does not need it. (Issue #64, reported by randelreiss.)
The Wi-Fi scan could come back empty. If a scan ran while the device was already connected, the automatic reconnect could cancel it mid-sweep and the list returned zero networks. The scan and the reconnect no longer fight over the radio, and the SSID list loads reliably again.
# --- Under the hood ---
Flood scoping was reworked so channel floods and direct floods are handled separately. Channel and group messages stay tagged with your region scope so public channels remain contained, while direct messages, logins and admin traffic stay unscoped by default to keep cross-region messaging working. The new region-scope toggle opts the direct traffic into the region tag (issue #64), and the advert path was moved onto the same scoping the phone companion already used (issue #68).
The Spectrum app and the mesh share one radio. While Spectrum is open it takes ownership of the SX1262 so the mesh loop does not re-tune or re-arm receive underneath the sweep, and leaving the app restores the mesh radio settings and resumes normal receive. The sweep also waits out the chip's RSSI settle time after each retune and takes a short peak reading per step, so the trace shows real energy instead of the reset floor.
A shared flag now tells the main loop when a Wi-Fi scan is running, so the periodic reconnect does not disconnect and reconnect during a sweep, and auto-reconnect is paused for the length of the scan.
The Heltec V4.3 front-end module (KCT8103L) is now driven for receive with its low-noise amplifier behind the new setting. This is the large amplifier and it is separate from the SX1262's own internal RX-boost bit, which is only a fraction of a dB.
The new settings (per-channel mute, custom sounds, saved Wi-Fi networks, the high-gain and region-scope toggles, and the trackball-navigation default) are stored with versioned migrations, so updating from an earlier beta keeps your existing settings.
# --- Changed ---
Trackball and keypad navigation is off by default now and marked experimental, because it still has rough edges. With it off you get the on-screen cursor as before. When it is on it can now reach the top-bar buttons and the back and cog buttons on settings and tool pages.
First-time setup no longer has a Wi-Fi step. Instead it explains that the device can run Wi-Fi and Bluetooth at the same time, as a standalone radio and a phone companion together, and that you set those up later in Settings.
RF Monitor and Spectrum now use the title bar with a back button, the same as the settings pages.
# --- Also ---
Everything from beta_19 is included.
+16
View File
@@ -0,0 +1,16 @@
# beta_21 (one user-facing note per non-blank, non-# line; # lines are section comments)
# --- New ---
Stable and test update channels. beta_21 is the first stable build. On devices that update over Wi-Fi there is now a "Get test builds (beta)" switch on the About screen: leave it off to stay on stable releases, or turn it on to receive test builds earlier — it switches both the "update available" check and the firmware you install to the test channel. You can go back to stable at any time.
MQTT bridge (highly experimental, off by default). An opt-in bridge that forwards the messages your node receives to an MQTT broker of your choice, so you can log your mesh or wire it into other tools. It is gated behind a privacy consent you have to tick, direct messages are excluded unless you deliberately enable them, and the payload can be sealed with an encryption key so only you can read it. Use a broker you control, never a public one. This is experimental and may change. Contributed by Samuel Coustet (@samuelcoustet, PR #71), reworked for privacy.
Portugal (Narrow) region preset, so Portuguese users can pick the local defaults in one tap. (Issue #74, requested by Salgueiron.)
Saved-contacts counter. The Contacts overflow menu now shows how many contacts you have saved out of the maximum it can hold (for example 42 / 2000). (Issue #72, asked by Buko84.)
# --- Fixed ---
The signal probe never travels past the first node now. The "auto-discover" signal pulse, and the probe behind the Signal popup, are sent strictly zero-hop, so a repeater can no longer re-flood them across the mesh. The probe only measures the link to whatever hears it first, which is what it was always meant to do.
Your most relevant contacts now show when you have a lot of them. The on-screen contact list is capped for performance, but it was trimming the list before sorting it, so some contacts could drop out of view. It now sorts first, shows the closest matches, and adds a "+N more — search to narrow the list" line at the bottom. (Issue #73, reported by 1nerdherder.)
The blocked-users list opened from Contacts now shows the proper two-line title bar with a back button, instead of an unlabelled screen.
The Contacts overflow popup fits the screen again after the saved-contacts counter was added.
# --- Also ---
Everything from beta_20 is included.
+8
View File
@@ -0,0 +1,8 @@
# beta_22 (TEST build — one user-facing note per non-blank, non-# line; # lines are section comments)
# --- Fixed ---
The signal probe reads a real signal again. In beta_21 the probe was changed to a zero-hop advert so it could never be re-flooded across the mesh — but nothing answers a zero-hop advert, so the signal reading stopped updating. It now sends a directed trace "ping" to your nearest repeater instead: the repeater replies directly (it still never floods the mesh) and the device reads the link signal from that reply. So you get both — no mesh flooding, and a live signal again. Thanks to Tarmo for spotting this and suggesting the fix.
Contacts discovered while your phone is connected now show on the device too. If the T-Deck was connected to the MeshCore phone app over Bluetooth and heard a new node, the node appeared on the phone but not on the device's own screen, and the Contacts list did not refresh. The device now updates its own Contacts list and notification whether or not a phone app is connected. (Issue #73, reported by 1nerdherder.)
# --- Also ---
This is a test-channel build (opt in with "Get test builds" on the About screen). Everything from beta_21, the first stable, is included.
+11
View File
@@ -0,0 +1,11 @@
# beta_23 (TEST build — one user-facing note per non-blank, non-# line; # lines are section comments)
# --- Fixed ---
Bluetooth companion reliability, especially messages and repeater admin. When a phone or Mac was connected over Bluetooth, the device was sending it a log entry for every LoRa packet it heard — on a busy mesh that swamped the Bluetooth link and starved the frames that actually matter. The device no longer sends that packet log over Bluetooth (USB and Wi-Fi companions still get it), so incoming messages reach the app and repeater / room-server admin (Manage Regions, saving parameters) stops timing out and dropping the connection. (Issues #46 and #54.)
# --- Also in this test line (carried from beta_22) ---
The signal probe reads a real signal again: it sends a directed trace "ping" to your nearest repeater, which replies, instead of a zero-hop advert that nothing answers. Thanks to Tarmo for spotting it.
Contacts discovered while your phone is connected now also show on the device's own screen, not just in the phone app. (Issue #73.)
# --- Also ---
This is a test-channel build (opt in with "Get test builds" on the About screen). Everything from the stable beta_21 is included.
+7
View File
@@ -0,0 +1,7 @@
# beta_24 (TEST build — one user-facing note per non-blank, non-# line; # lines are section comments)
# --- Fixed ---
The signal probe now reads a real signal to your nearest repeater. It sends the standard mesh node-discovery request that repeaters actually answer, instead of the trace packet from the last test build that they ignored, so the signal reading updates again. This is the same request the Ultra and KiekR apps use. Thanks to Tarmo for decoding the exact packet.
# --- Also ---
This is a test-channel build (opt in with "Get test builds" on the About screen). Everything from the stable beta_21 and the earlier test builds is included.
+21
View File
@@ -0,0 +1,21 @@
# beta_25 (STABLE — rolls up the beta_22..25 test builds on top of the previous stable, beta_21.
# One user-facing note per non-blank, non-# line; # lines are section comments.)
# --- Fixed ---
The signal probe reads a live signal to your nearest repeater again. It sends the standard mesh node-discovery request that repeaters actually answer (the same one the Ultra and KiekR apps use), so the signal reading updates. Thanks to Tarmo for decoding the exact packet.
Bluetooth companion reliability, especially messages and repeater / room-server admin. The device no longer floods the Bluetooth link with a per-packet log (USB and Wi-Fi companions still get it), so incoming messages reach the app and admin actions like Manage Regions stop timing out. (Issues #46 and #54.)
Contacts show up right away. A node you discover, even while you're on another screen or one the phone app adds over Bluetooth, now appears in your contact list immediately instead of only after you change the sort. Thanks to 1nerdherder for the report. (Issue #73.)
Manual telemetry and status requests now work on the first tap instead of needing a second. Thanks to Dan Vybiral (@Vybo) for the fix. (Issue #81.)
On the emoji picker and quick-chat replies, pressing Esc or the red X now closes just the popup instead of the whole chat screen.
Lock screen: you can now unlock the device after the screen wakes from an incoming message.
# --- New ---
New "Advertise on boot" option in the Advertise app (off by default): turn it on to send one advert when the device powers on, so nearby nodes refresh your details after a reflash. (Issue #76.)
# --- Tanmatsu ---
Tanmatsu: Wi-Fi map tiles now download and display, cached to the microSD card.
Tanmatsu: contacts and message history now survive a reboot (moved to the microSD card, so keep a card inserted).
Tanmatsu: fixed the crash that stopped the app from booting; popups now fit the screen and scroll, and keyboard navigation stays inside an open popup.
# --- Also ---
This is the new stable build, promoted from the beta_25 test channel. It includes everything from the beta_22, beta_23, beta_24 and beta_25 test builds.
+8
View File
@@ -0,0 +1,8 @@
# beta_26 (TEST) — storage-reliability fix on top of the current stable (beta_25).
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- Fixed ---
Fixed a reboot loop that could hit while the device saved its contact list. Internal-flash housekeeping during a save could freeze the device long enough to trip the watchdog and restart it; that save no longer trips it. Thanks to the crash report that pinned it down.
# --- Improved ---
On devices with a microSD card, contacts and channels are now kept on the card (like chat history and map tiles already are). That frees the small internal flash and puts the frequently-rewritten files on the more reliable card. Your existing contacts move to the card automatically the first time; your identity and settings stay in internal flash, so the node is unchanged.
+15
View File
@@ -0,0 +1,15 @@
# beta_27 (STABLE — rolls up the beta_26 + beta_27 test builds on top of the previous stable, beta_25.
# One user-facing note per non-blank, non-# line; # lines are section comments.)
# --- Fixed ---
Fixed a reboot loop that could hit while the device saved its contact list. Internal-flash housekeeping during a save could freeze the device long enough to trip the watchdog and restart it; that save no longer trips it. Thanks to the crash report that pinned it down.
Devices installed under the Launcher no longer show the on-device "Install update" button. That update path only works on a standalone flash; under the Launcher it failed with "won't fit: Bad Size Given". The "update available" note stays, but you update through the Launcher instead. Thanks to Keith (KM1USR) for the report.
# --- New ---
Keyboard backlight brightness control on the T-Deck: a live slider plus Off / 25 / 50 / 75 / Max presets in Settings > Keyboard. It dims smoothly (fine-grained at the low end), applies to the on and auto modes and the new-message flash, and survives a reboot. Thanks to Yazutsu (PR #85).
# --- Improved ---
On devices with a microSD card, contacts and channels are now kept on the card (like chat history and map tiles already are). Your existing contacts move over automatically on first boot; your identity and settings stay in internal flash, so the node is unchanged. Keep the card inserted afterwards - the contact list lives on it, and an older firmware won't look there if you ever downgrade.
# --- Also ---
This is the new stable build, promoted from the beta_27 test channel. It includes everything from the beta_26 and beta_27 test builds.
+13
View File
@@ -0,0 +1,13 @@
# beta_28 (TEST) — room-server session fix + the wyvern.red feedback batch, on top of stable beta_27.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- Fixed ---
Room servers no longer go one-way "frozen". The device now keeps its room login session alive in the background and automatically logs back in when the session drops, so posts keep being delivered and new room messages keep arriving. There is also a new "Log in again" option in the room chat's menu, and a clear warning when the room only granted you read-only access (posts would previously just vanish). Thanks to EricMW61 for the report (issue #89).
The screen now truly switches off in standby. Before, only the backlight went dark while the panel kept displaying the last image - the actual cause of the LCD ghosting / burn-in some of you noticed. The panel itself now sleeps, and on wake your screen is back instantly. Also: a lit lock screen now always dims on every board, a screen woken by a message re-darkens after 10 seconds, and the lock-screen clock drifts a few pixels to avoid ghosting. Thanks to wyvern.red for pinning this down.
The on-device "Install update" now also hides on mixed or older partition layouts where an update could crash the device halfway through (found via a user crash report - thanks!).
# --- New ---
Compact messages (IRC style): a new toggle in chat settings shows one dense line per message instead of bubbles, fitting two to three times more history on screen. Off by default. (wyvern.red's suggestion.)
Clearer delivery ticks: an up-arrow means sent but not yet confirmed, a double check means the other side confirmed it, and a failed message now says "tap to resend" - one tap resends it. (wyvern.red)
Keyboard navigation: the scroll keys (F/C) now zoom the map, and backspace in an empty message box exits typing mode so the navigation keys work again. (wyvern.red)
"Reset path" is now also in the chat's own menu next to Blocked users, so you can force a fresh route without going through Contacts.
+17
View File
@@ -0,0 +1,17 @@
# beta_29 (TEST) — clock hardening for room servers, BLE repeats-heard fix, deeper SD chat history, one-screen popups. On top of beta_28.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- Fixed ---
Room logins can no longer be silently locked out by the device clock. Room servers quietly reject anything older than the newest timestamp they have seen from you, so a clock that stepped backwards (a reboot without GPS or phone around, or a sync correcting a fast clock) black-holed every Join with zero feedback. The device now remembers the newest timestamp it has used across reboots so it can never fall behind itself, a Join that gets no reply shows a clear error after 20 seconds (including what the device thinks the time is) instead of spinning forever, and the device warns you when its clock and the room server's clock disagree. Thanks to EricMW61 for the follow-up testing on issue #89.
"Repeats heard" works again over Bluetooth. The companion app counts repeats from the per-packet RX log, which beta_23 stopped sending over BLE to fix congestion. The firmware now recognises packets that are repeats of your own messages and lets exactly those through to BLE, so the counter works and the congestion fix stays. Thanks to daryldakine for the clean bisect (issue #94).
Messages sent from the phone app through the device now also appear on the device itself, as your own outgoing message in that channel. This worked for direct messages but never for channels.
Deleting a chat or channel now sticks. Deleted messages could survive in storage and resurrect the chat later, and removing a channel could miss the underlying channel entry so the thread came right back.
Settings pages no longer jump down when opened on keyboard-navigation devices, and the red update dot can no longer steal the selection from the About button.
The update check no longer shows "couldn't reach the update server" for six hours after one failed attempt. It now waits for Wi-Fi to actually have an address and retries within a minute.
The Tanmatsu About page now shows the real firmware version instead of a stale hardcoded one.
# --- New ---
Much deeper chat history on devices with an SD card: up to 5000 messages instead of 500, with date separators (Today, Yesterday, day names) between messages from different days. Note for testers: if you later downgrade below beta_29, the older build will not read the larger history file and will start with an empty history.
Chat and channel rows in the messages tab now use the same compact style as the Contacts tab, fitting more conversations on screen.
The chat or channel settings sheet (long-press or the gear) now fits on one screen, laid out like the contact menu, instead of scrolling.
Compact chat mode is easier to read: messages alternate between two background tones.
+11
View File
@@ -0,0 +1,11 @@
# beta_30 (TEST) — the big-mesh sluggishness fix + a much faster Spectrum analyzer. On top of beta_29.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- Fixed ---
The periodic multi-second freezes on busy meshes are gone. Saving the contact list to storage took up to 3 seconds at ~570 contacts and ran on the UI thread every couple of minutes, which is exactly the trackball and touch hesitation people felt since beta_25. The save now writes the same data in a few large chunks instead of thousands of tiny ones, cutting it to a fraction. Thanks to guinnessbear for the report and Yazutsu for the precise measurements that pinned this down (issue #82).
The Contacts tab no longer rebuilds itself every 60 seconds just to refresh the "heard 2h ago" labels - they update in place now. That rebuild cost over a second on large contact lists and could yank the list while you were scrolling.
A burst of adverts can no longer queue several back-to-back Contacts rebuilds; they are now coalesced.
# --- Improved ---
The Spectrum analyzer feels live now: the waterfall gets a new line about four times per second instead of once per sweep, the trace advances across the band as it sweeps (like a bench analyzer), and a full sweep is about 20 percent faster.
The Spectrum trace also scales itself to the signal now: the chart tracks the live minimum and maximum, jumping out instantly for a strong signal (no more clipping flat at the top) and easing back once it is gone.
+9
View File
@@ -0,0 +1,9 @@
# beta_31 (TEST) — the new chats tab: avatars, message previews, custom channel icons. On top of beta_30.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- New ---
The chats tab has a fresh look: each conversation now shows a round avatar in that chat's signature colour (the same colour its bubbles use) with the first two letters of the name, the last message previewed under the name, the time top right and the unread count below it. Compact chat mode keeps the dense single-line rows instead, so both styles are available.
Channels can have a custom icon: open the channel's settings (long-press or the gear) and tap "Chat icon" to pick any emoji as its avatar. Long-press the same entry to go back to the automatic letters. The choice is remembered per channel.
# --- Fixed ---
Long chat names no longer wrap onto the message preview underneath.
+9
View File
@@ -0,0 +1,9 @@
# beta_32 (TEST) — the Wi-Fi freeze fix: a stale phone/web connection can no longer stall the device. On top of beta_31.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- Fixed ---
The 1-2 second freezes every ~10 seconds some users saw with Wi-Fi on are gone. When a phone app or web client was connected over Wi-Fi and that connection went stale (phone asleep, walked out of range), every received mesh packet made the device wait on the dead connection before the UI could continue. The device now only writes to a connection when it can accept data immediately, and a connection that stays unresponsive for 10 seconds straight is dropped so the client can reconnect cleanly.
Saving chat history could briefly hitch the UI on busy meshes: the history file now writes in a few large chunks instead of thousands of tiny ones (the same treatment the contact list got in beta_30), and background saves are spaced further apart while messages stream in.
# --- New ---
The About page now shows recent loop stalls (anything over 0.2 seconds, newest first), so freeze reports can be confirmed or ruled out at a glance.
+7
View File
@@ -0,0 +1,7 @@
# beta_33 (TEST) — the MQTT freeze fix + the About page no longer stalls itself. On top of beta_32.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- Fixed ---
The freezes with MQTT enabled are gone. Connecting to the broker (DNS lookup, TCP connect, login) could block the device for 2-10 seconds and retried every 15 seconds while the broker was unreachable, all on the UI thread. Connection attempts now run fully in the background, and writes to a broker that stops responding are skipped instead of waited on (the same treatment beta_32 gave phone/web connections). Tip: if your bridge keeps reconnecting, check the broker address and credentials - a healthy connection does not retry.
The About page caused a ~0.4 second hitch every second while it was open: computing the firmware image size checksums the whole ~3 MB app in flash and ran twice per refresh. It is now computed once per boot, so About (and its stall list) no longer disturbs the very measurements it shows.
Favoriting a contact when the favorites list is full (16) now says "Favorites full (max 16)" instead of the incorrect "Removed from favorites".
+11
View File
@@ -0,0 +1,11 @@
# beta_34 (TEST) - buffered receive: the missed-messages fix, ON by default on this test build. On top of beta_33.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- New ---
This build changes how the device collects packets from the LoRa radio, and the new way is ON by default so the test channel can prove it. Background: the radio chip holds only one received packet at a time, and until now the firmware only collected it when the main loop got around to it, the same loop that draws the screen. If the interface kept the loop busy while two packets arrived back to back, the first one was silently overwritten. That is the "my device missed a message another device right next to it heard" problem some of you reported, and why it affected this firmware but not stock MeshCore, which has no screen to draw. A small dedicated background task now collects every packet within about a millisecond of it arriving, no matter what the interface is doing.
Because the receive path is the heart of the device, this ships with a safety hatch: if you see anything odd with receiving, sending, or the Spectrum analyzer, turn off "Buffered receive (experimental)" in Settings > Radio & Mesh (applies instantly, no reboot) and you are back on the old path. Please report it either way, that is exactly what the test channel is for.
The About page now shows LoRa receive counters (heard / read / late-lost). "late-lost" counts packets the radio heard but the firmware failed to collect in time. With buffered receive on it should stay at zero. If you turn the toggle off on a busy mesh you may watch it creep up, which is the old bug happening live.
# --- Fixed ---
Messages sent by wadamesh no longer carry a few stray bytes at the end that stock MeshCore devices and some companion apps displayed as garbage characters after the text (issue #13).
A GPS module reporting a bogus date can no longer drag the device clock back to the year 1902 (core clock guard).
+11
View File
@@ -0,0 +1,11 @@
# beta_35 (TEST) - V4 breathing room: freezes fixed, 30 KB of internal RAM back, crash-safe settings. On top of beta_34.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- Fixed ---
Heltec V4: multi-second freezes right after receiving messages are gone, including the screen refusing to wake from dark mode. Saving chat history to the internal flash could stall the device for up to 6 seconds (the V4 has no SD card, and internal flash pauses for housekeeping); that save now runs on a background task, so the screen and touch stay responsive no matter what storage is doing.
Turning Bluetooth on while Wi-Fi is running no longer reboots the device when memory is tight: the switch now checks free memory first and tells you "Not enough free memory for Bluetooth. Turn Wi-Fi off first." instead of crashing.
The same protection applies the other way around: turning Wi-Fi on with Bluetooth active used to claim "Wi-Fi on" while the radio silently never started. It now either really starts or tells you why it cannot.
Settings could vanish for a single boot after an unlucky reboot (the device came up with a default name once, then recovered). Settings writes are now crash-safe: a reboot or power cut at any moment leaves at least one intact copy on storage, the loader self-heals from it, and a failed save now says "Couldn't save the name to storage" instead of pretending it worked.
# --- Improved ---
About 30 KB of the ESP32's scarce internal RAM has been freed by moving buffers to the external PSRAM and dropping unused USB components from the build. This is the headroom that makes Wi-Fi and Bluetooth run together comfortably again, especially on the Heltec V4.
+13
View File
@@ -0,0 +1,13 @@
# beta_36 (TEST) - Launcher SD storage really works now, Wi-Fi settings redesign, symbol passwords, lock-screen unread count. On top of beta_35.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- Fixed ---
The "Store data on SD" toggle is now actually honored after a reboot (fix contributed by scratchdiver, PR #123). On Launcher installs the setting silently reverted to internal storage every boot, which also meant those devices kept the slow internal-flash writes. If you run wadamesh under the Launcher and your mesh is busy, this one is for you: flip the toggle once and contacts and settings really move to the SD card.
Turning "Store data on SD" off works reliably now too, including on Launcher installs (follow-up to PR #123).
Passwords with special characters can now be entered everywhere: long-press any settings field, including the Wi-Fi password, for the Cut / Copy / Paste / All / Sym menu. Sym opens a picker with the characters the on-screen keyboard does not have (issue #122). Double-tap selects a word.
# --- New ---
The Wi-Fi network screens (join a network, hidden network, saved-network details) are full-screen pages now instead of small popups: same style as the other settings pages, title in the top bar, tap the bar to go back, and everything scrolls properly with the keyboard open.
The lock screen shows an envelope with the number of unread messages under the clock, so you can check for news without unlocking (idea from issue #93). It disappears when there is nothing unread.
For Launcher installs: a "Save update bin to SD" button on the About page downloads the latest firmware of your selected channel into a BINS folder on the SD card, named with the version and channel, ready to flash from the Launcher. The test-builds toggle is now visible on Launcher installs as well.
New region preset "Australia (Mid)": 915.075 MHz, 125 kHz, SF9, CR5 (requested in issue #121).
+15
View File
@@ -0,0 +1,15 @@
# beta_37 (TEST) - Tanmatsu ALT accents + language switching, GPS quick reply, T-Deck touch self-heal, SD history reliability. On top of beta_36.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- New ---
Tanmatsu: hold ALT and press a letter to open the accent picker (a with ALT gives an accent popup, just like long-press on the touch boards). Keep ALT held and press the letter again to cycle through the options; release ALT to insert the highlighted one. Requested by jochentj in issue #129.
Tanmatsu: double-tap the space bar to switch between your enabled keyboard languages, same as on the T-Deck.
Accented letters on the Tanmatsu now render at full size at the Large and Huge UI scales instead of noticeably smaller (issue #129). Made possible by moving all boards to compressed UI fonts, which also frees a chunk of firmware space.
Your GPS position is now the first row in the quick-reply picker, so you can send your current coordinates with one tap (contributed by Yazutsu, PR #125).
While in a chat or channel, the top bar shows the active keyboard language next to the clock (only when more than one layout is enabled), so you can see what double-tap-space switched to.
# --- Fixed ---
The keyboard-backlight "Off" preset now actually switches the backlight off and stays off, instead of leaving the previous mode running (contributed by Yazutsu, PR #88).
Chat history saves to the SD card are more robust: files are written atomically and a fallback save path covers the background worker not being armed yet, so a badly timed reboot can no longer truncate your message history (contributed by scratchdiver, PR #135).
T-Deck: if the touch controller glitches into a stuck "phantom finger" state (everything looked frozen, touch dead, trackball clicks eaten, and the screen never timed out), the driver now detects it and recovers within seconds. Previously only a full power cycle would clear it.
T-Deck: touch initialisation at boot now retries for a few seconds instead of giving up after a single attempt. A slow-to-wake touch controller could previously cost you touch AND keyboard for the whole session.
+17
View File
@@ -0,0 +1,17 @@
# beta_38 (TEST) - Full-history chat scrolling, message delete, two new boards, storage integrity fixes. On top of beta_37.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- New ---
Chat scrolling is fully virtualized (contributed by scratchdiver, PR #130): a chat now scrolls through its ENTIRE stored history, up to 5000 messages on SD-card devices, smoothly in both bubble and compact mode, instead of only the most recent rows. Several LVGL crash fixes came along with it.
Long-press any message for a new Delete option that removes just that message. The message menu now lays its buttons out two per row so it always fits on one screen.
The chat and channel settings sheet gains "Delete history": wipes the whole conversation after a confirmation, while the channel membership or contact stays.
The in-chat jump buttons are now slim arrows tucked against the right edge of the screen: fully visible while you scroll, fading to half opacity a second after you stop.
ThinkNode M9 hardware support is complete (contributed by ded, PR #138): GPS, microSD, buzzer, backlight control, lock screen, long-press context menus, and a d-pad-driven setup wizard. His fixes for focus scrolling in nested pages and the lock-screen paint order benefit every keyboard-nav board.
New board in the source tree: RAK WisMesh Tap V2 (contributed by Ethac.chen, PR #136), with a 30+ fps display driver. Prebuilt images follow once the web-flasher integration lands.
Tanmatsu: the app now builds size-optimized, roughly 300 KB smaller, leaving real headroom under the launcher's app-size limit for future features.
# --- Fixed ---
Messages no longer vanish when you reboot the device from the phone app (reported by Leon P). The history writer batches saves up to 30 seconds apart, and the app's reboot command skipped the final save that every on-device reboot path does - so the newest messages, read or unread, were lost. The reboot command now saves first.
T-Deck: turning on "Store data on SD" now migrates your existing profile, settings and identity to the card FIRST. Upgrading to beta_36/37 with the toggle already on could adopt an empty card and boot as a brand-new node while your real profile sat orphaned on internal flash.
T-Deck: for anyone already hit by that: Set -> General -> "Copy internal data to SD" restores the profile from internal flash onto the card (confirmation, then automatic reboot). Until you run it, toggling "Store data on SD" OFF and rebooting also brings the old profile back.
Tanmatsu: profile name, settings and the node identity now persist reliably across reboots. They lived on the internal flash partition whose metadata layer is unreliable on this board; they now live on the SD card with everything else, and existing data is rescued automatically on first boot.
+11
View File
@@ -0,0 +1,11 @@
# beta_39 (TEST) - Translations, deletes that stick and are fast, emoji fixes. On top of beta_38.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- New ---
Big translation pass: hundreds of previously-English strings across menus, settings, dialogs and the setup flow are now translated into all 11 non-English languages (Dutch, German, French, Spanish, Italian, Russian, Ukrainian, Bulgarian, Serbian, Greek, Brazilian Portuguese). Menu rows that carry an icon now translate too, instead of silently falling back to English. Switch language under Settings.
ThinkNode M9 and RAK WisMesh Tap V2 can now be installed straight from the web flasher.
# --- Fixed ---
Deleting a message, or clearing a whole chat, now sticks: it survives a reboot or a hardware reset instead of coming back (reported by Leon P and MrWeavis). The delete used to sit in a save that was batched up to 30 seconds later, so a reset in that window undid it.
Deleting is also much faster now. The chat history is written compactly - only the messages you actually have, instead of the full 5000-slot ring every time - so a delete, a history wipe, or a reboot no longer pauses the screen while it saves. Normal message saving is lighter too.
Emoji picker: the food and object emoji that showed as empty boxes (umbrella, coffee, pizza, cake, rocket, camera, phone, laptop, calendar, antenna, e-mail, house, car, watch) now render properly - they were listed but never baked into the font.
+14
View File
@@ -0,0 +1,14 @@
Replay a message's route on the map — it animates hop by hop, showing each repeater's name and ID on screen, with a button to replay it
"Repeats heard" and the route trace now name the repeaters, and long names are no longer cut off
Message Info popup scrolls, lists every hop, and colours each repeater to match its chat bubble
New mesh signal-strength bars in the top bar, plus a live signal readout in the home graph (tap for details)
Home top bar shows your profile name (scrolls if long); status icons reordered to battery, signal, Wi-Fi, Bluetooth, time
Charging shows just a lightning bolt; the home "unread" line is now tappable and opens Chats
Control center: long-press a tile to jump to its settings page; the slider and tiles follow your theme colour
Theme colour no longer reverts to the default after a reboot
Touch settings now live in files (SD/SPIFFS) instead of NVS — avoids the shared-storage "full" issue
First-boot setup is Wi-Fi only; turn Bluetooth on afterwards from the control center
Map prefetches a zoomed-out view (province to country to borders) for faster panning
Fixed several reboots: map zoom-out, a full tile cache, and flash-heavy moments
Smoother scrolling across the UI; the boot splash now shows MESHCOMOD
Power menu adds a Download-mode button to reflash over USB without holding BOOT
+32
View File
@@ -0,0 +1,32 @@
# beta_40 (STABLE) - the first stable since beta_35. Rolls up everything from beta_36 through beta_40: reliable SD storage, message delete + full-history scrolling, a big translation pass, two new boards, and a pile of storage/perf/stability fixes.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- New ---
Big translation pass: hundreds of previously-English strings across menus, settings, dialogs and the setup flow are now translated into all 11 non-English languages (Dutch, German, French, Spanish, Italian, Russian, Ukrainian, Bulgarian, Serbian, Greek, Brazilian Portuguese). Menu rows that carry an icon translate too. Switch language under Settings.
Chat scrolling is fully virtualized (contributed by scratchdiver, PR #130): a chat now scrolls through its ENTIRE stored history, up to 5000 messages on SD-card devices, smoothly in both bubble and compact mode, instead of only the most recent rows.
Long-press any message for a Delete option that removes just that message; the message menu lays its buttons two per row so it fits on one screen.
The chat and channel settings sheet gains "Delete history": wipes the whole conversation after a confirmation, while the channel membership or contact stays.
The in-chat jump buttons are slim arrows tucked against the right edge: fully visible while you scroll, fading to half opacity a second after you stop.
ThinkNode M9 hardware support is complete (contributed by ded, PR #138): GPS, microSD, buzzer, backlight control, lock screen, long-press context menus, and a d-pad-driven setup wizard. It can be installed straight from the web flasher.
New board: RAK WisMesh Tap V2 (contributed by Ethac.chen, PR #136), with a 30+ fps display driver, also installable from the web flasher.
The Wi-Fi network screens (join a network, hidden network, saved-network details) are full-screen pages now instead of small popups, matching the other settings pages and scrolling properly with the keyboard open.
The lock screen shows an envelope with the number of unread messages under the clock, so you can check for news without unlocking (idea from issue #93).
Passwords with special characters can be entered everywhere: long-press any settings field, including the Wi-Fi password, for a Cut / Copy / Paste / All / Sym menu. Sym opens a picker with the characters the on-screen keyboard lacks (issue #122). Double-tap selects a word.
For Launcher installs: a "Save update bin to SD" button on the About page downloads the latest firmware of your selected channel into a BINS folder on the SD card, ready to flash from the Launcher. The test-builds toggle is visible on Launcher installs too.
New region preset "Australia (Mid)": 915.075 MHz, 125 kHz, SF9, CR5 (requested in issue #121).
Your GPS position is now the first row in the quick-reply picker, so you can send your current coordinates with one tap (contributed by Yazutsu, PR #125).
Tanmatsu: hold ALT and press a letter to open the accent picker; keep ALT held and press again to cycle, release to insert (requested by jochentj, issue #129). Double-tap the space bar to switch between your enabled keyboard languages, same as the T-Deck.
While in a chat, the top bar shows the active keyboard language next to the clock when more than one layout is enabled.
# --- Fixed ---
The "Store data on SD" toggle is now actually honored after a reboot (fix contributed by scratchdiver, PR #123). On Launcher installs it silently reverted to internal storage every boot, which also kept the slow internal-flash writes. Turning it off works reliably now too.
Turning on "Store data on SD" migrates your existing profile, settings and identity to the card FIRST, so upgrading no longer risks adopting an empty card and booting as a brand-new node. If you were already hit by that, Settings -> General -> "Copy internal data to SD" restores the profile from internal flash.
Card-less devices (Heltec V4, and any board without an SD card) no longer freeze for several seconds at a time. A busy mesh re-writes the whole contacts file to internal flash on every advert refresh, and that flash garbage-collection stalled the whole device; contact saves are now coalesced so only real changes (a new or removed node) are written promptly.
Deleting a message, or clearing a whole chat, now sticks: it survives a reboot or a hardware reset instead of coming back (reported by Leon P and MrWeavis). Deleting is also much faster - chat history is written compactly, only the messages you actually have, so a delete, a history wipe, or a reboot no longer pauses the screen while it saves.
Messages no longer vanish when you reboot the device from the phone app (reported by Leon P): the app's reboot command now saves chat history first, like every on-device reboot path already did.
Chat history saves to the SD card are more robust: files are written atomically with a fallback save path, so a badly timed reboot can no longer truncate your message history (contributed by scratchdiver, PR #135).
Longer translated labels (French, German, etc.) that used to clip or wrap now shrink to fit: the message action menu, confirmation dialogs, and the app-drawer tiles all keep their text on one line. The signal card's "tap for details" hint no longer overlaps in other languages.
Emoji picker: the food and object emoji that showed as empty boxes (umbrella, coffee, pizza, cake, rocket, camera, phone, laptop, calendar, antenna, e-mail, house, car, watch) now render properly.
Tanmatsu: profile name, settings and the node identity persist reliably across reboots (moved off the unreliable internal-flash partition onto the SD card, with existing data rescued on first boot). Accented letters render at full size at the Large and Huge UI scales. The About page shows the correct firmware version now.
T-Deck: if the touch controller glitches into a stuck "phantom finger" state (touch dead, trackball clicks eaten, screen never timing out), the driver detects it and recovers within seconds instead of needing a full power cycle. Touch initialisation at boot also retries for a few seconds instead of giving up after one attempt.
The keyboard-backlight "Off" preset now actually switches the backlight off and stays off (contributed by Yazutsu, PR #88).
+10
View File
@@ -0,0 +1,10 @@
# beta_41 (TEST) - experimental web control: view and drive the wadamesh UI from a phone or desktop browser over Wi-Fi. Two new apps, VNC and REMOTE.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- New ---
New VNC app (in the app drawer, between Advertise and Signal): it mirrors the live device screen into a web browser over Wi-Fi and lets you drive it. Taps and typing in the browser control the device, and anything that changes on the device shows up in the browser. Turn it on, then open http://<device-ip>:8765/ from any browser on the same network. The app page itself is scrollable.
New REMOTE app: a headless mode that renders the whole UI at a larger, browser-friendly resolution and serves it over Wi-Fi, while the device's own screen drops to a placeholder showing the address to open. Meant for driving a device as a web client, and as groundwork for screenless hardware. Turning it on or off reboots the device. On the web page a Rotate button flips between landscape (desktop) and portrait (phone); on the device, double-tap the space bar to leave remote mode.
Both features are served by the device itself over plain Wi-Fi - no phone app, no account, no cloud, just a browser on the same network.
# --- Note ---
This is an experimental first cut, and an honest heads-up from Kaj: for me the remote UI still feels too slow, and I will most likely move toward a dedicated, lighter remote client instead of streaming the entire live UI. It is in this test build mainly because community feedback on it would be genuinely useful - if you try it, let us know how it runs for you and what you would actually want from a web or remote client.
+9
View File
@@ -0,0 +1,9 @@
# beta_42 (TEST) - fixes for the beta_41 REMOTE mode: it works on the Heltec V4 now, and keyboard-less boards can leave it.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- Fixed ---
REMOTE mode now works on the Heltec V4 TFT (and other 2 MB-PSRAM boards) instead of showing a black screen in the browser. Its off-screen renderer needed more free memory than those boards had, so nothing was streamed; the buffers are sized to fit now, and the render is a bit smaller and faster as a result. VNC was never affected and already worked.
Keyboard-less touch boards (Heltec V4, RAK WisMesh Tap) can leave REMOTE mode again: touch and hold the device screen for about 3 seconds. Those boards have no SPACE key, which used to be the only way out on the device.
# --- New ---
The web page has an "Exit remote" button (with a confirmation) that leaves REMOTE mode from any browser, on any board.
+41
View File
@@ -0,0 +1,41 @@
# beta_5 — what's new (one user-facing note per non-blank, non-# line)
# --- Contacts ---
Contacts now show a person icon for people and an antenna for repeaters, instead of a generic envelope and lightning bolt
Block or unblock anyone straight from their popup — blocked contacts get a red person icon and their messages are hidden
The contacts list is a proper table now: dedicated "last heard" and distance columns, a Sort & Filter button, and multi-select delete with an are-you-sure prompt and a progress bar (favourites are protected — unfavourite them first)
Time and distance are smaller and tucked to the right so names get more room; a name too long for two lines scrolls instead of spilling onto a third
Un-favouriting a contact removes its star right away
# --- Discovered nodes ---
The Discovered list now survives reboots (it used to empty every boot) and holds up to 48 nodes
New Discovered settings (cogwheel): auto-delete the oldest node when the list is full, and auto-delete anything heard via more than a set number of hops — keeps the list fresh and nearby
The Discovered count badge shows "48!" when the list is full
Adding a node from Discovered now appears in your Contacts immediately
A purge-all button sits right next to Close in the Discovered list
# --- Region / radio ---
Setting the region / default scope from the official MeshCore app now works — it used to time out ("no_event_received"); public-channel floods are now region-tagged (issue #31)
The US/Canada radio preset is corrected to the proper 910.525 MHz, 62.5 kHz, SF7 (was 915 MHz / 250 kHz / SF11)
T-Deck transmit power is restored to the full +22 dBm — the SX1262 antenna switch was mis-driven, leaking most of the power (~4 mW) (issue #6)
Auto-add settings moved to their own dedicated page (out of Radio), and are reachable from the Contacts menu too
# --- Chats & sound ---
Each chat and channel row now shows the time of its most recent message on the right
Mute messages and/or @-mentions per channel, from that channel's settings
The @-mentions screen has taller rows (fits two lines) and tapping a mention jumps to that exact message
Volume is now plus/minus buttons instead of a hard-to-drag slider; a master Sound switch silences everything, with independent Messages and @-mention switches
@-mentions can play their own distinct chime, with separate on/off and volume
# --- Map ---
The map remembers your zoom and pan when you leave and come back, instead of snapping back to your location every time
Tapping a contact on the map opens a compact popup (no Block button there, to keep it small)
# --- Keyboard & input ---
T-Deck keyboard: backspace deletes at the cursor instead of always chopping the last character, and editing respects where you place the caret
Quick replies page now scrolls so you can reach and edit every slot
# --- UI polish ---
Refined bottom tab bar — flat icons that take your theme colour with a soft glow under the active tab — plus an unread-count badge on the Chats icon
App drawer: colourful app icons, the command tile relabelled "Cmdr", the Apps button in a contrasting accent, and three-per-row on the Heltec V4
Full-screen settings pages with a clear X to close, a subtle new-message chip, and a full-page Signal view with a live readout and manual refresh
+34
View File
@@ -0,0 +1,34 @@
# beta_6 — what's new (one user-facing note per non-blank, non-# line)
# --- New: RF Monitor app ---
New Monitor app on the app drawer: a live RF sniffer for your node — see exactly what it is pulling out of the air in real time
A "Recently heard" feed lists recent received frames by type (text, advert, ack, route-trace…) with RSSI, SNR and hop count, colour-coded by type
A link-margin readout grades how much headroom your last packet had (green / amber / red), alongside a live packet rate and a noise-floor + peak-hold RSSI scope with an on-screen dBm scale
The Monitor lays out cleanly in both portrait and landscape, so it is readable on the Heltec V4 and the T-Deck
# --- Chat & contacts ---
Direct-message chats now show a person icon and channels a group icon, instead of the old loop / envelope
You can block a non-contact sender by name — handy for silencing an automated room or repeater bot you could not block before
Opening a chat or channel no longer jumps you to the newest message; it keeps your scroll position
The contacts Sort & Filter popup now closes after you pick a sort option
The chat/channel long-press popup is fixed: the close X no longer overlaps the buttons while you scroll it
# --- Time & timezone ---
New time-zone picker with named zones (no longer CET-only) — set yours in Settings → System
Fixed the clock getting stuck at 1902 and channels showing 1969–1970 dates: GPS no longer clobbers the clock with an invalid date, and the time now survives a reboot
# --- Launcher & Wi-Fi ---
Map tiles now download when running under Launcher (the tile cache was wrongly being reported as full)
GPS now acquires a fix when running under Launcher
The setup-wizard Wi-Fi scan no longer comes up empty ("no networks found") — Wi-Fi modem sleep is deferred until after you are connected
# --- Bluetooth ---
Bluetooth now advertises under your node / profile name instead of "NimBLE"
The Bluetooth PIN no longer resets on its own — it is saved and reused across reboots
# --- Power & performance ---
Turning the screen off now downclocks the CPU and enables Wi-Fi modem sleep to save power
On the map, your position dot is drawn before the tiles so it no longer lags behind when you pan
# --- Community ---
Merged map-server and SD-tile rendering fixes contributed by Vybo (PR #11)
+48
View File
@@ -0,0 +1,48 @@
# beta_7 — what's new (one user-facing note per non-blank, non-# line)
# --- Community-built ---
Most of beta_7 was contributed by the community — Dan Vybiral (Vybo) and Ruben Laban. Thank you both!
# --- Chat reliability ---
Fixed garbled and duplicated messages — chat text is no longer corrupted in transit, so messages arrive intact and duplicates are filtered out correctly (thanks to marcelverdult for the report, issue #13)
Unread badges are accurate again: the phantom and stuck unread counts on chats and channels are fixed (Ruben Laban, PR #15)
More of your chat history is kept and reloaded across reboots, and it is saved more reliably than before (Ruben Laban, PR #15)
# --- New: Snake game ---
A Snake game is now in the app drawer — steer by swiping the screen or with the T-Deck trackball, with pause/resume and restart after a game over (Vybo, PR #12)
While you play, touch stays inside the game and no longer leaks through to the screen behind it
# --- Battery & telemetry ---
Tap the battery in the status bar for a 24-hour history chart with an estimated time-left readout; battery level is logged every 5 minutes (Vybo, PR #12)
New per-node telemetry window: poll a node's battery, temperature and humidity, watch them on a live chart, and keep a per-node log (Vybo, PR #12)
# --- Map ---
New map night mode — the status bar and tab bar invert to match the dark map instead of glaring (Vybo, PR #12)
A live zoom-level readout sits above the zoom slider, and the zoom button now shows a proper magnifier icon (Vybo, PR #12)
Contacts have a new "Show on map" action that centres the map on that node (Vybo, PR #12)
# --- Status bar & tools ---
A microSD activity LED lights whenever the card is accessed, including file-manager reads and copies (Vybo, PR #12)
An activity spinner shows while a telemetry, admin, ping, trace, range or sightline request is in flight, kept clear of the clock (Vybo, PR #12)
New option to hide the device name and move the clock to the left (Vybo, PR #12)
Press and hold the status bar to save a full-screen screenshot to the SD card (Vybo, PR #12)
# --- Files & images ---
The file browser can preview PNG and BMP images now, not just JPEG, and no longer shows a stale image when you open several in a row (Vybo, PR #12)
Lock-screen wallpaper: open Files, view any JPEG on your SD card, and tap "Set as wallpaper" — no more slow scan or "no image found"
# --- App drawer ---
The app drawer fits all the apps again — the bottom row's labels are no longer cut off
A scrollbar clearly shows when there are more apps than fit, so it is obvious the page scrolls
# --- Bluetooth ---
You can set your own Bluetooth pairing code in Settings now, instead of being stuck with the default
# --- Storage & stability ---
Fixed map tiles failing to cache (with a false "storage full"), and black or missing tiles now re-download instead of staying blank
Replying to a message with @ no longer overwrites text you were already typing
Fixed an out-of-memory crash and made notification audio more reliable
# --- Community ---
Huge thanks to Dan Vybiral (Vybo) — most of beta_7 is his work: the Snake game, battery history, per-node telemetry, map night mode, PNG/BMP image viewer and the whole status-bar toolkit (PR #12)
Thanks to Ruben Laban for the accurate unread-badge and chat-history-persistence fixes (PR #15)
+16
View File
@@ -0,0 +1,16 @@
# beta_8 — what's new (one user-facing note per non-blank, non-# line)
# --- Chat ---
Quick replies now insert at the cursor instead of wiping whatever you'd already typed — tap one mid-message and it's added in place
Long-press any message for a new one-tap "Ack": it drops a ready-to-send reply into the composer with that message's signal strength (SNR/RSSI) and the route it took, so you can instantly confirm someone's test came through — on a channel it @mentions the sender
Fixed the message menu (Ack / Copy / Info / Mention / Block) running off the bottom of the screen — it now fits, and scrolls if it ever needs to
# --- Map ---
The zoom button now shows a clear "+/-" icon
Each on-map readout can be shown or hidden independently in Map settings — the coordinates line, the tile x/y/z line, and the contact markers
Map settings are reorganised: the microSD-tiles switch sits at the top under its own heading, separated from the new show/hide toggles
New auto-refresh button: turn it on and the map re-centres on you automatically whenever your position changes, with an on-screen "Auto-refresh on/off" confirmation
Fixed the close (X) in Map settings overlapping the switches when the panel was scrolled
# --- Performance ---
Freed about 19 KB of internal memory, leaving more headroom for Wi-Fi and trimming the idle-memory growth seen in recent betas
+26
View File
@@ -0,0 +1,26 @@
# beta_9 — what's new (one user-facing note per non-blank, non-# line)
# --- Stability ---
Wi-Fi now reconnects on its own after the link drops — no more rebooting just to get back online
Fixed a bug where a corrupted chat-history file could make the device reboot on every received message (and hide your messages) — the bad file is now detected and reset automatically so it recovers on its own
Hardened the boot path so a device with quirky memory degrades gracefully instead of getting stuck on the boot logo
# --- Crash reports ---
After a crash, Settings -> About now shows an "Export crash report" button that saves the crash dump to the microSD card, so you can send it to the devs and get the bug fixed
# --- Map ---
The reload button for corrupt tiles now actually clears the cached tiles — including when you're offline — and missing/blank "twilight zone" tiles re-download on their own
Fixed tiles not re-fetching at zoom levels 13 to 15
# --- Contacts ---
Favourited contacts now always appear in the contact list, even with hundreds of similarly-named contacts (they used to stay hidden until you searched for them)
# --- Chat ---
Room-server messages now show the time each message was actually sent, instead of when it arrived
Room-server messages now show who sent them
Sharing your contact QR code now produces a valid code that the MeshCore phone app can scan and import
Unread counts now show "99+" instead of an alarming raw number on busy channels
# --- App drawer ---
New icon-size setting: tap the cog at the top-right of the app drawer to switch between Compact and Large — bigger icons and labels, easier to read
Smoother scrolling through the app list (and the other scrollable lists)
+204
View File
@@ -0,0 +1,204 @@
#!/usr/bin/env python3
# Bake a few EXTRA Noto colour emoji into src/ui-touch/emoji_data.{c,h} WITHOUT
# regenerating (and re-downloading) the existing 252 — those stay byte-identical,
# so there's no Noto-`main` art drift. Splices new glyph arrays + sorted lookup
# entries into the file in place. Re-run is idempotent (new cps replace, existing
# untouched). PX must match the existing bake (16) and the imgfont in UITask.
#
# python3 scripts/build/add-emoji.py
#
# Source art: googlefonts/noto-emoji png/128 (OFL). RGB565+alpha, swap=0.
import os, re, sys, urllib.request
from PIL import Image
PX = 16
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
OUTDIR = os.path.join(ROOT, "src", "ui-touch")
OUTC = os.path.join(OUTDIR, "emoji_data.c")
OUTH = os.path.join(OUTDIR, "emoji_data.h")
CACHE = os.path.join(ROOT, "data", "emoji-cache")
BASE = "https://raw.githubusercontent.com/googlefonts/noto-emoji/main/png/128/emoji_u{}.png"
os.makedirs(CACHE, exist_ok=True)
# Single-codepoint emoji: (codepoint, noto-filename-stem)
SINGLE = [
(0x26BD, "26bd"), # soccer ball âš½
(0x1F3C8, "1f3c8"), # american football ðŸ
(0x1F5FF, "1f5ff"), # moai 🗿
# objects/food listed in the picker (k_emoji_items) but never baked -> they
# rendered as the notdef box on-device (the beta_38 "emoji tofu"). Bake them.
(0x2614, "2614"), # umbrella with rain
(0x2615, "2615"), # hot beverage
(0x1F355, "1f355"), # pizza
(0x1F382, "1f382"), # birthday cake
(0x1F680, "1f680"), # rocket
(0x1F4F7, "1f4f7"), # camera
(0x1F4F1, "1f4f1"), # mobile phone
(0x1F4BB, "1f4bb"), # laptop
(0x1F4C5, "1f4c5"), # calendar
(0x1F4E1, "1f4e1"), # satellite antenna
(0x1F4E7, "1f4e7"), # e-mail
(0x1F3E0, "1f3e0"), # house
(0x1F697, "1f697"), # car
(0x231A, "231a"), # watch (sat between car + bulb in the picker, still tofu)
]
# ZWJ sequences baked as ONE combined image, keyed on the lead codepoint, with the
# trailing visible symbol(s) mapped to the zero-width glyph so the whole UTF-8
# sequence renders as the single combined emoji. The bare lead cp (a plain waving
# white flag U+1F3F3) is not offered anywhere in the UI, so reusing it as the key
# is safe. (noto-emoji drops the FE0F variation selectors from png filenames.)
SEQ = [
# (key_cp, noto-stem, [extra cps -> zero])
(0x1F3F3, "1f3f3_200d_26a7", [0x26A7]), # transgender flag ðŸ³ï¸â€âš§ï¸
]
# Country/region flags: a regional-indicator PAIR (e.g. LS = U+1F1F1 U+1F1F8) baked
# as ONE image keyed on the LEAD indicator, the trailing indicator mapped to the
# zero-width glyph (same trick as SEQ). Noto's png/128 ships no flags, so pull the
# flat rectangle from the same Noto project's third_party/region-flags. No bare
# regional indicator is ever offered in the UI, so reusing the lead cp as the key
# is safe — and the picker currently has no other flag using these indicators.
FLAGS = [
# (lead_cp, trail_cp, ISO-3166-alpha-2)
(0x1F1F1, 0x1F1F8, "LS"), # Lesotho
]
def fetch(stem):
fn = os.path.join(CACHE, "u_{}.png".format(stem))
if os.path.exists(fn) and os.path.getsize(fn) > 0:
return fn
for url in (BASE.format(stem), BASE.format(stem + "_fe0f")):
try:
req = urllib.request.Request(url, headers={"User-Agent": "emoji-gen"})
data = urllib.request.urlopen(req, timeout=20).read()
with open(fn, "wb") as f:
f.write(data)
return fn
except Exception:
continue
return None
def fetch_flag(iso):
fn = os.path.join(CACHE, "flag_{}.png".format(iso))
if os.path.exists(fn) and os.path.getsize(fn) > 0:
return fn
url = ("https://raw.githubusercontent.com/googlefonts/noto-emoji/main"
"/third_party/region-flags/png/{}.png".format(iso))
try:
req = urllib.request.Request(url, headers={"User-Agent": "emoji-gen"})
data = urllib.request.urlopen(req, timeout=20).read()
with open(fn, "wb") as f:
f.write(data)
return fn
except Exception:
return None
def to_rgb565a8(img):
# IDENTICAL to gen-emoji-font.py: autocrop transparent margin, bottom-align to
# the text baseline, emit [lo, hi, alpha] per pixel (LV_COLOR_16_SWAP=0).
PAD_BOTTOM = 0
img = img.convert("RGBA")
bbox = img.split()[3].getbbox()
if bbox:
img = img.crop(bbox)
w, h = img.size
avail_h = PX - PAD_BOTTOM
scale = min(PX / w, avail_h / h)
nw, nh = max(1, round(w * scale)), max(1, round(h * scale))
content = img.resize((nw, nh), Image.LANCZOS)
canvas = Image.new("RGBA", (PX, PX), (0, 0, 0, 0))
canvas.paste(content, ((PX - nw) // 2, PX - PAD_BOTTOM - nh))
out = bytearray()
for (r, g, b, a) in canvas.getdata():
v = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
out += bytes((v & 0xFF, (v >> 8) & 0xFF, a))
return out
# ---- bake the new glyphs ----
new_defs = [] # (cp, ename, bytes)
new_entries = [] # (cp, ref)
for cp, stem in SINGLE:
fn = fetch(stem)
if not fn:
sys.exit("FAILED to fetch U+{:X} ({})".format(cp, stem))
new_defs.append((cp, "e_{:x}".format(cp), to_rgb565a8(Image.open(fn))))
new_entries.append((cp, "d_{:x}".format(cp)))
for key_cp, stem, zeros in SEQ:
fn = fetch(stem)
if not fn:
sys.exit("FAILED to fetch combined {} for U+{:X}".format(stem, key_cp))
new_defs.append((key_cp, "e_{:x}".format(key_cp), to_rgb565a8(Image.open(fn))))
new_entries.append((key_cp, "d_{:x}".format(key_cp)))
for z in zeros:
new_entries.append((z, "d_zero"))
for lead_cp, trail_cp, iso in FLAGS:
fn = fetch_flag(iso)
if not fn:
sys.exit("FAILED to fetch flag {} for U+{:X}".format(iso, lead_cp))
new_defs.append((lead_cp, "e_{:x}".format(lead_cp), to_rgb565a8(Image.open(fn))))
new_entries.append((lead_cp, "d_{:x}".format(lead_cp)))
new_entries.append((trail_cp, "d_zero"))
# ---- splice into emoji_data.c ----
src = open(OUTC).read()
TYPE_MARK = "\ntypedef struct { uint32_t cp;"
FUNC_MARK = "const lv_img_dsc_t* emojiGlyphLookup(uint32_t cp) {"
head = src[:src.index(TYPE_MARK)] # header + includes + all glyph defs
func = src[src.index(FUNC_MARK):] # the binary-search getter (verbatim)
# Idempotency: on a RE-RUN, `head` still holds the e_/d_ glyph defs spliced in by a
# PRIOR run (they live before the typedef), so blindly re-appending new_defs would
# redefine them ("redefinition of e_26bd"). Strip any def for a cp we're about to
# re-bake; the lookup table dedups separately (bycp), so this only touches defs.
for _cp, _ename, _data in new_defs:
_hx = "{:x}".format(_cp)
head = re.sub(r"static const uint8_t e_" + _hx + r"\[\] = [^;]*;\n", "", head)
head = re.sub(r"static const lv_img_dsc_t d_" + _hx + r" = [^;]*;\n", "", head)
# Existing lookup entries (includes the FE0F/FE0E/200D -> d_zero rows).
existing = [(int(cp, 16), ref) for cp, ref in
re.findall(r"\{\s*(0x[0-9A-Fa-f]+)u,\s*&(\w+)\s*\}", src)]
bycp = {cp: ref for cp, ref in existing}
for cp, ref in new_entries: # new cps win; never demote a real glyph to zero
if not (ref == "d_zero" and bycp.get(cp, "d_zero") != "d_zero"):
bycp[cp] = ref
entries = sorted(bycp.items())
count = sum(1 for _, ref in entries if ref != "d_zero")
# New glyph array + dsc definitions (appended after the existing ones).
defs = []
for cp, ename, data in new_defs:
defs.append("static const uint8_t {}[] = {{{}}};".format(ename, ",".join(str(b) for b in data)))
defs.append("static const lv_img_dsc_t d_{:x} = {{ {{ LV_IMG_CF_TRUE_COLOR_ALPHA, 0, 0, {}, {} }}, sizeof({}), {} }};"
.format(cp, PX, PX, ename, ename))
out = []
out.append(head.rstrip("\n"))
out.append("")
out.append("\n".join(defs))
out.append("")
out.append("typedef struct { uint32_t cp; const lv_img_dsc_t* dsc; } EmojiGlyph;")
out.append("static const EmojiGlyph kGlyphs[] = {")
for cp, ref in entries:
out.append(" {{ 0x{:X}u, &{} }},".format(cp, ref))
out.append("};")
out.append("const uint16_t kEmojiGlyphCount = {};".format(count))
out.append("")
out.append(func.rstrip("\n"))
out.append("")
open(OUTC, "w").write("\n".join(out))
# ---- header comment count ----
h = open(OUTH).read()
h = re.sub(r"// \d+ Noto colour emoji", "// {} Noto colour emoji".format(count), h, count=1)
open(OUTH, "w").write(h)
print("added {} glyphs -> {} total ({} lookup rows incl zero-width)".format(
len(new_defs), count, len(entries)))
+26 -10
View File
@@ -1,19 +1,27 @@
#!/usr/bin/env python3
# Generate the web-flasher metadata that must roll with every release:
# - version.json : {tag, notes[]} — the site shows this ("what's new")
# - version.json : {tag, channel, notes[]} — the site shows this ("what's new")
# - manifest-tdeck.json : esp-web-tools manifest (its "version" drives the
# - manifest-heltec-v4-tft.json install dialog, so it must equal the tag)
#
# These live next to the rolling bins at firmware.wadamesh.com/latest/, so the
# flasher always reflects the current release without a site redeploy.
# These live next to the rolling bins in a per-channel feed dir on
# firmware.wadamesh.com (latest/ for stable, latest-beta/ for the test channel),
# so the flasher always reflects the current release without a site redeploy. The
# manifests themselves point at the IMMUTABLE per-tag bins (releases/TOUCH for
# stable, releases/BETA for beta) to dodge the latest/*.bin 4h edge cache.
#
# Usage: gen-flasher-meta.py <tag> <outdir> [notes_file]
# notes_file: one note per non-empty line (plain text); optional.
# Usage: gen-flasher-meta.py <tag> <outdir> [notes_file] [channel]
# notes_file: one note per non-empty line (plain text); optional ("" to skip).
# channel: "stable" (default) or "beta" — picks the immutable archive dir
# (releases/TOUCH vs releases/BETA) the manifests point at, and is
# recorded in version.json so the site/flasher can label the channel.
import sys, json, os
tag = sys.argv[1]
outdir = sys.argv[2]
notes_file = sys.argv[3] if len(sys.argv) > 3 else None
notes_file = sys.argv[3] if len(sys.argv) > 3 and sys.argv[3] else None
channel = sys.argv[4] if len(sys.argv) > 4 else "stable"
archive = "BETA" if channel == "beta" else "TOUCH"
notes = []
if notes_file and os.path.exists(notes_file):
@@ -22,23 +30,31 @@ if notes_file and os.path.exists(notes_file):
os.makedirs(outdir, exist_ok=True)
with open(os.path.join(outdir, "version.json"), "w") as f:
json.dump({"tag": tag, "notes": notes}, f, indent=2)
json.dump({"tag": tag, "channel": channel, "notes": notes}, f, indent=2)
BOARDS = {
"manifest-tdeck.json": ("wadamesh — LilyGo T-Deck", "wadamesh-tdeck-merged.bin"),
"manifest-heltec-v4-tft.json": ("wadamesh — Heltec V4 TFT", "wadamesh-heltec-v4-tft-merged.bin"),
"manifest-thinknode-m9.json": ("wadamesh — ThinkNode M9", "wadamesh-thinknode-m9-merged.bin"),
"manifest-rak-tap-v2.json": ("wadamesh — RAK WisMesh Tap V2", "wadamesh-rak-tap-v2-merged.bin"),
}
for fn, (name, binf) in BOARDS.items():
manifest = {
"name": name,
"name": name + ("" if channel == "stable" else " (beta)"),
"version": tag,
"new_install_prompt_erase": True,
"builds": [{
"chipFamily": "ESP32-S3",
"parts": [{"path": f"https://firmware.wadamesh.com/latest/{binf}", "offset": 0}],
# Point at the IMMUTABLE per-tag bin, NOT the rolling feed bin. The feed
# *.bin URLs are stable filenames cached 4h and overwritten in place, so
# for up to 4h after a release the flasher could hand out the PREVIOUS
# build's bytes while version.json already advertised the new tag. The
# per-tag path is never overwritten, so its cache is always correct. The
# manifest itself is max-age=300, so the new path propagates in <=5min.
"parts": [{"path": f"https://firmware.wadamesh.com/releases/{archive}/{tag}/{binf}", "offset": 0}],
}],
}
with open(os.path.join(outdir, fn), "w") as f:
json.dump(manifest, f, indent=2)
print(f"wrote version.json + 2 manifests for {tag} -> {outdir} (notes: {len(notes)})")
print(f"wrote version.json + {len(BOARDS)} manifests for {tag} ({channel}) -> {outdir} (notes: {len(notes)})")
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
# Generate src/ui-touch/qr_icon.h: a small, recolour-able "QR code" glyph baked to
# an LVGL TRUE_COLOR_ALPHA lv_img_dsc (white opaque modules, transparent ground —
# tinted to the button text colour at runtime via img_recolor). There is no QR
# glyph in the Montserrat/symbol font, so the Chats-header Share button draws this
# instead of the old stand-in LV_SYMBOL_IMAGE ("picture") icon.
#
# python3 scripts/build/gen-qr-icon.py
#
# Same pixel encoding as the emoji bake: [lo, hi, alpha], LV_COLOR_16_SWAP=0.
import os
SCALE = 2 # px per module -> 9 modules * 2 = 18x18 px
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
OUT = os.path.join(ROOT, "src", "ui-touch", "qr_icon.h")
# 9x9 module grid. Three finder patterns (ring: 111/101/111) at TL/TR/BL corners,
# scattered data modules + a small alignment block bottom-right so it reads as a QR.
P = [
[1,1,1,0,1,0,1,1,1],
[1,0,1,0,0,0,1,0,1],
[1,1,1,0,1,0,1,1,1],
[0,0,0,0,0,0,0,0,0],
[1,0,1,0,1,1,0,1,0],
[0,0,0,0,1,1,0,0,1],
[1,1,1,0,0,0,1,0,1],
[1,0,1,0,1,0,0,1,0],
[1,1,1,0,0,1,1,0,1],
]
MODS = len(P)
W = H = MODS * SCALE
data = bytearray()
for y in range(H):
for x in range(W):
on = P[y // SCALE][x // SCALE]
if on:
data += bytes((0xFF, 0xFF, 0xFF)) # white RGB565 (lo,hi) + alpha 255
else:
data += bytes((0x00, 0x00, 0x00)) # transparent
with open(OUT, "w") as f:
f.write("// AUTO-GENERATED by scripts/build/gen-qr-icon.py — do not edit.\n")
f.write("// %dx%d recolour-able QR glyph for the Chats-header Share button.\n#pragma once\n" % (W, H))
f.write('#include "lvgl.h"\n\n')
f.write("static const uint8_t qr_icon_map[] = {%s};\n" % ",".join(str(b) for b in data))
f.write("static const lv_img_dsc_t qr_icon_dsc = { { LV_IMG_CF_TRUE_COLOR_ALPHA, 0, 0, %d, %d }, sizeof(qr_icon_map), qr_icon_map };\n"
% (W, H))
print("wrote %s (%dx%d, %d bytes)" % (OUT, W, H, len(data)))
+52
View File
@@ -0,0 +1,52 @@
# Patches the vendored RadioLib so LR11x0::config() tolerates old LR1110
# transceiver firmware that predates DriveDiosInSleepMode (opcode 0x012A,
# added in the 0x0308 firmware release). RadioLib >= 7.x sends it
# unconditionally during begin(); older chip firmware answers CMD_PERR,
# which turns an otherwise healthy radio.begin() into -706
# (RADIOLIB_ERR_SPI_CMD_INVALID). Preproduction Elecrow ThinkNode M9 units
# ship with such firmware (Meshtastic works on them only because it pins an
# older RadioLib that doesn't send the command).
#
# The command is an optional nicety (stops DIO glitches in sleep mode), so
# skipping it on old firmware is safe.
#
# Idempotent. Each PlatformIO env has its own libdeps copy, so this only
# affects envs that list this script in extra_scripts (the M9 env).
# NOTE: on a completely fresh checkout the first build may run before
# RadioLib is downloaded - the script then warns and the patch lands on the
# next build. Re-run `pio run` once if you see the warning.
Import("env")
import os
MARKER = "wadamesh-lr1110-oldfw-patch"
OLD = """ state = this->driveDiosInSleepMode(true);
RADIOLIB_ASSERT(state);"""
NEW = """ state = this->driveDiosInSleepMode(true);
// wadamesh-lr1110-oldfw-patch: LR1110 transceiver FW older than 0x0308
// doesn't implement DriveDiosInSleepMode (0x012A) and answers CMD_PERR,
// which would abort init (-706) on an otherwise healthy radio. The command
// only stops DIO glitches in sleep mode - safe to skip on old firmware.
if(state == RADIOLIB_ERR_SPI_CMD_INVALID) {
RADIOLIB_DEBUG_BASIC_PRINTLN("DriveDiosInSleepMode unsupported (old LR11x0 FW), skipping");
state = RADIOLIB_ERR_NONE;
}
RADIOLIB_ASSERT(state);"""
path = os.path.join(env.subst("$PROJECT_LIBDEPS_DIR"), env.subst("$PIOENV"),
"RadioLib", "src", "modules", "LR11x0", "LR11x0.cpp")
if not os.path.isfile(path):
print("[patch_radiolib_lr11x0] WARNING: %s not found (libdeps not fetched yet?)" % path)
print("[patch_radiolib_lr11x0] WARNING: RadioLib NOT patched - re-run the build once libdeps exist")
else:
with open(path) as f:
src = f.read()
if MARKER in src:
print("[patch_radiolib_lr11x0] already patched")
elif OLD in src:
with open(path, "w") as f:
f.write(src.replace(OLD, NEW, 1))
print("[patch_radiolib_lr11x0] patched LR11x0::config() for old-FW tolerance")
else:
print("[patch_radiolib_lr11x0] WARNING: pattern not found - RadioLib version drift?")
print("[patch_radiolib_lr11x0] WARNING: check LR11x0::config() by hand, NOT patched")
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""
Documentation screenshot capture for the DOC_CAPTURE firmware build.
Build the firmware with -DDOC_CAPTURE, flash it to the T-Deck, then run:
python3 scripts/doc/capture.py /dev/cu.usbmodemXXXX [outdir]
The device waits for a 'G' byte, then walks every screen and streams each
framebuffer over USB as a 16-bit BMP wrapped in <<<WMSHOT ...>>> / <<<WMEND>>>
markers. This script sends 'G', reads each frame, and writes a PNG per screen
(via macOS `sips`; falls back to leaving the .bmp if sips is unavailable).
"""
import sys, os, re, time, subprocess
try:
import serial
except ImportError:
sys.exit("pyserial not found. Try: /Library/Developer/CommandLineTools/usr/bin/python3 -m pip install pyserial")
if len(sys.argv) < 2:
sys.exit("usage: capture.py <serial-port> [outdir]")
port = sys.argv[1]
outdir = sys.argv[2] if len(sys.argv) > 2 else "doc-shots"
os.makedirs(outdir, exist_ok=True)
ser = serial.Serial(port, 115200, timeout=2)
print(f"listening on {port} -> {outdir}/")
hdr_re = re.compile(rb"<<<WMSHOT name=(\S+) w=(\d+) h=(\d+) bytes=(\d+)>>>")
def read_exact(n):
data = bytearray()
while len(data) < n:
chunk = ser.read(n - len(data))
if not chunk:
break
data += chunk
return bytes(data)
count = 0
started = False
last_go = 0.0
deadline = time.time() + 90 # give the device up to 90s to start
while time.time() < deadline:
if not started and time.time() - last_go > 0.4:
ser.write(b"G") # nudge the device to begin the tour
ser.flush()
last_go = time.time()
line = ser.readline()
if not line:
continue
if b"WMTOUR START" in line:
started = True
print("tour started")
continue
if b"WMTOUR END" in line:
print("tour ended")
break
m = hdr_re.search(line)
if not m:
continue
name = m.group(1).decode()
nbytes = int(m.group(4))
data = read_exact(nbytes)
if len(data) != nbytes:
print(f" ! {name}: short read {len(data)}/{nbytes}")
continue
bmp = os.path.join(outdir, name + ".bmp")
png = os.path.join(outdir, name + ".png")
with open(bmp, "wb") as f:
f.write(data)
try:
subprocess.run(["sips", "-s", "format", "png", bmp, "--out", png],
check=True, capture_output=True)
os.remove(bmp)
count += 1
print(f" [{count:2d}] {name}.png ({nbytes} bytes)")
except Exception as e:
count += 1
print(f" [{count:2d}] {name}.bmp ({nbytes} bytes; sips convert failed: {e})")
ser.close()
print(f"done: {count} screenshots in {outdir}/")
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""
Reference subscriber + decryptor for the wadamesh MQTT bridge.
The bridge publishes received mesh messages to:
wadamesh/{node_hex}/msg/dm direct / signed messages (only if DM publish is ON)
wadamesh/{node_hex}/msg/ch channel messages
wadamesh/{node_hex}/status "online" / "offline" (retained LWT)
Payload:
- No encryption key set on the device -> payload is plain UTF-8 JSON.
- Encryption key (PSK) set -> payload is
base64( nonce[12] || ciphertext || tag[16] ), AES-256-GCM,
key = SHA-256(psk).
Set PSK below to the same passphrase you typed on the device.
Requires: pip install paho-mqtt cryptography
"""
import base64, hashlib, json
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
BROKER = "homeassistant.local"
PORT = 1883
PSK = "" # same passphrase as Settings -> MQTT -> Encryption key; "" = plaintext
def decode(payload: bytes) -> dict:
"""Return the message dict, decrypting if a PSK is configured."""
if not PSK:
return json.loads(payload)
key = hashlib.sha256(PSK.encode()).digest() # 32-byte AES-256 key
blob = base64.b64decode(payload)
nonce, ct_and_tag = blob[:12], blob[12:] # GCM tag is the trailing 16 bytes
plain = AESGCM(key).decrypt(nonce, ct_and_tag, None)
return json.loads(plain)
def main():
import paho.mqtt.client as mqtt
def on_connect(c, *_):
c.subscribe("wadamesh/+/msg/#")
print("subscribed to wadamesh/+/msg/#")
def on_message(_c, _u, msg):
try:
data = decode(msg.payload)
print(f"{msg.topic}: {data}")
except Exception as e:
print(f"{msg.topic}: <undecodable: {e}>")
c = mqtt.Client()
c.on_connect, c.on_message = on_connect, on_message
c.connect(BROKER, PORT, 60)
c.loop_forever()
if __name__ == "__main__":
main()
+88 -31
View File
@@ -1,63 +1,120 @@
#!/usr/bin/env bash
# wadamesh release: build tagged bins for both touch boards, refresh the
# update-check listing, and publish to the firmware VPS (behind Cloudflare).
# wadamesh release — two channels: TEST (beta, default) and STABLE (promote).
#
# Usage:
# WADAMESH_VPS=user@your-vps WADAMESH_VPS_PATH=/srv/wadamesh/firmware \
# scripts/release.sh beta_2
# Cut a TEST build (builds both boards, publishes to the beta channel — fast/frequent):
# WADAMESH_VPS=user@host scripts/release.sh beta_21
#
# Promote an ALREADY-BUILT beta to STABLE (no rebuild — copies the tested bins):
# WADAMESH_VPS=user@host scripts/release.sh beta_21 --promote
#
# Channels on the VPS firmware root (firmware.wadamesh.com, behind Cloudflare):
# releases/BETA/<tag>/ immutable beta archives <- test channel + new-firmware beta check
# releases/TOUCH/<tag>/ immutable stable archives <- stable feed + legacy on-device check
# latest-beta/ beta feed (version.json + manifests + rolling bins)
# latest/ stable feed (version.json + manifests + rolling bins)
#
# The VPS target comes from the environment — never commit it (Cloudflare fronts
# the origin, so the IP isn't needed in the firmware anyway).
set -euo pipefail
TAG="${1:?usage: release.sh <tag> e.g. beta_2}"
TAG="${1:?usage: release.sh <tag> [--promote] e.g. beta_21 / beta_21 --promote}"
MODE="beta"
[ "${2:-}" = "--promote" ] && MODE="stable"
PIO="${PIO:-$HOME/Library/Python/3.9/bin/pio}"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
OUT="$ROOT/out/firmware" # local mirror of the VPS firmware root
REL="$OUT/releases/TOUCH"; LATEST="$OUT/latest"
OUT="$ROOT/out/firmware" # local mirror of the VPS firmware root
DEST="${WADAMESH_VPS:-}"; DEST_PATH="${WADAMESH_VPS_PATH:-/srv/wadamesh/firmware}"
# env:binname pairs — plain string form (works on macOS's bash 3.2; no associative arrays)
ENVS="heltec_v4_tft_companion_radio_usb_tcp_touch:wadamesh-heltec-v4-tft LilyGo_TDeck_companion_radio_touch:wadamesh-tdeck"
ENVS="heltec_v4_tft_companion_radio_usb_tcp_touch:wadamesh-heltec-v4-tft LilyGo_TDeck_companion_radio_touch:wadamesh-tdeck ThinkNode_M9_companion_radio_touch:wadamesh-thinknode-m9 rak_tap_v2_companion_radio_touch:wadamesh-rak-tap-v2"
# 1. Pull the current published tree so the listing stays complete across tags.
# Per-channel destination paths.
if [ "$MODE" = "stable" ]; then
ARCH="$OUT/releases/TOUCH"; FEED="$OUT/latest"
else
ARCH="$OUT/releases/BETA"; FEED="$OUT/latest-beta"
fi
# 1. Pull the current published tree so listings stay complete across tags.
if [ -n "$DEST" ]; then
mkdir -p "$OUT"
rsync -a "$DEST:$DEST_PATH/" "$OUT/" 2>/dev/null || echo "note: first publish (nothing to pull yet)"
fi
mkdir -p "$REL/$TAG" "$LATEST"
mkdir -p "$ARCH/$TAG" "$FEED"
# 2. Build both boards (app + merged), tag + version embedded.
export PLATFORMIO_BUILD_FLAGS="-DFIRMWARE_RELEASE_TAG='\"${TAG}\"' -DFIRMWARE_VERSION='\"wadamesh ${TAG}\"'"
for pair in $ENVS; do
env="${pair%%:*}"; name="${pair##*:}"
"$PIO" run -t mergebin -e "$env"
cp ".pio/build/$env/firmware.bin" "$REL/$TAG/$name.bin"
cp ".pio/build/$env/firmware-merged.bin" "$REL/$TAG/$name-merged.bin"
cp ".pio/build/$env/firmware-merged.bin" "$LATEST/$name-merged.bin" # rolling latest -> web flasher (standalone)
cp ".pio/build/$env/firmware.bin" "$LATEST/$name.bin" # rolling latest app image -> Launcher path
done
if [ "$MODE" = "beta" ]; then
# 2b. TEST build: compile both boards (tag + version embedded) into the beta archive + feed.
export PLATFORMIO_BUILD_FLAGS="-DFIRMWARE_RELEASE_TAG='\"${TAG}\"' -DFIRMWARE_VERSION='\"wadamesh ${TAG}\"'"
for pair in $ENVS; do
env="${pair%%:*}"; name="${pair##*:}"
"$PIO" run -t mergebin -e "$env"
cp ".pio/build/$env/firmware.bin" "$ARCH/$TAG/$name.bin"
cp ".pio/build/$env/firmware-merged.bin" "$ARCH/$TAG/$name-merged.bin"
cp ".pio/build/$env/firmware-merged.bin" "$FEED/$name-merged.bin" # rolling -> flasher (standalone)
cp ".pio/build/$env/firmware.bin" "$FEED/$name.bin" # rolling app image -> Launcher path
done
else
# 2s. STABLE promotion: NO rebuild — copy the already-tested beta bins so what
# ships to stable is byte-for-byte what the community tested.
SRC="$OUT/releases/BETA/$TAG"
if [ ! -d "$SRC" ] || ! ls "$SRC/"*.bin >/dev/null 2>&1; then
# beta_20 + any pre-channel build already lives under releases/TOUCH/<tag>/.
if ls "$ARCH/$TAG/"*.bin >/dev/null 2>&1; then
SRC="$ARCH/$TAG"; echo "note: $TAG not in releases/BETA — promoting from existing $ARCH/$TAG"
else
echo "ERROR: no built bins for $TAG (expected $OUT/releases/BETA/$TAG). Cut the beta first."; exit 1
fi
fi
for pair in $ENVS; do
name="${pair##*:}"
cp "$SRC/$name.bin" "$ARCH/$TAG/$name.bin"
cp "$SRC/$name-merged.bin" "$ARCH/$TAG/$name-merged.bin"
cp "$SRC/$name-merged.bin" "$FEED/$name-merged.bin"
cp "$SRC/$name.bin" "$FEED/$name.bin"
done
echo "promoted $TAG bins $SRC -> $ARCH/$TAG (+ $FEED)"
fi
# 3. Regenerate the update-check listing (the firmware scans the body for the
# highest beta_<N>). JSON array of dir names — GitHub-contents shape.
python3 - "$REL" > "$REL/index.json" <<'PY'
# 3. Regenerate this channel's update-check listing (firmware scans the body for
# the highest beta_<N>). JSON array of dir names — GitHub-contents shape.
python3 - "$ARCH" > "$ARCH/index.json" <<'PY'
import os, sys, json
rel = sys.argv[1]
betas = sorted(d for d in os.listdir(rel)
if d.startswith("beta_") and os.path.isdir(os.path.join(rel, d)))
print(json.dumps([{"name": b, "type": "dir"} for b in betas], indent=2))
PY
echo "listing: $(python3 -c 'import json,sys;print(", ".join(x["name"] for x in json.load(open(sys.argv[1]))))' "$REL/index.json")"
echo "listing ($MODE): $(python3 -c 'import json,sys;print(", ".join(x["name"] for x in json.load(open(sys.argv[1]))))' "$ARCH/index.json")"
# 3b. Web-flasher metadata (version.json + esp-web-tools manifests) — must roll
# with the release so the flasher shows the current version + notes, not a
# frozen one. Notes come from release-notes/<tag>.txt (one per line; optional).
python3 "$ROOT/scripts/build/gen-flasher-meta.py" "$TAG" "$LATEST" "$ROOT/release-notes/$TAG.txt"
# 3b. Web-flasher metadata for THIS channel (version.json + manifests -> the channel feed).
python3 "$ROOT/scripts/build/gen-flasher-meta.py" "$TAG" "$FEED" "$ROOT/release-notes/$TAG.txt" "$MODE"
# 3c. Mesh America Configurator provider catalog — STABLE only (it is a public
# flasher catalog served live from raw.githubusercontent main; test builds
# must not bump it). Points at the immutable releases/TOUCH/$TAG bins.
if [ "$MODE" = "stable" ]; then
CATALOG="$ROOT/deploy/meshamerica-catalog.json"
python3 "$ROOT/deploy/gen-meshamerica-catalog.py" "$TAG" > "$CATALOG.tmp" && mv "$CATALOG.tmp" "$CATALOG"
mkdir -p "$OUT/meshamerica" && cp "$CATALOG" "$OUT/meshamerica/catalog.json"
if git -C "$ROOT" diff --quiet -- "$CATALOG"; then
echo "Mesh America catalog already current for $TAG"
elif [ "$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null)" = "main" ]; then
if git -C "$ROOT" commit -q -m "chore: refresh Mesh America catalog for $TAG" -- "$CATALOG" \
&& git -C "$ROOT" push origin HEAD:main; then
echo "Mesh America catalog refreshed + pushed to main ($TAG)"
else
echo "WARN: Mesh America catalog commit/push failed — push deploy/meshamerica-catalog.json to main by hand"
fi
else
echo "NOTE: regenerated $CATALOG for $TAG but not on 'main' — commit + push it to main."
fi
fi
# 4. Publish to the VPS (Cloudflare caches at the edge).
if [ -n "$DEST" ]; then
rsync -av "$OUT/" "$DEST:$DEST_PATH/"
echo "published $TAG -> $DEST:$DEST_PATH"
echo "published $TAG ($MODE) -> $DEST:$DEST_PATH"
else
echo "WADAMESH_VPS not set — built + staged in $REL/$TAG only (no publish)."
echo "WADAMESH_VPS not set — built + staged in $ARCH/$TAG + $FEED only (no publish)."
fi
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env bash
#
# Publish the Tanmatsu build of WadaMesh to the Nicolai-Electronics app-repository
# (the official Tanmatsu launcher app-store). Run this as part of cutting a beta.
#
# scripts/tanmatsu-appstore-publish.sh beta_17 # -> version 1.0.17, revision 17
# scripts/tanmatsu-appstore-publish.sh 1.2.0 8 # -> explicit version 1.2.0, revision 8
# scripts/tanmatsu-appstore-publish.sh beta_17 --skip-build # use the existing build
#
# What it does:
# 1. builds the Tanmatsu app (unless --skip-build) and verifies it
# 2. syncs our fork (B0N3-GD/app-repository) to upstream main
# 3. (re)stages deploy/tanmatsu-app/com.wadamesh.wadamesh/ + the fresh binary,
# setting `version` and every application `revision`
# 4. pushes an `update-wadamesh-<version>` branch and opens/updates a PR to
# Nicolai-Electronics/app-repository
#
# NB:
# - WadaMesh on the Tanmatsu is KEYBOARD-driven (no touchscreen). Keep the
# listing wording free of "touch" — the canonical text lives in the template.
# - For an update you MUST bump BOTH `version` AND `revision` or the launcher
# won't offer it. This script bumps both.
# - The CLA is signed once per GitHub user (Kaj), so follow-up PRs auto-pass.
# - First run can't sync until PR #55 (the initial add) is merged; until then
# the fork's main lacks the folder, but step 3 re-stages it in full anyway.
set -euo pipefail
# ---- args ---------------------------------------------------------------
ARG="${1:?usage: tanmatsu-appstore-publish.sh <beta_N | version> [revision] [--skip-build]}"
shift || true
SKIP_BUILD=0; REV_ARG=""
for a in "$@"; do
case "$a" in
--skip-build) SKIP_BUILD=1 ;;
*) REV_ARG="$a" ;;
esac
done
if [[ "$ARG" =~ ^beta_([0-9]+)$ ]]; then
N="${BASH_REMATCH[1]}"; VERSION="1.0.$N"; REVISION="${REV_ARG:-$N}"
else
VERSION="$ARG"; REVISION="${REV_ARG:?explicit version needs a revision: tanmatsu-appstore-publish.sh <version> <revision>}"
fi
# ---- paths --------------------------------------------------------------
SLUG=com.wadamesh.wadamesh
WADAMESH="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # repo root
TPL="$WADAMESH/deploy/tanmatsu-app/$SLUG" # versioned template (icons/README/metadata)
BIN="$WADAMESH/tanmatsu/build/tanmatsu/application.bin" # IDF build output
FORK_DIR="${WADAMESH_APPSTORE_FORK:-/Users/kaj/wadamesh-appstore-fork}"
UPSTREAM=Nicolai-Electronics/app-repository
GHUSER="$(gh api user -q .login)"
echo "==> WadaMesh app-store publish: version $VERSION, revision $REVISION"
# ---- 1. build + verify the Tanmatsu app --------------------------------
if [ "$SKIP_BUILD" -eq 0 ]; then
# Bake the EXACT release tag into the P4 binary (About page + update check). We know
# it here; git describe inside build.sh would read a "-N-g<sha>" suffix (or the prior
# beta) whenever the beta_N tag isn't fetched locally.
[[ "$ARG" =~ ^beta_([0-9]+)$ ]] && export WADA_FW_TAG="$ARG"
echo "==> building Tanmatsu app (./build.sh build)..."
LOG="$(mktemp)"
( cd "$WADAMESH/tanmatsu" && ./build.sh build ) >"$LOG" 2>&1 || true # app_check_size exit!=0 is EXPECTED
# real compile/link errors abort; the cosmetic 'app partitions too small' does not
if grep -E "error:|undefined reference|was not declared" "$LOG" | grep -vE "app_check_size|too small|partitions are too small"; then
echo "!! real build error above — aborting"; rm -f "$LOG"; exit 1
fi
rm -f "$LOG"
fi
[ -f "$BIN" ] || { echo "!! no Tanmatsu binary at $BIN — build first (drop --skip-build)"; exit 1; }
echo "==> binary present: $(wc -c <"$BIN") bytes"
# ---- 2. sync the fork to upstream main ---------------------------------
[ -d "$FORK_DIR/.git" ] || { echo "!! fork clone missing at $FORK_DIR — clone $GHUSER/app-repository there first"; exit 1; }
git -C "$FORK_DIR" remote get-url upstream >/dev/null 2>&1 || \
git -C "$FORK_DIR" remote add upstream "https://github.com/$UPSTREAM.git"
git -C "$FORK_DIR" fetch -q upstream
git -C "$FORK_DIR" checkout -q main 2>/dev/null || git -C "$FORK_DIR" checkout -qb main upstream/main
git -C "$FORK_DIR" reset -q --hard upstream/main
# ---- 3. stage the app folder (template + fresh binary; set version/rev) -
APP="$FORK_DIR/$SLUG"
mkdir -p "$APP"
cp "$TPL/icon16.png" "$TPL/icon32.png" "$TPL/icon64.png" "$TPL/README.md" "$APP/"
cp "$WADAMESH/LICENSE" "$APP/LICENSE"
cp "$WADAMESH/NOTICE" "$APP/NOTICE" # MeshCore (MIT) + other third-party attribution
cp "$BIN" "$APP/wadamesh.bin"
python3 - "$TPL/metadata.json" "$APP/metadata.json" "$VERSION" "$REVISION" <<'PY'
import json, sys, re
src, dst, ver, rev = sys.argv[1], sys.argv[2], sys.argv[3], int(sys.argv[4])
m = json.load(open(src))
# The launcher store's verify_metadata check restricts name/description to this set
# (NO commas, NO em-dash). Fail loudly HERE instead of as a red check on the PR.
pat = r'^[A-Za-z0-9_\- .!@#$%^&*()\/\\]+$'
for f in ("name", "description"):
v = m.get(f, "")
if not re.match(pat, v) or len(v) > 256:
sys.stderr.write(f"!! metadata '{f}' fails the store regex (no comma / em-dash, <=256 chars): {v!r}\n")
sys.exit(1)
m["version"] = ver
for app in m["application"]:
app["revision"] = rev
json.dump(m, open(dst, "w"), indent=4); open(dst, "a").write("\n")
PY
echo "==> staged $SLUG (version $VERSION, revision $REVISION)"
# ---- 4. commit on a branch, push, open/update the PR -------------------
BR="update-wadamesh-$VERSION"
git -C "$FORK_DIR" checkout -qB "$BR"
git -C "$FORK_DIR" add "$SLUG"
GIT_COMMITTER_NAME="Kaj Schittecat" GIT_COMMITTER_EMAIL="kaj@schittecat.com" \
git -C "$FORK_DIR" commit -q --author="Kaj Schittecat <kaj@schittecat.com>" \
-m "Update WadaMesh to $VERSION (revision $REVISION)" || echo " (nothing new to commit)"
git -C "$FORK_DIR" push -fq -u origin "$BR"
if gh pr create --repo "$UPSTREAM" --base main --head "$GHUSER:$BR" \
--title "Update WadaMesh to $VERSION" \
--body "Updates WadaMesh ($SLUG) to v$VERSION (revision $REVISION) — new Tanmatsu build." 2>/dev/null; then
echo "==> opened PR for $VERSION"
else
echo "==> PR already open for $BR (force-push updated it)"
fi
echo "==> done. Track it: gh pr list --repo $UPSTREAM --author $GHUSER"
+487
View File
@@ -0,0 +1,487 @@
#!/usr/bin/env python3
import argparse
import struct
import sys
import time
try:
import serial
except ImportError:
print("Missing dependency: pyserial", file=sys.stderr)
print("Install with: python -m pip install pyserial", file=sys.stderr)
sys.exit(2)
CMD_APP_START = 1
CMD_SEND_TXT_MSG = 2
CMD_SYNC_NEXT_MESSAGE = 10
CMD_GET_BATT_AND_STORAGE = 20
CMD_DEVICE_QUERY = 22
CMD_SEND_TELEMETRY_REQ = 39
CMD_GET_CUSTOM_VARS = 40
CMD_GET_STATS = 56
RESP_CODE_SELF_INFO = 5
RESP_CODE_SENT = 6
RESP_CODE_CONTACT_MSG_RECV_V3 = 16
RESP_CODE_NO_MORE_MESSAGES = 10
RESP_CODE_BATT_AND_STORAGE = 12
RESP_CODE_DEVICE_INFO = 13
RESP_CODE_CUSTOM_VARS = 21
RESP_CODE_STATS = 24
PUSH_CODE_SEND_CONFIRMED = 0x82
PUSH_CODE_TELEMETRY_RESPONSE = 0x8B
PUSH_CODE_BINARY_RESPONSE = 0x8C
TXT_TYPE_CLI_DATA = 1
STATS_TYPE_CORE = 0
STATS_TYPE_RADIO = 1
STATS_TYPE_PACKETS = 2
MESHCOM_PREFIX = b"MESHCM"
def build_frame(payload: bytes) -> bytes:
return b"<" + struct.pack("<H", len(payload)) + payload
def read_frame(ser: serial.Serial, timeout_s: float = 2.0):
deadline = time.time() + timeout_s
while time.time() < deadline:
b = ser.read(1)
if not b:
continue
if b != b">":
continue
hdr = ser.read(2)
if len(hdr) != 2:
return None
length = struct.unpack("<H", hdr)[0]
payload = ser.read(length)
if len(payload) != length:
return None
return payload
return None
def send_and_collect(ser: serial.Serial, name: str, payload: bytes, listen_s: float = 1.0):
print(f"\n== {name} ==")
print("TX:", payload.hex(" "))
ser.write(build_frame(payload))
ser.flush()
frames = []
deadline = time.time() + listen_s
while time.time() < deadline:
frame = read_frame(ser, timeout_s=min(0.35, max(0.05, deadline - time.time())))
if frame is None:
continue
frames.append(frame)
if not frames:
print("RX: <timeout>")
return []
for idx, frame in enumerate(frames, start=1):
print(f"RX[{idx}]:", frame.hex(" "))
decode_frame(frame)
return frames
def decode_frame(frame: bytes):
code = frame[0]
print(f"Code: 0x{code:02X} ({code})")
if code == RESP_CODE_DEVICE_INFO:
decode_device_info(frame)
elif code == RESP_CODE_BATT_AND_STORAGE:
decode_batt_and_storage(frame)
elif code == RESP_CODE_SELF_INFO:
print("Self info response received")
elif code == RESP_CODE_CUSTOM_VARS:
decode_custom_vars(frame)
elif code == RESP_CODE_STATS:
decode_stats(frame)
elif code == RESP_CODE_CONTACT_MSG_RECV_V3:
decode_contact_msg_recv_v3(frame)
elif code == RESP_CODE_NO_MORE_MESSAGES:
print("No more queued messages")
elif code == RESP_CODE_SENT:
decode_sent(frame)
elif code == PUSH_CODE_SEND_CONFIRMED:
decode_send_confirmed(frame)
elif code == PUSH_CODE_TELEMETRY_RESPONSE:
decode_telemetry(frame)
elif code == PUSH_CODE_BINARY_RESPONSE:
decode_binary_response(frame)
else:
print("Unhandled response")
def decode_c_string(raw: bytes) -> str:
return raw.split(b"\x00", 1)[0].decode("utf-8", errors="replace")
def decode_device_info(frame: bytes):
if len(frame) < 80:
print("Short DEVICE_INFO frame")
return
fw_ver_code = frame[1]
max_contacts_x2 = frame[2]
max_channels = frame[3]
ble_pin = struct.unpack("<I", frame[4:8])[0]
build_date = decode_c_string(frame[8:20])
manufacturer = decode_c_string(frame[20:60])
version = decode_c_string(frame[60:80]) if len(frame) >= 80 else ""
print("Firmware code:", fw_ver_code)
print("Max contacts:", max_contacts_x2 * 2)
print("Max channels:", max_channels)
print("BLE pin:", ble_pin)
print("Build date:", build_date)
print("Manufacturer:", manufacturer)
if version:
print("Version:", version)
def decode_batt_and_storage(frame: bytes):
if len(frame) < 11:
print("Short BATT_AND_STORAGE frame")
return
battery_mv = struct.unpack("<H", frame[1:3])[0]
used_kb = struct.unpack("<I", frame[3:7])[0]
total_kb = struct.unpack("<I", frame[7:11])[0]
print("Battery:", f"{battery_mv} mV ({battery_mv / 1000.0:.2f} V)")
print("Storage used:", f"{used_kb} KB")
print("Storage total:", f"{total_kb} KB")
def decode_custom_vars(frame: bytes):
raw = frame[1:].decode("utf-8", errors="replace")
print("Custom vars:", raw if raw else "<empty>")
def decode_stats(frame: bytes):
if len(frame) < 2:
print("Short STATS frame")
return
stats_type = frame[1]
if stats_type == STATS_TYPE_CORE and len(frame) >= 11:
battery_mv = struct.unpack("<H", frame[2:4])[0]
uptime_secs = struct.unpack("<I", frame[4:8])[0]
err_flags = struct.unpack("<H", frame[8:10])[0]
queue_len = frame[10]
print("Stats type: core")
print("Battery:", f"{battery_mv} mV ({battery_mv / 1000.0:.2f} V)")
print("Uptime:", f"{uptime_secs} s")
print("Error flags:", f"0x{err_flags:04X}")
print("Outbound queue:", queue_len)
elif stats_type == STATS_TYPE_RADIO and len(frame) >= 14:
noise_floor = struct.unpack("<h", frame[2:4])[0]
last_rssi = struct.unpack("<b", frame[4:5])[0]
last_snr_q4 = struct.unpack("<b", frame[5:6])[0]
tx_air_secs = struct.unpack("<I", frame[6:10])[0]
rx_air_secs = struct.unpack("<I", frame[10:14])[0]
print("Stats type: radio")
print("Noise floor:", f"{noise_floor} dBm")
print("Last RSSI:", f"{last_rssi} dBm")
print("Last SNR:", f"{last_snr_q4 / 4.0:.2f} dB")
print("TX airtime:", f"{tx_air_secs} s")
print("RX airtime:", f"{rx_air_secs} s")
elif stats_type == STATS_TYPE_PACKETS and len(frame) >= 30:
values = struct.unpack("<IIIIIII", frame[2:30])
labels = [
"recv",
"sent",
"sent_flood",
"sent_direct",
"recv_flood",
"recv_direct",
"recv_errors",
]
print("Stats type: packets")
for label, value in zip(labels, values):
print(f"{label}:", value)
else:
print(f"Unknown stats subtype {stats_type} or short frame")
def decode_sent(frame: bytes):
if len(frame) < 10:
print("Short SENT frame")
return
flood = frame[1]
tag = struct.unpack("<I", frame[2:6])[0]
est_timeout = struct.unpack("<I", frame[6:10])[0]
print("Message accepted")
print("Flood:", flood)
print("Tag:", tag)
print("Estimated timeout:", est_timeout)
def decode_send_confirmed(frame: bytes):
if len(frame) < 9:
print("Short SEND_CONFIRMED frame")
return
ack = struct.unpack("<I", frame[1:5])[0]
trip_time = struct.unpack("<I", frame[5:9])[0]
print("Send confirmed")
print("Ack/tag:", ack)
print("Trip time:", trip_time)
def decode_binary_response(frame: bytes):
if len(frame) < 6:
print("Short BINARY_RESPONSE frame")
return
reserved = frame[1]
tag = struct.unpack("<I", frame[2:6])[0]
text = frame[6:].decode("utf-8", errors="replace").rstrip("\x00")
print("Binary response reserved:", reserved)
print("Binary response tag:", tag)
print("Binary response text:")
print(text if text else "<empty>")
def decode_contact_msg_recv_v3(frame: bytes):
if len(frame) < 16:
print("Short CONTACT_MSG_RECV_V3 frame")
return
snr_q4 = struct.unpack("<b", frame[1:2])[0]
reserved1 = frame[2]
reserved2 = frame[3]
pubkey_prefix = frame[4:10].hex()
path_len = frame[10]
txt_type = frame[11]
timestamp = struct.unpack("<I", frame[12:16])[0]
text = frame[16:].decode("utf-8", errors="replace").rstrip("\x00")
print("Queued contact message")
print("SNR:", f"{snr_q4 / 4.0:.2f} dB")
print("Reserved:", reserved1, reserved2)
print("From pubkey prefix:", pubkey_prefix)
print("Path len:", path_len)
print("Text type:", txt_type)
print("Timestamp:", timestamp)
print("Text:")
print(text if text else "<empty>")
def _read_i16_be(buf: bytes) -> int:
return struct.unpack(">h", buf)[0]
def _read_u16_be(buf: bytes) -> int:
return struct.unpack(">H", buf)[0]
def _read_u32_be(buf: bytes) -> int:
return struct.unpack(">I", buf)[0]
def _read_i24_be(buf: bytes) -> int:
val = int.from_bytes(buf, "big", signed=False)
if val & 0x800000:
val -= 1 << 24
return val
def parse_lpp(payload: bytes):
i = 0
items = []
while i + 2 <= len(payload):
channel = payload[i]
data_type = payload[i + 1]
i += 2
if data_type in (0, 1, 102, 120, 142):
if i + 1 > len(payload):
break
value = payload[i]
i += 1
items.append((channel, data_type, value))
elif data_type in (2, 3):
if i + 2 > len(payload):
break
value = _read_i16_be(payload[i:i + 2]) / 100.0
i += 2
items.append((channel, data_type, value))
elif data_type in (101, 115, 125, 128, 132):
if i + 2 > len(payload):
break
value = _read_u16_be(payload[i:i + 2])
i += 2
if data_type == 115:
value /= 10.0
items.append((channel, data_type, value))
elif data_type == 121:
if i + 2 > len(payload):
break
value = _read_i16_be(payload[i:i + 2])
i += 2
items.append((channel, data_type, value))
elif data_type == 103:
if i + 2 > len(payload):
break
value = _read_i16_be(payload[i:i + 2]) / 10.0
i += 2
items.append((channel, data_type, value))
elif data_type == 104:
if i + 1 > len(payload):
break
value = payload[i] / 2.0
i += 1
items.append((channel, data_type, value))
elif data_type in (116,):
if i + 2 > len(payload):
break
value = _read_u16_be(payload[i:i + 2]) / 100.0
i += 2
items.append((channel, data_type, value))
elif data_type in (117,):
if i + 2 > len(payload):
break
value = _read_u16_be(payload[i:i + 2]) / 1000.0
i += 2
items.append((channel, data_type, value))
elif data_type in (100, 118, 131, 133):
if i + 4 > len(payload):
break
value = _read_u32_be(payload[i:i + 4])
i += 4
if data_type == 131:
value /= 1000.0
items.append((channel, data_type, value))
elif data_type == 130:
if i + 4 > len(payload):
break
value = _read_u32_be(payload[i:i + 4]) / 1000.0
i += 4
items.append((channel, data_type, value))
elif data_type in (113, 134):
if i + 6 > len(payload):
break
x = _read_i16_be(payload[i:i + 2])
y = _read_i16_be(payload[i + 2:i + 4])
z = _read_i16_be(payload[i + 4:i + 6])
i += 6
scale = 1000.0 if data_type == 113 else 100.0
items.append((channel, data_type, (x / scale, y / scale, z / scale)))
elif data_type == 136:
if i + 9 > len(payload):
break
lat = _read_i24_be(payload[i:i + 3]) / 10000.0
lon = _read_i24_be(payload[i + 3:i + 6]) / 10000.0
alt = _read_i24_be(payload[i + 6:i + 9]) / 100.0
i += 9
items.append((channel, data_type, (lat, lon, alt)))
else:
items.append((channel, data_type, "<unknown>"))
break
return items
def format_lpp_item(channel: int, data_type: int, value):
names = {
0: "digital_input",
1: "digital_output",
2: "analog_input",
3: "analog_output",
100: "generic_sensor",
101: "luminosity",
102: "presence",
103: "temperature_c",
104: "humidity_pct",
113: "accelerometer_g",
115: "barometric_hpa",
116: "voltage_v",
117: "current_a",
118: "frequency_hz",
120: "percentage",
121: "altitude_m",
125: "concentration_ppm",
128: "power_w",
130: "distance_m",
131: "energy_kwh",
132: "direction_deg",
133: "unixtime",
134: "gyrometer_dps",
136: "gps",
142: "switch",
}
return f"ch{channel} {names.get(data_type, f'type_{data_type}')} = {value}"
def drain_sync_messages(ser: serial.Serial, max_reads: int = 8):
for idx in range(max_reads):
frames = send_and_collect(
ser,
f"CMD_SYNC_NEXT_MESSAGE #{idx + 1}",
bytes([CMD_SYNC_NEXT_MESSAGE]),
listen_s=0.7,
)
if not frames:
break
if any(frame[:1] == bytes([RESP_CODE_NO_MORE_MESSAGES]) for frame in frames):
break
def decode_telemetry(frame: bytes):
if len(frame) < 8:
print("Short TELEMETRY_RESPONSE frame")
return
reserved = frame[1]
pubkey_prefix = frame[2:8].hex()
lpp = frame[8:]
print("Telemetry reserved:", reserved)
print("Telemetry pubkey prefix:", pubkey_prefix)
print("Telemetry raw LPP:", lpp.hex(" "))
items = parse_lpp(lpp)
if not items:
print("Telemetry parsed: <empty>")
return
print("Telemetry parsed:")
for channel, data_type, value in items:
print(format_lpp_item(channel, data_type, value))
def build_meshcomod_cli(command: str) -> bytes:
return (
bytes([CMD_SEND_TXT_MSG, TXT_TYPE_CLI_DATA, 0])
+ struct.pack("<I", 0)
+ MESHCOM_PREFIX
+ command.encode("utf-8")
)
def main():
parser = argparse.ArgumentParser(description="Probe WADAMESH companion protocol over serial")
parser.add_argument("--port", required=True, help="Serial port, e.g. COM4")
parser.add_argument("--baud", type=int, default=115200, help="Serial baud rate")
args = parser.parse_args()
with serial.Serial(args.port, args.baud, timeout=0.25) as ser:
time.sleep(0.2)
try:
ser.reset_input_buffer()
ser.reset_output_buffer()
except Exception:
pass
device_query = bytes([CMD_DEVICE_QUERY, 3])
app_start = bytes([CMD_APP_START]) + bytes(7) + b"wadamesh-probe"
send_and_collect(ser, "CMD_DEVICE_QUERY", device_query, listen_s=0.8)
send_and_collect(ser, "CMD_APP_START", app_start, listen_s=0.8)
drain_sync_messages(ser, max_reads=4)
send_and_collect(ser, "CMD_GET_BATT_AND_STORAGE", bytes([CMD_GET_BATT_AND_STORAGE]), listen_s=0.8)
send_and_collect(ser, "CMD_GET_CUSTOM_VARS", bytes([CMD_GET_CUSTOM_VARS]), listen_s=0.8)
send_and_collect(ser, "CMD_GET_STATS core", bytes([CMD_GET_STATS, STATS_TYPE_CORE]), listen_s=0.8)
send_and_collect(ser, "CMD_GET_STATS radio", bytes([CMD_GET_STATS, STATS_TYPE_RADIO]), listen_s=0.8)
send_and_collect(ser, "CMD_GET_STATS packets", bytes([CMD_GET_STATS, STATS_TYPE_PACKETS]), listen_s=0.8)
send_and_collect(ser, "CMD_SEND_TELEMETRY_REQ self", bytes([CMD_SEND_TELEMETRY_REQ, 0, 0, 0]), listen_s=1.2)
send_and_collect(ser, "CMD_SEND_TXT_MSG meshcomod status", build_meshcomod_cli("status"), listen_s=1.5)
drain_sync_messages(ser, max_reads=4)
send_and_collect(ser, "CMD_SEND_TXT_MSG meshcomod wifi status", build_meshcomod_cli("wifi status"), listen_s=1.5)
drain_sync_messages(ser, max_reads=4)
send_and_collect(ser, "CMD_SEND_TXT_MSG meshcomod ota status", build_meshcomod_cli("ota status"), listen_s=1.5)
drain_sync_messages(ser, max_reads=4)
if __name__ == "__main__":
main()
+42
View File
@@ -72,8 +72,42 @@ public:
(void)is_flood; (void)snr_q4; (void)rssi;
newMsgFromPub(path_len, from_pub, from_name, text, msgcount);
}
/** Room-server post. Like newMsgFromPubWithMeta, but from_pub/from_name
* identify the ROOM (the chat thread) while `author_name` is the resolved
* display name of the original poster — a room signed message attributes
* the author only by a pubkey prefix, never in the text body. Default
* ignores the author and behaves like a normal message, so non-touch UIs
* are unaffected. */
virtual void newRoomMsgFromPubWithMeta(uint8_t path_len, bool is_flood,
const uint8_t* from_pub, const char* from_name,
const char* author_name,
const char* text, int msgcount,
int8_t snr_q4, int8_t rssi) {
(void)author_name;
newMsgFromPubWithMeta(path_len, is_flood, from_pub, from_name, text, msgcount, snr_q4, rssi);
}
/** A message the COMPANION APP sent on our behalf (CMD_SEND_TXT_MSG). The wire protocol
* deliberately doesn't echo sent messages back to clients, so the on-device UI is otherwise
* the only consumer that never sees app-originated sends — this lets it mirror one as a local
* outgoing bubble so it shows on-device too (issue #46). Default no-op for non-touch UIs. */
virtual void appSentMsgToContact(const uint8_t* to_pub, const char* to_name, const char* text,
uint32_t ack_hash) {
(void)to_pub; (void)to_name; (void)text; (void)ack_hash;
}
/** Mirror an app-originated CHANNEL message into the on-device UI — the channel-send path, like the
* DM path above, never shows companion-originated sends on screen otherwise. text is NUL-terminated
* by the caller. Default no-op for non-touch UIs. */
virtual void appSentMsgToChannel(const char* channel_name, const char* text) {
(void)channel_name; (void)text;
}
virtual void notify(UIEventType t = UIEventType::none) = 0;
virtual void appendDiag(const char* message) { (void)message; }
/** Flush any UI-side persistent state (chat history, thread table) to storage
* NOW. Called before programmatic reboots (the companion app's reboot
* command) — the touch UI's history writes are lazy (up to ~30 s apart on the
* deep SD ring), so a reboot path that skips this drops the newest messages.
* Default: no-op for UIs without lazy persistent state. */
virtual void persistHistoryNow() {}
/** Notify UI of an advert reception. `is_new=true` means the contact is NOT in
* the persistent contacts[] table (filtered by auto-add rules or contacts
* full); `is_new=false` means the contact is in contacts[] and was just
@@ -108,6 +142,14 @@ public:
virtual void onAdminLoginResult(const ContactInfo& contact, bool success, uint8_t perms) {
(void)contact; (void)success; (void)perms;
}
/** The server's own clock, from the timestamp prefix every server RESPONSE
* carries (first 4 bytes). Fired on LOGIN_OK so the UI can warn when the
* device clock and the server clock disagree — a skewed device clock makes
* the server's replay guard silently drop our logins/posts/keep-alives
* (issue #89). Default no-op. */
virtual void onServerClock(const ContactInfo& contact, uint32_t server_epoch) {
(void)contact; (void)server_epoch;
}
/** A CLI command reply from a repeater / room server / sensor (the
* TXT_TYPE_CLI_DATA path). `text` is the decrypted command output text
* the server returned. Default no-op; the touch UI overrides to append
+172 -22
View File
@@ -3,6 +3,10 @@
#include "DataStore.h"
#if defined(ESP32)
#include <SD.h>
#include "helpers/esp32/WdtHeavyGuard.h" // suspend core-0 idle WDT during the (SPIFFS-GC-prone) contact write
#endif
#if defined(HAS_TANMATSU)
#include <SD_MMC.h>
#endif
#if defined(EXTRAFS) || defined(QSPIFLASH)
@@ -70,6 +74,38 @@ void DataStore::begin() {
#else
// init 'blob store' support
_fs->mkdir("/bl");
#if defined(ESP32)
// One-time migration: when a secondary FS is active (an SD card is present)
// but contacts/channels still live on the primary FS from an earlier build,
// copy them onto the card ONCE and delete the primary copies. This preserves
// the user's contact list while getting the frequently-rewritten files off a
// small/near-full SPIFFS — whose garbage collection can otherwise stall the
// loop task for seconds during saveContacts and trip the task watchdog
// (the beta_25 reboot loop). Identity + prefs stay on _fs and never move.
// No-op once the card already holds the files, and a safe no-op on boards
// whose primary-FS metadata can't confirm the source exists (e.g. P4 FFat).
if (_fsExtra) {
static const char* const kMoveFiles[] = { "/contacts3", "/channels2" };
for (const char* nm : kMoveFiles) {
if (_fsExtra->exists(nm)) continue; // already on the card
if (!_fs->exists(nm)) continue; // nothing to move
File in = openRead(_fs, nm);
File out = openWrite(_fsExtra, nm);
bool ok = in && out;
if (ok) {
uint8_t buf[256];
int n;
while ((n = in.read(buf, sizeof(buf))) > 0) {
if (out.write(buf, (size_t)n) != (size_t)n) { ok = false; break; }
}
}
if (in) in.close();
if (out) out.close();
if (ok) _fs->remove(nm); // reclaim SPIFFS space
else if (_fsExtra->exists(nm)) _fsExtra->remove(nm); // don't leave a partial copy
}
}
#endif
#endif
}
@@ -207,12 +243,34 @@ bool DataStore::saveMainIdentity(const mesh::LocalIdentity &identity) {
}
void DataStore::loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon) {
if (_fs->exists(_rp("/new_prefs"))) {
// Probe by OPENING, never exists(): on the Tanmatsu's internal FFat the FAT
// metadata layer lies (f_stat garbage — the tile cache hit the same thing),
// and WHICH lie you get shifts with the build (the -Og to -Os switch turned
// "loaded fine by luck" into "no prefs found -> default name every boot").
// open()+read is truthful there, and identical in behavior everywhere else.
auto probe = [this](const char* nm) -> bool {
File f = openRead(nm);
if (!f) return false;
uint8_t b;
const bool ok = f.read(&b, 1) == 1; // size() lies on the P4's FFat too — actually read
f.close();
return ok;
};
if (probe("/new_prefs")) {
loadPrefsInt("/new_prefs", prefs, node_lat, node_lon); // new filename
} else if (_fs->exists(_rp("/node_prefs"))) {
} else if (probe("/new_prefs.tmp")) {
// Main file gone but a staged copy exists: a reboot landed between the temp
// write and the swap (or the swap was torn). Recover from it — this is the
// "device booted with the default name once" failure mode.
MESH_DEBUG_PRINTLN("DataStore: /new_prefs missing, recovering from .tmp");
loadPrefsInt("/new_prefs.tmp", prefs, node_lat, node_lon);
savePrefs(prefs, node_lat, node_lon); // re-establish the main file
} else if (probe("/node_prefs")) {
loadPrefsInt("/node_prefs", prefs, node_lat, node_lon);
savePrefs(prefs, node_lat, node_lon); // save to new filename
_fs->remove(_rp("/node_prefs")); // remove old
} else {
MESH_DEBUG_PRINTLN("DataStore: no prefs file found — using defaults");
}
}
@@ -306,8 +364,12 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
}
}
void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_lon) {
File file = openWrite(_fs, "/new_prefs");
bool DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_lon) {
// Write to a temp file first and swap it in afterwards: a reboot / power cut
// mid-write can then never destroy the only copy (the loader recovers from
// whichever file survived). SPIFFS has no atomic rename-over, so the swap is
// remove+rename — the loader handles the tiny between-steps window too.
File file = openWrite(_fs, "/new_prefs.tmp");
if (file) {
uint8_t pad[8];
memset(pad, 0, sizeof(pad));
@@ -344,10 +406,17 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 91
file.write((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 92
file.write((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 93
file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 125
size_t last = file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 125
file.close();
if (last != sizeof(_prefs.default_scope_key)) {
_fs->remove(_rp("/new_prefs.tmp")); // short write (storage full?) — keep the old main file
return false;
}
_fs->remove(_rp("/new_prefs"));
return _fs->rename(_rp("/new_prefs.tmp"), _rp("/new_prefs"));
}
return false;
}
void DataStore::loadContacts(DataStoreHost* host) {
@@ -382,33 +451,84 @@ File file = openRead(_getContactsChannelsFS(), "/contacts3");
}
void DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c)) {
#if defined(ESP32)
// A full/fragmenting contacts write on SPIFFS can trigger a multi-second GC
// pass that disables the flash cache and starves core 0's idle task, tripping
// the task watchdog (the beta_25 reboot loop, confirmed by coredump: loopTask
// stuck in spiffs_gc_clean under this exact call). Suspend that watchdog for
// the write so a bounded-but-slow pass stalls the UI instead of rebooting. The
// ref-count is shared with the UI history/backup guards, so a backup import
// that calls saveContacts stays balanced. On SD-routed devices this is cheap
// (FAT has no such GC); it matters most for card-less SPIFFS devices.
WdtHeavyGuard _wdt;
#endif
File file = openWrite(_getContactsChannelsFS(), "/contacts3");
if (file) {
uint32_t idx = 0;
ContactInfo c;
uint8_t unused = 0;
while (host->getContactForSave(idx, c)) {
if (filter && !filter(c)) {
// Field-by-field writes cost ~12 FS calls per contact — ~7000 tiny writes at
// ~570 contacts measured 2.6-3.0 s on SD, freezing the loop/UI for the whole
// save (issue #82). Pack the SAME 152-byte records into a heap chunk and
// flush in ~5 KB slabs instead: identical file bytes, ~20 large writes, tens
// of ms. Falls back to the original per-field path if the alloc fails.
static const size_t REC = 152; // 32+32+1+1+1+4+1+4+64+4+4+4
static const size_t CHUNK_RECS = 32;
uint8_t* buf = (uint8_t*)malloc(REC * CHUNK_RECS);
if (buf) {
size_t fill = 0;
bool ok = true;
while (ok && host->getContactForSave(idx, c)) {
if (filter && !filter(c)) {
idx++; // advance to next contact
continue;
}
uint8_t* p = buf + fill;
memcpy(p, c.id.pub_key, 32); p += 32;
memcpy(p, (uint8_t *)&c.name, 32); p += 32;
*p++ = c.type;
*p++ = c.flags;
*p++ = unused;
memcpy(p, (uint8_t *)&c.sync_since, 4); p += 4;
memcpy(p, (uint8_t *)&c.out_path_len, 1); p += 1;
memcpy(p, (uint8_t *)&c.last_advert_timestamp, 4); p += 4;
memcpy(p, c.out_path, 64); p += 64;
memcpy(p, (uint8_t *)&c.lastmod, 4); p += 4;
memcpy(p, (uint8_t *)&c.gps_lat, 4); p += 4;
memcpy(p, (uint8_t *)&c.gps_lon, 4); p += 4;
fill += REC;
if (fill == REC * CHUNK_RECS) {
ok = (file.write(buf, fill) == fill);
fill = 0;
}
idx++; // advance to next contact
continue;
}
bool success = (file.write(c.id.pub_key, 32) == 32);
success = success && (file.write((uint8_t *)&c.name, 32) == 32);
success = success && (file.write(&c.type, 1) == 1);
success = success && (file.write(&c.flags, 1) == 1);
success = success && (file.write(&unused, 1) == 1);
success = success && (file.write((uint8_t *)&c.sync_since, 4) == 4);
success = success && (file.write((uint8_t *)&c.out_path_len, 1) == 1);
success = success && (file.write((uint8_t *)&c.last_advert_timestamp, 4) == 4);
success = success && (file.write(c.out_path, 64) == 64);
success = success && (file.write((uint8_t *)&c.lastmod, 4) == 4);
success = success && (file.write((uint8_t *)&c.gps_lat, 4) == 4);
success = success && (file.write((uint8_t *)&c.gps_lon, 4) == 4);
if (ok && fill > 0) file.write(buf, fill);
free(buf);
} else {
while (host->getContactForSave(idx, c)) {
if (filter && !filter(c)) {
idx++; // advance to next contact
continue;
}
bool success = (file.write(c.id.pub_key, 32) == 32);
success = success && (file.write((uint8_t *)&c.name, 32) == 32);
success = success && (file.write(&c.type, 1) == 1);
success = success && (file.write(&c.flags, 1) == 1);
success = success && (file.write(&unused, 1) == 1);
success = success && (file.write((uint8_t *)&c.sync_since, 4) == 4);
success = success && (file.write((uint8_t *)&c.out_path_len, 1) == 1);
success = success && (file.write((uint8_t *)&c.last_advert_timestamp, 4) == 4);
success = success && (file.write(c.out_path, 64) == 64);
success = success && (file.write((uint8_t *)&c.lastmod, 4) == 4);
success = success && (file.write((uint8_t *)&c.gps_lat, 4) == 4);
success = success && (file.write((uint8_t *)&c.gps_lon, 4) == 4);
if (!success) break; // write failed
if (!success) break; // write failed
idx++; // advance to next contact
idx++; // advance to next contact
}
}
file.close();
}
@@ -714,3 +834,33 @@ bool DataStore::useSdStorage() {
return true;
}
#endif
#if defined(HAS_TANMATSU)
// Tanmatsu: same full-store adoption as useSdStorage(), but on the SD_MMC slot.
// The internal FFat 'locfd' has a broken FAT metadata layer (f_stat/exists lie —
// see the tile-cache notes), and the exists()-gated identity + prefs loads that
// "worked" at -Og read different garbage at -Os and came up empty: fresh node
// identity, default name, profile changes gone every reboot. The card's FAT
// metadata is truthful, so identity/prefs move there with everything else.
// The caller migrates any FFat-resident files first (open()-probed, never
// exists() on FFat).
bool DataStore::useSdMmcStorage() {
if (!SD_MMC.exists("/meshcomod")) SD_MMC.mkdir("/meshcomod");
if (!SD_MMC.exists("/meshcomod/bl")) SD_MMC.mkdir("/meshcomod/bl");
if (!SD_MMC.exists("/meshcomod/identity")) SD_MMC.mkdir("/meshcomod/identity");
// The old secondary-FS layout kept contacts/channels/blobs at the CARD ROOT;
// pull them under /meshcomod so the rooted store keeps reading them.
static const char* k_move[] = { "/contacts3", "/channels2", "/adv_blobs" };
for (const char* nm : k_move) {
char dst[40];
snprintf(dst, sizeof dst, "/meshcomod%s", nm);
if (SD_MMC.exists(nm) && !SD_MMC.exists(dst)) SD_MMC.rename(nm, dst);
}
strncpy(_root, "/meshcomod", sizeof(_root) - 1);
_root[sizeof(_root) - 1] = '\0';
_fs = &SD_MMC;
_fsExtra = nullptr;
identity_store.use(SD_MMC, "/meshcomod/identity");
return true;
}
#endif
+22 -1
View File
@@ -42,13 +42,26 @@ public:
// contacts, channels, blobs). Call after the SD card is mounted. Returns false
// if the dir can't be created. No-op on non-ESP32.
bool useSdStorage();
#endif
#if defined(HAS_TANMATSU)
// Full-store adoption of the SD_MMC card (Tanmatsu): identity + prefs move off
// the broken-metadata internal FFat (its exists()/f_stat lie, which made the
// gated loads come up empty). Caller migrates FFat-resident files first.
bool useSdMmcStorage();
#endif
FILESYSTEM* getPrimaryFS() const { return _fs; }
FILESYSTEM* getSecondaryFS() const { return _fsExtra; }
// Route contacts + channels (see _getContactsChannelsFS) to a different FS than identity/prefs.
// Used on the Tanmatsu to keep identity/prefs on the (working) internal FFat while putting the
// frequently-rewritten contacts/channels on the reliable SD card. Call before begin().
void setSecondaryFS(FILESYSTEM* fs) { _fsExtra = fs; }
bool loadMainIdentity(mesh::LocalIdentity &identity);
bool saveMainIdentity(const mesh::LocalIdentity &identity);
void loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon);
void savePrefs(const NodePrefs& prefs, double node_lat, double node_lon);
// Crash-safe: writes /new_prefs.tmp, then swaps it in. Returns false when the
// write or the swap failed (storage full / torn) so callers can surface it —
// the save used to fail silently and the UI toasted "saved" regardless.
bool savePrefs(const NodePrefs& prefs, double node_lat, double node_lon);
void loadContacts(DataStoreHost* host);
void saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c) = NULL);
void loadChannels(DataStoreHost* host);
@@ -63,6 +76,14 @@ public:
bool removeFile(FILESYSTEM* fs, const char* filename);
uint32_t getStorageUsedKb() const;
uint32_t getStorageTotalKb() const;
#if defined(ESP32)
// True while contacts/channels live on the internal flash FS (SPIFFS/LittleFS),
// whose garbage collection makes a full rewrite expensive — a multi-second GC
// pass that freezes the loop. False once routed to an SD card (useSdStorage sets
// _root; setSecondaryFS sets _fsExtra) — FAT has no such GC. Lets callers coalesce
// the advert-driven contacts save on card-less devices without changing SD boards.
bool contactsOnInternalFlash() const { return _root[0] == '\0' && _fsExtra == nullptr; }
#endif
private:
FILESYSTEM* _getContactsChannelsFS() const { if (_fsExtra) return _fsExtra; return _fs;};
Loaded 100 of 215 files, more files were not shown because too many files have changed in this diff. Show more