Previously empty slots used drawRect and filled slots used fillRect,
so at max value (e.g. volume=4) no drawRect was called and no slot
borders were visible when selected (inverted colors). Brightness at
default (2/5) always had visible outlines, making the two bars look
inconsistent.
Now drawRect is called for every slot, with fillRect(+1,+1,w-2,h-2)
on top for filled ones. All 5 slots are always visually distinct.
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>
##*- strips to the last hyphen, so wio-tracker-v1.15-plus.1.3 yielded
"plus.1.3" instead of "v1.15-plus.1.3". Switch to #*-v (shortest prefix
ending with -v) which preserves hyphens inside the version string.
Also remove the -Plus suffix appended in build_wio_tracker_l1_firmwares()
since the version scheme already encodes "plus" in the tag/version string,
making the suffix redundant and producing doubled output like "plus.1.3-Plus".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tone() dynamically computes seq_refresh so the DMA repeats 50% duty for
~13-30ms per note before re-reading its buffer — volume cannot take effect
until that delay passes, producing a staircase artifact.
Replace tone()/NonBlockingRtttl with a custom RTTTL parser that drives
NRF_PWM2 directly using REFRESH=0. DMA now re-reads _duty_buf every
PWM period so:
- Duty is set before SEQSTART fires → no initial full-volume burst
- applyVolume() writes to _duty_buf and takes effect within one period (<2.3ms at A4)
- setVolume() during playback adjusts loudness immediately and smoothly
Non-nRF52 platforms continue to use NonBlockingRtttl + analogWrite.
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>
- Swap left/right navigation in fullscreen view: left=newer, right=older
- Update arrow indicators to match new direction
- Remove # prefix from channel name in message and compose view titles
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move "A" (auto-advert) indicator left of BT "B" in status bar
- Ellipsize node name to fit available width instead of overflowing
- Shrink A and B background rectangles by 1px right, 2px bottom
- Change A blink to 2s on / 2s off at 1000ms refresh rate
- Auto-advert sends GPS position directly via createSelfAdvert/sendZeroHop
- Align Settings and Tools home page titles to y=22, same as Messages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BT indicator and mute icon were at y=1 (1px below text baseline).
Battery icon height reduced 10→8px to match text height.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Changed ~ to " A" (space + capital A, no inverted background)
- Blink: on 500ms / off 1500ms (once per 2s)
- Home screen refresh rate drops to 500ms when auto-advert is active
so the blink is actually visible
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Blinks at 4Hz (250ms toggle) with inverted background so it's clearly
visible without being distracting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace getAvailableLPPTypes() (hardware-detected) with a live querySensors()
call so only placeholders for sensors currently returning values appear in
the picker.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Scan does not discover others — it announces our own presence.
Updated labels: "Scan for nodes"→"Send my advert", "SCANNING..."→"ADVERTISING...", "[M]=Scan"→"[M]=Advert".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Detail view now shows "Seen: Xs/Xm/Xh ago" using ContactInfo.lastmod
- Periodic detail refresh exits back to list view if the contact is no
longer in the filtered list (e.g. removed from favourites)
- Rearranged detail layout to fit 6 rows within 64px display
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add "Fav" as first filter category (contacts with flags & 1)
- Show distance and bearing on separate lines in detail view
- Add cardinal direction to bearing: "Az: 245d (SW)"
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>
- Remove gps_lat/lon == 0 filter: screen now shows all contacts
reachable by radio; distance column shows ?GPS when coords unavailable
- Guard haversine: only compute distance when both own AND remote GPS
are valid (prevents bogus distance to 0 N 0 E)
- Fix sort: nodes without GPS (-1) sort to end instead of front
- Rename Chat filter label to Comp, type display to Companion
- Update empty list message to "No contacts found"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stale _ping_ack_seen=true from a previous ping attempt could cause an
immediate PING_OK (with wrong RTT) on the next ping if the new ACK entry
hadn't appeared in the table yet. Reset the flag everywhere _ping_state
is set to PING_IDLE.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Ping: add _ping_ack_seen flag; RTT is measured only after ACK entry
was first confirmed present in the table, preventing false 0ms when
isAckPending() returns false before the entry is registered
- NearbyScreen: replace unsupported degree symbol \xb0 with 'd'
in bearing format ("Dist:1.5km 45d")
- Clock page: hide page indicator dots (already hides node name/battery);
eliminates overlap with size-2 clock text drawn at y=0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BotScreen and DashboardConfigScreen were calling savePrefs() on every
exit regardless of whether any setting changed. Add _dirty flag set only
on actual value changes (toggle, channel select, keyboard confirm,
field cycle), reset on enter().
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>
- KeyboardWidget: replace inline placeholder overlay with PopupMenu
- UITask QuickMsgScreen: replace inline CONTACT_PICK/CHANNEL_PICK context
menus with PopupMenu; notif label stored in _ctx_notif_item at open time
- BotScreen: clear placeholder list for trigger field (expansion tokens
would never match incoming literal text)
- MsgExpand: fix APPEND macro off-by-one (oi+_l < out_len-1 → < out_len)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add PopupMenu.h: generic scrollable overlay with wrap-around nav (up from
first wraps to last, down from last wraps to first), closes on ENTER or
CANCEL/CONTEXT_MENU. Default constructor ensures active=false on creation.
- KeyboardWidget: replace inline placeholder overlay (~50 lines) with PopupMenu
- UITask QuickMsgScreen: replace inline context menus in CONTACT_PICK and
CHANNEL_PICK with PopupMenu; notif label stored in _ctx_notif_item at open time
- BotScreen: clear placeholder list for trigger field (placeholders are
expansion tokens that would never match incoming literal text)
- MsgExpand: fix APPEND macro off-by-one (oi+_l < out_len-1 → < out_len)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
expandMsg now expands {temp}, {hum}, {pres}, {batt}, {alt}, {lux}, {dist},
{co2} from live sensor readings. The [{}] picker in the on-screen keyboard
shows only sensors currently detected by SensorManager and scrolls when
more than 3 items fit the overlay.
New SensorPlaceholders.h helper keeps sensor-to-keyboard wiring out of
UITask.cpp. MsgExpand.h handles all expansion logic centrally; MyMeshBot
and QuickMsgScreen pass &sensors and batt_volts to each call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shared struct handles rendering and input for both DM and channel fullscreen
views, replacing ~80 lines of duplicated inline code. Fixes navigation arrow
indicators being swapped in channel fullscreen.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pressing ENTER on a selected DM message now opens a fullscreen view
(matching channel history behavior) instead of going directly to compose.
Fullscreen supports UP/DOWN scrolling for long messages, LEFT/RIGHT to
navigate between messages, and ENTER/CANCEL to return to the list.
Compose is still accessible via [+ send] (ENTER when nothing selected).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All three keyboard instances (Settings, QuickMsg, Bot) now show plain
text + cursor without any field-name prefix. Removes the label field
from KeyboardWidget entirely — the screen title already indicates context.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- preview[] increased from 32 to 40 bytes to safely fit max label (15 chars)
+ 20 chars text + cursor + null without snprintf truncation
- KEY_CONTEXT_MENU (long-press ENTER) now treated as CANCEL in keyboard
input handler, restoring BotScreen behavior lost in the refactor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All three keyboard implementations (SettingsScreen, QuickMsgScreen, BotScreen)
now share a single KeyboardWidget struct defined in KeyboardWidget.h, eliminating
~480 lines of duplicated render/input logic. Also fixes the SettingsScreen bug
where navigation keys were checked before the placeholder overlay, causing cursor
movement when the overlay was open.
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>
When companion app finishes syncing all messages (msgRead(0)),
clear both _dm_unread_table and _ch_unread so indicators match
the app's read state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add DM_HIST phase in QuickMsgScreen: 32-entry ring buffer tracks incoming
and outgoing DMs per contact; selecting a contact now opens history view
before compose
- Per-contact DM unread badge in contact list (replaces hop count display)
- Bot replies (DM and channel) stored in history so they appear on-screen
- Fix contact index truncation: _sorted was uint8_t (max 255), causing
contacts at indices 256-349 to map to wrong entries; changed to uint16_t
- Add addDMMsg() virtual to AbstractUITask; wire through MyMesh.cpp
- Incoming chat DMs call addDMMsg(); outgoing after successful send too
- msgRead(0) from companion app clears per-contact unread table
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>
Without the check, a DM-mode bot (bot_target_type==1) would also reply
to channel messages whenever the trigger phrase and channel index happened
to match.
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>