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>
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>
Restore commented-out EXTERNAL_FLASH_DEVICES/USE_QSPI for reference;
board has no QSPI flash chip — defines were RAK4631 leftovers causing
framework to lock P0.28/P0.30 (joystick UP/LEFT) as QSPI outputs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Board has no QSPI flash chip — EXTERNAL_FLASH_DEVICES/USE_QSPI were
copied from RAK4631. The framework was initialising the QSPI peripheral
on startup, locking P0.28/P0.30 (joystick UP/LEFT) as outputs. Removing
these defines prevents QSPI init entirely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
QSPI IO0=P0.30 and IO2=P0.28 are wired to joystick LEFT and UP on this
board. QSPI init configures those pins as OUTPUT, blocking button reads.
Disable QSPIFLASH so the joystick works; firmware falls back to internal
flash storage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Contacts with lastmod=0 or lastmod>now (RTC not synced after reboot but
contacts have timestamps from a prior session) were sorted to the top
because their raw lastmod > 0. Now both cases are normalised to 0 before
the comparison, so unknown-time entries always appear at the bottom.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TIME mode: call refresh() every 3 s so newly-heard contacts bubble up
to the correct position; previously the list froze at the order captured
when the filter was selected.
- All filters: right-align the distance/age value within the right column
(width - textWidth - 2 px) instead of pinning it to a fixed left edge,
so short strings like "5s" or "3m" sit flush with the longer ones.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Trail.h gpxWaypoints/gpxPoint: snprintf truncation clamp was `> sizeof`
(writing the NUL terminator into the XML stream); corrected to
`>= sizeof - 1` at all 3 sites (<wpt>, <time>, <trkpt>).
- UITask.cpp battMvToPercent: raise 100% ceiling from 4200 → 4170 mV —
the board never charges to 4200, so the indicator was stuck at ~97%.
- Trail.h header comment: corrected RAM cost from "256×12 B=3 KB" to
actual CAPACITY(512)×16 B=8 KB.
Co-Authored-By: Claude Sonnet 4.6 <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>
Single-file Web Serial app for GitHub Pages. Connects to the device
over USB CDC, auto-detects <?xml…</gpx> frame from the Export GPX
action, and offers the file as a browser download with an editable
filename (defaults to trail_YYYY-MM-DD.gpx). Resets automatically
after download so back-to-back exports work without reconnecting.
Requires Chrome/Edge (Web Serial API). Enable GitHub Pages from the
repo settings pointing at the branch root or docs/ as needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deduplicate the snprintf+showAlert pattern used in three mark-as-read
sites into a single private method.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MODE_SELECT already showed the count; CONTACT_PICK ("Mark as read") and
CHANNEL_PICK ("Mark all read") were silently clearing unread without
any feedback. Count is read before zeroing so the alert is accurate.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move all buzzer/melody dispatch (DM, channel, advert) into a new
SoundNotifier class in SoundNotifier.h, isolating fork-specific sound
code so upstream changes to notify() no longer cause merge conflicts.
The shared playSlot() helper also removes the custom_played
duplication that existed across the three cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The enum value and its case label were never emitted after PR #16 —
both dispatch sites in MyMesh.cpp now use advertReceivedFlood or
advertReceivedZeroHop exclusively.
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>
Reverts onDiscoveredContact to the upstream 4-param signature, adding a
separate virtual onDiscoveredAdvert(bool was_flood) with a default no-op
implementation. Only MyMesh overrides the new virtual; simple_secure_chat
and any future implementors need no changes on upstream merges.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After PR #16 added None (slot=3) to melody options, only notif_melody_ad
had a bounds check. Add matching clamps for dm and ch to prevent stray
bytes from an older prefs file triggering buildMelodyFromPrefs with an
out-of-range slot.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Upstream's native NRF52 companion power-saving (PR #1238) and the preamble
16->32 bump for SF<9 both landed in the v1.16 merge. Update the battery/power
roadmap entry: the MCU now sleeps via board.sleep(0) when !hasPendingWork()
(no toggle — not sleeping only wastes power; it doesn't touch the radio), and
the preamble fix unblocks the CAD RX-windowing experiment on feat/power-saving.
Note the radio_set_*() globals were removed upstream (replace with radio_driver.*)
so that branch will need a rebase.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The splash shows the upstream base version from the hardcoded MESHCORE_VERSION
build flag (distinct from FIRMWARE_VERSION, which is our solo version). The
upstream merge bumped FIRMWARE_VERSION to v1.16-solo.0 but left this flag at
1.15. Update all three occurrences (wio + eink) to 1.16.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Self-review caught a regression I'd just introduced: feeding hasConnection()
from isClientConnected() (BLE *or* USB) broke the GPX-export collision warning
in TrailScreen, which relies on hasConnection() meaning "BLE app connected"
(BLE → USB dump is safe; otherwise the app may be on USB → warn).
Correct split:
- hasConnection() ← isBLEConnected() — BLE-specific consumers: BT status
indicator, pairing PIN, GPX-export warning.
- isClientConnected() (BLE bonded OR open USB-CDC port) used directly only by
the Auto buzzer mute and message-wake, which should react to either transport.
Builds clean on dual (OLED + e-ink) and pure-BLE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
USB client presence IS detectable on nRF52 after all: (bool)Serial ==
tud_cdc_n_connected() (DTR — a host has the CDC port open). Add
isClientConnected() = BLE-bonded OR USB-CDC-open; DualSerialInterface
overrides it, base defaults to isConnected() (single-transport unchanged).
Wire it so each consumer gets the right signal:
- hasConnection() ← isClientConnected(): Auto buzzer mute + message-wake now
trigger on BLE or an open USB port (PR #14's intent, done correctly), but
not on charging-only (no host → DTR low).
- BT status indicator + pairing PIN stay on isBLEConnected() (BLE-specific).
Caveat: a plain serial monitor also asserts DTR, so it counts as connected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PR #14 (advert sound) changed setHasConnection() from isBLEConnected() to
isConnected() to also mute on USB — but DualSerialInterface::isConnected() is
hardcoded true (USB is always a send fallback) and ArduinoSerialInterface's is
too ("no way of knowing"), so USB-client presence is undetectable. The result
on every dual build: hasConnection() always true, which broke everything that
means "a companion app is connected":
- Auto buzzer mute → permanently muted (Auto behaved like Off)
- BT status indicator → always shown connected
- message-wake → display never woke for new messages
Revert to isBLEConnected() (the only detectable client signal). Mute-on-USB
isn't achievable; Buzzer = Off covers that case manually.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Bluetooth-page PIN prompt was gated on hasConnection(), which is fed by
the serial interface's isConnected(). DualSerialInterface::isConnected()
always returns true (USB always "connected"), so waiting_for_pair was always
false and the PIN was never drawn — on every dual build, i.e. the published
OLED + e-ink firmware. Pure-BLE builds were unaffected (there isConnected()
reflects bond state).
Gate the PIN on actual BLE-bonded state instead: add
AbstractUITask::isBLEConnected() (forwards to BaseSerialInterface::
isBLEConnected(), which DualSerialInterface overrides with the real BLE state
and pure-BLE inherits as its bond-aware isConnected()). The PIN now shows on
the Bluetooth page while BLE is on and not yet bonded.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Settings › Sound gains an "AD sound" row, and Auto-Advert notes the audible
"in range" heartbeat: the device chirps on every received advert (any node),
melody chosen by AD sound; there's no off, so silence it via the Buzzer
setting. built-in still chirps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a navigation tagline to the intro and a dedicated "GPS navigation"
feature-highlights block (waypoints, compass, navigate-to-anything,
location sharing, trail, global units); note location nav in the Messages
entry and expand the Tools entry to cover waypoints/compass/navigate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Release notes covering the navigation suite (waypoints, GPS compass,
navigate-to-point, mesh location share, waypoints in GPX), the global
metric/imperial Units setting, the long-message buffer fix, plus the
existing v1.14 items, under docs/releases/solo-v1.14.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- geo::parseLatLon now rounds lat/lon in double (matching the Add-by-coords
form) instead of through a float cast that lost ~1 m near 1e8.
- NearbyScreen: preserve the ping menu's selected row across a rebuild
instead of snapping back to "Send" when a result line arrives.
- NearbyScreen: guard the ping-menu input path so a vanished contact
(selectedStoredPubKey == false) closes the popup rather than acting on an
uninitialised pub-key buffer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a trail exists but the framed box collapses to one point (e.g. a
single recorded point, or all points coincident, with no GPS fix), the
degenerate branch was drawing waypoint[0] at the centre — but waypoints
aren't folded into the box in trail-priority mode, so a possibly-distant
waypoint appeared at the trail's location. Only centre a waypoint in the
genuine "lone waypoint, no trail" case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GPX dumps previously contained only the <trk>. Emit saved waypoints as
<wpt> elements (label → <name>, plus <time> when the RTC was set) before
the track, per GPX 1.1 ordering, in both live and saved-file exports. The
emitter is duck-typed over the waypoint store so Trail.h stays decoupled
from Waypoint.h; labels are XML-escaped. They now import as pins in OsmAnd /
Garmin / GPX Studio alongside the track.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Capture the plan to multiply battery life via radio power technique (the
dominant draw is continuous RX, not the framework): CAD-based RX windowing
(~10-15mA → ~3-5mA, behind a setting due to the latency tradeoff) and
Adaptive Power Control, inspired by the ZephCore Zephyr port but stayable in
this Arduino tree. GPS power gating explicitly excluded — the device is a
live navigator/trail recorder. Deferred until trail polishing is done; prefer
adopting upstream nRF52 powersaving work.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The form's keyboard is the full alphanumeric one, and atof() silently read
a non-numeric entry as 0.0 — so typing letters created a waypoint at
(0, 0). Validate each coordinate with strtod and require the whole field to
be numeric; bad input now reports "Lat/Lon invalid" and saves nothing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Waypoints list header now reads "WAYPOINTS <n>/<cap>" (e.g. 3/16) so
it's clear at a glance how many waypoints are stored and how much room is
left before the table is full.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add-by-coords sat in the top-level Trail action popup alongside Mark here.
Move it into the Waypoints view as a trailing "+ Add by coords" row, so the
popup stays focused on trail/marking actions and waypoint management lives
in one place. "Waypoints" is now always shown in the action menu (it hosts
the list, backtrack and the add entry), so the first waypoint can be added
by coordinates even with no trail or saved points yet.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The on-screen keyboard has no comma or minus, so a single "lat,lon" entry
was unworkable. Replace it with a WP_ADD form: separate Lat / Lon / Label
rows plus Save. Lat/Lon take a magnitude from the keyboard and a hemisphere
(N/S, E/W) toggled with LEFT/RIGHT, so negative coordinates need no minus
key. Save validates the range, auto-names a blank label, and stores the
point via the normal waypoint path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New Trail action "Add by coords" creates a waypoint without a GPS fix: the
keyboard takes a "lat,lon" string (decimal degrees), parsed by the shared
geo::parseLatLon; on success it falls through to the normal label keyboard
(blank → auto WP<n>). Useful for a meeting point or a spot read off a map.
Invalid input reports "Bad coordinates".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Range test → ✅: shipped as the Nearby Nodes ping (RTT + SNR, repeatable);
the dedicated continuous-ping/sparkline screen was not built (kept as future).
- Lock-screen unread count → ✅: total "<n> unread" badge on the lock screen.
- Mark-read at type level → ✅ pointer to the implemented priority entry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Waypoints/nav, GPS trail, Backtrack, Compass-to-contact flipped to ✅ with a
consolidated "shipped" summary noting deltas from the original spec
(two-char labels, heading tape compass, global Units, mesh location sharing,
trail-priority map framing, shared geo/gfx helpers).
- New backlog item: heading-up (track-up) map orientation, with the COG-only /
jitter / fit / grid / e-ink caveats and a lightweight rotate-the-fit plan.
- Audit: record the message-buffer length fix (history/compose sized to
MAX_TEXT_LEN) as ✅.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
While a trail exists the map now fits the recorded route (plus the live
position) and no longer folds waypoints into the bounding box, so a far-off
waypoint can't zoom the view out and squash the trail. Waypoints that
project outside the framed area are clamped to the nearest edge (marker +
label) instead of being lost off-screen. With no trail, the view still
auto-fits to the waypoints as before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The label was always drawn up-and-right of the marker (wx+4, wy-3) and was
dropped entirely if it would clip the right edge — so waypoints near the
top/bottom (and left) edges had their label spill off the map or vanish.
Add an edge-aware placement helper: prefer upper-right, flip to the left
when it would clip the right edge, and clamp vertically so edge/corner
waypoints keep a visible label. Used by both the normal and degenerate
single-point map paths.
Co-Authored-By: Claude Opus 4.8 <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>
Message history buffers were smaller than the over-the-air maximum, so long
messages lost their tail on display. Channel messages carry the sender
embedded as "Name: body" in the payload (up to MAX_TEXT_LEN = 160 B), but
ChHistEntry::text was only 140 B; DmHistEntry::text was just 80 B — and with
Polish text each accented char is two UTF-8 bytes, so the effective limit was
roughly half the visible characters.
Size both history buffers (and the fullscreen/preview split copies) to
MAX_TEXT_LEN + 1 so a full-length message is stored and shown intact. Sender
split buffers widened to the 32-char name limit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Previously a shared location was only actionable after opening the message
fullscreen; Hold Enter on a history row only ever offered Reply. Route the
DM and channel list context menus through the same buildFsMenu /
dispatchFsAction path as the fullscreen view, so Navigate / Save waypoint
appear directly on the list row when the selected message carries a
location (Navigate returns to the list on Back).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Waypoints list → Hold Enter → Send hands the point to the Messages screen
as "[WAY]<lat>,<lon> <label>" (same text format geo::parseLatLon already
reads). The user picks a contact or channel, the text lands prefilled in
the keyboard to confirm/edit, then sends — closing the loop with the
Navigate / Save waypoint actions on the receiving end.
- QuickMsgScreen: share-compose mode (startShare/beginShareCompose). Picking
a recipient jumps straight to the prefilled keyboard; cancel returns home;
afterSend clears the mode.
- UITask::shareToMessage hands off from TrailScreen to the Messages screen.
- TrailScreen: "Send" added to the waypoint Rename/Delete popup; builds the
[WAY] payload at the {loc} precision (5 dp).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The shrunken ring left only a few-pixel needle in the OLED's short vertical
space. Switch to a linear heading tape: a fixed travel-direction pointer at
centre over a scrolling N..E..S..W scale (30° ticks, N/E/S/W labelled),
with the large degrees+cardinal readout below. The tape is purely linear
(no trig) and uses the available width, so it stays readable on the small
panel. Update the Compass docs to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The N/E/S/W letters rode on the ring radius, so they overlapped the circle
as the card rotated. Place the letters on an outer radius and draw the ring
a glyph-height smaller, leaving a clear gap between them. The forward index
now sits in that gap, just outside the ring.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The no-heading hint was centred on the compass circle, overlapping the
ring. Draw it in the bottom readout band instead so the ring stays clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow the global Units preference in the two remaining Trail distance
spots that were still metric-only:
- Min dist gate: the index is now a unit-agnostic level (0=finest..3).
Metric stays 5/10/25/100 m; imperial uses round 15/30/75/300 ft (applied
as their metre equivalents in the sampling gate).
- Map scale grid: imperial mode uses round ft/mi steps (10 ft … 100 mi)
instead of metric steps with a converted label. Step storage moved to
float metres so sub-metre imperial steps keep their geometry; labels come
from a parallel table so the scale bar reads an exact round value.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the Settings › System "Units" (Metric/Imperial) row and update the
Trail action-menu entry from the old four-way km/h·mph·min/km·min/mi
"Units" cycle to a Speed/Pace "Readout" toggle within the global system.
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>
The last stray getLocationProvider/isValid duplicate (with its own
ENV_INCLUDE_GPS guard). Fold it into ownPos so every GPS lookup in the
nav screens goes through UITask::currentLocation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The static haversineKm/bearingDeg/bearingCardinal/fmtDist members were
thin pass-throughs to GeoUtils. Remove them and qualify the call sites
with geo:: so there's no parallel local API to keep in sync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
drawLine was duplicated verbatim in TrailScreen and CompassScreen, and
drawCircle lived only in CompassScreen. Lift both into a gfx:: namespace
header so the map and compass share one copy of each 1-px primitive.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
UITask::currentLocation() becomes the one place that reads the
LocationProvider fix. TrailScreen::ownPos, CompassScreen::gpsValid and
NearbyScreen::refresh now delegate to it instead of each duplicating the
getLocationProvider/isValid dance (and the ENV_INCLUDE_GPS guard).
pushCogFix now restarts the course-over-ground ring when the gap since the
previous fix exceeds 15 s: after losing GPS for a while, a window spanning
the stale old fixes would compute a bogus "teleport" heading. The
last-good heading is kept for display continuity.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clarify in both the Map view row and the Waypoints section that the
current GPS position and all waypoints are drawn continuously, so the Map
view is useful standalone without an active trail recording.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
One character was too ambiguous to tell adjacent waypoints apart on the
map. Draw the first two characters of the label (falling back to one when
the label is a single char) in both the normal and degenerate map paths,
and widen the right-edge fit guard to the rendered text width.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the map collapses to one coordinate (e.g. a single waypoint with no
trail and no GPS fix yet), the degenerate branch drew the waypoint marker
but never its label — so it appeared as a bare diamond. Draw the first
character of the waypoint's label beside the centred marker, matching the
normal map path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Trail Map is now useful as a plain "you are here + waypoints" view even
when no trail is being recorded:
- The bounding box folds in the live GPS fix (via ownPos) alongside the
trail and waypoints, so the current position is always in frame.
- The current-position marker is drawn at the live fix when available
(falling back to the last recorded trail point otherwise), instead of
only at the trail's end — so it shows with an empty trail too.
- Empty-everything message is now "No GPS / no trail" and only appears when
there is genuinely nothing to show (no trail, no waypoints, no fix).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Trail action-menu table gains Mark here / Waypoints / Clear waypoints.
- New Waypoints section: dropping (Mark here + label), independence from
the trail (/waypoints, survives reboot/Reset), map marker, the always-
present Trail-start row, the two-bearing nav view, and rename/delete.
- New Compass section: heads-up course-over-ground (no magnetometer),
rotating card, movement-only heading, GPS-fix gating.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a bulk clear for saved waypoints (Trail → Hold Enter → Clear
waypoints), shown only when at least one waypoint exists. Independent of
the trail — Reset trail still leaves waypoints intact; this is the
explicit way to wipe them all instead of deleting one by one.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Compass: switch to a heads-up card. The direction of travel is now the
fixed "up" index at the top and the compass card rotates underneath it —
a North needle plus N/E/S/W letters placed at screen angle (bearing - cog),
so North spins as you turn rather than the arrow pointing at your course.
Trail waypoints: the nav list now always starts with a synthetic
"Trail start" row whenever a trail exists, so you can navigate back to
where you began without having marked it. The "Waypoints" action appears
when there are saved waypoints OR a trail to backtrack to. Rename/Delete
apply only to saved waypoints; the start row is navigate-only. List/nav
indexing goes through rowTarget()/wpIndex() helpers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Tools menu rendered all items from listStart() with no scroll, so the
6th entry (Compass) fell off the bottom on the OLED's short panel and was
unreachable. Add a scroll offset that keeps the selection in view, with
the usual ^/v indicators.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
startPingForKey() set the "RTT: ..." line but didn't rebuild the ping
menu, and updatePingMenuState only rebuilt on a string-count change it
computed after the mutation — so the menu stayed at a lone "Send" row and
nothing visibly happened after pressing Send, whether or not a reply came.
- Rebuild the menu right after starting the ping so "RTT: ..." shows at
once; on send failure show "RTT: send fail" instead of a blank.
- Sync the menu against its actual row count (_ping_menu._count) as result
lines populate, so RTT/SNR rows appear when the reply lands.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tools › Compass shows the device's course over ground (from the GPS COG
ring — no magnetometer) as a rotating arrow in a ring with a north tick,
plus a large degrees + cardinal readout. "No GPS fix" without a fix;
"move to set heading" when stationary (course undefined). Reuses
UITask::currentCourse, so it works without recording a trail.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two fixes from on-device testing:
- Node navigation was bound to a bare Enter in the detail view, which was
undiscoverable. Hold Enter now opens an Options popup (Navigate / Ping),
consistent with the rest of the UI. Navigate opens the shared NavView;
Ping runs the ping and shows its result.
- The ping popup always carried three result rows (RTT / SNR out / SNR
back) even before a ping, so the user could scroll onto blank rows. The
menu is now rebuilt to contain only "Send" plus the result lines that are
actually populated, growing in place as the reply arrives.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review fixes:
- NearbyScreen: when the periodic detail refresh can't find the selected
contact any more, clear _nav as well as _detail so the nav view and the
input handler don't disagree about state.
- TrailScreen: bound the label keyboard to WAYPOINT_LABEL_LEN-1 (11) chars
for both Mark-here and Rename, so the user can't type more than the stored
label holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 6. In a node's detail view, Enter (when the ping popup is closed)
opens the shared NavView targeting that node's last-known advert position
— same To/Hdg/distance screen used by waypoints. Nodes with no position
report "No node GPS". This turns Nearby from a static snapshot into a
"walk toward this person" view and folds in the old compass-to-contact idea.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 5b. The Map view now overlays saved waypoints as a hollow-diamond
marker with the label's first character beside it. The bounding box folds
in every waypoint so off-track ones stay in frame, and the map renders
even when the trail is empty but waypoints exist. project() generalised to
projectLL(lat, lon) so the same projection draws track points and waypoints.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>