The flat 10-item Tools list grew crowded, so tools are now grouped into
collapsible sections (Location / Comms / System) that fold in place — the
same model as Settings.
Extracted that fold-in-place behaviour into a reusable AccordionList helper
(owns the collapse mask, flattened visible-row list, selection and scroll;
host supplies section sizes + two row painters) and migrated SettingsScreen
onto it, dropping its hand-rolled buildVis/visIndexOf/sectionIndex and five
state fields. AccordionList joins drawList/DigitEditor/NavView as a shared
UI element used by two screens.
Added four mini-icons (bot, note, chart, gear) so every Tools row and
section header carries a glyph.
Builds verified: WioTrackerL1 + e-ink solo (Flash 61.0%).
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>
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>
Trail map dots/diamonds/cross/start markers and the north indicator were
drawn as raw unscaled pixels, so they shrank to near-invisible specks on
landscape e-ink's larger font scale; they now route through the mini-icon
framework (icons.h) like the rest of the UI, and the grid intersection dots
scale with it too. The keyboard's preview/grid separator had the same bug
(hardcoded 1px) and now uses display.sepH().
Also makes UP/DOWN (and the keyboard's LEFT/RIGHT/UP/DOWN) wrap top<->bottom
across every list, menu and form selector, matching the behavior PopupMenu
already had.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes from a full ui-new audit (OLED + e-ink both build green):
- remove dead DisplayDriver::drawScrollArrows (all lists use drawScrollIndicator)
- SettingsScreen selection bar -> lineStep()-1, matching the drawList screens
- HomeScreen: don't force the 1s blink refresh on the CLOCK page (status icons
are hidden there anyway)
- FullscreenMsgView: clamp KEY_DOWN scroll to a cached _max_scroll instead of
over-incrementing and leaning on the next render to clamp
- DataStore: clamp use_lemon_font (>1 -> 0) on load, like the other enum fields
- move the ~1.5KB wrap scratch (trans/lines) off the render stack into shared
file-scope statics in FullscreenMsgView (single-threaded UI; the fullscreen
view and history list never lay out in the same frame)
Cleanup:
- drop dead members RingtoneEditorScreen::DUR_VALS and HomeScreen::sensors_scroll
- delete redundant manual scroll-clamps in handleInput across the drawList
screens (drawList already reclamps each render); remove the now-unused _visible
from QuickMsgScreen/NearbyScreen/BotScreen and BotScreen::scrollToSel()
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- drawList(): scrollable list skeleton (visible window, keep-sel-in-view,
scroll-column reserve, per-row callback, indicator). Migrate Tools, Bot,
Nearby, Waypoints list and the three QuickMsg lists onto it; centralises the
scroll-clamp/reserve/indicator that each had open-coded (and inconsistently).
- drawCenteredHeader(): centred title + separator, replacing the duplicated
two-liner across ~10 screens (incl. QuickMsg, which used width()/2 spacing and
was missed before); unifies the separator at headerH()-sepH().
- keyIsPrev()/keyIsNext() in UIScreen.h for the LEFT||PREV / RIGHT||NEXT idiom;
applied to 6 screens, behaviour-preserving. Cancel/context-menu stay
per-screen (they mean different things on different screens).
- rotLabel() dedups the 0/90/180/270 array in Settings.
- Unify selection-row height to lineStep()-1 (Tools/Bot matched the rest).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Replace single-letter M/B/A/G top-bar indicators with scalable mini-icons:
ICON_MUTE (speaker+cross), ICON_BLUETOOTH (rune), ICON_ADVERT (broadcast
mast+waves), ICON_TRAIL (map pin). Centred in the cw+2 indicator box;
disconnected BT shows the plain glyph instead of lowercase b.
- fix(settings): right-side values used display.valCol() without the scrollbar
reserve, so after expanding a section they rendered under the indicator.
Route all value cursors through valCol(display) = display.valCol() - _reserve.
- fix(settings): font toggle used ^=1, which on a stale value of 2 (older Hybrid
build) flips 2<->3 — both nonzero, locking applyFont() on Lemon with no way
back. Normalise: use_lemon_font = use_lemon_font ? 0 : 1.
- docs(settings): document Messages > Resend setting.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the text ^/v scroll arrows with a font-scaling indicator drawn in
icons.h: a proportional thumb between fixed up/down triangle mini-icon caps.
drawScrollIndicator() lives in a ~5px right-edge column and scales via
miniIconScale (1x OLED, 2x landscape e-ink).
- Mini-icons: ICON_SCROLL_UP/DOWN; miniIconDrawTop (exact placement) and
miniIconDrawHalo (shape-hugging 1px DARK halo, clip_top to spare the header
separator) so caps stay visible on the LIGHT selection bar without a box.
- Thumb is 3*s wide so it centres on the triangle column; caps are static end
markers (always drawn) so neither vanishes at the extremes.
- scrollIndicatorReserve(): right-edge gutter (0 when the list fits). Content,
the selection bar and the message-history bubbles are all narrowed by it so
nothing renders under the scrollbar. Portrait e-ink wrap width subtracts the
reserve in both the box-height pass and render so wrapped text can't spill.
- Drop the redundant ">" selection marker (the highlight bar already shows
selection) and shift rows to x=2, reclaiming left-edge space.
- Wire the indicator into every scrollable list (Bot, Settings, Nearby, Tools,
Trail, QuickMsg lists + DM/channel history) and add one to WaypointsView,
which scrolled with no indicator before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
End-to-end delivery indicators on the device UI, drawn next to outgoing
messages and auto-scaled to the font (legible on landscape e-ink).
DM (and room servers, which share the DM path):
- Pending / delivered / failed marker driven by the real end-to-end ACK.
Pending shows a row of dots — one per send — so auto-resend progress is
visible before it resolves to ✓ / ✗.
- Auto-resend: new pref dm_resend_count (0-5, default 2), Settings ›
Messages › Resend. A pending DM whose ACK times out is re-sent (reusing
the original timestamp) until resends run out, then ✗. Driven from
UITask::loop so it completes in the background, independent of screen.
- Incoming dedup: a retry reuses the sender timestamp + text but carries a
fresh packet hash, so addDMMsg drops copies matching prefix+ts+text.
Channels (flood, no recipient ACK):
- ✓ only once a repeater echo confirms the send was relayed into the mesh;
no echo is normal, not a failure (no pending/fail shown). A small ring
tracks a burst of sends so each matches its echo. Receive-path hashing is
gated so the hot flood path is untouched when idle.
Shared:
- Markers shown in both the history list and the fullscreen message view.
- Reusable scalable mini-icon facility in icons.h (bitmap + auto-scale);
adding a new status icon is a bitmap plus one draw call.
No changes to the upstream mesh library (src/).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pull repeated UI idioms into DisplayDriver and remove duplicated logic:
- DisplayDriver: add drawScrollArrows() and drawInvertedHeader(); replaces 11
copy-pasted scroll-arrow blocks across 5 screens and 3 inverted title-bar
blocks (NearbyScreen x2, NavView). Standardises the detail-view separator.
- useImperial(): single source on UITask; NearbyScreen/TrailScreen delegate
instead of each re-reading NodePrefs.units_imperial.
- NearbyScreen: extract saveSelectedWaypoint() (two identical blocks -> one);
drop the redundant local label buffer (WaypointStore truncates).
- UITask::shutdown(): power GPS off through LocationProvider::stop() instead of
poking PIN_GPS_EN directly — centralises enable/reset-pin + active-level logic.
Net -58 lines. Verified: GAT562 (SSD1306) and WioTrackerL1Eink (GxEPD) solo
builds compile clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two independent, default-off toggles under Settings › Radio.
Pwr save: hardware RX duty-cycle (SX126x SetRxDutyCycle via
startReceiveDutyCycleAuto). The chip cycles RX↔sleep and wakes on a preamble —
no MCU state machine; recvRaw reads the packet exactly as in continuous RX.
Falls back to continuous RX on non-SX126x. (Replaces an earlier software-CAD
state machine that fought the hardware: polling a warm-sleeping chip gave a
phantom-busy channel that stalled TX ~4 s and dropped ACKs in the scan gaps.)
Auto pwr: Adaptive Power Control. tx_power_dbm becomes a ceiling; actual TX
power tracks the reverse-link SNR margin (measured above the per-SF demod floor,
EWMA-smoothed, proportional step with a deadband). Feedback comes from direct /
room-server ACKs and, for channels (no ACK), from hearing a repeater rebroadcast
our own flood; a lost confirmation ramps power back up so channel sends can't get
stranded below what the repeaters can hear.
Prefs schema 0xC0DE0009 (rx_powersave, tx_apc). Radio page / name bar show the
live TX power; noise floor reads n/a while duty-cycling.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
NearbyScreen:
- Hold Enter on a list row now shows Navigate and Save waypoint directly,
without entering the detail view first
- TIME added as the last filter slot (LEFT/RIGHT cycles Fav→ALL→…→Snsr→TIME);
time-sort shows age in right column and sorts by last-heard descending
- Save waypoint also available from detail view Options menu
UITask:
- Extract addWaypoint() helper (two overloads: with/without explicit timestamp)
consolidating the full()+add()+saveWaypoints()+alert pattern; TrailScreen
and QuickMsgScreen updated to use it
Fixes:
- SoundNotifier.h: correct NodePrefs.h include path (../NodePrefs.h)
- kbAddSensorPlaceholders: dereference _kb pointer in SettingsScreen,
QuickMsgScreen and BotScreen (signature changed to KeyboardWidget&)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves KeyboardWidget from a per-screen value member to a single instance
owned by UITask, passed to SettingsScreen, QuickMsgScreen, and BotScreen
as a pointer. Only one screen is ever active at a time, so the shared
state is safe. Saves ~1.5 KB of always-resident heap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After widening the history buffers, the compose side was still the
bottleneck: the keyboard capped input at 139 bytes and the quick-message
expand buffers were 140. Raise KB_MAX_LEN to 160 (MeshCore's MAX_TEXT_LEN)
and size the expand/compose buffers to MSG_TEXT_BUF, so a message can be
typed, expanded, sent, stored and displayed at the full protocol length on
every path.
The custom-message editor now passes an explicit max bound to its 140-byte
store, so the wider keyboard buffer can't overflow it; the bot editor
already passed per-field maxima.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a single "Units" choice (Settings › System: Metric / Imperial) that
governs every distance and speed shown in Tools — Nearby Nodes, the Trail
views, and the navigate-to-point screen.
- NodePrefs: units_imperial (global) + trail_show_pace (Trail readout mode),
schema bumped 0xC0DE0005 → 0006. The old combined trail_units_idx is kept
for file-layout stability but no longer drives anything; upgraders default
to metric + speed.
- geo::fmtDist gains an imperial mode (ft below ~1000 ft, then miles).
- NavView / Nearby / Trail (waypoint list, Summary distance, List deltas,
map scale-bar label) all format through the global preference.
- Trail's action-menu "Units" row becomes a Speed/Pace readout toggle; the
km-vs-mi unit now follows the global setting instead of being one of four
combined options.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add per-channel favourite marking and a Settings filter (all/fav) for
the channel list, symmetric with the existing DM and Room filters.
NodePrefs:
- ch_fav_bitmask (uint64_t): bit i = channel i is marked favourite
- ch_fav_only (uint8_t): 0=show all channels, 1=favourites only
- SCHEMA_SENTINEL bumped to 0xC0DE0005
DataStore: save/load both new fields before the sentinel.
SettingsScreen: new CH_FILTER item ('Channels: all/fav') in the
Contacts section, positioned between DM_FILTER and ROOM_FILTER.
QuickMsgScreen:
- buildChannelList() skips non-favourite channels when ch_fav_only=1
- Channel context menu gains a 4th option 'Fav'/'Unfav' that toggles
ch_fav_bitmask for the selected channel and rebuilds the list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previous fix only evicted SHUTDOWN when cur_len == PAGE_ORDER_LEN (full).
If the array was not yet full but had SHUTDOWN + 2 missing pages (TOOLS and
QUICK_MSG), TOOLS would fill the last slot but QUICK_MSG would be left out.
Remove the cur_len guard: evict SHUTDOWN whenever any required page is
missing, regardless of array fullness. This frees one slot before the
append loop so all missing required pages can be added in a single pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previous fix appended missing pages only when there was room (cur_len <
PAGE_ORDER_LEN). On e-ink builds with GPS+SENSORS the default order fills
all 11 slots; old saves that included SHUTDOWN instead of TOOLS/QUICK_MSG
had no room to append the missing entries.
Now, if the array is full and a required page is absent, SHUTDOWN is evicted
first (it is appended at runtime by buildVisibleOrder's fallback and does not
need to be in the explicit list). This frees a slot for the missing page.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ensurePageOrderInit returned early after inserting FAVOURITES without
checking whether TOOLS/QUICK_MSG were present. Old page_order saves that
predated those pages had CLOCK+FAVOURITES but no Messages entry, so
movePageInOrder always found cur==-1 and silently did nothing.
Now recount valid entries after any insertion, then walk the full list of
required page bits and append any that are absent (if room exists).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Earlier commits deleted the SECTION_GPS enum entry along with its only
remaining child (Trail dist moved to the Trail tool), but isSection()
and sectionName() still referenced it under the ENV_INCLUDE_GPS guard.
Both helpers no longer need a GPS case.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per-tool config belongs in the tool, matching Auto-Advert's pattern.
SettingsScreen drops:
- TRAIL_MIN_DELTA enum entry + the entire SECTION_GPS gate (it was its
only remaining item after GPS upd / Trail int were removed)
- Render and input handler blocks
TrailScreen gains a small modal popup opened with Hold Enter (the
KEY_CONTEXT_MENU input). Enter cycles through the four min-dist values
(5/10/25/100 m), Esc closes the popup. The popup overlays whichever
view is active; savePrefs runs on each cycle so the value persists.
NodePrefs::trail_min_delta_idx and the TrailStore::MIN_DELTA_* helpers
are unchanged — only the entry point shifted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GPS NMEA polling is cheap, and a slower cadence just hides motion from
the trail; the min-delta gate already filters jitter without throttling
the GPS. Both knobs that exposed cadence-style controls to the user are
removed:
- Trail sampling is fixed at TrailStore::SAMPLING_SECS = 1 s, matching
the sensor manager's built-in GPS update default. The
intervalSecs / intervalLabel / INTERVAL_COUNT helpers go with it.
- GPS update rate ("GPS upd" entry) leaves the sensor manager on its
built-in 1 s default. applyGPSInterval and the GPS_INTERVAL_OPTS /
LABELS tables / gpsIntervalIndex helper are deleted.
Both NodePrefs fields (gps_interval, trail_interval_idx) are retained
as reserved so the schema sentinel doesn't have to bump — older saves
load cleanly, the bytes are just ignored.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two new entries in the existing GPS section of SettingsScreen, gated by
the same ENV_INCLUDE_GPS guard. LEFT/RIGHT cycles through the option
table from Trail.h:
- "Trail int" : 30 s / 10 s / 20 s / 1 min / 5 min / 15 min
- "Trail dist": 5 m / 10 m / 25 m / 100 m
Changes are stored to p->trail_interval_idx / trail_min_delta_idx and
take effect on the next sampling tick in UITask::loop (already gated on
those fields), so no separate apply* helper is needed. _dirty triggers
the normal savePrefs on settings exit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ensurePageOrderInit only inserted FAVOURITES when len < PAGE_ORDER_LEN.
Older saves with all 11 slots filled (default order with GPS + SENSORS)
hit the full-array branch and were left without FAVOURITES in the
explicit order, so the page was shown via the missing-page fallback at
end of nav but movePageInOrder couldn't reorder it.
Now drop the last entry (typically SHUTDOWN, which the same fallback
already re-appends at end of nav) to make room. End result: FAVOURITES
sits after CLOCK, reorderable; nav layout matches the new default.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Render issues:
- Drop the "FAVOURITES" title — it overlapped the node-name/battery top
bar; the page-dots indicator already identifies the page
- Use the locally-computed content_y (matches other home pages) so tiles
sit below the dots row instead of starting at headerH() + sepH()
- Add 2 px top/bottom margin inside the grid so cells don't hug the dots
or the panel edge
Upgrade migration (visibility + position):
- Newly-loaded prefs with an older sentinel OR a non-default mask had
HP_FAVOURITES off and could not be enabled — DataStore::loadPrefsInt
now ORs HP_FAVOURITES into home_pages_mask whenever the sentinel
doesn't match the current schema
- Existing page_order_set users were stuck because FAVOURITES wasn't in
page_order, so movePageInOrder bailed — ensurePageOrderInit now
detects the missing FAVOURITES entry and inserts it right after CLOCK,
shifting the tail; if the array is already full the page still
appears via the buildVisibleOrder fallback
Bug fix in ensurePageOrderInit verification loop: iterated to HPB_COUNT
(12) over a PAGE_ORDER_LEN (11) array → one OOB read per call. Now
bounded to PAGE_ORDER_LEN.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New home page rendering 6 pinned-contact slots. Grid transposes to
orientation: landscape → 3×2, portrait → 2×3. Joystick claims UP/DOWN
and inner LEFT/RIGHT for grid nav; edge LEFT/RIGHT fall through to
page navigation. Selected tile inverts via drawSelectionRow, filled
tile shows contact name + unread badge, empty tile shows "+".
Storage:
- NodePrefs::favourite_contacts[6][6] (6-byte pub_key prefix per slot,
all-zero = empty; collision probability with a real key is 2^-48)
- HomePageBit gains HPB_FAVOURITES = 11 (HPB_COUNT = 12), with a new
HP_FAVOURITES mask bit so the page is toggleable in Settings
- New PAGE_ORDER_LEN constant decouples the persisted array length (11)
from the page-type count; loops over page_order now use it, value-range
checks still use HPB_COUNT
- homePageBit returns 0 for HPB_SETTINGS / HPB_QUICK_MSG explicitly
rather than by bit-index range, so HPB_FAVOURITES (bit 11) is correctly
treated as toggleable
- Default order inserts FAVOURITES after CLOCK; SHUTDOWN drops to the
fallback "missing pages" append (still reachable, lands at end)
Schema sentinel bumped to 0xC0DE0002. DataStore::loadPrefsInt and
savePrefs read/write the new field. Older saves leave the field
zero-initialised (all slots empty), which is the natural starting state.
Phase 2 (Pin from Contact options) and Phase 3 (Pin from empty-slot
mini picker) wire interactions; this commit handles render + nav only.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pageBit/bitToPage in HomeScreen and homePageBitIndex in SettingsScreen
each spelled out the bit assignment with magic numbers 0..10. Lift the
numbering into a NodePrefs::HomePageBit enum (HPB_CLOCK … HPB_QUICK_MSG,
HPB_COUNT) and route every reference through it: mapping functions, the
HP_* bitmasks, the homePageLabel table sizing, loop bounds over
page_order, the bounds check on page_order[0], the "always-visible"
discriminator for SETTINGS/QUICK_MSG.
Per-class enums (HomeScreen::HomePage, SettingsScreen::SettingItem)
stay — they index different concerns (navigation page vs. settings
row). The mapping functions still exist, but now reference a single
source of truth for the bit values.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extends Features.h with preprocessor flags (FEAT_BRIGHTNESS_SETTING,
FEAT_CLOCK_SECONDS_SETTING, FEAT_DISPLAY_ROTATION_SETTING,
FEAT_JOYSTICK_ROTATION_SETTING, FEAT_FULL_REFRESH_SETTING) since
constexpr can't gate enum members or class fields. SettingsScreen.h
now uses #if FEAT_X instead of #if defined(EINK_DISPLAY_MODEL) — grep
on the feature name finds the toggle and every use site, and the
condition names *what* the gate controls.
Driver headers (DisplayDriver.h, GxEPDDisplay.h) keep raw
EINK_DISPLAY_MODEL — they're below the companion_radio layer and the
ifdef there gates on the build's panel type, not a UI feature.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mechanical sweep across UITask.cpp and SettingsScreen.h. All current
format strings already fit their buffers, but a future format change
would silently overflow with sprintf. snprintf with sizeof makes the
buffer bound explicit and traceable to the array declaration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Eight screens repeated the same if/else block to invert the row
background for selected items. Add DisplayDriver::drawSelectionRow that
sets LIGHT, optionally fills the rect, and leaves the colour as DARK
when sel (so the caller's text renders inverted). 61 lines removed
across SettingsScreen, KeyboardWidget (cells + special row), ToolsScreen,
DashboardConfigScreen, BotScreen, RingtoneEditorScreen (note slots +
menu), NearbyScreen, QuickMsgScreen (4 list views).
Card-style rows (QuickMsg hist, NearbyScreen discover, RingtoneEditor
notes display) keep their original drawRect/partial-fill outline for
unselected — they don't match the simple-invert pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously page_order_set status was inferred from page_order[0] being
in 1..11, so a single junk byte in that range falsely triggered
custom-order mode. Add explicit page_order_set magic (0xA5) written
alongside the array; pre-magic saves are migrated once on load when
their first byte still passes the legacy range check.
Also fix(ui): keep "@[nick] " prefix out of placeholder expansion in
QuickMsgScreen reply path, so a nick containing a token like {loc}
isn't substituted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add "Full rfsh" setting (e-ink only) with options off/5/10/20/30
partial refreshes between full refreshes. Prevents ghosting on panels
that accumulate artifacts after many partial updates.
GxEPDDisplay counts partial refreshes and calls display.display(false)
every N frames when the interval is set. Persisted in DataStore and
applied at startup via applyFullRefreshInterval().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ensurePageOrderInit now scans for CLOCK (bit 0, stored as 1) after the
existing page_order[0] validity check — if Clock is absent (partial or
corrupted saved order), it resets the array and re-initialises all pages.
renderItem calls ensurePageOrderInit before displaying home page positions
so that position numbers appear immediately on entering Settings rather
than only after the first reorder action.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- home_pages_mask XOR toggle initialises to HP_ALL before flipping bit,
preventing accidental hiding of all other pages when mask starts at 0
- buildVisibleOrder appends any visible page missing from custom page_order,
so pages absent from migrated/corrupted prefs remain navigable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add NodePrefs::joystick_rotation (0-3 steps CW), persisted in DataStore
alongside display_rotation. rotateJoystickKey() now takes a runtime rot
parameter instead of compile-time JOYSTICK_ROTATION macro. UITask::loop()
reads joystick_rotation from NodePrefs each frame. Settings screen exposes
"Joystick" rotation item on any build with UI_HAS_JOYSTICK. JOYSTICK_ROTATION
macro still works as compile-time default for legacy/non-prefs builds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Column at width()-6*charWidth so "always" touches the right edge;
ON and OFF start at the same x position.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use %2d (right-aligned, always 3 chars) instead of %d so label x-position
stays constant when going from single-digit (1-9) to double-digit (10-11).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolved conflicts to preserve eink-ui layout helpers (valCol, lineStep, sepH)
while integrating the page_order persistence, buildVisibleOrder(), and
Settings/Messages ordering UI from the wio-tracker-l1-improvements branch.
display_rotation is now written before page_order in the save/load sequence.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NodePrefs: add page_order[11] (1-indexed bit-index, 0=end/legacy).
DataStore: persist page_order appended after use_lemon_font.
UITask/HomeScreen:
- bitToPage(): maps stable bit indices 0-10 to HomePage enum values
- buildVisibleOrder(): respects page_order when initialised, else
falls back to default enum sequence
- navPage() and navigation dots use buildVisibleOrder()
SettingsScreen Home Pages section:
- Add Settings (bit 9) and Messages (bit 10) — show as "always"
- LEFT/RIGHT on any home page item moves it earlier/later in order
- ENTER toggles ON/OFF for pages that can be disabled
- Position number shown before label once custom order is active
- ensurePageOrderInit() seeds default order on first reorder action
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GxEPDDisplay: fontAscender only applies to sz=1 with Lemon; sz=2 (NULL font)
was incorrectly getting a 16px cursor offset, causing clock text to render
16px lower than expected and overlap the date line
- KeyboardWidget: compute cell_h from available screen height instead of lh+1;
tighten sep_y to lh so the grid fits within 122px even with Lemon font (lh=20)
- SettingsScreen renderBar: constrain box size to available width so the 5th
buzzer-volume square doesn't overflow past the right edge with Lemon font
- NearbyScreen discover detail: manually truncate b64 public key by charWidth
to guarantee one-line rendering; use dynamic step = (height-hdr)/5 so
Status line doesn't fall off the bottom of the screen
- NearbyScreen contacts detail: merge dist+az into one line and use dynamic
step to fit 5 lines within the display; removes the off-screen Seen: row
- AutoAdvertScreen: replace lineStep() gap before hints with 4px fixed gap
so both hint lines fit within the display with large fonts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>