- The AW35615 USB-C controller (FUSB302-style registers, ID 0x91) only
reports VBUSOK with its measure block on; POWER resets to bandgap only,
so isExternalPowered() was always false. Board init turns it on
- Status bar shows a charge bolt on the cable, Diagnostics > Live has a
Power row, the GPS screen notes that charging disturbs GPS, and the
low-battery warning is skipped on USB as intended
- Roadmap: USB detection closed; GPS indoors still worse than the L1
even on battery, left open for later
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Home > GPS and Settings > System > GPS details: satellites on a sky plot
(filled = used in the fix, colour = C/N0), a bar per satellite with its
dB-Hz, fix state with time to first fix, DOPs, position, altitude, speed,
UTC and per-constellation counts; a Turn on button while GPS is off
- helpers/sensors/GpsSky.h parses GSV / GSA / GGA / RMC from any talker,
fed from MicroNMEALocationProvider behind -D GPS_SKYVIEW (L2 and the
sim only; L1 unchanged); the sim feeds it made-up NMEA
- Roadmap: GPS indoors diagnosed with it - on the USB cable the L76K
tracks satellites but never decodes navigation data; on battery it
gets UTC within seconds and a fix
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Tile fetch and OTA tasks run at idle priority: HTTPClient's read loop
only yields with delay(0), which at priority 1 starved IDLE0 until the
task watchdog reset the device (decoded from the core dump)
- A failed tile is retried once; failures in a row pause 5 s ... 60 s on
the same tile, and the download gives up only after ~5 min of nothing
- The source's last zoom level (OpenTopoMap 17, else 19, or line 3 of
source.txt) caps the download range and live tiles; past it the map
magnifies the parent tile
- OpenTopoMap's "no tile" picture is never saved, and copies saved by
earlier builds are dropped when read
- The placeholder check only runs on a fresh area and for pictures with
content: plain sea / empty land tiles are genuinely identical
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Message history archived per conversation on the SD card (HIST_ARCHIVE):
restored on boot, Older/Newer paging in threads, kept count selectable
- Settings > System > Storage: card and flash usage, breakdown by maps,
messages, trails and other, kept-per-chat setting, delete history;
reads the mounted volume and warns when the FAT partition is much
smaller than the card
- Saved trails on the card: every Save is a new file, Load opens a list
with load (framed on the map), GPX export and delete
- Diagnostics: GPS data rows, last start reason, last crash summary
- Messages restored before the clock is set show their date instead of 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Unlocking fades the lock screen out while the screen underneath drifts
into place; the slider unlocks on letting go at the end and springs back
when let go earlier
- Channel names no longer get a "# " prefix (thread title, channel popup,
live share targets) -- "#" marks a hashtag channel; favourite contacts in
the live share list are starred instead
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Anim.h: screens emerge from the middle (background cover fades, content
drifts 6 px), Home pages slide, popups and toasts rise and fade in
- Theme.h: accent rules (fill = primary action, dim fill = selected, text =
names/counts/modes), theme layer over the default theme, radius tokens,
accent colour picker (Settings > Display & power > LOOK, NVS mc_ui)
- Splash with the Solo and upstream versions
- Status icons in equal cells, L1-style relay marks, path window with a hop
diagram, tile download popup fits the screen (WiFi only in Settings)
- Perf: uncompressed fonts, two 120-line PSRAM buffers (~48 -> ~33 ms per
transition frame); UI_PERF_TEST harness
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Screen PIN (Settings > Display & power > Security): 4-8 digits in NVS,
keypad on the lock card after every wake and on boot, 5 misses -> 30 s pause
- Live map tiles: missing tiles at the current zoom are fetched over the saved
WiFi, written to the card and drawn; WiFi drops 30 s after leaving the map;
"Live tiles" switch in Map tools
- Firmware update (Settings > System): checks the fork's latest release,
streams the -ota.bin into the spare app slot over TLS verified with the
Arduino 3.x CA bundle, checks image magic/chip id, restarts
- Release workflow publishes *_solo_lvgl envs (merged image + -ota.bin)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Map tools hold trail, live share and arrival alert, with their options
(schema page "Map options") behind one button; back returns to the map
- Nearby gets an advert button: send now (nearby / everyone) + auto-advert
- Home page 3: Repeater, Admin (list of repeaters and room servers),
Diagnostics
- Settings: display, sound, radio (Bluetooth, WiFi), system, keyboard,
contacts & messages; units moved to Display & power
- Status bar: Bluetooth, GPS, alarm, mute, then auto-advert, trail, live
share, repeater, arrival alert in the accent colour
- Fonts: key and route glyphs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New env Wio_Tracker_L2_companion_solo_lvgl_v3 on pioarduino 55.03.312-1;
the 2.0.17 env stays until the new one passes the hardware checklist
- SerialBLEInterface: NimBLE variant (PIN pairing through encrypted,
authenticated characteristic properties; setAccessPermissions is a no-op)
- buzzer: i2s_std driver on IDF 5 (legacy driver kept for 2.0.17)
- ESPNOWRadio: IDF 5 callback signatures
- ui-lvgl: psramBuf() for trail drawing, tile cache, map marks, list rows,
keyboard maps and message metadata; internal heap back to 2.0.17 level
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- genericBuzzer I2S path (BUZZER_I2S): an audio task synthesises the notes
with eased edges and keeps the DMA queue fed with silence between sounds;
ES8311 setup; amp powered through the board only around sounds
- ui-core/SoundControl.h (mode, volume, built-in sounds, melody model),
SoundNotifier moved to the Core, chanctl::melody, schema Sound page
- L2: notifications, alarm / timer ring, locator sounds, Settings > Sound,
melody editor with the playing note lit, per-chat sound, mute icon
- noteIndex() on every buzzer backend; scrolling popups past screen height
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BotConfig (tabs, rows, the NodePrefs field behind each, reply placeholders)
moves into ui-core; ui-new's Tools > Bot renders it. L2: Home > Bot with
switches, list pickers, text popups with placeholder buttons, quiet hours.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AdminSession (login with saved password, CLI round trips, timeouts, field
table, typed values) moves into ui-core; UiCore offers login answers and
CLI replies to it first. ui-new's Tools > Admin becomes a view over it.
L2: node detail > Admin with tabs and per-field popups.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ChannelControl (add Public/Hashtag/Private, rename, delete, notif/fav/scope)
and Favourites slots move into ui-core; ui-new uses them. Contact / channel
pref cleanup moves from ui-new's UITask into UiCore so it runs on every
frontend (L2 had none). L2: channel options popup (hold a row or the gear
in a channel) and the Add / Edit channel form.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shared by ui-new (L1) and ui-lvgl (L2) so both firmwares have the same core
features:
- WaypointModel (/waypoints, UTF-8-safe labels, [WAY] share text),
EtaTracker, GpsAverager, TrackBack; TrailEngine save/load/reset actions;
UiCore::setGpsEnabled / applyGpsInterval.
- Locator target kind 2: a channel live share followed by sender name
(LiveTrackStore::activeByName).
- SettingsSchema: pages (navigation, display & power, messages & contacts),
value tables over 1/2/4-byte NodePrefs fields, side effects via UiCore.
- RadioControl: apply params / TX power / APC and the preset list; ui-new's
Settings > Radio now goes through it.
- NodePrefs::display_brightness_pct (sentinel 0xC0DE002F, size unchanged)
for frontends with a brightness slider.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The map's download button fetches the visible area (overview levels up to
a chosen zoom) into /sdcard/maps from OpenTopoMap or maps/source.txt,
skipping tiles already on the card and rejecting placeholder answers.
Settings > WiFi stores the network in NVS (with scan); WiFi is on only
while a download runs. HTTP runs on a worker task on core 0; the popup
shows link, internal heap and the last TLS/HTTP error.
Internal RAM was the blocker (~13 KB free with WiFi + Bluedroid): the L2
LVGL env now allocates the_mesh in PSRAM (MESH_IN_PSRAM, static RAM
210 -> 87 KB) and mbedTLS allocates from PSRAM. Confirmed on hardware.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Map screen (Home > Map): Web-Mercator tiles from /maps/{z}/{x}/{y}.png on
the card (Meshtastic MUI layout; per-column TPK1 .pak also read), drag to
pan, +/- zoom, re-centre/follow GPS, Nearby nodes with a position as
markers opening node detail. Tiles decode one per loop pass into a
12-slot RGB565 cache in PSRAM. map/TileProvider.h is the swap point for a
later vector renderer: the screen only calls renderTile(). The tile set's
credit line comes from maps/attribution.txt.
L2 mounts the card over SDMMC 1-bit (CLK 2, CMD 3, D0 1). LVGL's lodepng
is enabled; note its LVGL patch returns an lv_draw_buf_t*, not pixels.
LVGL pool 2 MB for PNG decoding. Fonts gain map / users icons.
tools/maps/fetch_tiles.py prepares a card for an area (presets for
Poland), requires an explicit tile URL (tile.openstreetmap.org blocks
offline use) and stops on placeholder images. The LVGL sim preloads
web/maps into the L2's /sdcard and puts its GPS on Kraków.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ui-core/NearbyModel.h builds the Nearby list (contacts + live [LOC] shares
+ heard adverts, or discover-scan results) with type filter and sort,
moved verbatim out of ui-new's NearbyScreen, which now inherits it.
ui-lvgl: Home is three tiles (Messages, Nearby, Settings). Nearby lists
known nodes with filter chips and a Dist/Recent toggle; tapping a row opens
node detail (distance/bearing, position, last heard, ID) with Message,
Ping, favourite, Add and two-tap Delete. Scan is a popup over the list
with its own model: nodes in range now, not the known set. Fonts gain the
FontAwesome star for favourites. The LVGL sim page adds an optional
repeater so Scan has something to find.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
- 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>
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>
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>
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>
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>
- 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>
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>
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>
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>