- 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>
Renamed the vestigial Lemon/default font-switch naming (setLemonFont/
isLemonFont/drawLemonChar/lemonXAdvance/_use_lemon -> setSingleFont/
isSingleFont/drawGlyph/glyphXAdvance/_single_font) across DisplayDriver.h
and both concrete drivers -- both have been permanently single-font for
several commits, so the old names invited a future reader to think a
real switch still existed. Pure identifier rename, no logic changed.
Also extracted the byte-identical "SenderName: " prefix-splitting in
MyMeshBot.h's tryBotReplyChannel()/tryBotChannelCommand() into a shared
botChannelSenderSplit(), mirroring the existing botRoomSenderName().
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>
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>
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>
- Context menus (PopupMenu, Nearby/QuickMsg call sites) go back to centring on
screen; the header's discoverable-menu glyph stays, but dropping the popup
out of its corner looked bad on some screens.
- Unread pill badge digit wasn't centred: Adafruit_GFX's classic built-in font
(SH1106/SSD1306) always pads a measured string by one trailing advance
column regardless of the glyph drawn, so centring on the raw width left 1px
more slack on the right than the left. New DisplayDriver::
textWidthTrailingGap() (0 by default) corrects for it on those two backends.
- On-device room Logout: mirrors the app's CMD_LOGOUT (drops keep-alive
tracking, forgets the saved password) so a room can be deliberately signed
out of from the Room options menu, not just re-logged-in.
Not build-verified — no PlatformIO toolchain available this session.
Turn Nearby Nodes into the single node hub and make its navigation legible.
Node management (no phone app needed):
- MyMesh gains addDiscoveredContact() and deleteContactByKey(); CMD_REMOVE_CONTACT
now reuses deleteContactByKey() (one delete path: contact + blob + room password
+ UI cleanup + lazy write).
- Action menu (Hold Enter) gains Add contact (a new scanned node), Favourite /
Unfavourite (pin to the first free dial slot), and Delete contact (confirm first,
defaults to Cancel). Actions are offered contextually per row.
- A pinned contact shows a star in the list row, next to any live-share diamond.
Absorb passively-heard adverts: refreshStored() folds getRecentlyHeard() nodes that
aren't already listed as name+age rows (All filter only — AdvertPath has no type),
so "recently heard" no longer needs its own page.
Legible filter navigation: the filter (LEFT/RIGHT) is now a visible tab strip — the
active filter is a centred inverted pill, neighbours fan out and wrap around
(circular, first<->last). Only whole tabs are drawn so labels never wrap a line.
Rename Tools > "Nearby Nodes" to "Nodes".
Both solo envs build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A discover response often arrives twice — the zero-hop direct copy and a
re-flooded copy relayed by another repeater carry different packet hashes,
so the mesh duplicate filter passes both. The standalone scan appended the
same node twice and app-driven discover forwarded both copies, so one
repeater showed up as two.
Track (tag, responder pubkey) for a short window in isDupDiscoverResp() and
drop the second copy in both the standalone and app-forward paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The on-device login path already saved room passwords to /room_pw; do the
same for logins issued by the phone/USB app (CMD_SEND_LOGIN). The password
is stashed when the login is sent and persisted once the server confirms,
gated to ADV_TYPE_ROOM contacts; a failed login forgets any stale saved
password, mirroring the on-device path. This lets the device post to a
room standalone after a reboot without re-prompting.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Log in to a room server from the device UI with no phone app: picking a
room prompts for its password (blank allowed for open rooms), and a
context-menu "Login..." allows re-login. Successful passwords are
persisted to a dedicated /room_pw file so a previously-used room logs
back in after reboot without retyping; a failed login forgets the
(now-stale) saved password so the next attempt prompts again.
The room-password file is written via a temp file + atomic rename
(new DataStore::commitFile helper), matching the crash-safety of
contacts/channels persistence.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
isValidRepeaterProfile() hard-coded the 150-960 MHz SX1262 range, which
would wrongly reject legal frequencies on any other chip. Take the freq
bounds as parameters and pass radio_driver.getFreqBounds() at both call
sites (repeaterProfileValid() and the load-time migration), so the chip's
own validated range is the single source of truth. DataStore.cpp gains a
target.h include for radio_driver (declared extern there).
DigitEditor::render() now zero-pads the integer part to int_digits: the
cursor addresses place values (100/10/1/0.1…), so a value with fewer
integer digits than int_digits would shift every glyph and misplace the
highlight. No visual change for the frequency field (always 3 digits).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rename the repeater filter knobs from "politeness" to "forwarding
filters" across code comments, FEATURES.md and the docs, and correct
references that hadn't followed the controls when they moved from
Settings › Radio to the dedicated Tools › Repeater screen.
Comment/doc accuracy fixes:
- NodePrefs: user_radio_presets are now written by both Settings and
Repeater (shared picker), not Settings only.
- DiagnosticsScreen: class header listed only some rows; spell out the
full set (forwarded, signal, pool/queue, error flags, reset gesture).
- RepeaterScreen header no longer claims to show live forwarding stats
(they live on Diagnostics).
- tools_screen.md: drop the stale paragraph claiming the Repeater screen
shows live stats at the bottom — it is config-only.
Comments/docs only; no behaviour change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Settings > Radio 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>
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>
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>
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>
handleScreenshotRequest() reinterpret-cast every DisplayDriver* first to
SH1106Display* then to SSD1306Display*; both casts always produce a
non-null pointer, so the SSD1306 fallback was unreachable and on any
SSD1306 build we called SH1106::getBuffer() on a SSD1306 object — UB
that only happened to work because both backing classes share an
Adafruit_GFX layout at offset zero.
Replace the cast hack with virtual getBuffer()/getBufferSize() on
DisplayDriver, overridden in SH1106Display and SSD1306Display. MyMesh
no longer needs to know about either concrete type. const-correct the
buffer pointer and widen bufferSize to uint16_t while passing through.
Also fix the matching Python decoder: it sanity-checked length before
dispatching on resp_code, so a 2-byte RESP_CODE_ERR frame was reported
as "Frame too short" instead of the actual device error. Move the
length check after the resp_code dispatch and log the error code byte.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Adds the possibility to capture the device screen and save it as a PNG
image
- Wrap the code behind ENABLE_SCREENSHOT build flag, as per instructions
in README
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>
Replaces "Type: Repeater" line with the node's full public key encoded
as base64 (44 chars). drawTextEllipsized clips it with ... to fit the
128px wide display. Type is already visible in the inverted header.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each discovered node now renders as a boxed 2-line card (same style as
message history):
┌──────────────────────────────┐
│ NodeName (or [Sensor]) Type │ ← inverted header
│ RSSI:-87 SNR:7 Rem:5 │ ← signal stats
└──────────────────────────────┘
Add snr_x4 and remote_snr_x4 to DiscoverResult:
snr_x4 = _radio->getLastSNR()*4 (how well we heard the response)
remote_snr_x4 = payload[1] (how well responder heard our request)
2 items visible at a time; UP/DOWN scroll through results.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add rssi field to DiscoverResult, captured from _radio->getLastRSSI()
when the discover response is received.
Display format in the right column:
known node → "Rpt-87" / "Snsr-92" (type + dBm)
new node → "*-87" (asterisk + dBm; type already visible in "[Rpt]" label)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the inline advert scan in the nearby list with a proper
CTL_TYPE_NODE_DISCOVER_RESP-based sub-screen accessible via the context menu.
- DiscoveredEntry → DiscoverResult: adds name[32] and is_known flag so
both known contacts and unknown nodes are shown with useful labels
- getDiscoveredNodes → getDiscoverResults, capacity 8 → 16
- onControlDataRecv populates name from contacts for known nodes and
records all respondents (not just unknowns)
- NearbyScreen gains _discover_mode sub-screen: 8-second scan window,
live result polling each render cycle, UP/DOWN scroll, CANCEL to
return, ENTER/M to re-scan; known nodes show actual name + "known",
unknown nodes show "[Type]" + "NEW"
- List view is clean: no discovered nodes mixed in, no scanning banner
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NearbyScreen now sends CTL_TYPE_NODE_DISCOVER_REQ (same protocol as the
companion app) instead of a plain self-advert. Nearby repeaters, sensors
and room servers respond with their pub_key and type. Known contacts get
their lastmod refreshed; unknown nodes appear as temporary entries (e.g.
"Repeater") in the list until their advert is received.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Single shared _bot_last_reply_ms caused a DM reply to block the
channel bot for 10s and vice versa. Split into _bot_last_dm_reply_ms
and _bot_last_ch_reply_ms so each cooldown is independent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ping sent empty messages appearing in target's chat history, and RTT
measurement never worked for direct sendMessage() calls. Removed all
ping state machinery and registerExpectedAck(); detail view now shows
node info only (lat/lon/dist/bearing/type).
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>
Direct sendMessage() calls bypass the BT serial handler that populates
expected_ack_table; add registerExpectedAck() to MyMesh and call it from
NearbyScreen so isAckPending() can actually track the outstanding ping.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The #define was inside the private section, after isAckPending() which
references it — preprocessor hadn't seen the symbol yet at that point.
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>
Move tryBotReplyDM() and tryBotReplyChannel() implementations to
MyMeshBot.h (included at end of MyMesh.cpp). onMessageRecv and
onChannelMessageRecv now contain only a single-line call each,
minimising the diff against upstream in those methods.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace BotScreen's custom embedded keyboard with the shared global
keyboard layout (KB_* constants): char rows at KB_CHARS_Y, special row
at KB_SPECIAL_Y=48 with 5 buttons ([^][Sp][Del][{}][OK]), fixing the
previous layout that rendered below the 64 px display boundary.
Proportional column scaling between char rows (10 cols) and special row
(5 cols). [{}] opens the placeholder picker overlay.
- Add MsgExpand.h: standalone inline expandMsg() with no framework
dependencies, expanding {loc} and {time} placeholders. Used by both
MyMesh (bot channel/DM replies) and QuickMsgScreen, replacing the
duplicate expandBotMsg() static and the inline expandMsg() method.
- Bump FIRMWARE_VERSION to v1.15.3.
- ToolsScreen: remove key-hint footer line.
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>
- Fix placeholder picker colors: dark box + white border, light fill
for selected item (consistent with rest of UI)
- Fix placeholders not expanded when sending from keyboard (expandMsg
was only called in MSG_PICK path, not KEYBOARD path)
- Fix sent messages incrementing unread counter: set _phase=CHANNEL_HIST
before addChannelMsg so viewing=true
- Fix keyboard column inverse mapping on UP from special row
- Remove old_row dead code from QuickMsgScreen keyboard handler
- Alert banner shown only on home screen, suppressed in settings/messages
- Bump FIRMWARE_VERSION to v1.15.1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Save last known unix timestamp to /rtc_save on shutdown and restore it
on startup, so the clock shows correct time immediately after restart
without waiting for GPS fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>