New home page rendering 6 pinned-contact slots. Grid transposes to
orientation: landscape → 3×2, portrait → 2×3. Joystick claims UP/DOWN
and inner LEFT/RIGHT for grid nav; edge LEFT/RIGHT fall through to
page navigation. Selected tile inverts via drawSelectionRow, filled
tile shows contact name + unread badge, empty tile shows "+".
Storage:
- NodePrefs::favourite_contacts[6][6] (6-byte pub_key prefix per slot,
all-zero = empty; collision probability with a real key is 2^-48)
- HomePageBit gains HPB_FAVOURITES = 11 (HPB_COUNT = 12), with a new
HP_FAVOURITES mask bit so the page is toggleable in Settings
- New PAGE_ORDER_LEN constant decouples the persisted array length (11)
from the page-type count; loops over page_order now use it, value-range
checks still use HPB_COUNT
- homePageBit returns 0 for HPB_SETTINGS / HPB_QUICK_MSG explicitly
rather than by bit-index range, so HPB_FAVOURITES (bit 11) is correctly
treated as toggleable
- Default order inserts FAVOURITES after CLOCK; SHUTDOWN drops to the
fallback "missing pages" append (still reachable, lands at end)
Schema sentinel bumped to 0xC0DE0002. DataStore::loadPrefsInt and
savePrefs read/write the new field. Older saves leave the field
zero-initialised (all slots empty), which is the natural starting state.
Phase 2 (Pin from Contact options) and Phase 3 (Pin from empty-slot
mini picker) wire interactions; this commit handles render + nav only.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pageBit/bitToPage in HomeScreen and homePageBitIndex in SettingsScreen
each spelled out the bit assignment with magic numbers 0..10. Lift the
numbering into a NodePrefs::HomePageBit enum (HPB_CLOCK … HPB_QUICK_MSG,
HPB_COUNT) and route every reference through it: mapping functions, the
HP_* bitmasks, the homePageLabel table sizing, loop bounds over
page_order, the bounds check on page_order[0], the "always-visible"
discriminator for SETTINGS/QUICK_MSG.
Per-class enums (HomeScreen::HomePage, SettingsScreen::SettingItem)
stay — they index different concerns (navigation page vs. settings
row). The mapping functions still exist, but now reference a single
source of truth for the bit values.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Old saves (without sentinel) load unchanged — rd() lambda zero-inits
anything not on disk. New saves append a 4-byte sentinel that encodes
the schema revision; the low byte is bumped on every layout change in
loadPrefsInt/savePrefs. Mismatch on load logs a warning and continues
with defaults; the next savePrefs writes the current sentinel so the
file self-heals.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously page_order_set status was inferred from page_order[0] being
in 1..11, so a single junk byte in that range falsely triggered
custom-order mode. Add explicit page_order_set magic (0xA5) written
alongside the array; pre-magic saves are migrated once on load when
their first byte still passes the legacy range check.
Also fix(ui): keep "@[nick] " prefix out of placeholder expansion in
QuickMsgScreen reply path, so a nick containing a token like {loc}
isn't substituted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add "Full rfsh" setting (e-ink only) with options off/5/10/20/30
partial refreshes between full refreshes. Prevents ghosting on panels
that accumulate artifacts after many partial updates.
GxEPDDisplay counts partial refreshes and calls display.display(false)
every N frames when the interval is set. Persisted in DataStore and
applied at startup via applyFullRefreshInterval().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add NodePrefs::joystick_rotation (0-3 steps CW), persisted in DataStore
alongside display_rotation. rotateJoystickKey() now takes a runtime rot
parameter instead of compile-time JOYSTICK_ROTATION macro. UITask::loop()
reads joystick_rotation from NodePrefs each frame. Settings screen exposes
"Joystick" rotation item on any build with UI_HAS_JOYSTICK. JOYSTICK_ROTATION
macro still works as compile-time default for legacy/non-prefs builds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolved conflicts to preserve eink-ui layout helpers (valCol, lineStep, sepH)
while integrating the page_order persistence, buildVisibleOrder(), and
Settings/Messages ordering UI from the wio-tracker-l1-improvements branch.
display_rotation is now written before page_order in the save/load sequence.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NodePrefs: add page_order[11] (1-indexed bit-index, 0=end/legacy).
DataStore: persist page_order appended after use_lemon_font.
UITask/HomeScreen:
- bitToPage(): maps stable bit indices 0-10 to HomePage enum values
- buildVisibleOrder(): respects page_order when initialised, else
falls back to default enum sequence
- navPage() and navigation dots use buildVisibleOrder()
SettingsScreen Home Pages section:
- Add Settings (bit 9) and Messages (bit 10) — show as "always"
- LEFT/RIGHT on any home page item moves it earlier/later in order
- ENTER toggles ON/OFF for pages that can be disabled
- Position number shown before label once custom order is active
- ensurePageOrderInit() seeds default order on first reorder action
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds Settings > Display > Rotation (0°/90°/180°/270°) that persists to
NodePrefs and is applied on startup. Falls back to compile-time
DISPLAY_ROTATION when the field is missing from an older prefs file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Lemon font (LemonFont.h, LemonIcons.h) to the standard build
alongside the Adafruit built-in font; ~101 KB added to flash
- SH1106Display: add _use_lemon flag + setLemonFont(bool) to switch
at runtime; print() and getTextWidth() use Lemon path when active;
translateUTF8ToBlocks() passes UTF-8 through unchanged (Lemon supports
full Unicode U+0020-U+04FF) vs transliterating for the default font
- DisplayDriver: add getCharWidth()/getLineHeight() virtuals (defaults 6/8);
SH1106Display returns 5/9 for Lemon, 6/8 for default; add setLemonFont()
no-op virtual; fix orphaned UTF-8 leading byte in drawTextEllipsized
- FullscreenMsgView: replace fixed-char-count word-wrap with pixel-accurate
wrap using display.getTextWidth(); use getLineHeight() for spacing so
Lemon (9 px) and default (8 px) both render correctly
- NodePrefs: add use_lemon_font field (0=default, 1=Lemon)
- SettingsScreen: add "Font" item in Display section (Default / Lemon),
calls UITask::applyFont() on change
- UITask: add applyFont() that calls display->setLemonFont(use_lemon_font);
called at startup (begin()) and when the setting changes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Toggle in Settings > Display > Format. In 12h mode the clock shows
h:mm AM/PM (or h:mm:ss AM/PM with seconds, no space before AM/PM).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Long press Back starts lock sequence; 3 Enter presses within 3s toggle lock
- Locked screen wakes for 5 seconds on any key, showing clock and unlock hint
- Incoming messages do not wake the display while locked
- Auto Lock setting in Display section: locks automatically when screen turns off
- Lock state persisted in UITask; NodePrefs gains auto_lock flag
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add second ringtone slot (ringtone2_*) to NodePrefs and DataStore.
Add per-channel and per-DM melody assignment (built-in / melody 1 / 2)
stored in NodePrefs bitmasks and DmMelodyEntry table.
Settings screen exposes DM/CH sound items; RingtoneEditorScreen supports
switching between slots. notify() uses buildMelodyFromPrefs() to select
the correct RTTTL string per contact.
Remove broken nRF52 volume control from buzzer.cpp: tone() uses
NRF_PWM2 with DECODER.MODE=Auto so TASKS_NEXTSTEP has no effect and
duty cannot be changed before the first DMA read without patching the
Arduino core. applyVolume() is now a no-op on nRF52.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds configurable periodic 0-hop self-advert that includes GPS coordinates,
making the device's position visible in other nodes' Nearby Nodes screen.
Options: off / 1min / 2min / 5min / 10min / 30min / 1h.
Accessible via Tools → Auto-Advert. Timer runs in MyMesh::loop().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add NearbyScreen: GPS contact list with haversine distance sort,
type filter (ALL/Chat/Rpt/Room/Snsr), detail view with ping (RTT via
ACK poll), node scan via advert(), context menu
- Add DashboardConfigScreen: configure up to 3 data fields on the clock
page (Battery, Temp, Humidity, Pressure, GPS, Altitude, Lux, CO2,
Contacts); entered via long-press ENTER on clock page
- Rework HP_CLOCK layout: full-screen clock (size 2 at y=0), date at
y=19, separator at y=28, three data field rows at y=31/41/51;
hide node name/battery indicator on clock page
- Persist dashboard_fields[3] in NodePrefs and DataStore
- Add isAckPending() to MyMesh for NearbyScreen ping detection
- Wire NearbyScreen into ToolsScreen and UITask
- Reduce NearbyScreen entry buffer from MAX_CONTACTS(100) to 32,
saving ~3.3 KB RAM
- Switch haversine/bearing math from double to float (sinf/cosf/atan2f),
eliminating double libm and saving ~5-10 KB flash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Long-press ENTER in the DM contact list opens a context menu with:
- "Mark as read": clears unread counter for that contact
- "Notif: default/OFF/ON": cycles notification state (same as channels)
Notification state is persisted in NodePrefs (dm_notif[16] table,
keyed by 4-byte pub_key prefix). When muted, the buzzer is silenced
for that contact's messages; when force-on, buzzer plays even in
quiet mode. Default follows the global buzzer setting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bot redesign: DM bot active for all private messages (no pubkey filter),
channel bot optional with separate enable flag; both can run simultaneously
- Bot: one shared trigger, separate reply texts for DM and channel
- Buzzer: add Auto mode (mutes when BT connected, unmutes on disconnect);
per-channel force-on overrides still work as intended
- Settings: defer flash writes to menu exit (only when dirty)
- Settings: add Seconds toggle (hide clock seconds, refresh drops 1s→60s)
- Fullscreen msg preview: fix truncation (fmsg buf 79→140 chars)
- RingtoneEditor: fix melody playback cut after first note (RTTTL lib holds
pointer — moved play buffer to member variable)
- BotScreen: remove key hint footer
- .gitattributes: protect README.md from upstream merge conflicts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The bot can now reply to a specific DM contact instead of a channel.
UI (BotScreen): item 1 cycles through all channels followed by all DM
contacts (ADV_TYPE_CHAT). The label switches between "Channel" / "Contact"
and the value shows the channel or contact name. Left/right/enter navigate
the combined list.
Logic (MyMesh::onMessageRecv): when bot_target_type==1, match the trigger
against incoming DM text and reply via sendMessage(). A 4-byte pubkey
prefix identifies the target; all-zero prefix matches any DM sender.
Persistence: bot_target_type and bot_dm_pubkey[4] appended to prefs with
backward-compat read guard (old firmware defaults to channel mode).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New 'Tools' page on the home screen carousel with Ringtone Editor and Auto-Reply Bot items
- Bot settings: enable/disable toggle, channel picker, trigger phrase, reply text (keyboard with pre-fill)
- Bot logic in MyMesh: case-insensitive substring match, 10 s cooldown to prevent reply loops
- Settings: Home Pages visibility control (which carousel pages are shown; Settings and Messages always visible)
- DataStore: persist home_pages_mask and bot fields with backward-compat guards on read
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New "Home Pages" section in Settings lets the user toggle which pages
appear in the home screen carousel. Settings and Messages are always
shown; Clock, Recent, Radio, Bluetooth, Advert, GPS, Sensors, Tools
and Shutdown can each be hidden individually.
- NodePrefs: home_pages_mask uint16_t (bit per page, 0=all visible)
- HomeScreen: isPageVisible/navPage skip hidden pages; dots indicator
counts only visible pages; enforces visible page on re-entry
- SettingsScreen: SECTION_HOME_PAGES with per-page ON/OFF toggles
- DataStore: persisted at offset 1598
- Default: all pages visible (0x01FF)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- RingtoneEditorScreen: 32-note step sequencer accessible via new Tools page on home screen. U/D=pitch, ENTER=octave cycle, L/R=cursor, MENU=options (play, duration, BPM, insert/delete, save). Notes stored packed in NodePrefs and persisted via DataStore.
- ToolsScreen: new "Tools" home page entry launching the editor
- Buzzer volume: 0-4 bar control in Settings > Sound, persisted to prefs
- Buzzer notifications: fixed notify() firing inside wrong serial-guard branch and before addChannelMsg set the channel index
- Unread counter: fixed newest-first index direction; watermark formula now correct
- OLED brightness: wider range via pre-charge register 0xD9 combined with contrast
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- NodePrefs: add buzzer_volume field (0=min..4=max, default 4)
- DataStore: persist buzzer_volume at end of prefs file (backward-compat)
- buzzer: setVolume()/getVolume(); applyVolume() sets PWM duty cycle via
analogWrite() after each rtttl::play() call to control output amplitude
- Settings UI: new BzrVol bar item in Sound section (left/right to adjust)
- UITask: apply saved volume on startup alongside brightness
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds SECTION_CONTACTS with two items under Settings:
- DM: "fav" (default) / "all" — controlled by dm_show_all in NodePrefs
- Rooms: "all" (default) / "fav" — controlled by room_fav_only in NodePrefs
Both fields default to 0, preserving current behaviour on firmware upgrade.
buildContactList() reads the prefs to filter accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Long-pressing ENTER on a channel in the channel picker opens a
2-item context menu: "Mark all read" and "Notif: default/OFF/ON".
Muted channels still increment the unread counter silently; force-ON
overrides global buzzer mute. Settings persist to flash via NodePrefs
(ch_notif_override / ch_notif_muted bitmasks).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Settings → Messages section: 10 user-editable message slots (M1–M10)
with on-screen keyboard editor; slot M1 defaults to "OK" on first boot
- Keyboard: add Caps Lock [^] button — stays highlighted when active,
all letters display and insert as uppercase until toggled off
- Keyboard: add [{}] placeholder picker overlay — inserts {loc} or {time}
into the message at cursor position; available in both compose and
settings slot editor
- Max message length raised to 139 chars to match the companion app;
ChHistEntry text buffer extended to 140 chars
- NodePrefs: custom_msgs slot size extended to 140 chars per slot
- KB constants (KB_MAX_LEN, KB_CHARS, etc.) moved before SettingsScreen
so both SettingsScreen and QuickMsgScreen can reference them
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Change > to >= so stored value 1 means direct/0-hop only (liamcottle)
- Clamp max_hops to 63 on write since getPathHashCount() caps at 63 (robekl)
- Update comments to reflect encoding: 0=no limit, 1=direct only, N=up to N-1 hops
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Filter auto-add of new contacts by hop count (issues #1533, #1546).
Setting is configurable from the companion app via extended
CMD_SET/GET_AUTOADD_CONFIG protocol (0 = no limit, 1-63 = max hops).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* companion: new field in CMD_SET_OTHER_PARAMS, path_hash_mode
* companion: CMD_SEND_SELF_ADVERT, cmd_frame[1] now holds the path hash size (0 = zero hop, 1..3 = flood path hash size)
Add GPS configuration to NodePrefs structure and persist the GPS
enabled state when toggled via UI. This ensures GPS settings are
retained across device restarts.