NodePrefs is now NodePrefsFields (the fields) plus how they're saved: one
JSON key per field, upstream's keys under "radio", "gps", "repeat", "comp"
and "custom" (so either firmware reads what the other wrote of them),
ours under "solo". Saved atomically (temp file, then renamed), through a
buffer. 2.0-rc's binary /prefs is removed, not converted: 2.0 starts from
the defaults; /new_prefs from before 2.0 is still imported.
From upstream with it: txdelay / direct.txdelay / agc.reset.interval
(prefs and CLI), and the board's own settings through getCustom()
(Board::attachDynamicPrefs) in place of the merge's stopgap adapter.
ConfigSerializer reads values up to 191 characters (was 127): 140-byte
texts, an 80-byte table in hex.
Test: every field round-trips through the JSON byte for byte; the alarm
label tests expect "Once" (the label changed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/prefs holds a header (magic, format version, size, CRC-32), the node's
position and NodePrefs as it is in memory, in place of /new_prefs' two
hand-kept field lists and 0xC0DE00xx schema bumps. A new field goes at the
end of the struct; moving one bumps PREFS_VERSION. sanitize() keeps every
value in range.
The first boot after an update from before 2.0 imports only the first
block of the old file, laid out the same in upstream MeshCore and Solo
(name, position, radio, BLE PIN, scope), then deletes it; everything else
starts from the defaults. Contacts, channels, scopes, waypoints and trails
are untouched.
Gone: the page-order and GPS-interval migrations, the unused
trail_interval_idx / trail_units_idx / repeat_extra_scopes; fav_sort_off
and msg_wake_screen_off become fav_sort and msg_wake (on by default).
Co-Authored-By: Claude Opus 5.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>
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>
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>
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>
- Settings > Radio > Scope: type a community/region name on-device (derives
the shared key the same "#name" -> SHA256 way as DEFAULT_FLOOD_SCOPE_NAME),
previously only settable from a connected app.
- Tools > Repeater > Scope only + Extra scopes: only relay flood traffic
matching the device's own scope or a comma-separated list of additional
scopes, without changing what scope the device's own messages send under.
No-op while unconfigured.
- getCADEnabled()/getInterferenceThreshold() were hardcoded off on
companion_radio; CAD now auto-enables whenever RX power-save (duty-cycle)
is active, since the noise floor isn't kept fresh during duty-cycle sleep.
- Message truncation to fit the send frame could split a multi-byte UTF-8
character in half; now stops at the last complete character.
- The default "Public" channel was unconditionally re-added at every boot
before the saved channel list was loaded, so deleting it never stuck.
Only seeded now on a genuinely fresh device (no channel file yet).
- Tools > Nodes read contacts from the wrong starting offset, landing on
internally-reserved bookkeeping slots instead of real contacts -- showed
as blank "Unknown" rows and silently dropped that many real contacts off
the end of the list.
- resetContacts() only cleared the first few reserved slots, not the whole
contact table, contrary to its own comment; only reachable today via
private-key import, fixed to match stated intent regardless.
- Keyboard's multi-line text preview could render the cursor on an empty
line below short typed text instead of right after it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Log in to a room server from the device UI with no phone app: picking a
room prompts for its password (blank allowed for open rooms), and a
context-menu "Login..." allows re-login. Successful passwords are
persisted to a dedicated /room_pw file so a previously-used room logs
back in after reboot without retyping; a failed login forgets the
(now-stale) saved password so the next attempt prompts again.
The room-password file is written via a temp file + atomic rename
(new DataStore::commitFile helper), matching the crash-safety of
contacts/channels persistence.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Single-slot persistence as agreed. Action popup grows three entries
(conditional on what makes sense for the current state):
- "Save trail" — writes the current RAM ring to /trail in LittleFS.
- "Load trail" — only shown when /trail exists; replaces the live ring
with the snapshot (any active session ends first).
- "Export GPX" — dumps the trail as a minimal GPX 1.1 document over
Serial so the user can capture it from a USB host.
Plumbing:
- TrailStore gains writeTo / readFrom (template on the file handle, so
the platform-specific File flavour stays out of Trail.h) plus
exportGpx(Stream&) for the GPX serializer. Header carries magic
"TRAL", a version byte, point count, and the accumulated active time
so elapsed survives the round trip.
- DataStore::openWrite is exposed publicly so callers outside DataStore
don't have to duplicate the platform open-mode dance.
- MyMesh gains a public getDataStore() accessor.
After Load, _pending_seg_break is set so any subsequent Start opens a
new segment instead of joining the loaded last point.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Save last known unix timestamp to /rtc_save on shutdown and restore it
on startup, so the clock shows correct time immediately after restart
without waiting for GPS fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>