Commit Graph
4086 Commits
Author SHA1 Message Date
JakubandClaude Opus 5.5 87fad71af3 feat(ui-lvgl): 320x240 browser simulator, compose field fixes
Simulator: SIM_UI=lvgl variants/sim/build_wasm.sh builds ui-lvgl for the
browser. SimLcdDisplay blits LVGL's RGB565 flush to a 320x240 canvas and
LvglPort.h's sim branch feeds the mouse in as touch; LVGL comes from the
L2 env's libdeps, its objects cached in web/build/obj_lvgl/. web/lvgl.html
runs it beside a ui-new peer on a direct link, with a window.sim API for
headless checks. New hooks: sim_lcd_touch, sim_lcd_button,
sim_test_send_channel_msg.

Compose: the theme's padding left less than one line in the field, so it
scrolled vertically and text jumped; pad it to fit exactly one line. The
cursor is drawn only while FOCUSED, now set while the keyboard is up.
With the keyboard up the screen header hides so the conversation keeps a
message in view; the accent popup stays inside the keyboard, clear of the
field; channel bubbles put the age next to the sender (one line shorter);
the thread scrollbar shows only while scrolling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 00:36:26 +02:00
JakubandClaude Opus 5.5 bf1e6fcaa0 feat(ui-lvgl): European fonts and phone-style keyboard
Fonts: Noto Sans generated with lv_font_conv (ui-lvgl/fonts/generate.sh,
OFL) replaces ASCII-only Montserrat: European Latin (Latin-1, Extended-A/B),
Greek, Cyrillic + supplement, typographic punctuation, currency, LVGL
symbols + globe; 12/14/16/20 px, clock 40 px digits only (RLE, so
LV_USE_FONT_COMPRESSED is on).

Keyboard (ui-lvgl/Keyboard.h): one layout per script (Latin QWERTY,
Cyrillic, Greek) picked by the same keyboard_main/alt_alphabet prefs as
ui-new, globe key to switch, hold a key for its variants in a popup above
it, one-shot shift, symbols page; compose limit counted in UTF-8 bytes.
First Settings screen (Keyboard: Main / Additional).

Long-press variants and UTF-8 case mapping move to ui-core/KeyboardData.h
and are shared with ui-new, whose Hold-Enter accents gain Romanian,
Turkish, Hungarian, Baltic and Icelandic letters (and ș/ț, Ukrainian /
Serbian letters now capitalise).

Verified on L2 hardware.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 00:08:15 +02:00
JakubandClaude Opus 5.5 cd926b7e45 feat(ui-lvgl): LVGL 9 skeleton frontend for Wio Tracker L2
New examples/companion_radio/ui-lvgl/ over the shared UI Core (env
Wio_Tracker_L2_companion_solo_lvgl, LVGL 9.2.2, pool in PSRAM):
status bar, home (clock, unread), conversation list (channels + recent
DMs with unread badges), contact picker, conversation view with bubbles,
delivery / relay markers and on-screen keyboard, toasts for Core events,
display auto-off with wake on touch / button. Board glue (LovyanGFX
flush + GT911 touch) isolated in LvglPort.h; design tokens in Theme.h.

UiCore gains the first frontend actions, sendDirectText() and
sendChannelText(), and drives DM auto-resend + unread reconcile from its
own loop (removed from ui-new's loop). MessageHistory exposes newest-first
DM ring iteration and a public contactByPrefix().

Verified on L2 hardware: navigation, send/receive, sleep/wake.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:49:42 +02:00
JakubandClaude Opus 5.5 74db0d69a5 refactor(ui-core): UiCore becomes MyMesh's Listener for ui-new
ui-new hands MyMesh the UI Core (UITaskBase::meshListener()). The Core
applies the display filter, labels room posts, files history, keeps DM /
channel / room unread and the queue count, and reports incoming messages
and adverts as MessageArrived / AdvertHeard events. UITask now derives
UITaskBase + UiCoreHost: it answers "is this conversation on screen" and
keeps the selection after an insert, forwards not-yet-extracted parts
(room login / admin, bot device actions, removal cleanup, shutdown), and
turns events into alert / wake / sound, drained at the start and end of
its loop(). Mesh callbacks no longer touch the display or buzzer.

AbstractUITask is split into UITaskBase (lifecycle, link state, helpers)
and the Listener glue ui-orig / ui-tiny keep using unchanged. Screenshot
handler reaches ui_task directly instead of casting the Listener.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:28:58 +02:00
JakubandClaude Opus 5.5 124265b8be refactor(ui-core): move GPS trail sampling into TrailEngine
TrailStore, background sampling, auto-pause and the pre-shutdown
auto-save move from UITask to ui-core/TrailEngine.h. TrailScreen still
drives the store directly. The 512-point store now lives on the heap
with the rest of UiCore (static RAM -8 KB on Wio L1, same total).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:17:03 +02:00
JakubandClaude Opus 5.5 9e5ce72c86 refactor(ui-core): move locator into LocatorEngine
Active target (set/clear/resolve person position), geofence crossing
state machine and proximity beeper move from UITask to
ui-core/LocatorEngine.h, emitting LocatorCrossed / LocatorBeep events.
UiEvent gains a flag and a 24-byte text. tickCore() now runs at the end
of UITask::loop(), where the moved engines used to run, so a fresh GPS
wake re-seeds the locator before it evaluates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:15:18 +02:00
JakubandClaude Opus 5.5 74baaddd0c refactor(ui-core): move live location sharing into LiveShareEngine
Auto-[LOC] session (duration, movement/heartbeat gate, scoped send) and
the peers' LiveTrackStore + expiry move from UITask to
ui-core/LiveShareEngine.h. "Live share ended" becomes a
UiEventType::LiveShareEnded event; UITask keeps thin wrappers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:09:26 +02:00
JakubandClaude Opus 5.5 79a3a47ff1 refactor(ui-core): move position + course over ground into CourseEngine
COG ring sampling (every ~1 s from UiCore::loop), outlier rejection and
currentCourse/currentLocation move from UITask to
ui-core/CourseEngine.h. UITask keeps one-line wrappers; screens unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:07:32 +02:00
JakubandClaude Opus 5.5 311a3484e2 refactor(ui-core): move ping state into PingEngine
Single in-flight ping + last result move from UITask to
ui-core/PingEngine.h; MyMesh's ping callback routes to the engine
instead of casting the Listener back to UITask. UITask keeps thin
wrappers (and the "not supported" alert), NearbyScreen is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:05:39 +02:00
JakubandClaude Opus 5.5 dd826a447e refactor(ui-core): move clock tools into ClockEngine; add Core event queue
Alarm / countdown / ring logic moves from UITask to ui-core/ClockEngine.h
unchanged. New ui-core/UiEvents.h: fixed-size Core -> frontend event
queue; UITask::tickCore() runs UiCore::loop() and reacts to ClockAlert /
ClockRingEnded (wake, alert overlay, melody). ClockToolsScreen API unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:03:57 +02:00
JakubandClaude Opus 5.5 3b7aa28ff9 refactor(ui-core): add UI Core skeleton; move MessageHistory + DM unread into it
New examples/companion_radio/ui-core/ (header-only for now, reached from
ui-new/UITask.cpp by relative include, so no platformio.ini changes):
UiCore facade, MessageHistory (moved verbatim), DmUnreadTable (extracted
from UITask). UITask heap-allocates the Core in begin(); MessagesScreen
binds to core().history. Behaviour unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:00:46 +02:00
JakubandClaude Opus 5.5 2a8d4598d4 refactor: port upstream MyMesh::Listener; MyMesh no longer calls the UI directly
Ports upstream's "Abstract UI overhaul" (PR #3431 + follow-ups) with
identical names/signatures. MyMesh talks to the UI only through
MyMesh::Listener (setListener()); a second block holds Solo extensions
(own-send mirroring, relay echoes, room login/admin replies, [LOC] shares,
contact/channel removal, bot device actions, requestShutdown), all no-op
by default.

AbstractUITask becomes the Listener and carries the glue MyMesh used to
run for the UI (display filter, room-post labels, notifications,
connection polling). BLE PIN randomisation moves to main.cpp as upstream.
Also fixes ui-orig's stale advertReceived event on buzzer boards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 22:54:40 +02:00
JakubandClaude Opus 5.5 f768e73f9f docs: UI Core + frontends design (draft)
Shared hardware-independent UI Core (engines, models, declarative
settings schema) behind MyMesh's AbstractUITask, with ui-lvgl (rich,
Wio Tracker L2 first) and ui-new (lite, OLED/e-ink/nRF52) as frontends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 22:22:37 +02:00
JakubandClaude Opus 5.5 221c33ec5f feat(wio-tracker-l2): board layer + ui-new bring-up env
Seeed Wio Tracker L2 (ESP32-S3R8 16MB/8MB PSRAM, SX1262, NV3031B 320x240
QSPI LCD + GT911 touch + LP5814 backlight, TCA9535 expander gating every
rail, ADS1115 battery ADC, AW35615 USB-C, L76K GNSS).

Board layer ported from meshcore-dev/MeshCore PR #3381 (Hacuchino-hash),
kept at the same paths for an easy upstream merge. Differences:
- display: dropped the UIColor theme (not adopted in this fork)
- GpsTap diagnostics wrapper omitted; GNSS reads Serial1 directly
- nv3031b ODR fix dropped: LovyanGFX >= 1.2.30 (now pinned) defines
  Panel_NV3031B::init_cmds itself -- the fix became a duplicate symbol
- MESH_DEBUG_PRINTLN with a non-literal format fixed (broke MESH_DEBUG builds)
- upload_speed 115200: baud is virtual on native USB; skips the switch

solo/wio-tracker-l2 adds Wio_Tracker_L2_companion_solo_bringup: ui-new
zoomed 2x, BOOT button only, no touch -- a hardware smoke test, not a
release target (deliberately not *_solo_dual, so CI doesn't publish it).
The real UI for this board will be a new LVGL frontend.

Verified on hardware: boot, display, radio, GPS, battery, BLE app link.
First boot takes tens of seconds while SPIFFS formats its 3.4 MB partition.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 21:53:18 +02:00
JakubandClaude Opus 5.5 1620df6c10 fix: move FEAT_RX_POWERSAVE to Features.h; no extra [LOC] on "Stop after" change
- FEAT_RX_POWERSAVE lived in MyMesh.h, but SettingsScreen.h only pulls
  Features.h -- it saw the flag only via include order, and an undefined
  macro in `#if` silently reads as 0, which would split the build the day
  it's flipped to 1. Now defined in Features.h (the home of FEAT_* flags),
  which MyMesh.h includes; comment/doc pointers updated.
- Changing Live Share's "Stop after" restarted the whole session, forcing
  an immediate re-announce [LOC] although the position hadn't changed. It
  now restarts only the duration clock (restartLocShareClock()).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:50:37 +02:00
JakubandClaude Opus 5.5 299fcf2c13 fix(ui): correct DM unread overflow check and aggregate/scope edge cases
- DM unread: the ring-eviction check ran in newMsg(), which MyMesh calls
  before addDMMsg(), so the new message itself read as evicted -- the
  first DM from a contact showed no badge and every later one was one
  short with a false "+". Moved into addDMMsg() after the insert, and
  run for every slot, so a contact whose unread entry was evicted by
  someone else's message is flagged right away too.
- Channel overflow aggregate: ignore channels whose unread count is 0
  (eviction can zero it with the flag left set), so the dashboard no
  longer shows "0+ msgs" or a "+" borrowed from another conversation.
- Live Share scope: a stale index past the list's end now follows the
  target, as the screen shows, instead of silently sending unscoped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:46:18 +02:00
JakubandClaude Opus 5.5 22eba3c244 fix(radio): keep the radio serviced while an e-ink refresh blocks the loop
GxEPD2 polls the panel's BUSY pin for the whole refresh (up to ~1-2s),
stalling the main loop. A TX finishing in that window left the SX126x in
standby -- deaf -- until the refresh ended; packets received meanwhile could
be lost (only the latest is readable, and a later CRC failure rejects a good
one still pending). Hook GxEPD2's busy callback to finish TX / drain RX into
a small staging queue and re-arm right away, without touching dispatch/UI.
Wired only where the display driver supports it (GxEPD2: the two e-ink Solo
boards); OLED and sim builds are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:21:38 +02:00
JakubandClaude Sonnet 5 81cee3846f feat(ui): flag unread counts understated by a full history ring
DM and channel history each live in a fixed-size ring (32 DM / 48
channel entries). A conversation that outpaces reading can fill its
ring, evicting the oldest entries — including unread ones that were
never opened — so the unread badge silently caps at ring capacity
instead of reflecting how many messages actually arrived.

MessageHistory now tracks a sticky per-channel overflow flag, set at
the exact point an unread channel entry gets evicted (mirroring the
existing unread-decrement logic) and cleared once the count reads back
down to 0. DMUnreadEntry gains the same bit for DM contacts, detected
by comparing the raw per-contact counter against actual ring occupancy
on each new message (the DM ring, unlike the channel ring, doesn't
proactively decrement on eviction).

DisplayDriver::drawUnreadBadge/unreadBadgeWidth/fmtBadgeCount take an
optional overflow flag (default false, existing call sites unchanged)
that appends "+" to the badge. Wired into every place an unread count
is shown: DM/favourites and channel lists, the DM/Channels/Rooms
mode-select screen, favourite tiles, and the aggregate "Msgs" field on
the clock and lock dashboards plus the Messages home-carousel page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 13:45:07 +02:00
JakubandClaude Sonnet 5 26e6bc877a fix(radio): disable Pwr save (RX duty-cycle) — unsafe across mixed firmware
Field report: a user on the stock SF8 "EU/UK (Narrow)" preset saw reception
drop from ~1-5 msg/min to ~1/3h with Pwr save on, unaffected by a better
antenna. Root cause, confirmed against the SX1262 datasheet and RadioLib's
own maintainers (jgromes/RadioLib#1597, closed as inherent chip behaviour):
the SX126x's duty-cycle preamble detection needs the sender's actual
preamble to closely match what we've configured our receiver to expect.
A mismatch — e.g. a repeater still on pre-v1.16 firmware sending 16 symbols
against our 32 — doesn't cost a little sensitivity, it silently drops every
packet from that sender regardless of signal strength. There's no local
parameter fix: shortening minSymbols to tolerate shorter preambles directly
shortens the wake-window's correlator dwell time, trading the preamble
mismatch failure mode for a marginal-signal one instead. Checked whether
IoTThinks' MeshCore fork solved this differently — it doesn't hit the
problem at all, because its "power saving" only light-sleeps the MCU on the
radio's own GPIO interrupt and never touches RX duty-cycle.

examples/companion_radio/MyMesh.h now defines FEAT_RX_POWERSAVE 0, gating
out the Settings row, the Diagnostics RXPS watchdog row, and every call site
that would apply rx_powersave to the radio or CAD auto-enable — including
forcing setPowerSaving(false) unconditionally so a stale rx_powersave=1 byte
from before this change can't do anything either. The real duty-cycle
implementation in RadioLibWrapper/CustomSX1262Wrapper is left in place,
unneutered, for if a network-wide compatibility mechanism (e.g. the
still-unused ADV_FEAT1/FEAT2 advert bits) is ever built to make it safe.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 20:04:42 +02:00
JakubandClaude Sonnet 5 f8cd24dd4d fix(ui): keep the hop/ack marker clear of the age on long sender names
The marker was placed after the sender's full width while the name itself
was only ellipsized against the age, so a long name pushed the hop count
(worst with two digits) onto the age. The name now yields to the marker and
the age, the marker follows the width the name actually got, and a 2px gap
separates marker from age.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 20:18:25 +02:00
JakubandClaude Sonnet 5 4af6d7d013 feat(live-share): own scope for [LOC] posts and time-limited sessions (#23, #24)
- Live Share gets a "Scope" row (Target / * / any scope from the shared
  list). It applies to [LOC] sends only, beats the app's send_scope and a
  channel's own pick, and never touches normal messages. Target keeps the
  previous behaviour. removeScope() fixes the stored index up.
- Auto share now always ends: "Stop after" 1/2/4/8/12 h (default 1 h, no
  unlimited option). On expiry it switches itself off and shows "Live share
  ended". The session clock is RAM-only, so a reboot starts a new session.
- NodePrefs: loc_share_scope + loc_share_duration_idx appended, sentinel
  0xC0DE002C -> 0xC0DE002E, sizeof unchanged (2824).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 14:22:27 +02:00
JakubandClaude Sonnet 5 dafd6c1266 ci(sim): show the real version in the WASM demo's splash
build_wasm.sh now takes FIRMWARE_VERSION (tag name for a release,
dev-<commit> otherwise) exported by build-solo-sim.yml, so the live demo
no longer falls back to "dev-<compile date>" on tagged releases.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 14:22:27 +02:00
JakubandClaude Sonnet 5 d49acc4e55 fix(ui): age messages from the device clock, not the phone's timestamp
A DM/channel post sent from the phone app was mirrored into the device history
with the app's timestamp. A phone clock ahead of the device's (or on local
time) made "now - timestamp" negative, which fmtAgeShort clamps to 0, so the
age sat at "0s" until the device clock caught up.

MessageHistory::displayTimestamp(): outgoing entries use the device's own
clock (they were sent just now); incoming entries with an unknown or future
sender timestamp use receipt time. Sender timestamps still drive incoming
dedupe and resends (msg_ts is untouched); an on-air timestamp is unchanged.

Release notes: add this fix, and say "older than v1.27" for the prefs cleanup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
v1.28
2026-09-19 13:19:46 +02:00
JakubandClaude Sonnet 5 dff56a3c53 fix(companion): restore the v1.27 -> v1.28 scope migrations
eed6d31d dropped two one-time migrations as "old cruft", but both are needed
by exactly the upgrade path this release ships: v1.27 has sentinel 0xC0DE002A
and the scope list is new in this cycle.

- loadScopeList() again turns an existing single default_scope_name/key into
  list entry 1 (default) and MyMesh::begin() seeds the channels that already
  exist with it. Without it an upgrader's DMs and channels silently go out
  unscoped, contradicting the release note.
- loadPrefsInt() again zeroes repeat_extra_scope_mask/ch_scope_idx when the
  file predates 0xC0DE002B, so the old sentinel tail can't read back as real
  scope picks.

The older-than-v1.27 backfills stay removed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 13:19:46 +02:00
JakubandClaude Sonnet 5 745aefcee1 fix(radio): init SX1262 at a safe power on boards with an external-PA curve
95e3cf26 raised LORA_TX_POWER to 30 for the GAT562 30S, but CustomSX1262 passed
it straight to RadioLib's begin(), whose setOutputPower() rejects anything over
22 dBm (ERR_INVALID_OUTPUT_POWER) -- so radio init would fail on that board.
With NUM_PA_POINTS/TX_GAIN_LORA defined, LORA_TX_POWER is a radiated power, not
a chip register value: init at register 0 and let RadioLibWrapper::setTxPower()
apply the real power through the curve right after (companion applyApc(),
repeater setTxPower at begin). Other boards are unchanged.

Verified by build (GAT562 30S companion + repeater, Heltec V3 repeater) and by
reading RadioLib's SX1262::checkOutputPower(); not tested on hardware.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 13:19:46 +02:00
JakubandClaude Sonnet 5 b3e3bae21b fix(ui): refuse adding a channel whose secret already exists
The on-device "+ Add channel" flow (Public quick-add, Hashtag, Private) wrote
to the first free slot with no check, so a second Public -- or any channel
already present -- could be added, giving the same on-air channel two list
entries with split history/unread state. A channel's identity is its secret,
not its name, so ChannelsView now rejects a save whose 16-byte secret is held
by a different slot. Edit skips its own slot, so renaming still works. Public
quick-add says "Already added"; the forms say "Channel already exists" and stay
open. App-side CMD_SET_CHANNEL and already-saved duplicates are untouched.

Verified: sim_companion_radio builds; in the WASM sim, adding Public with
Public present shows the alert and leaves one entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 13:01:41 +02:00
JakubandClaude Sonnet 5 461107e31e fix(ui): keep a gutter beside chat bubbles; docs + release notes catch-up for v1.28
- Message bubbles are capped at the row width minus a gutter (2 char widths,
  min 8px) so a long message no longer fills the whole row and loses the
  left/right anchoring that shows who sent it. Body wrapping uses the capped
  width. DM and channel history.
- Docs: shared scope list (Settings, Repeater Extra scopes, channel Scope row),
  contact Expire / Prune now, history hop count / wrap / scope tag, external
  PA TX curve build flags.
- release-notes.md: v1.28 gains GAT562 30S TX power, Heltec V3/V4 joystick
  Enter fix, prefs-migration removal note, and the bubble gutter fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 12:58:37 +02:00
JakubandClaude Sonnet 5 1981826a85 fix(heltec_v3/v4): user_btn needs pull-up + multiclick=false in joystick mode
With UI_HAS_JOYSTICK, user_btn (Enter) was left with pulldownup=false and
multiclick=true (the plain-PRG-button defaults), instead of matching the
other wired joystick contacts. Two effects on a wired Enter button:

- No internal pull-up on the Enter pin, unlike the direction/back contacts.
- multiclick=true buffers a click for ~280ms waiting for a possible double/
  triple click, but UITask.cpp's UI_HAS_JOYSTICK loop only ever checks
  user_btn for CLICK/LONG_PRESS — so a quick double-tap collapses into a
  DOUBLE_CLICK event that's silently dropped, making Enter feel laggy and
  unreliable.

wio-tracker-l1 (factory joystick) already builds user_btn with
pulldownup/multiclick matching its other contacts; apply the same pattern
here, conditional on UI_HAS_JOYSTICK so the stock external-pull-up PRG
button path (no joystick) is unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 09:57:29 +02:00
JakubandClaude Sonnet 5 eed6d31d42 refactor(companion): drop one-time prefs-migration cruft, fix stale Msg wake comments
loadPrefsInt()'s "sentinel mismatch" block had accumulated ~62 lines of
one-shot backfills tied to specific old NodePrefs::SCHEMA_SENTINEL values
(favourites/map home-page bits, split bot-command toggles, trail_units_idx,
bot_trigger_ch seed, scope mask/index zeroing). Reduce it to just the
sentinel read + mismatch log line -- an upgrade from a version old enough
to hit any of these now lands on today's plain defaults for that field
instead of a silently-seeded value, same outcome a fresh device already
gets. The ongoing per-field range clamps (defensive against any corrupt or
out-of-range byte, not version-specific) are untouched.

Same treatment for DataStore::loadScopeList()'s one-time synthesis of a
scope-list entry from the old single default_scope_name/key field: removed,
along with the scope_migrated_legacy plumbing in MyMesh::begin() that only
ran once that migration had just fired. loadScopeList() drops its now-
unused NodePrefs parameter and bool return.

Also trimmed ~25 per-field comments that over-specified which exact old
sentinel produced which stray byte, down to what the clamp actually
defends against -- except where the hex value explained a genuinely
non-obvious byte-aliasing interaction, which stays. One comment (scope
mask/index) was flagged and rewritten because it described the very
zeroing logic this change removes.

Drive-by: two comments still said Settings > Sound > "Msg wake" -- it's
been under Settings > Display for a while, per the enum's section grouping
in SettingsScreen.h.

Verified: sim_companion_radio and WioTrackerL1_companion_solo_dual build
clean, RAM/Flash usage unchanged (pure logic removal, no schema/layout
change).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 15:41:26 +02:00
JakubandClaude Sonnet 5 95e3cf2618 feat(radio): honest TX power for GAT562 30S external PA
gat562_30s_mesh_kit has an always-on external PA rated at +30dBm, but the
firmware wrote tx_power_dbm straight to the SX1262 register (capped at 22),
so the app/CLI/UI never matched the real radiated power and couldn't ask
for more than 22 even though the hardware supports it.

Add a generic, macro-gated PA gain-curve resolution in
RadioLibWrapper::setTxPower() (the single choke point every TX-power path
already funnels through), reusable by any future board with a similar PA.
For this board, reuse the vendor-measured 869MHz curve from the open
upstream Meshtastic PR that added it for the same physical module
(meshtastic/firmware#11212): pick the lowest SX1262 setting whose measured
output reaches the requested dBm, clamped at the PA's saturation knee.
Bump LORA_TX_POWER/MAX_LORA_TX_POWER to 30 for this variant only -- the
other three GAT562 boards use the SX1262's own PA_BOOST (22dBm, no
external PA) and are unaffected.

Since a request below the PA's floor gain (14dBm) still radiates at that
floor, also make CommonCLICallbacks::setTxPower() return what was actually
applied, and have every setter (BLE CMD_SET_RADIO_TX_POWER, CLI "tx",
simple_repeater/room_server/sensor) persist and report that value instead
of the raw request -- so tx_power_dbm never silently disagrees with the
physical output in either direction.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 12:54:48 +02:00
JakubandClaude Opus 5 1d5dbe2f9a feat(companion): contact expiry + prune, and scope-list follow-up fixes
Settings > Contacts gains "Expire" (Off/7d/30d/90d) and a "Prune now" action
that counts first and asks before removing anything. A contact with no
advert/update within the threshold is eligible; favourites are always kept.
Thresholds and their labels live in one NodePrefs table so the age shown and
the age enforced can't drift. SCHEMA_SENTINEL -> 0xC0DE002C (sizeof unchanged
at 2824, confirmed on native and a real WioTrackerL1 build).

Also fixes four bugs in the scope list from 89c02eea:

- removeScope() saved /scopes1 but never savePrefs(), so the ch_scope_idx[]
  and repeat_extra_scope_mask fix-ups it makes were lost on reboot, leaving
  shifted entries against unshifted indices.
- CMD_SET_DEFAULT_FLOOD_SCOPE wrote the legacy fields directly instead of
  going through setPrimaryScope(), so the app's default-scope setting had
  nothing reading it once sends resolved through the list.
- The reverse direction was stale too: an on-device "Set as default" never
  refreshed default_scope_name/key, so CMD_GET_DEFAULT_FLOOD_SCOPE reported
  a scope the device had stopped using. New syncLegacyDefaultScope().
- Upgrading from the old single Scope field set the default (so DMs kept it)
  but left every channel unscoped, since "*" means unscoped, not "inherit".
  loadScopeList() now reports when it migrated and begin() seeds the channels
  that already exist, leaving empty slots alone.

"Set default" -> "Set as default", and setting it now says what it governs
("Default: DMs + relay") rather than leaving the [default] marker to imply
more than it does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 21:55:46 +02:00
JakubandClaude Sonnet 5 89c02eea27 feat(companion): shared named-scope list, per-channel/repeater popup pickers
Replace the single device-wide Scope text field with a small named list
(wildcard "*" fixed first, plus a movable default): each channel picks one
scope of its own (matching the app's per-channel region picker, surfaced in
the channel-history title), and the repeater's Extra scopes multi-selects
from the same list instead of comma-typed names. Both pickers are popups
(PopupMenu gains addCheckItem()/a fillable-square checkbox glyph) over the
existing screen rather than a full-screen takeover.

Also fixes a stray-bits bug where a pre-existing prefs file's own sentinel
tail could read into the new repeat_extra_scope_mask/ch_scope_idx fields and
silently reappear as a real pick later once the scope list grew into that
range.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 00:05:40 +02:00
JakubandClaude Sonnet 5 7533c70ee2 fix(ui): message bubble color, received-message hop count, list wrap-around, age fallback; add sim WASM build workflow
- Unselected message bubbles no longer carry a stray light-filled header
  strip behind the sender name -- the one place with a filled patch on an
  otherwise plain row. Selected fill is unchanged.
- Received DMs and channel posts now show their hop count as the same tiny
  digit icon a sent message already uses for its repeater/echo count, using
  the hop path the mesh already records for incoming messages.
- DM/channel history list: UP at the oldest message now wraps straight to
  the newest (not the compose row), closing the loop the same way every
  other list in the UI does at its ends.
- A received message's age no longer goes blank when its timestamp reads
  slightly ahead of the device's own clock (sender/receiver skew, or an
  unsynced receiver) or is genuinely unknown -- both now show the receipt
  time instead of leaving the row with just a name and hop count.
- release-notes.md: add the v1.28 section covering the above plus this
  cycle's already-committed dev fixes (GPS altitude field, units
  compliance, noise floor, Msg wake location, bot relay tracking, PR #34).
- Add .github/workflows/build-solo-sim.yml: builds the companion+repeater
  WASM sim via Emscripten and attaches it as a fixed-name asset to the tag's
  draft release, independent of the hardware firmware build job.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 12:30:42 +02:00
JakubandClaude Sonnet 5 64b79d00eb fix(ui): wrap-around at both ends of the DM/channel history list
Every other list in the UI (contacts, channels, message-pick, the
mode tabs) wraps at its ends; the DM and channel history lists were the
one exception -- UP at the oldest message (top of the list) just
stopped instead of wrapping to the compose row (the list's own
bottom-most stop), and DOWN from the compose row didn't wrap back to
the oldest either. Both directions now close the loop, same as
everywhere else.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 21:31:31 +02:00
JakubandClaude Sonnet 5 aa73400414 fix(ui): community v1.27 feedback — GPS altitude field, noise floor, Msg wake location, units
- Add a separate "Altitude (GPS)" dashboard field (label AltG) alongside
  the existing barometric one (now labelled "Altitude (Baro)") -- the
  original single Altitude field only ever read a barometric sensor's
  CayenneLPP telemetry, showing "--" on any board without one even with
  a perfectly good GPS fix.
- Every altitude reading (baro + GPS, Clock and Lock screen) and the GPS
  home page's "alt" row now go through a shared fmtAlt() helper and
  respect Settings > System > Units, same as every other distance in the
  UI -- none of them checked units_imperial before. Same fix for
  Locator's Radius row, which always showed metric regardless.
- Radio home page's noise floor showed "n/a" whenever Pwr save (RX
  duty-cycle) was on, on the assumption duty-cycle RX can't sample the
  floor. That's stale: RadioLibWrapper already periodically recalibrates
  it (noiseFloorCalibCheck(), every 60s) even under duty-cycle, which is
  exactly why Diagnostics' unconditional read of the same value was
  already showing something real. Radio home page now matches.
- Moved "Msg wake" from Settings > Sound to Settings > Display -- more
  logical home for a display-wake toggle than notifications. Menu-order
  only; NodePrefs storage is unaffected by this enum's ordering.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 20:12:29 +02:00
JakubandClaude Sonnet 5 6e0e981048 fix(companion): arm the "Relayed by" tracker for bot-originated channel posts
Remote Bot's channel trigger reply, channel command reply, and !gps fix
result all mirrored their send into the on-device history with
addOwnChannelMsg() but never called armChannelRelay() afterward, unlike
the app's CMD_SEND_CHANNEL_TXT_MSG send which already did both -- so a
bot's own channel posts never showed which repeaters echoed them back,
while everything else did.

Factor the always-together pair (mirror into history, then arm relay
tracking from the send sendGroupMessage's sendFloodScoped already
tracked) into MyMesh::mirrorOwnChannelMsg(), and route all four call
sites through it instead of pairing the two calls by hand -- the bot
bug was exactly a dropped second call, so a new call site can no longer
add the first half without the second.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 16:22:04 +02:00
Jakub 42afd180f9 Merge pull request #34 from 3urobeat/fix/message-channel-create-refresh
fix: Fix message channel list not updating instantly

Creating a channel didn't rebuild the Messages screen's cached channel
list until you backed all the way out and reopened it. Tracks whether
the Add/Edit channel form was active on the previous render and rebuilds
the list once it closes.
2026-09-09 14:19:32 +02:00
Jakub 48cfdff1f3 Merge main into dev (README live-demo link) 2026-09-08 23:50:48 +02:00
JakubandClaude Sonnet 5 de2fceca7c fix(companion): stop the FIRMWARE_VERSION fallback claiming a fixed version
Same staleness bug as the build-date fallback: bypassing build.sh (a
direct `pio run`, an IDE Build button, the sim) fell back to a hardcoded
"v1.27-dev" literal that would silently keep claiming to BE v1.27 forever,
even once development has moved well past it. build.sh/CI's real
git-tag-derived FIRMWARE_VERSION is untouched -- only the bypass fallback
changes, to a version-agnostic "dev-<build date>".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 23:48:27 +02:00
JakubandClaude Sonnet 5 d72fed66c1 docs(readme): link the live browser demo at the top
Fills in the "Try it out live" placeholder that's sat empty since the
website went up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 23:48:20 +02:00
3urobeat 39f2d7edc9 fix: Fix creating message channels not rebuilding channel list immediately 2026-09-08 19:18:51 +02:00
JakubandClaude Sonnet 5 6426ef9319 docs(readme): add 3urobeat to contributors
PR #32 (lock screen title bar), merged into v1.27.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
v1.27
2026-09-08 16:04:54 +02:00
JakubandClaude Sonnet 5 e9f590ad40 docs(release-notes): add contacts-flush fix, Sats field, popup marquee fix
Three v1.27 changes this cycle weren't reflected yet: the low-battery/
reboot contacts-persistence fix (matching the settings one already
documented), the Clock page's new Sats field, and the Path/Relayed by
popup's marquee glitch + slowness fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 16:03:37 +02:00
JakubandClaude Sonnet 5 e3279646f9 refactor(companion): unify every reboot/power-off through UITask::shutdown()
Settings > Reboot, the phone app's CMD_REBOOT, and the serial CLI's
"reboot" command each hand-rolled their own flush-then-board.reboot()
sequence, so each could drift independently (Settings > Reboot and the
CLI command were both missing the contacts flush added in the previous
commit). Add a pure-virtual AbstractUITask::shutdown(bool restart) --
every UI variant (ui-new/ui-tiny/ui-orig) already implements a method
with this exact signature -- and route all three call sites through it
instead. CMD_FACTORY_RESET intentionally keeps calling board.reboot()
directly: flushing stale in-RAM prefs back out would defeat the format
it just did.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 16:03:37 +02:00
JakubandClaude Sonnet 5 a868a60935 fix(companion): flush pending lazy contacts write on shutdown/power-off
Contact updates (advert path cache, new/removed contacts, favourites) are
lazily debounced (dirty_contacts_expiry, 5s) to avoid wearing flash on
every mesh packet. CMD_REBOOT already flushed this before rebooting, but
UITask::shutdown() -- the low-battery auto-shutdown and long-press
power-off path -- didn't, so under continuous mesh traffic (which keeps
re-arming the debounce timer) a pending write could sit unflushed for a
whole session and be lost when one of those paths fired. Add
MyMesh::flushDirtyContacts() and call it from shutdown() alongside the
existing savePrefs()/saveRTCTime() flush.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 13:27:45 +02:00
JakubandClaude Sonnet 5 23ccc06d1d fix(ui): stop the Path/Relayed by popup fighting its own row for marquee state
Two distinct bugs behind "Relayed by" scrolling looking broken:

1. The DM/channel history row underneath a context menu (Path/Relayed by,
   Options, etc.) keeps rendering every frame with sel=true regardless of
   the popup on top of it, and both it and the popup's own selected item
   share DisplayDriver's single marquee slot. Whichever drew last each
   frame kept is_new-resetting the other's animation -- looked like the
   scroll "remembered" a stale, wrong position. Now suppressed via
   `sel && !_ctx_menu.active` at all four call sites that had it (DM/
   channel history bodies, contact/channel pick names).

2. PopupMenu::render() returns 50 (it wants a fast, smooth redraw), but
   all 7 `_ctx_menu.render(display)` call sites discarded that return
   value, so the enclosing phase's own return statement (500-2000ms)
   governed the actual redraw cadence instead -- the popup's marquee only
   ever advanced whenever the much slower underlying screen happened to
   redraw. Now folded into the existing mq_delay accumulator, same as
   every other marquee source in this file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 13:04:36 +02:00
Jakub 333d739103 Revert "fix(ui): marquee's hold no longer dominates a barely-overflowing label"
This reverts commit e300db7908.
2026-09-08 11:38:31 +02:00
JakubandClaude Sonnet 5 34c2792099 docs: catch up solo_features docs with this cycle's UI changes
- settings_screen.md: document Settings > Sound > Msg wake
- clock_screen.md: document the new Sats dashboard field
- screen_lock.md: mention the lock screen's title bar (PR #32)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 11:35:19 +02:00
JakubandClaude Sonnet 5 e300db7908 fix(ui): marquee's hold no longer dominates a barely-overflowing label
User report: a repeater path popup's short "?A1B2C3D4"-style hop tags
(often just a couple pixels over the box width) felt stuck rather than
scrolling, while longer names (Nodes list) already felt right after the
recent marqueeHoldMs() speedup.

Root cause: the swing marquee's hold applies uniformly regardless of how
much text is actually hidden. When only 1-2 codepoints overflow, the full
cycle is hold + a single, barely-there step + hold + step back -- nearly
all hold, for almost no new information revealed each side. New
marqueeHoldForSkip(skip_cp) scales the hold down (to 1/3 or 1/2) when the
computed reveal distance is small; longer overflows keep the full hold,
since there's real text worth pausing to read.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 11:12:23 +02:00
JakubandClaude Sonnet 5 57e029ccc4 feat(ui): add Sats dashboard field
New DASH_SATS clock/lock-screen dashboard field, reading
LocationProvider::satellitesCount() (already used by the GPS Tools
panel, just not previously exposed as a dashboard option). Follows the
existing DASH_GPS pattern: always selectable, "--" when built without
GPS support. Added to both dashboard-render paths (HomeScreen's CLOCK
page inline loop and formatDashVal(), shared by the LOCK page).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 11:04:14 +02:00