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>
Network mode, the dedicated profile and the flood filters were only
built into the row list once client_repeat was already ON, so setting
up a repeater meant enabling it blind first. Every other on/off tool
in ui-new (Bot, Live Share, Locator) already keeps its settings
visible while off; Repeater now matches.
Continuing the consistency review: sweep for the same three defect shapes
elsewhere in ui-new/ (own read pass plus two parallel research agents),
verified against source before acting.
Three destructive actions fired on a single Enter with no way back, unlike
contact-delete's existing confirm-defaulted-to-Cancel popup: Trail's "Reset
trail" (wipes the whole recorded route, no undo short of a prior manual
Save -- reuses Trail's own multi-level menu machinery, alongside its
GPS-off confirm), Messages' channel Delete, and RadioPresetPicker's saved-
preset delete (shared by Settings > Radio and Tools > Repeater, so one fix
covers both). All three now confirm the same way, defaulting to Cancel.
KeyboardWidget was the one place Hold-Enter still doubled as Cancel: Shift,
Backspace and a Latin letter's accent popup already have real, kept
meanings under a hold, but every other special-row cell (Space, OK/Done,
the {} placeholder) fell through to a bare CANCELLED, closing the keyboard
exactly like the real Cancel key. Now a no-op there too, matching the "only
Back closes it" rule already applied to popups and screens.
MessagesScreen defined the same two label arrays (Notif states, melody
slots) four times over, once per context-menu handler. Hoisted to one
pair of static class members -- constexpr wasn't enough to get the linker
to emit them on this toolchain, so they follow the same declare-in-class/
define-out-of-class shape NearbyScreen::FILTER_LABELS already uses.
Alert text: "Advert sent!"/"Advert failed.." and "Sent!" were the only
toasts anywhere with trailing punctuation; normalized to the plain style
every other confirmation uses. Unpinning from the Favourites Dial reported
the freed slot number from the Messages screens but not from Nodes or the
dial's own tile menu; now consistent everywhere pinning already was.
DiagnosticsScreen's Live/System/Font tab renderers hand-rolled the same
scroll-clamp/loop/indicator skeleton drawList() already bundles; switched
both to drawList (passing the screen's own _scroll as its `sel` too, since
neither tab has a row cursor -- makes drawList's internal clamp a no-op and
leaves clampScroll() as the only thing bounding it, unchanged). Pure
internal tidy, no behavior change.
Three interaction inconsistencies found while auditing the favourites work,
all of the same shape: the same gesture meaning different things depending
on which screen you were on.
Value rows in popup menus. Rows like "Notif: ON" or "Sort: Dist" show a
value the user steps through with LEFT/RIGHT, but Enter treated them as
ordinary menu picks and closed the popup, so changing two of them meant
reopening the menu in between. Trail's settings submenu was the lone
exception, working around it by rebuilding and re-selecting after each
Enter. PopupMenu now knows the difference: addValueItem() marks a row, and
Enter on it returns the new VALUE_NEXT instead of SELECTED, leaving the
menu open. Only Back closes a menu now. Applied to the Messages
contact/room/channel menus, Nodes, the Ringtone editor and Trail, which
drops its reopenSettingsAt() workaround. The LEFT/RIGHT cycling bodies
moved into one helper per menu, since Enter and RIGHT now share them.
Nodes' Fav row was the worst case: LEFT/RIGHT did nothing there at all, so
the only way to toggle a favourite was an Enter that dismissed the menu on
every flip. Its label moved to a member buffer (as the Pin row already had)
so it can be retitled in place.
Settings rows Auto-off, Low battery, GPS pwr and Battery ignored Enter,
though their options wrap exactly like the melody/keyboard/clock rows
beside them, where Enter has always stepped forward. They accept it now.
Rows that ramp between fixed ends (Brightness, Volume, TX Pwr, Timezone,
SF/BW/CR) stay LEFT/RIGHT-only -- there is nothing to wrap to.
Hold Enter no longer doubles as Back. It quietly meant "go back" on Tools,
Locator, Live Share, Repeater, Bot, Auto-Advert, GPIO, Compass, the
Dashboard config and the Messages navigate view, while elsewhere the same
long press opens a context menu. It now only ever opens a menu, or does
nothing where there is none. Same for dismissing an open popup, which it
used to do. Checked that this strands nobody: every board that can reach
these screens has a real Back key (back_btn on joystick boards, Esc on
CardKB/TCA8418/T-Deck). Single-button boards produce no KEY_ENTER at all,
so they never leave the home pages in the first place.
- RepeaterScreen: drop the redundant "Rpt " prefix on the profile fields
(Preset/Freq/SF/BW/CR), matching Settings > Radio's own terminology --
the screen is already dedicated to the repeater's own profile, so the
prefix disambiguated nothing.
- Channel context menu: "Fav: yes/no" -> "Fav: ON/OFF", matching every
other toggle in the app.
- Settings (System tab): six labels left as raw concatenated identifiers
(AutoOff/AutoLock/TimeZone/LowBat/BattDisp/BzrVol) now read like their
space-separated neighbours (Auto pwr/Pwr save/DM sound/GPS pwr), and
BzrVol no longer clashes with "Buzzer" one row above it for the same
feature. Three value-label arrays also had one mismatched-case entry
fixed to match its siblings: Auto-off's "never" -> "OFF", Batt display's
"icon" -> "Icon", Sound's "built-in" -> "Built-in".
- ToolsScreen: re-enabled the mini-icons next to each tool, which had
been commented out ("don't fit visually"). Root cause: the screen's own
drawIcon() centred against lineStep() (line height + inter-row gap)
instead of getLineHeight() alone, 2px too generous for a near-full-
height icon -- now delegates to the already-correct miniIconDraw() used
elsewhere (e.g. the message-list ack checkmark). Also gave Admin and
GPIO their own icons (padlock, 3-pin header) instead of both sharing
System's cog.
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>
The shared on-screen keyboard seeds {loc}/{time} placeholders on begin()
for message composition, but they make no sense in a radio-preset name.
Clear them right after opening the keyboard from the "+ Save current..."
flow in both Settings > Radio and Tools > Repeater; the message-slot
editor still keeps them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every drawList/AccordionList row opened with the same hand-written
display.drawSelectionRow(0, y-1, width-reserve, lineStep-1, sel) line —
14 copies of the same geometry and magic offsets. Add a drawRowSelection(d,
y, sel, reserve) helper in icons.h next to drawList and route the canonical
sites through it (Bot/LiveShare/Locator/Repeater/Settings/Tools/Nearby/
QuickMsg/Waypoints).
Rows that intentionally differ (full-width DashboardConfig/QuickMsg, the
keyboard/ringtone grids) keep their explicit drawSelectionRow() call — the
helper is opt-in rather than baked into drawList, so legitimate variants
aren't forced into one geometry. Framework doc updated to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "_dirty bool, then if (_dirty) the_mesh.savePrefs() on exit" pattern was
duplicated across 9 screens with subtle divergence: some cleared the flag
after saving, some left it set and relied on onShow() to reset. Replace all
12 exit sites with a single _task->savePrefsIfDirty(flag) helper that saves
once only if dirty and always clears the flag, so the "did we touch flash?"
answer lives in one place and the reset is consistent.
Edit sites still mark the flag manually (inherent to change tracking); only
the persist-on-exit boilerplate is centralised.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the ad-hoc enter()/markClean() entry methods (which lived outside
the UIScreen interface and were invoked via casts from each gotoX) with a
virtual onShow() lifecycle hook called centrally by setCurrScreen().
This removes the "forgot to call enter() in a new navigator" footgun and
the unchecked cast smell: 12 navigators collapse to one-line
setCurrScreen(x) calls, and override enforces signature match. Two entries
that carry a parameter/variant keep an explicit typed call after
setCurrScreen(): RingtoneEditor::selectSlot(slot) and TrailScreen::showMapView().
Behaviour-preserving: only screens that previously had enter() get an
onShow() override; Splash/Home/QuickMsg/Diag keep no reset as before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audit pass against docs/design/solo_ui_framework.md, converging deviations:
- prev/next: replace hand-rolled (c == KEY_LEFT || c == KEY_PREV) with
keyIsPrev()/keyIsNext() in QuickMsgScreen, RingtoneEditor, TrailScreen,
FullscreenMsgView. Two were real rotary-encoder gaps, not just style:
NearbyScreen's filter and WaypointsView's hemisphere toggle used raw
KEY_LEFT/RIGHT with no encoder twin, so the encoder couldn't drive them.
- NearbyScreen's list right column used a manual setCursor(width -
getTextWidth(...)) instead of the existing drawTextRightAlign() helper
(which also UTF-8-translates) — now uses it, like Diagnostics/Repeater.
- RepeaterScreen hand-rolled the whole drawList skeleton (scroll clamp +
reserve + row loop + indicator); collapsed onto drawList().
No D-pad behaviour change; more uniform encoder support. Net -19 lines.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The manual Freq/SF/BW/CR editing was duplicated near-verbatim between
Settings › Radio and Tools › Repeater — same digit-by-digit Freq editor,
same SF/BW/CR step bounds, differing only in the target fields and the
apply call. Extract RadioParamsEditor (freq DigitEditor + static SF/BW/CR
steppers), with no dependency on UITask or the radio driver: the screen
passes the freq bounds in, the same way RadioPresetPicker stays decoupled.
Completes the preset-picker consolidation.
Behaviour preserved: a step at the range limit changes nothing and isn't
consumed (falls through), as before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Settings › Radio and Tools › Repeater had near-identical preset-picker
logic (name/list-index lookup, save/delete, popup build, selection
handling) plus their own copy of the nearest-bandwidth search — only the
target fields (companion params vs. dedicated repeater profile) and the
apply call differed.
Extract the shared logic into RadioPresetPicker (a Target of field
pointers + a Result the screen acts on), with no dependency on UITask so
each screen keeps its own apply/dirty/keyboard/alert handling. Move the
nearest-bandwidth search to nearestBwIndex() in RadioPresets.h. Removes
~6 duplicated methods and 4 state fields from each screen.
Verified: WioTrackerL1_companion_solo_dual builds clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Repeater screen's preset picker only let you pick an existing
preset, not save the current profile as a new one or remove a saved
one — the only way to manage user presets was from Settings > Radio,
even though the repeater profile is a separate set of freq/sf/bw/cr
values.
Ports Settings' "+ Save current..." / "- Delete preset..." popup flow
onto the repeater profile fields, reusing the same shared on-screen
keyboard (UITask::keyboard()) and the same 4 NodePrefs::user_radio_presets
slots — a preset saved from either screen shows up in both, since it's
the same underlying array.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Settings > Radio gains a repeater toggle, 16 community-suggested radio
presets plus manual Freq/SF/BW/CR tuning (digit-by-digit Freq editor),
4 persisted user preset slots, and the packet pool bumped 16->32 so
queued retransmits stop starving incoming-packet allocation while
relaying.
Four opt-in politeness knobs (skip-advert, max-hops, yield, min-SNR),
all flood-only and off by default, plus overhear suppression that
cancels a queued retransmit if a peer relays the same packet first.
Adaptive Power Control is suppressed while relaying (pins TX power to
the ceiling) and duty-cycle RX is forced off, since a repeater needs
to hear and relay at consistent power.
A dedicated Tools > Repeater screen consolidates the toggle, the
politeness knobs, and live forwarding stats, plus an optional "Custom"
radio profile — a dedicated frequency/SF/BW/CR for relaying, separate
from the companion's own network, band-matched to the companion
frequency by default and used everywhere a radio change can happen
(boot, on-device toggle, app-driven CMD_SET_RADIO_PARAMS, Settings
radio edits) so the device never silently falls back to the wrong
params mid-relay.
Diagnostics gains the actually-forwarded packet count, real heap-free
via mallinfo(), a reset-counters popup, and hardened loop-detect
bounds. Also: a frequency-floor fix and a float-equality preset-match
fix in the repeater profile logic, deduped BW-table/valCol/profile-
seeding helpers shared between Settings and the Repeater screen, and
a build.sh fix tolerating control characters in `pio project config`'s
JSON dump.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>