Commit Graph

891 Commits

Author SHA1 Message Date
Jakub
44b2519df4 fix: with_sender buffer too small and FS_LINE_H mismatch
- with_sender[160] could hold only ~160 chars but node_name(32) +
  expanded(200) needs up to 234; snprintf would silently truncate
  the UI display entry. Increased to 240.
- FS_LINE_H was 9 but Adafruit 5x7 font line height is 8, wasting
  1px per line and showing 5 lines instead of 6.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 23:23:20 +02:00
Jakub
1b9dc42250 fix: bot trigger matched against message body only, not sender name
Channel messages have the format "sender_name: body". The trigger was
being searched across the whole string, so a nick containing the trigger
word would fire a reply even with an unrelated message. Now the match
skips past the ": " separator and only checks the message body.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 23:19:12 +02:00
Jakub
5bc250888f fix: apply saved display brightness before UI task starts
Loading... screen was always shown at full brightness because prefs
are not yet loaded at that point. Apply setBrightness() in the first
common code path after the_mesh.begin() loads prefs, so at least the
tail of the loading screen and the entire splash screen respect the
saved brightness level.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 23:15:29 +02:00
Jakub
b91afa05b9 fix: reduce low battery shutdown text size to fit 128px OLED
Size 2 rendered text wider than display width. Switched to size 1
and adjusted y-positions to keep both lines visible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 17:47:22 +02:00
Jakub
6a7f8b7ca7 fix: transliterate message text in FullscreenMsgView before wrapping
Raw UTF-8 message body was passed directly to display.print(), rendering
Polish and other accented chars as garbage on the Adafruit font.
Transliterating before wrapLines also fixes byte-count wrapping for
multi-byte UTF-8 characters.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 15:24:36 +02:00
Jakub
fbe636a05a feat: show plus version number on splash screen
Splash screen bar changes from "Plus for Wio" to "Plus 1.4 for Wio"
by extracting the suffix after "plus." from FIRMWARE_VERSION.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 00:33:13 +02:00
Jakub
9c40a253e2 update README and fix quick message / melody naming collision
- Expand README: ringtone editor now documents two melody slots, full
  note spec, and melody assignment to DM/channel notifications; settings
  section lists DM Melody and Channel Melody options; context menu
  description updated with melody override detail
- Rename quick reply template labels from M1-M10 to Q1-Q10 to avoid
  ambiguity with melody slot identifiers M1/M2
- Increase DM message history buffer from 32 to 64 entries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 00:09:57 +02:00
Jakub
1d04930c1e quick fix 2026-05-15 16:33:19 +02:00
Jakub
ec601ef118 fix: renderBar always shows slot outlines regardless of value
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>
2026-05-15 15:48:05 +02:00
Jakub
484f0891b1 fix: three bugs found in code review
- 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>
2026-05-15 15:34:10 +02:00
Jakub
d02c1cf098 feat: play preview tone when buzzer volume changes in settings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 11:46:48 +02:00
Jakub
b3c6416874 feat: melody assignment per channel/DM, dual ringtone slots, volume cleanup
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>
2026-05-15 11:43:38 +02:00
Jakub
c86e6c803e fix: reverse fullscreen message nav order and remove channel # prefix
- 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>
2026-05-14 20:21:13 +02:00
Jakub
1fcad7776a feat: add unread message count field to clock dashboard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 18:20:49 +02:00
Jakub
9426c86e79 feat: polish status bar and home screen layout
- 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>
2026-05-14 18:13:18 +02:00
Jakub
b36c970a2d fix: auto-advert indicator - 300ms refresh, background shifted 1px left
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 17:45:26 +02:00
Jakub
e205f3712d fix: auto-advert indicator - inverted A, 50% duty 1s blink, 1s refresh
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 17:41:33 +02:00
Jakub
20151cc609 fix: align status bar icons to y=0, reduce battery icon to 8px height
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>
2026-05-14 17:34:57 +02:00
Jakub
8efff38a4f fix: auto-advert indicator - capital A with space, slow blink, proper refresh
- 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>
2026-05-14 17:25:27 +02:00
Jakub
e38dc87c25 feat: blinking ~ indicator in status bar when auto-advert is active
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>
2026-05-14 16:51:57 +02:00
Jakub
e43047f561 fix: show keyboard placeholders only for sensors with actual live data
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>
2026-05-14 16:41:04 +02:00
Jakub
9c4dc0039a fix: clarify advert button labels in NearbyScreen
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>
2026-05-14 16:36:35 +02:00
Jakub
f434852706 feat: show last-heard time in detail view, fix disappearing contact edge case
- 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>
2026-05-14 16:29:04 +02:00
Jakub
404cb6956d feat: add Fav filter and clearer bearing display in NearbyScreen
- 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>
2026-05-14 16:21:11 +02:00
Jakub
083ddac85c refactor: remove ping from NearbyScreen, rely on auto-advert for presence
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>
2026-05-14 16:17:52 +02:00
Jakub
09ac455806 feat: add periodic auto-advert with GPS position (Auto-Advert tool)
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>
2026-05-14 16:08:33 +02:00
Jakub
d59687647f fix: wire NearbyScreen ping through expected_ack_table so RTT is measurable
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>
2026-05-14 15:57:00 +02:00
Jakub
11cb0ae25e fix: NearbyScreen shows all contacts, not only those with GPS
- 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>
2026-05-14 15:49:23 +02:00
Jakub
862714982e fix: reset _ping_ack_seen on detail view enter and exit
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>
2026-05-14 13:55:35 +02:00
Jakub
10307d3a03 fix: ping RTT 0ms, degree symbol, clock page dots overlap
- 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>
2026-05-14 13:28:52 +02:00
Jakub
85de0b02be fix: guard savePrefs with dirty flag in BotScreen and DashboardConfigScreen
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>
2026-05-14 11:36:11 +02:00
Jakub
9e5c0e79e7 fix: move EXPECTED_ACK_TABLE_SIZE define before class for isAckPending
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>
2026-05-14 11:30:43 +02:00
Jakub
24474f67fa feat: NearbyScreen, configurable clock dashboard, memory optimizations
- 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>
2026-05-14 11:28:36 +02:00
Jakub
7b63af1068 refactor: use PopupMenu in keyboard and context menus, fix bugs
- 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>
2026-05-14 08:06:26 +02:00
Jakub
2f32683494 refactor: extract popup menu into reusable PopupMenu struct with wrap-around navigation
- 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>
2026-05-14 08:06:07 +02:00
Jakub
42b51935b1 feat: sensor placeholders in messages and keyboard picker
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>
2026-05-14 00:12:07 +02:00
Jakub
26d3d7ab00 refactor: extract fullscreen message view into reusable FullscreenMsgView module
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>
2026-05-13 23:54:26 +02:00
Jakub
c7887a940d feat: DM history fullscreen message view on ENTER
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>
2026-05-13 23:40:15 +02:00
Jakub
4063c2d296 style: shorten Del label to [Dl] to fit 25px special row cell
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 21:09:47 +02:00
Jakub
464f5cd7a2 fix: remove label prefix from keyboard preview for visual consistency
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>
2026-05-13 21:09:47 +02:00
Jakub
5fecafd4f9 fix: KeyboardWidget preview buffer size and KEY_CONTEXT_MENU handling
- 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>
2026-05-13 21:09:47 +02:00
Jakub
07bd5f1167 refactor: extract on-screen keyboard into reusable KeyboardWidget module
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>
2026-05-13 21:09:47 +02:00
Jakub
60867a4c9e fix: check getContactByIdx return value in DM context menu handler
Prevents using uninitialized ContactInfo if the lookup fails.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 21:09:47 +02:00
Jakub
eb0f436924 feat: DM per-contact notification settings and mark-as-read
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>
2026-05-13 21:09:47 +02:00
Jakub
b50f21060c fix: clear DM and channel unread counters on app sync completion
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>
2026-05-13 21:09:47 +02:00
Jakub
4a4ff76f10 feat: DM chat history, per-contact unread badges, bot history, contact index fix
- 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>
2026-05-13 21:09:47 +02:00
Jakub
f6e251cfe3 chore: adopt vX.Y-plus.N versioning scheme
vX.Y tracks upstream major.minor; plus.N is fork-specific revision.
Bump to v1.15-plus.1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 21:09:47 +02:00
Jakub
71635e5e6f Bot: DM+channel modes with separate replies; buzzer auto-mute; UX fixes
- 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>
2026-05-13 21:09:47 +02:00
Jakub
af7da7906a Fix: tryBotReplyChannel must check bot_target_type == 0
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>
2026-05-13 21:09:47 +02:00
Jakub
dd2c09174f Extract bot logic from MyMesh into MyMeshBot.h
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>
2026-05-13 21:09:47 +02:00