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>
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>
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>
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>
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>
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>
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>
- 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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
'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>
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>
#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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>