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>
Five sites in UITask.cpp + MyMesh.cpp branched on EINK_DISPLAY_MODEL
purely for runtime-shape decisions (blink rate, default pref values,
refresh intervals). Replace with a constexpr Features namespace:
IS_EINK, BLINK_INDICATORS, CLOCK_HIDE_SECONDS_DEFAULT, HOME_REFRESH_MS,
LOCKSCREEN_REFRESH_MS. Compiler dead-branch-eliminates so cost is
identical to the preprocessor branch, but the code stays reachable to
tooling and the policy lives in one file.
SettingsScreen and driver headers still need real #ifdef — they
condition enum members and class field layouts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mechanical sweep across UITask.cpp and SettingsScreen.h. All current
format strings already fit their buffers, but a future format change
would silently overflow with sprintf. snprintf with sizeof makes the
buffer bound explicit and traceable to the array declaration.
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 MESHCORE_VERSION build flag ("1.15") to companion base configs.
Splash screen now shows MESHCORE_VERSION in the large text and the
plus firmware version (stripped of commit hash) in the Plus bar,
e.g. "1.15" large + "Plus v1.11 for Wio" below.
Change CI tag pattern to v* and simplify version extraction so tag
v1.11 maps directly to FIRMWARE_VERSION=v1.11.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- home_pages_mask XOR toggle initialises to HP_ALL before flipping bit,
preventing accidental hiding of all other pages when mask starts at 0
- buildVisibleOrder appends any visible page missing from custom page_order,
so pages absent from migrated/corrupted prefs remain navigable
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>
- GxEPDDisplay::print() renders '\xDB' as filled rect for Lemon font,
matching OLED Lemon block behavior
- Remove translateUTF8ToBlocks override (base class passthrough sufficient)
- transliterateCodepoint fallback changed '?' → '\xDB' for consistency
- drawTextCentered/Right/LeftAlign now call translateUTF8ToBlocks
- isLemonFont() added to DisplayDriver, SH1106Display, GxEPDDisplay
- isLandscape() guarded by EINK_DISPLAY_MODEL to distinguish OLED vs e-ink
- ind_h trims indicator box height only for Lemon font
- getTextWidth disables wrap before getTextBounds, restores after
- QuickMsgScreen: tighter hist_item padding; 2 messages visible with Lemon
- Hibernate hint fits on one line when width allows, splits otherwise
- BLE PIN page shows toggle hint on landscape e-ink
- Remove unused muted_icon bitmap
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Revert logo width clamp — instead let x go negative so GxEPD's drawPixel
clips symmetrically. In portrait (122px wide) x = (122-128)/2 = -3,
cropping 3px from each side rather than 6px from the right only.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move applyRotation() before setCurrScreen(splash) so e-ink startup
screen respects the saved display_rotation preference
- Clamp meshcore logo width to display.width() so it doesn't overflow
in portrait mode (122px screen vs 128px logo)
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>
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
E-ink panels take ~2 s for a full refresh and visible flicker on every update.
Showing seconds caused the clock page to redraw every 1 s, triggering a real
panel refresh each time the CRC changed.
- Default clock_hide_seconds=1 for EINK_DISPLAY_MODEL builds so fresh
installs don't flicker out of the box.
- Remove the Seconds setting from SettingsScreen on e-ink so users can't
accidentally re-enable it.
- Clock page now returns 60 s refresh interval on e-ink (content changes at
most once per minute); other home pages capped at 30 s (new messages still
force immediate redraw via notify()).
- Lock screen refresh interval raised from 1 s to 60 s on e-ink; button
presses already reset _next_refresh=0, so unlock feedback is instant.
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>
Without _next_refresh = 0 the Settings screen would continue using its
2-second refresh interval after a font change, showing stale rendering
for up to 2 seconds. Mirrors the pattern used by setBrightnessLevel().
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>
- Use inverted PWM polarity (0x8000) with PCT {2,5,12,25,50} giving
~6-8 dB perceptual steps (-24/-16/-9/-3/0 dB) vs the original
uneven steps where levels 4 and 5 were only 1 dB apart
- Guard against cmp=0 truncation in both _nrfStartPwm and applyVolume:
at level 1 with high notes (freq > ~2.5 kHz) integer math gives
cmp=0 which with inverted polarity means 100% HIGH → complete silence
- Change volume preview note from C5 (523 Hz) to C6 (1047 Hz) to better
match the piezo resonant frequency range and actual notification sounds
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously each LPP sensor field called querySensors() separately.
Now a single querySensors() call fills a shared buffer used by both fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously every key press while locked reset _lock_wake_until to now+5s,
so multiple presses (or spurious button events) kept extending the window.
Now the 5s timer is set only when the display first wakes from dark.
The unlock sequence still extends the window on each Back+Enter step.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Turn display on at first Back+Enter press so hints are visible even when screen was dark
- Extend _lock_wake_until on every sequence step to prevent screen blanking mid-press
- Set _next_refresh=0 on every Enter press (not just on completion) for immediate hint redraw
- Add _lock_seq_used flag to suppress the Back CLICK that fires on release after unlock
- Block triple-Back (buzzer toggle) when screen is locked
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Long press detection on back_btn was unreliable — the release generated
a spurious CLICK that cancelled the sequence. Now the Enter click handler
checks back_btn.isPressed() directly: holding Back and clicking Enter 3×
within 3 seconds locks/unlocks the screen.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reuses dashboard_fields[0] and [1] from NodePrefs — left-aligned and
right-aligned in one row under the clock, no labels to save space.
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>
Static screens (SettingsScreen, BotScreen, FullscreenMsgView, message
list, contact picker) were re-rendering every 300ms with no visible
benefit. New messages still trigger immediate refresh via newMsg() →
_next_refresh=100, so responsiveness is unchanged. Reduces unnecessary
SPI transfers and CPU wakeups ~6x on idle static screens.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
- 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>
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>
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>