Commit Graph

159 Commits

Author SHA1 Message Date
MarekZegare4
151892e1ce refactor(ui): extract shared helpers, dedup screens, GPS shutdown via provider
Pull repeated UI idioms into DisplayDriver and remove duplicated logic:

- DisplayDriver: add drawScrollArrows() and drawInvertedHeader(); replaces 11
  copy-pasted scroll-arrow blocks across 5 screens and 3 inverted title-bar
  blocks (NearbyScreen x2, NavView). Standardises the detail-view separator.
- useImperial(): single source on UITask; NearbyScreen/TrailScreen delegate
  instead of each re-reading NodePrefs.units_imperial.
- NearbyScreen: extract saveSelectedWaypoint() (two identical blocks -> one);
  drop the redundant local label buffer (WaypointStore truncates).
- UITask::shutdown(): power GPS off through LocationProvider::stop() instead of
  poking PIN_GPS_EN directly — centralises enable/reset-pin + active-level logic.

Net -58 lines. Verified: GAT562 (SSD1306) and WioTrackerL1Eink (GxEPD) solo
builds compile clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 13:21:59 +02:00
MarekZegare4
fcdacaa3bc fix: GPS shutdown active-level, buzzer octave-8 consistency, waypoint header read checks
- UITask::shutdown(): power GPS off using !PIN_GPS_EN_ACTIVE instead of hardcoded
  LOW, matching MicroNMEALocationProvider::stop() (still LOW on current active-high
  devices, but correct for a future active-low GPS).
- buzzer _noteFreq(): clamp octave to 8 instead of 7 so the parser's accepted range
  (4-8) is fully consumed; previously an octave-8 digit was clamped away and could
  leak into the stream as the next note's duration.
- Waypoint::readFrom(): check read() return for version/reserved/count header bytes
  so a truncated file is rejected instead of using garbage count.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 12:49:15 +02:00
Jakub
4815e6f18d feat(gat562-30s): add solo build envs, fix joystick/GPS/buzzer/display
- SSD1306Display: track _text_sz in setTextSize(); override getCharWidth()
  and getLineHeight() so line spacing scales with text size (fixes text
  overlap on splash/clock screens at size 2)
- gat562_30s_mesh_kit variant.h: fix PIN_GPS_EN 33→34 (IO2/P1.02 controls
  GPS module power via Q3/Q5 transistors; 33 = BEE_EN = buzzer)
- gat562_30s_mesh_kit variant.h: correct joystick pin comments (P0.xx labels
  were cyclically wrong; pin numbers already correct)
- gat562_30s_mesh_kit target.h/cpp: declare and instantiate joystick_up/down
  under UI_HAS_JOYSTICK_UPDOWN guard (fixes compile error in solo envs)
- gat562_30s_mesh_kit platformio.ini: add solo_ble and solo_dual environments
  with GPS, BLE, joystick up/down, QSPI flash, and buzzer (NonBlockingRTTTL)
- gat562_mesh_watch13 platformio.ini: add solo_ble environment with GPS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 00:39:46 +02:00
Jakub
f5b3213f80 Merge upstream/main (v1.16.0) into wio-unified
Merge 254 upstream commits. Highlights relevant to the Wio fork: native
NRF52 companion power-saving (sleep when !hasPendingWork), preamble 16->32
for SF<9, generic sensor-registration model, CMD_SEND_RAW_PACKET, isEink()
display abstraction, KEEP_DISPLAY_ON_USB, contacts-sync/transient fixes.

Conflicts resolved (11 files):
- buzzer.h/.cpp: keep NRF52 include guard + our begin()->startup(); fold in
  upstream doc comment.
- platformio (wio + eink): keep our slim env layout, add upstream kiss_modem
  env (+ debug_tool=stlink on eink ble).
- GxEPDDisplay.h: take upstream isEink() override.
- EnvironmentSensorManager.h: take upstream's wider #if guard.
- DataStore.cpp: take upstream saveContacts(filter) signature, keep our
  ::openWrite (member openWrite would shadow the global 2-arg overload).
- main.cpp: keep our watchdog init + add board.onBootComplete(); adopt
  upstream's `if(!hasPendingWork()) sleep` loop.
- MyMesh.h: version -> v1.16-solo.0, build date 6 Jun 2026.
- MyMesh.cpp: keep both CMD codes (SCREENSHOT 66 + RAW_PACKET 65), both field
  inits, both handlers, our screenshot fns + upstream saveContacts/save_filter,
  upstream hasPendingWork() + our MyMeshBot include.
- UITask.cpp: keep our solo splash/clock page/snprintf/buzzer init; merge
  auto-off (+ opt-in KEEP_DISPLAY_ON_USB); merge low-batt shutdown (our EMA +
  prefs threshold + upstream's !isExternalPowered() guard and eink-aware delay).

Post-merge drift fixes (upstream API/architecture changes, not conflicts):
- applyTxPower(): radio_set_tx_power() removed upstream -> radio_driver.setTxPower().
- Sensor refactor dropped the per-sensor *_initialized flags our
  getAvailableLPPTypes() relied on; removed that override and the SENSORS page
  now derives available LPP types from the populated sensors_lpp buffer.

README kept ours via .gitattributes merge=ours. Builds verified on all 5 Wio
envs (dual, eink dual, radio_ble, dual_dev, eink_dual_dev). Not yet
hardware-verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 17:21:41 +02:00
Liam Cottle
b83355e702 Merge pull request #2671 from sefinek/fix/typos-grammar-formatting
Fix typos and formatting in documentation and comments
2026-06-04 15:57:48 +12:00
Jakub
5b25d1eb32 feat(ui/clock): stacked big-digit clock on portrait e-ink
On a tall portrait e-ink panel the clock and lock screens wasted most of
the vertical space on a small inline "HH:MM". Render HH and MM on two
lines in a new size-4 font (built-in GFX scaled 7×, ~42×56 px) so the
digits roughly double in height and fill the narrow width. Wide panels
(OLED, landscape e-ink) keep the classic single-line size-2 layout.

- New shared drawClockTime() helper used by both the Clock home page and
  the lock screen; returns the y below the time block so the date and
  dashboard rows flow beneath it.
- GxEPDDisplay: size 4 = built-in font × BIG_TEXT_SCALE (7); getCharWidth /
  getLineHeight / setTextSize handle it; fontAscender stays 0 (built-in is
  top-left origin).
- Centre the big digits on their visible glyph width — the built-in font
  advances 6 px per char but the glyph is 5 px wide, so getTextWidth over-
  reports by one trailing column and the digits would sit ~half a column
  left of centre. AM/PM rendered one size larger (size 2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 23:32:27 +02:00
Sefinek
cc05ae555a docs: fix typos, grammar, and formatting in FAQ and code comments 2026-06-02 22:12:18 +02:00
taco
5e3edd0bbc add bool isEink() for eink display drivers 2026-06-02 18:36:07 +10:00
Jakub
d1a88c8277 fix(screenshot): send GxEPD2 rotation in header; full rot 0-3 decode in Python
Firmware (7-byte header):
  Byte 7 = display->screenshotRotation() — GxEPD2/GFX rotation value (0-3).
  DisplayDriver::screenshotRotation() defaults to 0 (OLED).
  GxEPDDisplay::screenshotRotation() returns display.getRotation(), the
  live GxEPD2 value (reflects runtime setDisplayRotation() calls, not just
  the compile-time DISPLAY_ROTATION macro).

Python decoder:
  - Parse 7-byte header; pass rotation to eink_buffer_to_image().
  - Implement all four GxEPD2 drawPixel coordinate transforms:
      rot 0: phys_x=lx,            phys_y=ly
      rot 1: phys_x=vis_w-1-ly,    phys_y=lx
      rot 2: phys_x=vis_w-1-lx,    phys_y=vis_h-1-ly  (180° flip)
      rot 3: phys_x=ly,            phys_y=vis_h-1-lx
  - vis_w/vis_h derived from log dimensions + rotation parity (no constants).
  - Print rot= in the status line so the user sees which rotation was used.

This fixes the 180° rotated image seen with rotation=2 (portrait inverted)
without hardcoding a flip — correct for any rotation value.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 08:10:16 +02:00
Jakub
1332358ee7 fix(screenshot): use GxEPD2 visible dimensions; fix phys_x formula
Firmware: add screenshotWidth()/screenshotHeight() virtual pair to
DisplayDriver (default: width()/height()).  GxEPDDisplay overrides to
return display.width()/display.height() — the GxEPD2-reported values
that use WIDTH_VISIBLE (e.g. 122 for GxEPD2_213_B74), not the full
physical WIDTH stored in DisplayDriver (128).  MyMesh uses these
instead of display->width()/height() for the screenshot header bytes.

Result for GxEPD2_213_B74 + DISPLAY_ROTATION=1:
  header was 250×128 → now 250×122  (correct visible canvas)

Python decoder (tools/screenshot.py):
- Remove hardcoded EINK_PHYS_WIDTH=128 / EINK_VISIBLE_W=122 constants.
- Derive phys_stride from buffer_size / log_width (works for any panel).
- Fix phys_x formula: was (EINK_PHYS_WIDTH-1-ly = 127-ly),
  now (vis_w-1-ly = log_height-1-ly = 121-ly for 213_B74).
  Old formula addressed invisible columns 122-127; new formula correctly
  maps logical rows 0..121 to visible physical columns 121..0.
- vis_w = log_height (no hardcoded trim; header now carries correct value).

This also works for square panels (e.g. 128×128 where WIDTH=WIDTH_VISIBLE):
  header will carry 128×128, decoder produces a correct 128×128 image.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 01:19:22 +02:00
Jakub
7d9d5f847d feat(screenshot): extend e-ink screenshot support via GxEPD2 patch
- Add lib/GxEPD2-patch/src/GxEPD2_BW.h: local patched copy of GxEPD2_BW.h
  that exposes getBuffer()/getBufferSize() under ENABLE_SCREENSHOT guard.
  Include guard (_GxEPD2_BW_H_) prevents double-inclusion of the installed
  library version.

- GxEPDDisplay.h: use patched header via relative include when
  ENABLE_SCREENSHOT so the patch takes precedence over the installed lib
  (PlatformIO adds library paths before -I build_flags).

- DisplayDriver.h: add virtual getBuffer()/getBufferSize()/getDisplayType()
  defaults (nullptr/0/0) under ENABLE_SCREENSHOT.

- SH1106Display.h / SSD1306Display.h / GxEPDDisplay.h: add concrete overrides;
  getDisplayType() returns 0 (OLED) or 1 (e-ink).

- MyMesh.cpp/h: replace fragile C-cast with virtual dispatch in
  handleScreenshotRequest(); extend 5-byte header to 6 bytes by appending
  display_type so the host tool can decode the correct pixel layout.

- tools/screenshot.py: parse 6-byte header; add eink_buffer_to_image()
  that decodes the row-major MSB-first GxEPD2 buffer with DISPLAY_ROTATION=1
  (phys_x = 127-ly, phys_y = lx); dispatch on display_type.

- variants/wio-tracker-l1-eink/platformio.ini: add
  [env:WioTrackerL1Eink_companion_dual_dev] with ENABLE_SCREENSHOT.
- variants/wio-tracker-l1/platformio.ini: unchanged (OLED env already exists).

Builds verified: WioTrackerL1Eink_companion_dual_dev SUCCESS,
                 WioTrackerL1Eink_companion_radio_ble SUCCESS (unaffected).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 01:03:47 +02:00
vanous
7fc8b4b5a5 Screenshot tool and a build flag for conditional screenshot functionality:
- 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
2026-05-27 22:08:28 +02:00
Jakub
2e3d6381a4 fix(ui): e-ink block char position in default-font path
In GxEPDDisplay::print(), the 0xDB fallback block was drawn at
cy - 8*scale, which assumed the Lemon baseline convention
(cy = original_y + 8*scale). For the default GFX font fontAscender
returns 0, so cy = original_y (top of cell) — the block landed
16 px above the character on e-ink (scale=2).

Fix: draw at cy (top of cell) for the non-Lemon path.

Also remove the unused forceNextRefreshFull() virtual from
DisplayDriver — it was added based on a wrong hypothesis (partial
refresh ghosting) and was never wired into endFrame().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 17:33:31 +02:00
Jakub
559fe86f20 style(ui): use NSDMI for SH1106Display::_use_lemon default
Match the rest of the file's defaults; constructor body no longer
re-initialises a field that the in-class initializer already set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:33:59 +02:00
Jakub
d981f93e6e refactor(ui): drawSelectionRow helper for the 5-line invert/restore pattern
Eight screens repeated the same if/else block to invert the row
background for selected items. Add DisplayDriver::drawSelectionRow that
sets LIGHT, optionally fills the rect, and leaves the colour as DARK
when sel (so the caller's text renders inverted). 61 lines removed
across SettingsScreen, KeyboardWidget (cells + special row), ToolsScreen,
DashboardConfigScreen, BotScreen, RingtoneEditorScreen (note slots +
menu), NearbyScreen, QuickMsgScreen (4 list views).

Card-style rows (QuickMsg hist, NearbyScreen discover, RingtoneEditor
notes display) keep their original drawRect/partial-fill outline for
unselected — they don't match the simple-invert pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:33:02 +02:00
Jakub
52a7081a13 refactor(ui): dedupe UTF-8 codepoint decoder
Three copies of the same routine: DisplayDriver::decodeCodepoint (added
in the FullscreenMsgView wrap fix) plus per-driver duplicates in
SH1106Display and GxEPDDisplay. Consolidate on DisplayDriver and have
translateUTF8Static also route through it instead of its own inline
decoder. Net: 63 lines removed, single source of truth.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:25:35 +02:00
Jakub
74764aed2b perf(ui): O(n) FullscreenMsgView wrap via getCodepointWidth virtual
The wrap loop used to memcpy a growing substring and call getTextWidth
on it for every appended codepoint — O(n²) UTF-8 decoding with Lemon
font, ~5-20 ms on a full e-ink redraw of a long message.

Add DisplayDriver::getCodepointWidth(cp) (default: build a UTF-8 buffer
and forward to getTextWidth) plus DisplayDriver::decodeCodepoint() as
the shared UTF-8 walker. Override getCodepointWidth in GxEPDDisplay
(direct lemonXAdvance for Lemon size 1) and SH1106Display (lemonXAdvance
for Lemon, fixed 6*sz for built-in font). FullscreenMsgView now walks
codepoints once and sums widths incrementally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:22:16 +02:00
Jakub
f82d23b200 perf(ui): replace transliterateCodepoint switch with sorted-table binary search
Old 80+ case switch generated a jump table around ~600-1200 B of flash;
new layout is two parallel static-const arrays (uint16 codepoint + char
ascii, 144 entries each = 432 B) and a 5-line bsearch loop. Same input
range, same output, faster lookup (~log2(144) ≈ 7 compares vs jump-table
indirection + bounds check).

Adds a static_assert that the two arrays stay in sync.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:14:01 +02:00
Jakub
6410a346f2 refactor(eink): centralise scale() helper, hoist out of Lemon hot loop
Per-char recomputation of (width() >= height() ? 2 : 1) in drawLemonChar
and lemonXAdvance is now passed in by the caller. print() and
getTextWidth() compute scale() once and feed it to every glyph. All
other callers in the file route through the helper for consistency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:01:46 +02:00
Jakub
bf724ce692 perf(bot): lower body once and strstr instead of O(n*m) per-offset tolower
For a 140-byte channel msg with a 64-byte trigger, the old loop did
~8960 tolower() calls per inbound message; now it's 140 tolower calls
plus one strstr.

perf(buzzer): skip nRF52 STOPPED wait when PWM was not running

TASKS_STOP on a disabled PWM never fires EVENTS_STOPPED, so the wait
always timed out at 2 ms. Gating on _pwm_on removes the wait on the
first note of a melody and after pauses, where there's nothing to wait
for.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 19:59:21 +02:00
Jakub
10b2629b36 fix(eink): gate Lemon print path on _text_sz == 1
setTextSize(2/3) switches GFX to non-Lemon fonts (built-in scaled or
FreeSans18pt). The Lemon path bypassed GFX entirely and rendered glyphs
at size 1 scale, shrinking splash version and clock when Lemon font
was active.

Refresh CODE_REVIEW.md: remove resolved issues (signed-int timing vars,
abs() no-op, XBM CRC, loadPrefs nesting, lock-screen LPP thrash, reply
prefix raw UTF-8), add new findings (upstream-merge surface, virtual
width()/height() per glyph, page_order sentinel), list enhancement
ideas.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 19:45:19 +02:00
Jakub
7dcb763d83 fix(eink): align unknown-glyph fallback box to top of cell
The placeholder rectangle for codepoints outside U+0020-U+04FF was drawn
1 scale unit too low (y-7*sc instead of y-8*sc relative to the GFX
baseline), causing it to sit below full-height glyphs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 16:55:50 +02:00
Jakub
9c03e9a2db fix(eink): render Lemon font UTF-8 correctly by bypassing GFX write()
Adafruit GFX write(uint8_t) treats each byte as a glyph index and cannot
decode multi-byte UTF-8 sequences, so Cyrillic and other non-ASCII chars
were rendered as garbage even with the Lemon font selected.

Add decodeUtf8 + drawLemonChar + lemonXAdvance to GxEPDDisplay, mirroring
the existing SH1106Display implementation. Override print() for Lemon mode
to decode UTF-8 codepoints and draw each glyph directly; override
getTextWidth() to sum per-codepoint advances.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 16:39:41 +02:00
Jakub
c375c4b217 fix(eink): pass UTF-8 through unchanged when Lemon font is active
drawTextCentered/Left/Right/Ellipsized all call translateUTF8ToBlocks
before passing text to print(), which converted Cyrillic (and other
Unicode) to block characters even when the Lemon font was selected.

Override translateUTF8ToBlocks in GxEPDDisplay to skip conversion when
_use_lemon is true — the Lemon/GFX font handles UTF-8 natively.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 14:47:20 +02:00
Jakub
a6bf3ee08f feat(eink): full refresh interval setting in Settings › Display
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>
2026-05-24 10:22:03 +02:00
Jakub
b279b95b2d refactor(ui): apply code review fixes — bugs, risk, optimisations, cleanup
Fixes 7 bugs (timing UB on unsigned long, abs() no-op, XBM CRC PROGMEM
scan, CayenneLPP heap thrash ×2, UTF-8 reply prefix, lock-seq reset on
wake), 3 risk items (notif melody stop guard, bot trigger pre-lowercase,
lock-seq cleared on display wake), plus optimisations (fmtMsgAge/
getTextWidth caching, RTTTL builder consolidated to NodePrefs, HP_*
constants and homePageLabel table moved to NodePrefs, DataStore flat
lambda schema, translateUTF8Static factored out) and minor cleanup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:45 +02:00
Jakub
fea1be8e81 feat(ui): runtime joystick rotation independent of display rotation
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>
2026-05-23 15:20:40 +02:00
Jakub
85cfb6f1af fix(ui): unified unknown-char block rendering and display layout fixes
- 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>
2026-05-23 14:45:24 +02:00
Jakub
9c671891d9 fix: e-ink portrait/landscape layout — separators, battery, keyboard, screens
- DisplayDriver: add sepH() (2px landscape, 1px OLED); fix transliterateCodepoint
  fallback '?' (was '\xDB' which broke UTF-8 word-wrap and rendered tiny block char)
- GxEPDDisplay: drawRect draws double border on landscape for visible 2px weight
- UITask: navigation dots scale with lh; content_y saves 16px by using dots_y+6;
  battery iconW=lh*2, fill margin bm scales with orientation; indicator spacing fixed;
  clock separator uses sepH(); alert popup tight and vertically centred
- KeyboardWidget: compact cell height (no stretch), multi-line preview, cursor always
  on last visible line
- BotScreen / NearbyScreen detail: use lineStep() as natural item height, shrink only
  when items don't fit (fixes portrait stretch)
- QuickMsgScreen: send button pinned to display bottom (height-lh-2), not floating
- SettingsScreen: AUTO_OFF guarded with #if AUTO_OFF_MILLIS>0; default sel=AUTO_LOCK
- All screens: separators updated to sepH()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 23:13:27 +02:00
Jakub
118f29b0a1 fix: landscape e-ink layout corrections — clock overlap, keyboard overflow, bar sizing, nearby detail
- GxEPDDisplay: fontAscender only applies to sz=1 with Lemon; sz=2 (NULL font)
  was incorrectly getting a 16px cursor offset, causing clock text to render
  16px lower than expected and overlap the date line
- KeyboardWidget: compute cell_h from available screen height instead of lh+1;
  tighten sep_y to lh so the grid fits within 122px even with Lemon font (lh=20)
- SettingsScreen renderBar: constrain box size to available width so the 5th
  buzzer-volume square doesn't overflow past the right edge with Lemon font
- NearbyScreen discover detail: manually truncate b64 public key by charWidth
  to guarantee one-line rendering; use dynamic step = (height-hdr)/5 so
  Status line doesn't fall off the bottom of the screen
- NearbyScreen contacts detail: merge dist+az into one line and use dynamic
  step to fit 5 lines within the display; removes the off-screen Seen: row
- AutoAdvertScreen: replace lineStep() gap before hints with 4px fixed gap
  so both hint lines fit within the display with large fonts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 19:11:25 +02:00
Jakub
1d8ca25469 fix: adaptive layout for e-ink landscape across all remaining screens
- GxEPDDisplay: sz2 uses 4× scale on landscape (clock 32px vs body 16px)
- UITask: clock date_y and splash date_y computed from lh2 after setTextSize(2)
- UITask: battery/mute/BT/advert indicators scale with lh; muted uses text 'M'
- UITask: hibernate hint split into two lines to prevent wrapping
- ToolsScreen: full refactor — headerH, listStart, lineStep, getCharWidth
- KeyboardWidget: all cell sizes computed from display metrics (cell_w, cell_h, spec_w)
- RingtoneEditorScreen: cell_w = charWidth×2+6 (was hardcoded 18)
- SettingsScreen: BRIGHTNESS hidden on e-ink; renderBar boxes scale with lh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:34:20 +02:00
Jakub
744f263932 refactor: replace all hardcoded pixel positions with layout helpers
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>
2026-05-22 18:05:09 +02:00
Jakub
18867a85c0 feat(eink): scale size-1 font with display orientation
Portrait mode (122px wide) matches the OLED's ~21 chars/line with a 1× font.
Landscape mode (250px wide) was using the same small 6×8 font, leaving text
physically tiny and the wide display underutilised.

Size 1 now scales by orientation: 1× (6×8 / Lemon 5×10) in portrait,
2× (12×16 / Lemon 10×20) in landscape. Size 2 stays at 12×16 and size 3
at FreeSans18pt in both orientations — their layout Y-positions are hardcoded
so they cannot be scaled without a full layout refactor.

fontAscender() updated to accept a scale parameter; Lemon ascender becomes
8×scale (8px portrait, 16px landscape) to keep top-of-cell coordinate
semantics consistent with the rest of the UI code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 17:26:03 +02:00
Jakub
4c37986737 fix(eink): fix font layout and add Lemon font support to GxEPDDisplay
Layout fix: GxEPDDisplay was using proportional GFX fonts (FreeSans9pt/12pt)
for sizes 1 and 2, giving lineHeight=16/20 instead of the 8/16 the UI layout
expects (designed for the OLED's bitmap font). Replaced with the GFX built-in
6×8 bitmap font (size 1) and its 2× scaled variant 12×16 (size 2). Size 3
keeps FreeSans18pt for large headings. fontAscender updated accordingly:
built-in font cursor is top-left (offset=0), Lemon GFX font adds 8px,
only FreeSans18pt still adds 26px.

Lemon font: LemonFont.h already stores the font in GFXfont format, so
GxEPDDisplay can use it directly via display.setFont(&Lemon). Added
_use_lemon flag, setLemonFont() override, and size-1 font selection in
setTextSize()/startFrame(). Adafruit GFX handles UTF-8 decoding for GFX
fonts, so Unicode characters (Cyrillic etc.) render correctly. The FONT
setting in SettingsScreen now works on e-ink the same as on OLED.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 17:20:20 +02:00
Jakub
6a3b8e3135 feat: add runtime display rotation setting for e-ink builds
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>
2026-05-22 09:58:33 +02:00
Jakub
cffe960f3f feat: rework GxEPDDisplay for rotation support and integrate e-ink build
- Remove fixed 128×128 virtual space and scaling; use 1:1 pixel mapping
- Display dimensions derived from panel native size + DISPLAY_ROTATION
- setCursor compensates for GFX font baseline offset (ascender per font size)
- Add getLineHeight() / getCharWidth() overrides for FreeSans fonts
- New envs: landscape (250×122) and portrait (122×250), BLE and dual variants
- Integrate all wio-tracker-l1-improvements features into e-ink build

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 09:53:55 +02:00
Liam Cottle
a35b1bbbfd Merge pull request #2590 from jocasmark/fix/typos
Fix typos in comments and docs
2026-05-22 19:38:19 +12:00
Jakub
fa842b202a fix: scale Lemon font glyphs with setTextSize on lock screen clock
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 09:34:24 +02:00
Jakub
a115916d8f feat: runtime font switcher — Default vs Lemon in Settings > Display
- 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>
2026-05-22 09:33:35 +02:00
taco
5058415fa3 u8g2 wrapper: allow larger font when size >= 2 2026-05-21 19:58:10 +10:00
pelgraine
2462cf6c97 add wrapper for u8g2 display driver 2026-05-21 19:58:10 +10:00
Jakub
6399df1f4a fix: improve buzzer volume control on nRF52
- 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>
2026-05-20 09:06:00 +02:00
Mark Jocas
63fe3d42a2 Corrects typos and grammatical errors
Improves readability across documentation and inline comments by fixing common spelling mistakes and duplicate words.
2026-05-20 08:33:48 +02:00
Jakub
2bc6dae78e feat: add Icelandic and Romanian legacy cedilla transliteration
Adds ð/Ð (eth), þ/Þ (thorn) for Icelandic, and ţ/Ţ (t with cedilla)
for legacy-encoded Romanian text.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 12:11:42 +02:00
Jakub
b0552e246a feat: extend UTF-8 transliteration to cover all major European languages
Adds Scandinavian (å ø æ), Hungarian (ő ű), Romanian (ă ș ț),
Croatian (đ), Turkish (ğ ş ı), and Baltic/Lithuanian/Latvian
(ā ē ī ū ģ ķ ļ ņ ŗ ų ė į) diacritics.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 12:08:20 +02:00
Jakub
6492fd490e feat: add Czech and Slovak diacritic transliteration
Adds háček and other Czech/Slovak characters (č š ž ř ě ů ď ť ň ľ ĺ ŕ)
to the UTF-8 transliteration table so they display as ASCII letters
instead of █ blocks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 12:01:51 +02:00
Jakub
31962726ec fix: apply UTF-8 transliteration in drawTextEllipsized
Names printed via drawTextEllipsized (contacts, senders) bypassed
translateUTF8ToBlocks, causing raw UTF-8 bytes to render as garbage
on the Adafruit font. Now transliteration happens inside the method.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 15:11:12 +02:00
Jakub
f6b13c5cab fix: use block char █ fallback for unknown codepoints in transliteration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 15:05:53 +02:00
Jakub
8d3b955da1 feat: transliterate accented/diacritic characters to ASCII in translateUTF8ToBlocks
Replaces the block-character fallback with proper transliteration for
Polish (ą→a, ć→c, ę→e, ł→l, ń→n, ó→o, ś→s, ź→z, ż→z), German (ä ö ü ß),
and common French/Spanish/Portuguese accents. Unknown codepoints fall back
to '?' instead of █.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 14:46:53 +02:00
Jakub
bf05a13549 fix: implement nRF52 RTTTL player using direct NRF_PWM2 control for working volume
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>
2026-05-15 13:09:38 +02:00