The status-bar GPS icon appeared whenever the board had GPS hardware,
even if the user had turned GPS off in settings, implying a search
that wasn't actually happening.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adafruit_USBD_CDC::write() blocks as long as DTR is asserted, even if
the host stopped draining the FIFO. Every writeFrame() call and the
GPX trail export wrote directly to Serial, so a serial monitor left
open but idle could stall the main loop until the 30s watchdog reset
the device.
streamWriteUntil() caps the wait with an absolute deadline shared
across chained writes, and only enters the wait loop when
availableForWrite() is actually exhausted, so a healthy host sees no
behavior change. The GPX export latches after the first stall to
avoid paying the timeout once per write call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Status bar: add a boxed/plain GPS reticle icon reflecting LocationProvider
fix state, shown only on boards that have a location provider. Drop the
now-redundant "Fix:y/n" text from the map page's info line.
- Home minimap: replace the bottom-left scale bar with a "->dist" label to
the nearest live-tracked point when one is available, falling back to a
smaller single-tick scale indicator otherwise. Less likely to be covered
by a plotted point than the old full-width bar, and more useful at a
glance. Drop the now-duplicate nearest-distance text from the map page's
info line.
- Tools screen: comment out per-row/section icons (kept for easy restore) —
they don't fit visually in that list for now.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two receive→UI wiring gaps of the same class as the room-history fix:
- onSignedMessageRecv (room posts) didn't parse [LOC] live-location shares,
while DM (onMessageRecv) and channel (onChannelMessageRecv) both do — so a
guest sharing position in a room never appeared on the map/nearby. Resolve
the signed sender prefix to a name and track by name, mirroring the channel
path (unverified: only a 4-byte prefix is available here).
- UIEventType::roomMessage was defined but never emitted; room notifications
rode on contactMessage and always played the global DM melody. Emit
roomMessage for room posts and handle it explicitly (default DM melody, no
per-sender lookup since the author varies); drop the now-duplicate dead case.
Build verified: WioTrackerL1 solo.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the home carousel's page-indicator dots with a small per-page glyph
set (uniform 5x5, smaller than the menu/status icons), the current page
underlined rather than inverted (an inverted box read too much like the top
status bar). Icons follow buildVisibleOrder, so custom page order and
hide/show toggles from Settings carry over automatically; the row pitch
shrinks to fit when many pages are enabled, and the band geometry scales with
the font (identical layout to the old dots at 1x, clears header/content on 2x
e-ink).
Adds 13 ICON_PG_* glyphs (clock, star, recent, radio, bluetooth, advert,
gps, sensors, settings, map, tools, msg, power).
Builds verified: WioTrackerL1 + e-ink solo.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Incoming room-server posts fired the notification and reached the app via
the offline queue, but never appeared when the room was opened directly on
the device: the on-device history (addDMMsg) was gated to ADV_TYPE_CHAT only,
while rooms (ADV_TYPE_ROOM) share that same history list (keyed by the
server's pubkey). Include ADV_TYPE_ROOM so room posts are stored too.
A room carries many guests, so prefix each stored post with its author —
resolved from the signed message's sender pubkey prefix (fallback: short
hex) — and have the DM history view split "Sender: text" for room contacts
so each line is attributed to the guest who wrote it, instead of showing the
room server's name for every message.
Builds verified: WioTrackerL1 + GAT562 30S solo.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Mirror the OLED solo size optimisation (44ae2745) on the remaining solo
builds so CI artifacts ship the reduced firmware too. The nRF52 Arduino
core defaults to -Ofast (≈ -O3 + -ffast-math); overriding with
build_unflags = -Ofast + -Os reclaims ~200 KB with no feature loss.
E-ink solo: Flash 62.1% (440140 B)
GAT562 solo_ble: SUCCESS
GAT562 solo_dual: Flash 61.9% (441164 B)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The nRF52 Arduino core compiles with -Ofast (≈ -O3 + -ffast-math) by
default, which had pushed this feature-rich solo build to ~90% flash.
Unflag -Ofast and build with -Os: flash drops 90.1% → 60.9% (638 KB →
431 KB, ~200 KB reclaimed) with no loss of functionality and a negligible
speed cost for a UI/mesh workload. -Os also avoids fast-math float
reassociation, so it's marginally safer for the geo/distance maths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A channel [LOC] broadcast already carries the sender (sendGroupMessage
prepends "<name>: "), but the DM path sent a bare "[LOC]<lat>,<lon>" with
no name in it. Embed the node name as a trailing token after the
coordinate so a DM share is self-describing in any chat client;
parseLocShare ignores the trailing text, so receivers are unaffected.
Verified: WioTrackerL1_companion_solo_dual builds clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The label collision-avoidance reserved waypoint label slots first, so a
tracked contact's name was dropped whenever it sat near a waypoint —
leaving only the waypoint label. Label live-tracked contacts first: on a
live map the moving person's name should win the slot over a nearby static
waypoint.
Verified: WioTrackerL1_companion_solo_dual builds clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Home Map page mini-preview gains a north mini-icon and a fillRect scale
bar with a round-distance label (metric/imperial), and its status line now
shows the distance to the nearest tracked contact alongside fix/track.
Trail map labels no longer smear in dense clusters: each placed label
reserves a rect and a new one tries four offsets around its marker,
skipping (marker still shown) when none fit without overlap.
Verified: WioTrackerL1_companion_solo_dual builds clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The stored list only re-merged + re-sorted periodically under the "recent"
sort; under distance sort it refreshed only on entry. So a [LOC] share
arriving while the list was open (sorted by distance) wasn't re-sorted and
the now-nearest node never moved to the top. Refresh on a timer under both
sorts, keeping the highlighted node selected across the rebuild. Factor the
identity-preserving reselection into refreshKeepingSelection(), shared by
the list and the detail/nav refresh paths.
Verified: WioTrackerL1_companion_solo_dual builds clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enter on the Target row now opens a picker instead of only cycling: it
lists favourites first (the quick path), then active verified live
senders, then waypoints — UP/DOWN + Enter to choose. This lets you arm an
alert against a person ahead of time by pubkey, without waiting for them
to share a [LOC] (the previous cycle only offered currently-active live
senders, so a person you hadn't received a fresh share from couldn't be
selected at all). LEFT/RIGHT still quick-cycles the same unified set.
Verified: WioTrackerL1_companion_solo_dual builds clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A position-sharing node that isn't a stored contact was appended to the
Nearby list regardless of the active type filter, so a companion would
show up (and, having a live distance, sort to the top) under the Repeater
/ Room / Sensor filters. Gate the append on the type filter — a non-contact
[LOC] sender is treated as a companion, so it only appears under All/Comp.
Also drop the "share must be newer than the advert" guard when overlaying
a share onto a stored contact: a [LOC] message is an explicit current
position, so it always defines the pin and the distance used for proximity
sorting (recency for the time sort stays the most recent of advert/share).
Verified: WioTrackerL1_companion_solo_dual builds clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the text keyboard for the "Add by coords" Lat/Lon fields with the
shared digit-by-digit DigitEditor (the radio-frequency widget): Enter
opens the editor on the magnitude (LEFT/RIGHT move the cursor, UP/DOWN
change the digit), and LEFT/RIGHT on the row still toggles the hemisphere
when the editor is closed. The keyboard never fit numeric entry — no
minus, easy to type stray letters. Label still uses the keyboard. Docs
updated.
Verified: WioTrackerL1_companion_solo_dual builds clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The proximity beeper is its own opt-in toggle, so enabling it is an
explicit "I want to hear this" — drop the isBuzzerQuiet() guard so it
ticks even when the global buzzer is muted (manual Off or BT auto-mute).
The arrive/leave crossing alert has no dedicated toggle and keeps
respecting the mute. Docs updated.
Verified: WioTrackerL1_companion_solo_dual builds clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document the live-following additions in the Tools docs: Geo Alert target
can be a waypoint or a live contact (Near/Away wording, follows a moving
person), the ♦ broadcast marker and "Sharing pos:" in Nearby, the
ETA line in the navigate view, and the home Map page Hold-Enter one-shot
share.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Builds on live location sharing:
- Nearby "Navigate" now follows a moving live target: the navigate view
re-reads the shared position every second and re-locates the entry by
name for non-contact senders, so it no longer freezes or drops out
(previously only refreshed for stored contacts entered via detail).
- NavView gains a closing-speed / ETA line (EtaTracker), shown in the
Nearby navigate view while approaching.
- Geo Alert target can be a live contact, not just a waypoint: the engine
re-reads the contact's latest [LOC] position each evaluation (keyed by
pubkey prefix) so the geofence follows a moving person; alert wording
switches to Near/Away. The picker cycles waypoints + verified live
contacts (shown with an '@' prefix). Schema bumped 0xC0DE0014 -> 0x15.
- Nearby marks a position-broadcasting node with the map's ♦ diamond
(was a cryptic '*'/'~' prefix) and spells out "Sharing pos:" in detail.
- Home Map page Hold-Enter is a one-shot position share: pushes to the
Live Share target while auto-sharing is on, else opens the picker.
Verified: WioTrackerL1_companion_solo_dual builds clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tools Screen docs gain Live Share (send/track [LOC] positions, target +
movement/heartbeat gates) and Geo Alert (waypoint geofence, arrive/leave
modes, proximity beeper) sections, plus the Trail auto-pause setting, the
"Share my pos" action, live-tracked contacts on the map, and the home Map
page note. README feature lists updated to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review cleanup of the new location features:
- GeoAlertScreen + LiveShareScreen use the shared drawList() helper instead
of hand-rolling the scroll/clamp/scrollbar maths, matching every other
list screen.
- Extract UITask::geoAlertDistance() — one place for the target-distance
maths, shared by the crossing evaluator and the proximity beeper.
- Geo Alert re-seeds its crossing engine after every edit, so changing the
target/radius while armed can't fire a stale arrive/leave before exit.
- Trail auto-pause gets its own coarse movement gate
(NodePrefs::TRAIL_AUTOPAUSE_MOVE_M = 15 m) instead of reusing the trail
min-delta, so GPS jitter while parked doesn't keep the idle timer alive.
- Fix stale include comments.
Verified: WioTrackerL1_companion_solo_dual builds clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Geo Alert (Tools › Geo Alert): a single geofence around a snapshotted
waypoint. Beeps + alerts on crossing into (arrive) / out of (leave) the
radius per mode, with edge hysteresis and silent first-eval seeding. Adds
a proximity Beeper that ticks faster the closer to the target while inside
the radius — independent of the discrete arrive/leave alert.
Trail auto-pause (Trail › Settings): freezes the trail timer + sampling
after the device sits still for a configurable delay, resuming on the next
real movement. New paused state in TrailStore (distinct from Stop); Summary
shows "paused".
Persisted via new NodePrefs fields; schema bumped 0xC0DE0012 → 0xC0DE0014
with clamps so upgraders start with both features off.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Live Share tool: add scrollbar, drop non-clickable section headers and
the one-shot "Share now" (keep Auto-share toggle; manual share stays on
the Map). Ellipsize the "To" target so long names don't wrap.
- Move "Track loc" (receive) from Settings into Live Share.
- Map: 2-char contact labels (distinguishable); drop "press" hint from the
Home Map page status line so it fits on OLED.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address the open items from the FEATURES.md audit backlog:
- saveChannels() now skips unused slots (all-zero secret) instead of
writing all 40 every time, so /channels2 holds only configured channels
(was always ~2.7 KB) and wears the flash less. loadChannels() already
compacted empties on read, so the loaded result is unchanged.
- findChannelIdx() returns -1 for an all-zero secret, so a corrupted/empty
channel can't match an unused all-zero slot and misroute messages.
- TrailStore gains a static_assert that CAPACITY fits the uint16_t save
header count, failing the build instead of silently truncating.
Also re-classify two audit items verified to be non-issues in current
code: the bot strstr "199-char" truncation (BOT_SCRATCH=200 >= MAX_TEXT_LEN
=160, unreachable) and the MSG_PICK reply title (rlen clamped to 20, so
title[24] never overflows).
Verified: WioTrackerL1_companion_solo_dual builds clean.
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>
isValidRepeaterProfile() hard-coded the 150-960 MHz SX1262 range, which
would wrongly reject legal frequencies on any other chip. Take the freq
bounds as parameters and pass radio_driver.getFreqBounds() at both call
sites (repeaterProfileValid() and the load-time migration), so the chip's
own validated range is the single source of truth. DataStore.cpp gains a
target.h include for radio_driver (declared extern there).
DigitEditor::render() now zero-pads the integer part to int_digits: the
cursor addresses place values (100/10/1/0.1…), so a value with fewer
integer digits than int_digits would shift every glyph and misplace the
highlight. No visual change for the frequency field (always 3 digits).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rename the repeater filter knobs from "politeness" to "forwarding
filters" across code comments, FEATURES.md and the docs, and correct
references that hadn't followed the controls when they moved from
Settings › Radio to the dedicated Tools › Repeater screen.
Comment/doc accuracy fixes:
- NodePrefs: user_radio_presets are now written by both Settings and
Repeater (shared picker), not Settings only.
- DiagnosticsScreen: class header listed only some rows; spell out the
full set (forwarded, signal, pool/queue, error flags, reset gesture).
- RepeaterScreen header no longer claims to show live forwarding stats
(they live on Diagnostics).
- tools_screen.md: drop the stale paragraph claiming the Repeater screen
shows live stats at the bottom — it is config-only.
Comments/docs only; no behaviour change.
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>
Shows packet counts by category (RX/TX), radio noise floor/RSSI/SNR,
packet-pool free count and outbound queue length, uptime, and heap/stack
headroom on a single scrollable screen.
Adds generic per-payload-type RX/TX counters to Dispatcher (mesh layer),
plus pool-free/queue-length getters, and a new DeviceDiag helper for
nRF52 heap (linker-symbol + sbrk) and stack (FreeRTOS high-water-mark)
stats — the only platform needed for the 3 in-scope boards (Wio Tracker
L1 OLED/Eink, GAT562 30S), all nRF52840.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Matches every other list (drawList's row width - reserve): the highlight
shouldn't paint over the indicator's column, just frame it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tick marks, the scale baseline and the travel-direction pointer triangle
were hardcoded pixel constants, so they shrank to near-invisible slivers
next to the 2x cardinal labels and numeric readout on landscape e-ink.
Same fix pattern as the trail map / keyboard separator: derive sizes from
miniIconScale()/sepH() instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PopupMenu drew its own unscaled ^/v text markers instead of the
track+thumb indicator the rest of the UI uses, so it didn't show position/
proportion and looked inconsistent on e-ink. drawScrollIndicator(Px) assumed
a full-width list (anchored to d.width()), which doesn't hold for a centred,
narrower popup box — added a right_x-aware overload so it can anchor to the
box's own edge instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PopupMenu is compact and stands alone (no full-screen header to separate
from), so the gap meant for listStart() just wasted vertical space and let
the selection bar's edges miss the box border by a few px.
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>
Content drawn under the title separator touched the line on every standard
screen, because listStart() == headerH() (the row right after the separator).
Graphical screens worked around it with a hand-rolled hdr+2.
Bake a 2px breathing gap into listStart() so every list gains it at once, and
switch the screens that hand-rolled the offset (Compass, Nav, Nearby detail) to
listStart() so the content top is computed the same way everywhere.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The popup menu used fixed pixel constants (box width, item height, text
positions) sized for the OLED 128x64. On landscape e-ink the font is ~2x, so
rows overlapped, text ran past the box edge, and the box never adapted to its
contents.
Rebuild render() from live font metrics: box width fits the widest title/item
(clamped to the screen, centred), row height and title bar derive from the line
height, items and title are ellipsized so nothing overflows, and the scroll
arrows live in a reserved gutter. The title separator uses the scaled sepH()
with a 2px gap before the first row so content no longer touches the line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The joystick rotation setting never survived a reboot: loadPrefsInt cleared it
on every load. The post-read override guarded on FEAT_JOYSTICK_ROTATION_SETTING
(and JOYSTICK_ROTATION), but DataStore.cpp included neither Features.h nor the
macro's header, so both were undefined — `#if !FEAT_JOYSTICK_ROTATION_SETTING`
was always true and the stored value was forced back to 0 every time.
Include Features.h so the flag resolves per build, and only force the default
when the setting isn't user-editable (OLED). On e-ink the stored value is now
kept; stale values migrated from another build are still corrected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The solo build already copies firmware.zip (the adafruit-nrfutil DFU package
used for OTA / BLE-DFU updates) into out/, so it was present in the Actions
artifact but never in the tagged GitHub Release. Give it a friendly name
(solo-<ver>-<device>-ota.zip) and include out/*.zip in the release files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Portrait e-ink message history had two scrollbar glitches:
- Track length tracked the last visible box's bottom, so a long message at
the bottom that left empty space shrank the whole bar. Pin the track to the
full list area (hist_start_y..cby); only the thumb sizes/moves.
- Thumb size was derived from the per-frame visible-box count, which fluctuates
with variable-height boxes. Drive it off pixel sums with a constant viewport
(view_px = track_h) so the thumb stays stable while scrolling one list.
- Gutter reserve used last frame's _hist_visible, so an incoming message briefly
toggled the gutter and reflowed box widths. Decide it from a whole-list fit
test at the widest layout instead — stable, no flicker.
Unify both fixes in computeHistScroll() (shared by DM + channel views) and
split drawScrollIndicator into a pixel core (drawScrollIndicatorPx) + an
item-count wrapper.
Co-Authored-By: Claude Opus 4.8 <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>
The DM/channel history views size the scroll-column reserve from the previous
frame's visible count. A new message bumps the count so the reserve briefly
appears until the next frame settles it — normally invisible, but the alert
overlay froze the screen (_next_refresh = _alert_expiry) for the alert's whole
duration, leaving the content shifted as if a scrollbar were needed. Keep the
underlying screen refreshing at its own cadence (capped at the alert expiry);
the display CRC skips unchanged frames so e-ink isn't thrashed.
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>
- Extract drawBoxedIcon()/drawSlotIcon() in icons.h; collapse the four
near-identical mute/BT/advert/trail blocks in renderBatteryIndicator.
- Centre the glyph on the actual indicator box (box_h) instead of the text
line, fixing the 1px vertical offset in Lemon mode.
- Extract blinkOn() for the shared advert/trail blink cadence.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the old ^/v scroll arrows with the proportional scroll indicator
(thumb + triangle caps) used by the list screens. Reserve its right-edge
column and re-wrap the message text to the narrower width so nothing renders
under it.
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>