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>