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 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>
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>
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>
Reply-bot overhaul on top of the trigger/reply auto-reply:
Robustness
- single botTriggerMatches() shared by DM/channel paths; named constants
(BOT_REPLY_COOLDOWN_MS / BOT_SCRATCH) replace magic numbers.
- per-contact DM throttle (8-entry ring) so a second sender isn't starved
while one contact is on cooldown.
- channel anti-loop: skip only when an incoming message equals our own reply
(was: any reply containing the trigger word — which silently killed channel
replies for the common "trigger word in reply" setup).
Features
- away / reply-to-all: a lone "*" trigger matches every message.
- independent DM vs channel triggers (bot_trigger / bot_trigger_ch); "*" works
on the channel too, bounded by cooldown + echo guard.
- query commands: a DM or monitored-channel message is scanned for "!word"
tokens — !ping/!batt/!loc/!time/!temp/!hops/!status/!help — merged into one
combined reply. DM = per-contact throttle, ignores quiet hours; channel =
broadcast, per-channel cooldown, respects quiet hours. !hops uses
getPathHashCount() (0 = direct).
- quiet hours (bot_quiet_start/end, local, wraps midnight) silence push replies.
- reply counter shown in the BotScreen header.
UI / storage
- BotScreen: 9 rows, scrolling list (no more cramming), field I/O via
fieldBuf()/fieldCap().
- prefs bot_commands_enabled / bot_quiet_start / bot_quiet_end / bot_trigger_ch
persisted; schema 0xC0DE000A → 0xC0DE000C with clamping + migration
(bot_trigger_ch seeded from bot_trigger on upgrade).
- docs: tools_screen bot section rewritten; FEATURES.md refreshed.
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>
Trigger is now stored as typed (e.g. "Ping" stays "Ping").
Case-insensitive matching is preserved by lowercasing both
the trigger and incoming message at comparison time.
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>
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>
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>
C1/C2: onChannelMessageRecv and onChannelDataRecv both passed
findChannelIdx() through (uint8_t), so an unknown-secret packet
turned -1 into 255 and the message flowed into the offline queue,
UI history, notification and bot reply with a bogus channel index.
Drop such packets at the recv path; also harden addChannelMsg with
an MAX_GROUP_CHANNELS bounds check so a future caller can't poison
the channel-history ring buffer either.
H4: loadPrefsInt always reset trail_units_idx on sentinel mismatch,
even for jumps where the field was saved correctly (e.g. 0xC0DE0004
→ 0xC0DE0005). Scope the reset to sentinel == 0xC0DE0003, which is
the only case where the byte at that offset is actually the stale
sentinel low byte.
FEATURES.md: mark these three items as ✅ in the audit backlog.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
An all-zero secret in /channels2 means the entry is uninitialised or the
file format was corrupted by a previous firmware (e.g. different on-disk
layout from an older fork). Loading such a channel makes findChannelIdx()
match the wrong slot for incoming messages — they end up associated with
the bogus channel index and never display, even when the companion app
receives them fine over BLE.
Skip empty-secret entries during load and log how many were dropped.
Also defensively null-terminate the loaded name so callers can treat it
as a C string regardless of how the file was written.
Reported via user feedback: "only public visible, hashtag channels not;
public was empty; phone could read messages; works after wipe".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous fix set joystick_rotation=0 before reading from file, but
the subsequent rd() call overwrote it with whatever was stored (e.g. 2
from a prior e-ink firmware). Move the hardware-enforce block to after
rd() so stale values in flash are always corrected on OLED builds
(FEAT_JOYSTICK_ROTATION_SETTING=0). Remove the now-redundant pre-read
block.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
UITask: when a key is pressed while the buzzer is playing, defer the
next screen refresh by 300 ms instead of triggering it immediately
(via _next_refresh = 100). The blocking e-ink endFrame() was extending
the first note by the full refresh duration (~150-200 ms). Subsequent
notes were unaffected because _next_refresh was already future-dated
after the first render.
DataStore: for builds where JOYSTICK_ROTATION is not defined and
FEAT_JOYSTICK_ROTATION_SETTING is 0 (OLED), always reset
joystick_rotation to 0 on prefs load. Without this, stale e-ink prefs
(which allow changing joystick rotation in Settings) could silently
leave a non-zero value that reversed all joystick directions on OLED.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adding the public DataStore::openWrite(const char*) member shadowed
the file-static openWrite(FILESYSTEM*, const char*) helper inside
DataStore.cpp's method bodies, so every internal call broke ("no
matching function" with two args). Qualify each internal call with
::openWrite so name lookup picks the static helper. The new public
member's own implementation already does that.
Trail.h's gpxPoint template called gmtime unqualified; ADL couldn't
find it from the template definition because no argument was
template-dependent. Include <time.h> and call ::gmtime — both passes
the dependency rule and avoids the -fpermissive-only acceptance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Single-slot persistence as agreed. Action popup grows three entries
(conditional on what makes sense for the current state):
- "Save trail" — writes the current RAM ring to /trail in LittleFS.
- "Load trail" — only shown when /trail exists; replaces the live ring
with the snapshot (any active session ends first).
- "Export GPX" — dumps the trail as a minimal GPX 1.1 document over
Serial so the user can capture it from a USB host.
Plumbing:
- TrailStore gains writeTo / readFrom (template on the file handle, so
the platform-specific File flavour stays out of Trail.h) plus
exportGpx(Stream&) for the GPX serializer. Header carries magic
"TRAL", a version byte, point count, and the accumulated active time
so elapsed survives the round trip.
- DataStore::openWrite is exposed publicly so callers outside DataStore
don't have to duplicate the platform open-mode dance.
- MyMesh gains a public getDataStore() accessor.
After Load, _pending_seg_break is set so any subsequent Start opens a
new segment instead of joining the loaded last point.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Second row in the Config view: UP/DOWN moves focus between Min dist
and Units, LEFT/RIGHT cycles the focused row's value. Units propagates
to the Summary view, which now shows either "Avg: N km/h" / "N mph"
or "Pace: M:SS /km" / "/mi" depending on the choice.
Pace renders as "--:-- /unit" when distance or elapsed time is zero
(avoids divide-by-zero glitches with one-point trails). mph conversion
uses the 0.621371 ratio with rounding; pace works in floating point
off raw metres and seconds for accuracy at low speeds.
NodePrefs gains trail_units_idx as another reserved-ish byte; old saves
load with 0 = km/h, no schema bump needed since the rd() lambda already
defaults missing fields to zero.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.
New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
km/h, bounding-box helper
New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
toggles tracking, Esc returns to Tools
Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.
Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.
Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.
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>
Old saves (without sentinel) load unchanged — rd() lambda zero-inits
anything not on disk. New saves append a 4-byte sentinel that encodes
the schema revision; the low byte is bumped on every layout change in
loadPrefsInt/savePrefs. Mismatch on load logs a warning and continues
with defaults; the next savePrefs writes the current sentinel so the
file self-heals.
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>
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>
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>
Adds Settings > Display > Rotation (0°/90°/180°/270°) that persists to
NodePrefs and is applied on startup. Falls back to compile-time
DISPLAY_ROTATION when the field is missing from an older prefs file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The font preference was not saved to or loaded from flash, so it would
reset to default (Adafruit font) on every boot. Added to the versioned
DataStore read/write chain in the same pattern as other recently added
fields (auto_lock, dm_melody etc.).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Toggle in Settings > Display > Format. In 12h mode the clock shows
h:mm AM/PM (or h:mm:ss AM/PM with seconds, no space before AM/PM).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
auto_lock was defined in NodePrefs but missing from both loadPrefsInt
and savePrefs in DataStore.cpp. Added at the end of the serialization
chain (inside if(file.available())) for backwards compatibility with
existing saved files — old files default to auto_lock=0 (disabled).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DataStore: validate ringtone2_len <= 32 after loading from flash
(same guard as ringtone_len on line 252; we introduced this field
but forgot the validation)
- UITask: fix shutdown buzzer timeout to conventional unsigned form
(millis() - start) < timeout to avoid unsigned underflow if called
within first 2.5s of boot
- UITask: guard _last_notif_ch_idx < 64 before 1ULL shift in notify()
(defensive; MAX_GROUP_CHANNELS=40 so currently unreachable, but
prevents UB if the value is ever unexpectedly out of range)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add second ringtone slot (ringtone2_*) to NodePrefs and DataStore.
Add per-channel and per-DM melody assignment (built-in / melody 1 / 2)
stored in NodePrefs bitmasks and DmMelodyEntry table.
Settings screen exposes DM/CH sound items; RingtoneEditorScreen supports
switching between slots. notify() uses buildMelodyFromPrefs() to select
the correct RTTTL string per contact.
Remove broken nRF52 volume control from buzzer.cpp: tone() uses
NRF_PWM2 with DECODER.MODE=Auto so TASKS_NEXTSTEP has no effect and
duty cannot be changed before the first DMA read without patching the
Arduino core. applyVolume() is now a no-op on nRF52.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds configurable periodic 0-hop self-advert that includes GPS coordinates,
making the device's position visible in other nodes' Nearby Nodes screen.
Options: off / 1min / 2min / 5min / 10min / 30min / 1h.
Accessible via Tools → Auto-Advert. Timer runs in MyMesh::loop().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add NearbyScreen: GPS contact list with haversine distance sort,
type filter (ALL/Chat/Rpt/Room/Snsr), detail view with ping (RTT via
ACK poll), node scan via advert(), context menu
- Add DashboardConfigScreen: configure up to 3 data fields on the clock
page (Battery, Temp, Humidity, Pressure, GPS, Altitude, Lux, CO2,
Contacts); entered via long-press ENTER on clock page
- Rework HP_CLOCK layout: full-screen clock (size 2 at y=0), date at
y=19, separator at y=28, three data field rows at y=31/41/51;
hide node name/battery indicator on clock page
- Persist dashboard_fields[3] in NodePrefs and DataStore
- Add isAckPending() to MyMesh for NearbyScreen ping detection
- Wire NearbyScreen into ToolsScreen and UITask
- Reduce NearbyScreen entry buffer from MAX_CONTACTS(100) to 32,
saving ~3.3 KB RAM
- Switch haversine/bearing math from double to float (sinf/cosf/atan2f),
eliminating double libm and saving ~5-10 KB flash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Long-press ENTER in the DM contact list opens a context menu with:
- "Mark as read": clears unread counter for that contact
- "Notif: default/OFF/ON": cycles notification state (same as channels)
Notification state is persisted in NodePrefs (dm_notif[16] table,
keyed by 4-byte pub_key prefix). When muted, the buzzer is silenced
for that contact's messages; when force-on, buzzer plays even in
quiet mode. Default follows the global buzzer setting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bot redesign: DM bot active for all private messages (no pubkey filter),
channel bot optional with separate enable flag; both can run simultaneously
- Bot: one shared trigger, separate reply texts for DM and channel
- Buzzer: add Auto mode (mutes when BT connected, unmutes on disconnect);
per-channel force-on overrides still work as intended
- Settings: defer flash writes to menu exit (only when dirty)
- Settings: add Seconds toggle (hide clock seconds, refresh drops 1s→60s)
- Fullscreen msg preview: fix truncation (fmsg buf 79→140 chars)
- RingtoneEditor: fix melody playback cut after first note (RTTTL lib holds
pointer — moved play buffer to member variable)
- BotScreen: remove key hint footer
- .gitattributes: protect README.md from upstream merge conflicts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The bot can now reply to a specific DM contact instead of a channel.
UI (BotScreen): item 1 cycles through all channels followed by all DM
contacts (ADV_TYPE_CHAT). The label switches between "Channel" / "Contact"
and the value shows the channel or contact name. Left/right/enter navigate
the combined list.
Logic (MyMesh::onMessageRecv): when bot_target_type==1, match the trigger
against incoming DM text and reply via sendMessage(). A 4-byte pubkey
prefix identifies the target; all-zero prefix matches any DM sender.
Persistence: bot_target_type and bot_dm_pubkey[4] appended to prefs with
backward-compat read guard (old firmware defaults to channel mode).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New 'Tools' page on the home screen carousel with Ringtone Editor and Auto-Reply Bot items
- Bot settings: enable/disable toggle, channel picker, trigger phrase, reply text (keyboard with pre-fill)
- Bot logic in MyMesh: case-insensitive substring match, 10 s cooldown to prevent reply loops
- Settings: Home Pages visibility control (which carousel pages are shown; Settings and Messages always visible)
- DataStore: persist home_pages_mask and bot fields with backward-compat guards on read
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The hardcoded offset numbers were inaccurate from the start (e.g.
autoadd_config listed as //87 but actually at //90) and drifted further
with each new field. The read/write is sequential with no seeks, so the
numbers add no value and only mislead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New "Home Pages" section in Settings lets the user toggle which pages
appear in the home screen carousel. Settings and Messages are always
shown; Clock, Recent, Radio, Bluetooth, Advert, GPS, Sensors, Tools
and Shutdown can each be hidden individually.
- NodePrefs: home_pages_mask uint16_t (bit per page, 0=all visible)
- HomeScreen: isPageVisible/navPage skip hidden pages; dots indicator
counts only visible pages; enforces visible page on re-entry
- SettingsScreen: SECTION_HOME_PAGES with per-page ON/OFF toggles
- DataStore: persisted at offset 1598
- Default: all pages visible (0x01FF)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>