Encoder direction was backwards on hardware (confirmed by user testing):
kQuadTable's CW/CCW sign didn't match this physical part's wiring, so
turning the way you'd expect to move forward/down moved focus and
scroll backward/up instead. Negated once in pagerEncoderReadDelta()
so every consumer (nav NEXT/PREV, Alt+turn tab-switch, Alt+turn page
scroll) inherits the corrected sense from one place.
Also scope the pager's Alt+encoder gesture: it used to always jump
between main tabs, even from inside a settings sheet or chat, silently
abandoning it. Now Alt+turn only tab-switches at the main-tab level
(navOnMainPage()); inside a nested scrollable screen it instead
free-scrolls the page like a touch drag (navScrollFocused()'s ANIMATED
lv_obj_scroll_by, unlike navFocusCb's one-shot LV_ANIM_OFF
scroll_to_view, reliably repaints on this board), then snaps focus to
whatever's now nearest the top of the visible viewport
(navRefocusFirstVisible()) so plain NEXT/PREV resumes from a visible
spot instead of a focus stuck off-glass.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Boot TX power was left at a conservative 10dBm (Heltec V4's pattern) with
no way to raise it, unlike the T-Deck which boots straight at the chip's
real 22dBm ceiling. On hardware this was silently degrading outbound
range: adverts/DMs sent from the pager were unreliable while inbound
reception was fine, since the far node's own TX power was unaffected.
22dBm is confirmed as this chip's actual sub-GHz HP-PA ceiling via
RadioLib's LR1120::checkOutputPower() (LR1121 inherits it) and matches
trail-mate's own working config for this board.
Also widen the three RX-boosted-gain gates in MyMesh.cpp/DataStore.cpp
that only checked USE_SX1262/USE_SX1268 to include USE_LR1121 --
CustomLR1121Wrapper already implements setRxBoostedGainMode/
getRxBoostedGainMode, so the setting was silently a no-op for this radio.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Two related first-boot-wizard bugs found on hardware:
- updatePagerEncoder() woke the screen on either a turn OR a bare click.
Waking via click let the SAME press-release cycle fall straight into
the short-click logic further down as an ordinary click on whatever was
already focused -- concretely, it selected "Skip" on the wizard's
welcome screen the instant the screen lit up. Only a turn wakes the
screen now; BOOT is unchanged and still wakes via its own path.
- The region step's 20-preset list auto-jumped focus to Next on the
FIRST click, before the row's colour change was even visible -- no way
to confirm the right region got picked before focus moved off the
list. Changed to two-step: the first click on a row selects/recolours
it and focus stays put; a second click on the row that's ALREADY
selected is treated as the explicit confirm and jumps focus to Next.
Both gated CAP_KEYPAD_NAV (Tanmatsu + pager); T-Deck/Heltec V4 touch
flows are unaffected either way (moving the internal group focus is
invisible without active keypad-nav).
User-verified fixed on hardware (fresh first-boot flash, region select,
Wi-Fi connect, and map tile auto-download all working).
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
The bottom tab bar (Chats/Contacts/Home/Map/Settings) is deliberately not
a nav-group focus target, same as T-Deck/Tanmatsu -- but those boards
reach it via their own dedicated hotkeys (programmable keys / coloured
shape keys), which the pager has none of. Plain rotary turning could
therefore only ever move focus within the current screen; the tab bar
icons were unreachable from Home (or anywhere else).
Exposed pagerKeyboardAltHeld() (raw modifier state already tracked by
pagerKeyboardPoll() to drive the symbol layer, just never surfaced
before) and used it in updatePagerEncoder(): holding Alt (the bottom-left
orange key) while turning the encoder now calls navSwitchTab(+-1) per
detent instead of the normal focus-move NEXT/PREV, cycling through the
5 main tabs directly. Alt alone still types nothing, so this doesn't
take anything away from the keyboard.
User-verified fixed on hardware.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Two gaps left "hold to go back" not actually working anywhere but inside
a popup:
- The popup registry (anyPopupOpen()/hwKeyDismissTopPopup(), which drives
the actual dismiss) only recognized HAS_TDECK_GT911/HAS_TANMATSU, so it
never knew how to close the pager's own Terminal fullscreen view or its
command picker -- the encoder's long-press was sending a raw Esc key
that nothing handled. Widened to TLORA_PAGER.
- Even with that fixed, a BARE main tab (Chats, Contacts, Map, Settings)
is neither a popup nor an open chat/channel detail, so the back cascade
fell through to a no-op Esc. T-Deck/Tanmatsu don't hit this because they
have separate dedicated Home hotkeys; the pager has none (no
programmable keys, and the bottom tab bar is deliberately not a
nav-group target). Added a third rung: fall back to the Home tab before
plain Esc, matching every other "close" path in this app (the
Terminal/Files Home button, the fullscreen-view popup closer) already
landing on Home rather than a real back-stack.
User-verified fixed on hardware.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Neither input path called noteUserInput() for the common no-field-focused
case, so the idle timeout kept counting down during active use:
- handleHwKey()'s no-textarea branch only calls noteUserInput() when
dismissing a popup or matching a tab hotkey. tabForKey() no longer maps
any letters (tab jumps moved to the trackball-only programmable
hotkeys) and isDismissKey() always returns false for the pager's full
QWERTY, so with no field focused, every keypress silently no-op'd here.
- updatePagerEncoder() only called noteUserInput() to wake an already-off
screen; turning or clicking the encoder while the screen was ON never
reset the timer.
This board has no touch and no trackball to fall back on for idle-reset,
so both gaps were fatal: the screen dimmed after ~30s of real use.
User-verified fixed on hardware.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
navMaybeRebuild() re-roots keyboard/rotary focus onto lv_layer_top() as
soon as ANY child is visible there, but collected the WHOLE top layer.
Fullscreen views that spawn their own popups (Terminal's command picker,
file manager prompts/action sheets) add those popups as separate
lv_layer_top() siblings rather than nesting them in the view, so with two
visible top-layer children at once, nav walked the hidden view underneath
before ever reaching the popup on top of it (reported: opening the
terminal's command picker left rotary focus on the terminal behind it).
LVGL has no z-index — child order is stacking order — so the last
visible, non-NAV_SKIP child is the one actually on top. Added
navTopFrontmostChild() and root nav on it instead of the whole top layer.
Single-popup screens are unaffected (same one child either way).
Not board-gated: this is a general nav-focus correctness fix that
benefits any board using keypad/trackball navigation, not pager-specific.
User-verified fixed on hardware.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Two related non-touch navigation bugs on the pager's Terminal screen:
- handleHwKey()'s ENTER handling only routed to terminalSubmit() under
HAS_TDECK_GT911, so physical-keyboard Enter fell through to hideKb()
instead of running the typed command. Widened to TLORA_PAGER.
- s_nav_show (whether the amber keypad-focus ring is painted) only
defaulted true for HAS_TANMATSU ("keyboard-only device: focus highlight
is always visible"); every other board, including the pager, defaulted
false and only flips true inside CAP_TRACKBALL-gated code, which never
runs on the pager (no trackball). Since the pager also has no touch,
rotary-driven focus was moving but never rendered, so the terminal's
send button looked unselectable. Widened to TLORA_PAGER, matching the
Tanmatsu's rationale exactly.
User-verified fixed on hardware.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
makeHome()'s chart_w gate only widened the chart's cleared strip for
HAS_TDECK_GT911/HAS_TANMATSU, so the pager (landscape, same right-hand
Advert/Terminal/Apps/Control column) fell into the full-width else branch
and the chart border/Sig chip ran under the buttons. Add TLORA_PAGER to
the gate, matching the other landscape boards.
User-verified fixed on hardware.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Six real bugs found and fixed across two hardware sessions (full account in
TLORA_PAGER_M7_HW_DEBUG_LOG.md): stale-NVS BLE bond crash, no screen-wake
path for keyboard/encoder input, wrong TFT_eSPI SPI-port index on ESP32-S3,
missing panel INVON for this ST7796 batch, two SPI hosts fighting over the
shared radio/display pins, and a floating XL9555 ch6 panel hardware-reset
line (the actual root cause of the intermittent black screen, not the SPI
race originally suspected). TLoraPagerBoard::begin() now drives the full
vendor rail set including the display reset pulse and parks shared-SPI
selects high before first bus traffic.
Boot logo, full LVGL UI, QWERTY keyboard navigation, and rotary encoder
nav/select are now verified working on real hardware. Trackers updated:
Milestone 7's UI portion is done; the remaining radio/USB-companion/SD
gates move to a new Milestone 7b so they don't block Milestone 8's
on-device UI pass, which is now active.
All three envs (Heltec V4, T-Deck, pager) build green.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Adds the pager as a first-class non-touch UI target: new device_caps.h
block (no touch, hw keyboard + rotary-encoder nav, 480x222, GPS/OTA/lock
screen; SD/filesystem left off since the mount code is still hardcoded to
HAS_TDECK_GT911, not genuinely CAP_SD-generic), KEYPAD indev registration
reusing the Tanmatsu keypad plumbing, forced 480x222 landscape rotation,
keyboard/encoder input drain in the main loop, and AW9364-correct
brightness wiring ahead of the existing PWM backlight branch.
Also fixes two latent gaps this surfaced: navMaybeRebuild() was
unreachable for the pager's cap combination (would have left the focus
group permanently empty), and several HAS_TDECK_KEYBOARD-gated helpers
needed their reopened regions individually widened rather than the file
touched wholesale.
All three envs build green; Heltec V4 TFT and T-Deck binaries are
byte-identical to pre-change (25.3%/73.5%), confirming every edit stayed
behind the pager gate.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
PagerKeyboard.{h,cpp}: TCA8418 matrix keyboard over I2C. Unlike the
T-Deck's keyboard (a second MCU that resolves ASCII itself), the
TCA8418 only reports raw row/col events, so the keymap and
shift/sym/alt state machine live here -- keymap tables cribbed from
trail-mate's LilyGoKeyboard (same physical PCB). Press/release
polarity verified against the TCA8418 datasheet directly, since it
disagrees with the Adafruit library's own header comment. Backlight is
direct LEDC PWM on GPIO 46, using this framework version's actual
channel-based API (confirmed before writing the call, not assumed).
PagerEncoder.{h,cpp}: true A/B quadrature decode via a standard
Gray-code transition table, ISR-cheap-arithmetic-only like
TDeckTrackball's pattern (that driver's 4 discrete direction pins
aren't actually quadrature, so its exact shape doesn't transfer, only
the ISR/snapshot-read style does). Detent scaling (4 raw transitions
per detent) is an unverified assumption pending hardware bring-up.
Both gated on HAS_PAGER_KEYBOARD/HAS_PAGER_ENCODER, already reached by
every env's existing helpers/input build filter. All three envs build
green.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
[env:tlora_pager_lr1121_companion_radio_touch], cloned from the T-Deck
env with board/radio/display/GPS deltas for the LR1121 + ST7796 pager.
Two structural fixes were needed to actually get this to compile:
- pins_arduino.h moves to its own variants/lilygo_tlora_pager_pins/
folder (board JSON's "variant" field updated to match). PlatformIO's
arduino-esp32 build script unconditionally compiles every source
file under board_build.variants_dir/<board.variant>/ as a separate
framework-level library with none of our own lib_deps -- since this
board (unlike T-Deck/Heltec, which point at framework-bundled
variants) has no framework-bundled variant of its own, our
variants_dir override was colliding with our own board-glue files
and failing to find Wire.h/RadioLib.h.
- CustomLR1121Wrapper.h's bare quoted includes of RadioLibWrappers.h/
LR11x0Reset.h only resolve for core-lib files living in the same
directory as their targets; switched to angle-bracket includes with
the full core-lib subpath.
Also required one small, forced addition to UITask.cpp: its display
class selection only recognized the Tanmatsu vs. everything-else split,
which made this env's own hard compile gate unreachable. Added a single
#elif arm for the pager's display type: no other UITask.cpp behavior
changes, indev/resolution/layout wiring is still a later milestone.
All three envs (Heltec V4, T-Deck, pager) build green.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
ST7796LCDDisplay implements the core's DisplayDriver interface on
TFT_eSPI for the pager's 480x222 landscape panel -- the first real
TFT_eSPI consumer in this codebase, since ST7789LCDDisplay (the sibling
this was modeled on) turns out to be Adafruit_GFX-based for both
Heltec V4 and T-Deck. Every TFT_eSPI call was verified against the
pinned library source rather than assumed.
Backlight is the AW9364 stepped pulse-dimmer, wrapped directly in the
display class via SensorLib's AW9364LedDriver (already a dependency)
rather than a hand-rolled pulse timer, exposing a pct-based
setBrightness()/getBrightness() hook for a later UITask.cpp wiring
pass.
Also adds a compile-time guard for CGRAM_OFFSET: this panel's 222px
glass is narrower than the ST7796 controller's native GRAM, and
TFT_eSPI only applies the required coordinate offset when that flag is
set. Without the guard, a missing flag would build clean but silently
shift every frame by 49px.
Nothing references these files yet, so the shipping T-Deck/Heltec V4
envs are unaffected.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
TLORA_PAGER_PORT.md is the living status/decisions/risks tracker for
the T-LoRa Pager port; TLORA_PAGER_PORT_MILESTONES.md breaks the work
into ordered, agent-executable milestones with gates. Kept off main
until the port is further along.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
CustomLR1121{,Wrapper}.h are adapted from the core fork's CustomLR1110
pair (upstream MeshCore has no LR1121 support yet to crib instead).
target.cpp's radio_init() spells out the LR1121 bring-up sequence
directly since there's no std_init() helper for this chip: begin()
with the NodePrefs-driven params and TCXO voltage passed straight in,
the DIO5/DIO6 RF-switch table, and a CRC-length override to match the
other radio wrappers' wire-protocol framing. Nothing references these
files yet, so the shipping T-Deck/Heltec V4 envs are unaffected.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
Board class, pins, and partition table for the LilyGo T-LoRa Pager
(ESP32-S3, LR1121 radio variant), modeled on the T-Deck's shape.
TLoraPagerBoard brings up the XL9555 power-rail expander and BQ27220
fuel gauge over I2C (this board has no VBAT ADC divider). Nothing
references these files yet, so the shipping T-Deck/Heltec V4 envs are
unaffected; radio/display/env wiring land in follow-up commits.
Signed-off-by: Tesso M Costa <tesso.martins@gmail.com>
- 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>
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>
- 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>
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>
- 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>
- 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>
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>
- 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>
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>
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>
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>
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>
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>
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>
- 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>
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.
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>
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>