PA PL1 re-targets the PA's DC-DC supply rail rather than selecting a
logic-level gain, and the serial CLI is serviced on every main-loop pass
regardless of whether a transmit is in flight. A `set radio.fem.txgain`
write could therefore move the rail mid-transmit, while the SX1262 was
still driving the PA at full input power.
Record the requested level in setPAGainEnable() and drive the pin from
setTxModeEnable(), which runs from onBeforeTransmit() ahead of
startTransmit(). The level only matters while transmitting, so deferring
costs nothing.
Document that the pref is saved immediately but applied at the next
transmit, so `get radio.fem.txgain` can lead the hardware until then.
Settings screen doc gets the new "GPS pwr" row and a watchdog note on
"Pwr save"; Diagnostics doc gets the new "RXPS wd s/h" row and updates
what "Reset counters" actually clears; release-notes.md gets Unreleased
entries for all three additions from the prior commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
README had drifted from the firmware in several places:
- Supported Devices listed only the three nRF52 boards; Heltec V3/V4 were
missing entirely, as was any mention that ESP32-S3 flashes differently.
Flashing is now split per MCU, with the merged-vs-app-only .bin trap
spelled out — that one costs an afternoon to diagnose from a dark screen.
- It advertised a Lemon/Default font switch that was retired in v1.23; there
is one unified misc-fixed 6x9 font now and no font setting at all.
- tools/README claimed ENABLE_SCREENSHOT had to be added by hand, directly
contradicting README's "no special build flags required". The envs have
carried the flag for a while; rewrote the file to cover all four tools.
- "S key for screenshot" described screenshot.py's menu, not the device —
screenshots are driven entirely from the host via CMD_GET_SCREENSHOT.
Structurally, general notes (factory reset on migration, BLE-over-USB
priority) sat at the tail of the ESP32 subsection and read as ESP32-specific;
they're now placed where they apply. Heltec wiring moved out of Supported
Devices into its own section so the device table stays scannable, and the
Solo Tools heading is no longer a link (it was generating a garbage anchor).
New docs/solo_features/external_keyboard.md covers CardKB and the wired
joystick: shortcut table, Full vs Compact, and the pin assignment. The pins
are marked as verified on real V4 hardware only — V3 inherits them from
Heltec's documented pin-compatibility and hasn't been checked on a board.
FEATURES.md (roadmap + code audit, developer-only) moves to
docs/development/roadmap.md; nothing referenced it by path.
Adds Building from source / Releasing / Repository layout, since the release
flow was only discoverable by reading the workflow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mkdocs will only consider the first H1 (if any) and subheaders under it for the table of contents
this increases the header levels of everything below "important concepts" by 1 so that the table of contents correctly resolves them
- Bot Actions (!buzz/!gps/!advert/!gpio1-4) ran their side effect
immediately during botScanCommands(), before quiet-hours/cooldown/
per-contact throttle were checked -- those gates only suppressed the
reply text, not the actual buzz/GPS toggle/advert/pin write. botCommandReply()
now only records what was requested; applyPendingBotActions() runs the
deferred effects once a wrapper's throttle checks pass and the ack sent,
mirroring the existing _locfix_requested pattern. resetPendingBotActions()
clears everything on every throttled/aborted path.
- CardKB's Fn+<letter> accent-popup shortcut bypassed the locked-input gate
(it called into KeyboardWidget directly instead of through the
enqueueKey()/dequeue path every other key uses, so it wasn't discarded
while _locked). Now checks _locked itself.
- Since a locked device now correctly ignores CardKB entirely, Fn+Esc
(single press) is added as CardKB's own lock/unlock gesture -- otherwise
a CardKB-only setup had no way to unlock. Esc rather than the adjacent
Fn+Backspace, to avoid an accidental press.
- botScanCommands() now parses up to two arguments per command instead of
one. Used by "!gps fix [seconds]" to override the default 90s timeout
(clamped 15-300s) for a poor sky view where 90s isn't always enough to
reach isLocFixReady()'s HDOP/satellite bar.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Satellite count alone is a poor proxy for fix quality -- few satellites
in good geometry can beat many in poor geometry. LocationProvider now
exposes getHDOP() (default -1 = unsupported); MicroNMEA implements it.
isLocFixReady() prefers HDOP <= 2.0 when available, falling back to the
old >=8 satellite threshold for providers that don't report it (e.g.
RAK12500/u-blox).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Turns GPS on (if it wasn't already), waits for a stabilised fix
(isValid() + >=8 satellites, then averages 10s of readings), sends the
position, and restores GPS to whatever state it was in before -- up to
a 90s timeout, after which it reports a partial fix (if it got any
samples) or plain failure.
Replies in two parts since a fix takes seconds-to-minutes, unlike every
other bot command here: an immediate "acquiring fix..." ack (through
the existing synchronous command path), then the actual position as a
separate follow-up message once ready, delivered to whichever
destination (DM/room/channel) the request came from. Only one fix can
be in flight at a time -- a second request while one is pending gets an
immediate "already pending" instead of silently replacing it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The bot outgrew "auto-reply" once it gained Actions (!buzz/!gps/!advert/
!gpio1-4) that control device hardware remotely, not just answer messages.
Renames the Tools screen entry and all doc cross-references; already-shipped
release notes (v1.23) are left as-is to match what actually shipped under
that name.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Auto-Reply Bot gains Actions (!buzz/!gps/!advert) behind a new per-target
toggle nested under Commands (bot_actions_dm/ch/room); off by default.
- Bot Trigger fields accept comma-separated multiple phrases, matching any
one fires the reply.
- New user-assignable GPIO feature (Wio Tracker L1): !gpio1..!gpio4 bot
commands plus a Tools > GPIO screen. Each pin cycles Off/Input/Output;
GPIO1/GPIO2 (P0.02/P0.29, the nRF52840's AIN0/AIN5) also offer a read-only
Analog mode via direct SAADC access. GPIO3/GPIO4 (P0.09/P0.10) are the
chip's NFC1/NFC2 pins, repurposed as plain GPIO via a one-time UICR
NFCPINS bit-clear in initVariant() (adapted from Adafruit's own
nfc_to_gpio example) -- confirmed working on real hardware.
- Fix: DM/room reply-prefix ("@[nick] ") stripping happened at the wrong
layer, hiding the "To:" header on DM replies and leaking the raw prefix
into room messages' list view; a related mismatch had the history
scrollbar's sizing pass wrap room messages with the sender name still
attached, disagreeing with the actual rendered text.
Build-verified: WioTrackerL1_companion_solo_dual and
WioTrackerL1Eink_companion_solo_dual both compile and link clean
(sizeof(NodePrefs) confirmed 2720 via real build, not guessed).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Switch ESP32-S3 KISS modem environments to HWCDC and move outbound KISS writes to a non-blocking queued frame path so loop() and TX completion keep progressing when the host reads slowly. Add native backpressure regression tests and correct the native_kiss_modem test filter so this suite runs directly with pio test.
"+ Add channel" jumped straight into a generic Name+Secret form, so the
"hashtag channel" convention documented in docs/companion_protocol.md
(secret = first 16 bytes of sha256("#topic")) was only reachable by
already knowing to type a literal "#topic" into the passphrase field --
nothing in the UI surfaced it. The phone app instead shows an explicit
channel-type picker; this adds the same on-device.
"+ Add channel" now asks Public / Hashtag / Private first:
- Public commits immediately with the well-known default channel's name
and secret (8b3387e9c5cdea6ac9e5edbaa115cd72, confirmed to match
MyMesh.cpp's PUBLIC_GROUP_PSK and the docs' published key) -- useful
to restore it if deleted.
- Hashtag shows a single Topic field; Save synthesizes name="#topic" and
derives the secret via the existing SHA-256 passphrase path -- same
underlying mechanism Private already had, just discoverable without
knowing the "#" convention.
- Private is today's manual Name+Secret form, unchanged.
Editing an existing channel skips the picker (no ambiguity to resolve
there). Extracted hexToSecret() out of deriveSecret()'s hex-mode branch
so Public's fixed key parses through the same code instead of a second
hand-rolled loop. No other file needed changes -- openAdd()/openEdit()/
active()/render()/handleInput() keep their existing signatures.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Page 0 was hardcoded to Latin -- Cyrillic/Greek could only ever be the
second, cycled-to page. Settings > Keyboard's Alphabet row splits into
Main (which script the keyboard opens on by default) and Additional
(the second one reached via #@/abc), so a Cyrillic/Greek typist can make
their own script the default instead of always landing on Latin first.
Setting Additional equal to Main collapses back to a 2-page cycle (that
script + Symbols), same rule the old Latin-hardcoded design already used
implicitly.
KeyboardWidget.h: cellStr()/t9GroupStr() now dispatch through
scriptCellStr()/scriptT9GroupStr(), treating Latin as an ordinary peer of
Cyrillic/Greek instead of a special case; scriptHint() replaces
altAlphabetHint() so the #@/abc key's "next page" hint is correct
regardless of which script that lands on; the accent popup's gating
checks the current page's actual script instead of assuming page 0 is
always Latin. Removed the now-dead pageIsAltAlphabet().
NodePrefs gains keyboard_main_alphabet (schema sentinel 0xC0DE001F ->
0xC0DE0020, same append-at-tail/clamp-on-load pattern as every prior
schema growth this file uses). Verified via a real build that the new
field lands in existing tail padding -- sizeof(NodePrefs) is unchanged
at 2712.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the 8 separate Latin-diacritic alt-alphabet pages (Polish, Czech,
Slovak, German, French, Spanish, Portuguese, Nordic) with one popup: Hold
Enter on a Latin letter that has accented variants (a c d e i l n o r s
t u y z) opens a floating horizontal strip of that letter's accents,
anchored over its own row so the grid stays visible underneath (LEFT/
RIGHT picks, Enter inserts via a new shared insertGlyph() helper, Cancel
dismisses). Holding a letter with no variants is a no-op. Cyrillic/Greek
remain full alt-alphabet pages; NodePrefs::keyboard_alt_alphabet shrinks
from 11 to 3 values accordingly -- an old saved Polish..Nordic value just
clamps to Latin via DataStore.cpp's existing range check, no migration
code needed.
Freeing Hold-Enter on letter cells required moving cursor-mode's own
trigger: UP from the top letter row now enters it instead of wrapping to
the special row. To keep that wrap reachable, cursor mode's UP/DOWN
(Home/End) continue the wrap once already at that boundary -- UP again
lands on the special row, DOWN again back on the letter grid -- reusing
the same proportional column mapping the old direct wrap used.
Diagnostics' font-coverage sample swaps its 8 per-language lines for one
line sampling the new accent table. Docs (message_screen, settings_screen,
solo_ui_framework) and release-notes updated to match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Tools > Admin: split the Radio tab's single "f,bw,sf,cr" comma-string
field (free-text keyboard) into 4 independent, type-appropriate rows --
Frequency (same digit-cursor DigitEditor Settings/Repeater use locally),
Bandwidth/Spreading factor/Coding rate (discrete-set LEFT/RIGHT stepping),
plus TX power and the 3 Routing numeric fields as number steppers and
Repeat as an ON/OFF toggle. Edits happen locally (zero mesh traffic per
keystroke); only Enter sends one combined `set`, Cancel sends nothing.
Full pre-release audit of all 16 commits since v1.22 (5 parallel focus
areas: display/font, keyboard/messages, Nodes/Admin, Bot/channels,
UITask+NodePrefs schema) turned up and fixed:
- Admin: fetched FK_NUMBER values weren't clamped to the field's range,
so a value already out-of-range could get stuck unreachable; fixed
commit-time float format noise (%.6f -> %.3f); Cancel/failed-login
always returned to the Nodes picker even when Admin was opened
directly from a node's Hold-Enter action -- now returns to wherever
it was actually opened from (AdminScreen::_from_picker).
- Keyboard: one-shot Shift was consumed after the *first* T9 multi-tap,
so cycling to the 2nd/3rd candidate always came out lowercase --
fixed by caching the cycle's caps state (t9_caps).
- Messages: history is numbered newest-first, so a message arriving
while scrolled up to an older one silently relabeled the view onto a
different message -- selection now shifts with the insert.
- Channels: onChannelRemoved() didn't clear ch_notif_override/
ch_notif_muted/ch_fav_bitmask despite its own contract comment
requiring it (now far more reachable via the on-device Delete);
an all-zero hex secret silently self-deleted the channel it was
just saved into (collides with the empty-slot sentinel) -- rejected.
- Room login: isRoomLoggedIn() indexed the login-tracking ring
directly instead of via its head offset, silently wrong once the
ring wraps (8+ rooms/session) -- the new on-device Logout depends
on this being right.
- Font/display: removed the now fully-inert Settings > Display > Font
toggle and the dead LemonFont.h (retired by the earlier misc-fixed
font unification, zero remaining includes); fixed a copy-pasted
"5x7" comment (font is 6x9), two meaningless dead ternaries, and an
OLED/e-ink inconsistency in the undefined-glyph fallback box offset.
- AdminField's `kind`/bounds fields no longer rely on default member
initializers inside aggregate-init: this toolchain's actual nRF52
build (unlike env:native) has no explicit -std= override, so it
predates C++14's aggregate-with-default-member-initializer rule.
Given an explicit constructor instead -- portable regardless of
standard, all existing field-table literals unchanged.
Docs updated to match: tools_screen.md (Diagnostics as a 3-tab
carousel, was documented as one flat screen), message_screen.md
(chat bubbles, newest-at-bottom, cursor mode, secret validation),
settings_screen.md (dropped the dead Font row), solo_ui_framework.md
(header menu_hint signatures, icon priority-drop, KeyboardWidget's
T9/alphabets/cursor-mode). release-notes.md gains the v1.23 section
covering all of the above plus the other 15 commits since v1.22.
Build-verified on WioTrackerL1_companion_solo_dual.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a small allocation for groupdata packets for the Meshcore firmware for the StreamSensor product. It was previously a LoRaWAN platform, and we've moved to Meshcore.
Let me know when and how to demonstrate.
A device administering itself via the Admin tool read as awkward, so the
local mode is gone and its options relocated to Settings.
- AdminScreen is remote-only: drop the CHOOSER phase, _local_mode, the
LocalField/LocalKind model and activateLocalField/commitLocalField.
Phases collapse to LOGIN/COMMAND/REPLY, entered only via startFor().
- Tools > Admin opens straight to the Nodes picker (ACT_ADMIN ->
pickAdminTarget()). Backing out of a command screen returns to that
picker; the picker's Cancel returns to Tools. gotoAdminScreen() removed.
- Settings > System gains Name (keyboard-edited node_name) and Reboot
(action row, placed last so it isn't the default cursor). Radio + TX
power were already in Settings; Send advert is the home ADVERT page.
Docs: tools_screen.md Admin section rewritten remote-only; settings_screen.md
System table gains Name + Reboot.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tools > Admin now opens on a This-device / Remote-node chooser. "This
device" is a login-free 2-tab carousel (System: Name/Radio/TX power/
Lat/Lon, Actions: Send advert/Reboot) mapped onto NodePrefs/sensors and
reusing Settings' own apply chains -- no CommonCLI port. "Remote node..."
opens Tools > Nodes in a pick-mode (borrow-another-screen's-list idiom),
and Nodes gains an "Admin" Hold-Enter action; both converge on the single
canonical UITask::openAdminFor() -> AdminScreen::startFor().
Also:
- KeyboardWidget: opt-in PlaceholderRefreshFn hook for contextual {}-key
CLI command-name completion (word-replacing); default behaviour and all
other keyboard uses unchanged.
- TabBar: ellipsize() re-measures the real "text..." candidate per step
(+ width clamp) so dots can't spill into a neighbour/reserved icon.
- ChannelsView: form rows truncate instead of wrapping.
- Reboot moved off the default-selected Actions row (fires with no confirm).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reordered BotScreen's carousel to Channel / Room / Direct / Other and
made Channel the default opening tab, per feedback that the screen
should start from the first tab shown. Docs updated to match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Auto-Reply Bot gains a third target (room servers, alongside DM and
channel), new {name}/{hops} reply placeholders, and a DM all/favourites
allow-list. BotScreen is redesigned as a circular tab carousel (Direct /
Channel / Room / Other, same interaction as Nearby Nodes' filter tabs)
instead of one long scrolling list, which also exposed and fixed a
leftover coupling where Channel/Room trigger-replies and their !command
handling secretly depended on the DM tab's Enable/Commands toggles —
each target's Enable and Commands are now fully independent.
- NodePrefs: bot_room_enabled/prefix/trigger/reply, bot_dm_scope,
bot_commands_ch/bot_commands_room (bot_commands_enabled repurposed as
DM-only). SCHEMA_SENTINEL 0xC0DE001D -> 0xC0DE001F; sizeof unchanged
at 2712 (new bytes absorbed existing padding, verified via a
standalone host compile + offsetof check).
- DataStore: persists all new fields; seeds bot_commands_ch/room from
the old shared bot_commands_enabled on upgrade so existing
channel/room command behaviour isn't silently lost.
- MyMeshBot: tryBotReplyRoom/tryBotRoomCommand mirror the channel bot's
shape but post via sendMessage (room relays to members itself);
requires an existing login session with that room, same as a manual
post would. botDmSenderAllowed() gates DM trigger-reply/commands on
the favourites bit when bot_dm_scope=Fav.
- MsgExpand: {name}/{hops} as optional trailing params (default
nullptr/-1, no existing caller affected) — deliberately not exposed
on the general compose keyboard, only on bot Reply fields.
- QuickMsgScreen/UITask: room-target picker (mirrors the channel
picker), routing through the existing room-login prompt when there's
no saved password yet.
- BotScreen: tab carousel (LEFT/RIGHT switches tabs, UP/DOWN moves
rows, Enter is now the only way to change a value); Enable split out
of Channel/Room's combo row; Quiet Hours gained a stepper sub-mode;
Commands moved from a shared toggle into each tab.
- docs/tools_screen.md updated for the new tab layout and behaviour.
Not build-verified — no PlatformIO toolchain in this environment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
Adds a small allocation for groupdata packets for the Meshcore firmware for the StreamSensor product. It was previously a LoRaWAN platform, and we've moved to Meshcore.
Logging in to a room server (first-time prompt or a remembered password) used
to leave the user on the room list, needing a second Enter to open the chat.
onRoomLoginResult() now opens the room history on success via a shared
openDmHistory() helper (extracted from the Enter-on-contact path).
The login result is async, so the auto-enter is gated on the user still being
on that room in the picker: phase CONTACT_PICK, room mode, no context menu /
share / pick-target sub-flow active, and the result pubkey matching the
selected contact. Otherwise it stays put (no yanking the user into a screen
they navigated away from).
Resolves the "auto-enter after login" code-review item. Builds green:
WioTrackerL1_companion_solo_dual.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
T9 keys now read like a phone keypad — each cell is labelled <digit><group>
(e.g. 2abc), with the digit matching what the multi-tap cycle lands on after
the letters. No separator space: the widest group (".,!?'-") plus the digit
already fills a narrow 128px OLED cell at 3 columns.
Also fills in the v1.22 release notes (T9 keyboard, trail auto-save, Clock
Tools in Tools, Map/Shutdown reorderable, battery-read fix, the UI-audit fix
batch, discover-dedup, favourites self-heal, preset-name placeholder, OLED
frame-skip, e-ink full-refresh) and syncs the solo-feature docs: new Keyboard
settings section, Trail Auto-save row, and Clock Tools reachable from Tools.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- release-notes v1.21: add Clock tools (alarm/timer/stopwatch) to What's new,
e-ink button responsiveness + double-tap debounce to Fixes, and the input
edge-capture/key-queue and hardware-timer ringtone notes to Under the hood.
- README: note clock tools on the Clock Screen entries.
- solo_ui_framework: document the hardware->handleInput path (mandatory
begin() per button, IRQ edge capture, key queue + coalesced redraw).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a Clock Tools screen (Enter on the home Clock page) with three time
utilities, plus the engine that drives them from UITask::loop() so they
work regardless of the current screen / display state:
- Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local
time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed
from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump
the clock) — small corrections still fire on time, a jump over the
target still fires (late, up to 6 h). Disarms after firing. A bell
icon signals an armed alarm on the clock face and the top status bar.
- Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout,
rings even when off-screen.
- Stopwatch: millis-based, keeps running in the background.
Numeric fields use the shared framework DigitEditor (digit-by-digit,
min/max enforced). The alarm/timer/ring engine lives in UITask alongside
the locator/live-share engines (no screen-cast for time-critical logic).
E-ink: live readouts refresh coarsely (and on any key); timing is exact
regardless, and the countdown's buzzer fires on time. Rings override mute
and are dismissed by any key (auto-stop after 1 min). Cannot wake from a
full Shutdown (CPU/RAM powered down).
NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding,
sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017,
DataStore read/clamp/write in lockstep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add Tools › Trail → Hold Enter → Track back (shown when the trail has ≥2
points). It reuses NavView but, instead of a single fixed target, snaps onto
the route at the nearest recorded breadcrumb and walks the points in reverse,
auto-advancing the target as each is reached (within 20 m). The header shows
points remaining ("Back: 12 pt" → "Trail start"); reaching the start toasts
"Back at start" and exits. Cancel leaves at any time.
Lives in WaypointsView as a new sub-mode; advancement runs in poll()
(forwarded from TrailScreen) so it tracks GPS independently of the render
cadence. An EtaTracker drives the approach/ETA line and is reset on each
breadcrumb advance to avoid a bogus closing-speed spike.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a "Mark avg" trail setting (Off/5/10/30 s). When set, Tools › Trail ›
Mark here samples the GPS fix once a second for the chosen window and stores
the mean position instead of one instantaneous fix — a steadier, more
accurate mark for a precise spot. A progress screen shows time left + sample
count; Cancel aborts; the window then opens the label keyboard as usual.
Off (default) keeps marking instant.
Sampling runs in WaypointsView::poll() (forwarded from TrailScreen::poll())
so it ticks on the main loop, independent of the slow e-ink render cadence;
int64 accumulators avoid overflow over 30 samples.
Persisted as NodePrefs::gps_avg_idx (schema 0xC0DE0016): struct field +
option table, DataStore rd/write/clamp in lockstep. sizeof unchanged (the
uint8_t fits existing tail padding) so the 2488 tripwire still holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three spots lagged the recent refactors:
- §8 + §10 still taught the inline `if (_dirty) { savePrefs(); _dirty=false; }`
pattern; now point at `_task->savePrefsIfDirty(_dirty)`.
- §1 wiring contract now notes the nullptr-init / setCurrScreen null-guard
fail-safe (steps 1/3/4 are compile-checked, only a missed `new` slips through).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every drawList/AccordionList row opened with the same hand-written
display.drawSelectionRow(0, y-1, width-reserve, lineStep-1, sel) line —
14 copies of the same geometry and magic offsets. Add a drawRowSelection(d,
y, sel, reserve) helper in icons.h next to drawList and route the canonical
sites through it (Bot/LiveShare/Locator/Repeater/Settings/Tools/Nearby/
QuickMsg/Waypoints).
Rows that intentionally differ (full-width DashboardConfig/QuickMsg, the
keyboard/ringtone grids) keep their explicit drawSelectionRow() call — the
helper is opt-in rather than baked into drawList, so legitimate variants
aren't forced into one geometry. Framework doc updated to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the ad-hoc enter()/markClean() entry methods (which lived outside
the UIScreen interface and were invoked via casts from each gotoX) with a
virtual onShow() lifecycle hook called centrally by setCurrScreen().
This removes the "forgot to call enter() in a new navigator" footgun and
the unchecked cast smell: 12 navigators collapse to one-line
setCurrScreen(x) calls, and override enforces signature match. Two entries
that carry a parameter/variant keep an explicit typed call after
setCurrScreen(): RingtoneEditor::selectSlot(slot) and TrailScreen::showMapView().
Behaviour-preserving: only screens that previously had enter() get an
onShow() override; Splash/Home/QuickMsg/Diag keep no reset as before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ui-new/*.h screens are header fragments compiled only as part of
UITask.cpp, in include order. Two implicit rules a contributor can trip on:
include order (a static inline helper / shared scratch is visible only to
later fragments) and single-TU-only (some fragments define external-linkage
symbols at file scope, e.g. NearbyScreen::FILTER_LABELS, so reusing one from a
second .cpp is a duplicate-symbol link error).
Spell both out in a contract comment at the inclusion block, and raise the
single-TU note to a callout in the framework guide. Comments only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document the reusable building blocks the solo UI has grown — the UIScreen
model and screen wiring, DisplayDriver layout metrics, drawList/AccordionList,
the popup/keyboard/digit-editor/fullscreen/nav components, the geo + state-store
+ persistence helpers, mini-icons, input conventions, and the cross-cutting
gotchas (single-thread render, e-ink pacing, reference cleanup). Ends with a
worked "add a new Tools screen" example.
No code change; the helper layer is already well-factored (the recent dedup
passes closed the remaining gaps), so this captures it for contributors rather
than extracting more. Linked from the README docs table.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
onContactOverwrite() (the contact-table-full LRU eviction path) deleted the
contact's blob and notified the companion app, but never called the new
onContactRemoved() cleanup -- so a Favourites Dial slot, Locator target, or
Live Share target could still go stale, just via the silent auto-evict path
instead of an explicit removal. This is likely the main real-world cause of
the "(gone)" tile the docs described, since auto-eviction happens far more
often than an explicit CMD_REMOVE_CONTACT.
Also sync the two docs that described the old (now wrong) behaviour:
Locator's "survives delete" claim, and Favourites Dial's "(gone) until
reassigned" claim.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Starting trail recording with GPS switched off used to call setActive(true)
immediately -- the session timer ran while the screen sat on "Waiting for
GPS fix" forever and recorded nothing, with no hint that GPS was the
problem. Choosing "Start tracking" now opens a "GPS is off" confirmation
(Enable GPS & start / Cancel); confirming enables GPS and starts the
session. Behaviour is unchanged when GPS is already on.
Gated on a new UITask::hasGPS() so the prompt only appears on boards that
expose a toggleable GPS, not where GPS is simply absent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a "Rooms — logging in" section and a room context-menu entry to the
Messages screen docs: auto password prompt, passwords remembered across
reboots, self-healing on failure, re-login via Login…, app-entered
passwords also saved, and the ASCII-only keyboard caveat.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
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>