From cc4774436fa081eeb0facce3ca70a21688351dc0 Mon Sep 17 00:00:00 2001 From: pmow Date: Fri, 9 Oct 2026 08:47:35 -1000 Subject: [PATCH 1/6] feat(solo): adds support for Good Display GDEQ0426 eink 4.26 inch New solo env WioTrackerL1_GDEQ0426T82_companion_solo_dual: the Wio Tracker L1 (Lite) with a Good Display GDEQ0426T82 (SSD1677) on the board's 24-pin e-ink FPC. Build config only. Extends WioTrackerL1Eink with its own full build_flags list so the stock panel's model and rotation never reach the compiler. RAM: the 800x480 framebuffer is 48 KB, so OFFLINE_QUEUE_SIZE is 32 and MAX_CONTACTS 250 -- with the stock limits UiCore's heap allocations fail at boot and it loops. Tested on hardware: Wio Tracker L1 Lite + GDEQ0426T82. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KxAZVAbhhYr18Dd4guTQM1 --- solo/wio-tracker-l1-gdeq0426/platformio.ini | 60 +++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 solo/wio-tracker-l1-gdeq0426/platformio.ini diff --git a/solo/wio-tracker-l1-gdeq0426/platformio.ini b/solo/wio-tracker-l1-gdeq0426/platformio.ini new file mode 100644 index 00000000..3691e66f --- /dev/null +++ b/solo/wio-tracker-l1-gdeq0426/platformio.ini @@ -0,0 +1,60 @@ +; Solo build for Seeed Wio Tracker L1 (Lite) + Good Display GDEQ0426T82 +; 4.26" 800x480 e-ink, plugged into the board's 24-pin e-ink FPC connector. +; Build config only -- no source changes. Extends WioTrackerL1Eink +; (variants/wio-tracker-l1-eink/platformio.ini) for its source files and +; libraries, but carries its own full build_flags list, so the stock panel's +; -D EINK_DISPLAY_MODEL / DISPLAY_ROTATION never reach the compiler too. +; +; - EINK_DISPLAY_MODEL GxEPD2 driver for this panel (GxEPD2 1.6.2+) +; - DISPLAY_ROTATION 0: the panel's native landscape +; - RAM: the 800x480 framebuffer is 48 KB (the stock 2.13" needs ~4 KB), +; and UiCore plus every screen is allocated from the heap at startup -- +; a failed allocation there crashes into a boot loop. So: +; OFFLINE_QUEUE_SIZE 32 (stock e-ink: 128) frees ~17 KB +; MAX_CONTACTS 250 (stock: 350) frees ~18 KB +; +; Build: pio run -e WioTrackerL1_GDEQ0426T82_companion_solo_dual +[env:WioTrackerL1_GDEQ0426T82_companion_solo_dual] +extends = WioTrackerL1Eink +build_unflags = -Ofast +build_flags = ${nrf52_base.build_flags} + ${sensor_base.build_flags} + -I lib/nrf52/s140_nrf52_7.3.0_API/include + -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52 + -I variants/wio-tracker-l1 + -I examples/companion_radio/ui-new + -I lib/GxEPD2-patch/src + -D WIO_TRACKER_L1 + -D WIO_TRACKER_L1_EINK + -D USE_SX1262 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D EINK_DISPLAY_MODEL=GxEPD2_426_GDEQ0426T82 + -D DISPLAY_ROTATION=0 + -D JOYSTICK_UPRIGHT_ROTATION=0 + -D GPS_BAUD_RATE=9600 + -D ENV_PIN_SDA=PIN_WIRE1_SDA + -D ENV_PIN_SCL=PIN_WIRE1_SCL + -D DISABLE_DIAGNOSTIC_OUTPUT + -D AUTO_OFF_MILLIS=0 + -D MAX_CONTACTS=250 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=32 + -D DISPLAY_CLASS=GxEPDDisplay + -D BUTTON_USE_INTERRUPTS=1 + -D UI_HAS_JOYSTICK=1 + -D UI_HAS_JOYSTICK_UPDOWN=1 + -D PIN_BUZZER=12 + -D QSPIFLASH=1 + -D FIRMWARE_SOLO_BUILD=1 + -D MESHCORE_VERSION='"1.17.1"' + -D UI_SENSORS_PAGE=1 + -D BLE_PIN_CODE=123456 + -D DUAL_SERIAL=1 + -D ENABLE_SCREENSHOT + -D KEEP_DISPLAY_ON_USB + -Os +extra_scripts = post:create-uf2.py From da5ff56cff50a45597607ebaedb21cb5ce6564c5 Mon Sep 17 00:00:00 2001 From: pmow Date: Fri, 9 Oct 2026 08:47:35 -1000 Subject: [PATCH 2/6] feat(eink): bulk SPI row writes for the GDEQ0426T82, at 8 MHz GxEPD2 sends the panel RAM a byte at a time; on the nRF52's SPIM the per-call overhead dominates, and an 800x480 partial refresh writes the whole 48 KB several times. GxEPD2_426_GDEQ0426T82_Fast overrides the three image writes to send each row in one transfer() call, falling back to the stock path for the cases it doesn't cover (first write, mirror_y, PROGMEM). Byte-for-byte the same data as the stock driver. Opt-in per env: -D EINK_FAST_WRITES=1, and EINK_SPI_HZ (default 4 MHz, 8 MHz here, SPIM2's ceiling). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KxAZVAbhhYr18Dd4guTQM1 --- solo/wio-tracker-l1-gdeq0426/platformio.ini | 2 + src/helpers/ui/GxEPD2_FastWrite.h | 108 ++++++++++++++++++++ src/helpers/ui/GxEPDDisplay.cpp | 10 +- src/helpers/ui/GxEPDDisplay.h | 16 ++- 4 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 src/helpers/ui/GxEPD2_FastWrite.h diff --git a/solo/wio-tracker-l1-gdeq0426/platformio.ini b/solo/wio-tracker-l1-gdeq0426/platformio.ini index 3691e66f..7f90a27d 100644 --- a/solo/wio-tracker-l1-gdeq0426/platformio.ini +++ b/solo/wio-tracker-l1-gdeq0426/platformio.ini @@ -56,5 +56,7 @@ build_flags = ${nrf52_base.build_flags} -D DUAL_SERIAL=1 -D ENABLE_SCREENSHOT -D KEEP_DISPLAY_ON_USB + -D EINK_FAST_WRITES=1 + -D EINK_SPI_HZ=8000000 -Os extra_scripts = post:create-uf2.py diff --git a/src/helpers/ui/GxEPD2_FastWrite.h b/src/helpers/ui/GxEPD2_FastWrite.h new file mode 100644 index 00000000..a4bdf703 --- /dev/null +++ b/src/helpers/ui/GxEPD2_FastWrite.h @@ -0,0 +1,108 @@ +#pragma once +// Faster image writes for large GxEPD2 panels. +// +// GxEPD2 sends the framebuffer to the panel one byte at a time: every byte is +// its own SPI.transfer(uint8_t) call, and on nRF52 each of those is a complete +// EasyDMA transaction with several microseconds of fixed setup overhead -- +// independent of the SPI clock. A partial refresh here writes the full screen +// five times (GxEPDDisplay::endFrame's inverse-redrive pass, then +// GxEPD2_BW::display()), so on an 800x480 panel (48 KB a screen) that overhead +// alone blocks the main loop for well over a second per refresh. +// +// These subclasses override only the image writes, sending each row as one +// bulk SPI.transfer(buf, nullptr, n) -- the same bytes, in the same order, to +// the same RAM window -- so the cost becomes just the wire time. Everything +// else (init, refresh, power) is the stock driver. The first write (panel init +// and the initial screen clear) and the rarely used mirror/PGM variants go +// through the stock path unchanged. +// +// Use by naming the subclass as the panel: -D EINK_DISPLAY_MODEL=GxEPD2_426_GDEQ0426T82_Fast + +#include + +#ifdef _GxEPD2_426_GDEQ0426T82_H_ +class GxEPD2_426_GDEQ0426T82_Fast : public GxEPD2_426_GDEQ0426T82 { + typedef GxEPD2_426_GDEQ0426T82 Base; + uint8_t _row[WIDTH / 8]; // one panel row, in RAM (EasyDMA can't read flash) + + bool canFast(bool mirror_y, bool pgm) const { + return _init_display_done && !_initial_write && !mirror_y && !pgm; + } + + // Same RAM window as the stock driver's (private) _setPartialRamArea(): + // the gates are reversed on this panel, so y is mirrored and entered decreasing. + void setRamArea(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { + y = HEIGHT - y - h; + _writeCommand(0x11); + _writeData(0x01); + _writeCommand(0x44); + _writeData(x % 256); + _writeData(x / 256); + _writeData((x + w - 1) % 256); + _writeData((x + w - 1) / 256); + _writeCommand(0x45); + _writeData((y + h - 1) % 256); + _writeData((y + h - 1) / 256); + _writeData(y % 256); + _writeData(y / 256); + _writeCommand(0x4e); + _writeData(x % 256); + _writeData(x / 256); + _writeCommand(0x4f); + _writeData((y + h - 1) % 256); + _writeData((y + h - 1) / 256); + } + + // The stock _writeImage(), with the per-byte loop replaced by one bulk + // transfer per row (clipping and indexing unchanged). + void fastWrite(uint8_t command, const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert) { + delay(1); // as the stock driver: yield + int16_t wb = (w + 7) / 8; + x -= x % 8; + w = wb * 8; + int16_t x1 = x < 0 ? 0 : x; + int16_t y1 = y < 0 ? 0 : y; + int16_t w1 = x + w < int16_t(WIDTH) ? w : int16_t(WIDTH) - x; + int16_t h1 = y + h < int16_t(HEIGHT) ? h : int16_t(HEIGHT) - y; + int16_t dx = x1 - x; + int16_t dy = y1 - y; + w1 -= dx; + h1 -= dy; + if ((w1 <= 0) || (h1 <= 0)) return; + setRamArea(x1, y1, w1, h1); + _writeCommand(command); + _startTransfer(); + const int16_t nb = w1 / 8; + for (int16_t i = 0; i < h1; i++) { + const uint8_t* src = &bitmap[dx / 8 + int32_t(i + dy) * wb]; + if (invert) { for (int16_t j = 0; j < nb; j++) _row[j] = ~src[j]; } + else { memcpy(_row, src, nb); } + _pSPIx->transfer(_row, nullptr, nb); + } + _endTransfer(); + delay(1); + } + +public: + GxEPD2_426_GDEQ0426T82_Fast(int16_t cs, int16_t dc, int16_t rst, int16_t busy) : Base(cs, dc, rst, busy) {} + using Base::writeImage; // keep the other overloads visible + + void writeImage(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, + bool invert = false, bool mirror_y = false, bool pgm = false) override { + if (!canFast(mirror_y, pgm)) { Base::writeImage(bitmap, x, y, w, h, invert, mirror_y, pgm); return; } + fastWrite(0x24, bitmap, x, y, w, h, invert); + } + void writeImageForFullRefresh(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, + bool invert = false, bool mirror_y = false, bool pgm = false) override { + if (!canFast(mirror_y, pgm)) { Base::writeImageForFullRefresh(bitmap, x, y, w, h, invert, mirror_y, pgm); return; } + fastWrite(0x26, bitmap, x, y, w, h, invert); // set previous + fastWrite(0x24, bitmap, x, y, w, h, invert); // set current + } + void writeImageAgain(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, + bool invert = false, bool mirror_y = false, bool pgm = false) override { + if (!canFast(mirror_y, pgm)) { Base::writeImageAgain(bitmap, x, y, w, h, invert, mirror_y, pgm); return; } + fastWrite(0x26, bitmap, x, y, w, h, invert); // set previous + fastWrite(0x24, bitmap, x, y, w, h, invert); // set current + } +}; +#endif diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 7548a69e..510dfd84 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -14,8 +14,16 @@ SPIClass SPI1 = SPIClass(FSPI); #endif +// SPI clock for the panel. 4 MHz is the conservative default every board has +// always used; a short FPC run can go faster (SSD16xx-family controllers are +// rated ~20 MHz for writes). On nRF52840 the Adafruit core puts SPI1 on SPIM2, +// which caps at 8 MHz -- higher requests are silently clamped to 8 MHz there. +#ifndef EINK_SPI_HZ + #define EINK_SPI_HZ 4000000 +#endif + bool GxEPDDisplay::begin() { - display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0)); + display.epd2.selectSPI(SPI1, SPISettings(EINK_SPI_HZ, MSBFIRST, SPI_MODE0)); #ifdef ESP32 SPI1.begin(PIN_DISPLAY_SCLK, PIN_DISPLAY_MISO, PIN_DISPLAY_MOSI, PIN_DISPLAY_CS); #else diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index ffb5e96b..89f58610 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -18,6 +18,18 @@ #include #include "EinkGfxDisplay.h" +#include "GxEPD2_FastWrite.h" + +// EINK_FAST_WRITES=1: drive the panel through its _Fast subclass +// (GxEPD2_FastWrite.h) -- the same bytes, sent a row at a time in bulk instead +// of one SPI call per byte. Only for panels that have one. +#define EINK_CAT_(a, b) a##b +#define EINK_CAT(a, b) EINK_CAT_(a, b) +#if defined(EINK_DISPLAY_MODEL) && defined(EINK_FAST_WRITES) && EINK_FAST_WRITES + #define EINK_PANEL_TYPE EINK_CAT(EINK_DISPLAY_MODEL, _Fast) +#elif defined(EINK_DISPLAY_MODEL) + #define EINK_PANEL_TYPE EINK_DISPLAY_MODEL +#endif // This driver calls callBusyPump() during its BUSY-pin waits; app code keys // its setBusyPumpFn() wiring off this so it isn't compiled for displays (or @@ -53,8 +65,8 @@ // The panel object, as a base so it's built before EinkGfxDisplay draws on it. struct GxEPDPanel { #if defined(EINK_DISPLAY_MODEL) - GxEPD2_BW display; - GxEPDPanel() : display(EINK_DISPLAY_MODEL(PIN_DISPLAY_CS, PIN_DISPLAY_DC, PIN_DISPLAY_RST, PIN_DISPLAY_BUSY)) {} + GxEPD2_BW display; + GxEPDPanel() : display(EINK_PANEL_TYPE(PIN_DISPLAY_CS, PIN_DISPLAY_DC, PIN_DISPLAY_RST, PIN_DISPLAY_BUSY)) {} #else GxEPD2_BW display; GxEPDPanel() : display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)) {} From 2c1e1fa9302385018dc1c899cb35333a84958249 Mon Sep 17 00:00:00 2001 From: pmow Date: Fri, 9 Oct 2026 08:48:02 -1000 Subject: [PATCH 3/6] feat(eink): Text size and Icon size settings for big panels On an 800x480 panel the doubled 6x9 font and the status bar's icons are tiny. Two opt-in build flags, each also a setting under Settings > Display: - EINK_FONT_SCALE=: the landscape font's pixel multiplier (stock 2). Settings > Display > Text size (1x-5x) overrides it at run time (DisplayDriver::setUiScale -> EinkGfxDisplay re-lays out). - STATUS_ICON_SCALE=: Home's status icons, battery and page-icon row drawn at n x their pixel grid, the bar growing to fit. 0 = Auto, 1.5x the text's pixel scale. Settings > Display > Icon size (Auto, 1x-8x). Both live in their own /ui_scale file, so NodePrefs is untouched. Icon size appears only where STATUS_ICON_SCALE is set; Text size appears on every e-ink build with the full-refresh setting, and builds that set neither flag draw as before until it is changed. The GDEQ0426T82 env defaults to 4x text with Auto icons. Tested in the native sim at 800x480. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KxAZVAbhhYr18Dd4guTQM1 --- .../companion_radio/ui-new/SettingsScreen.h | 31 +++++++ examples/companion_radio/ui-new/UITask.cpp | 91 +++++++++++++++---- examples/companion_radio/ui-new/UITask.h | 9 ++ examples/companion_radio/ui-new/icons.h | 49 ++++++++++ solo/wio-tracker-l1-gdeq0426/platformio.ini | 8 ++ src/helpers/ui/DisplayDriver.h | 1 + src/helpers/ui/EinkGfxDisplay.h | 4 + src/helpers/ui/EinkGfxDisplayImpl.h | 11 ++- 8 files changed, 186 insertions(+), 18 deletions(-) diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 90c0b3bc..ea79579a 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -26,6 +26,10 @@ class SettingsScreen : public UIScreen { #endif #if FEAT_FULL_REFRESH_SETTING EINK_FULL_REFRESH, + UI_TEXT_SCALE, // Text size: 1x..5x the 6x9 font +#if STATUS_ICONS_SCALED + UI_ICON_SCALE, // Icon size: Auto, 1x..8x +#endif #endif // Sound section SECTION_SOUND, @@ -526,6 +530,18 @@ class SettingsScreen : public UIScreen { uint8_t idx = p ? p->eink_full_refresh_every : 0; if (idx >= EINK_FULL_REFRESH_COUNT) idx = 0; val("Full rfsh", EINK_FULL_REFRESH_LABELS[idx]); + } else if (item == UI_TEXT_SCALE) { + char b[4]; + snprintf(b, sizeof(b), "%ux", (unsigned)_task->getUiFontScale()); + val("Text size", b); +#if STATUS_ICONS_SCALED + } else if (item == UI_ICON_SCALE) { + char b[12]; + const uint8_t v = _task->getStatusIconScale(); + if (v == 0) snprintf(b, sizeof(b), "Auto"); + else snprintf(b, sizeof(b), "%ux", (unsigned)v); + val("Icon size", b); +#endif #endif } else if (item == DM_FILTER) { val("DMs", (p && p->dm_show_all) ? "All" : "Fav"); @@ -1036,6 +1052,21 @@ public: _dirty = true; return true; } + if (_selected == UI_TEXT_SCALE && (left || right || enter)) { + int v = _task->getUiFontScale(); // 1..5, wrapping + v = left ? (v <= 1 ? 5 : v - 1) : (v >= 5 ? 1 : v + 1); + _task->setUiFontScale(v); // saved to /ui_scale immediately + return true; + } +#if STATUS_ICONS_SCALED + if (_selected == UI_ICON_SCALE && (left || right || enter)) { + const int n = STATUS_ICON_SCALE_MAX + 1; // Auto, 1x..8x + int v = _task->getStatusIconScale(); + v = (v + (left ? n - 1 : 1)) % n; + _task->setStatusIconScale(v); // saved to /ui_scale immediately + return true; + } +#endif #endif if (_selected == DM_FILTER && p && (left || right || enter)) { p->dm_show_all = p->dm_show_all ? 0 : 1; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 5a923257..01ee711a 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -337,8 +337,8 @@ class HomeScreen : public UIScreen { // First row under the header and the page-icon row (see render()). static int contentTop(DisplayDriver& d) { - const int pg_half = (PAGE_ICON_PX * miniIconScale(d) + 1) / 2; - return d.getLineHeight() + 2 * pg_half + 4; + const int pg_half = (PAGE_ICON_PX * statusIconScale(d) + 1) / 2; + return statusBarH(d) + 2 * pg_half + 4; } // 0..1 eased progress of the running slide, or -1 when none. float slideProgress() { @@ -724,8 +724,11 @@ class HomeScreen : public UIScreen { const int lh = display.getLineHeight(); const int cw = display.getCharWidth(); - const int ind = cw + 2; // single-char indicator width - const int ind_h = display.isSingleFont() ? lh - 2 : lh; + const int sis = statusIconScale(display); // STATUS_ICON_SCALE, or the font's + const int bar_h = statusBarH(display); + const int ind = STATUS_ICONS_SCALED ? 7 * sis + 2 : cw + 2; // indicator slot width + const int ind_h = (bar_h > lh) ? bar_h - 2 : (display.isSingleFont() ? lh - 2 : lh); + const int text_y = (bar_h > lh) ? (bar_h - lh) / 2 : 0; // % / V text centred in a tall bar const int ind_gap = display.pixelScale() > 1 ? 3 : 1; // gap between indicator boxes int battLeftX; @@ -733,17 +736,17 @@ class HomeScreen : public UIScreen { char buf[6]; snprintf(buf, sizeof(buf),"%d%%", pct); battLeftX = display.width() - display.getTextWidth(buf) - 1; - display.setCursor(battLeftX, 0); + display.setCursor(battLeftX, text_y); display.print(buf); } else if (mode == battery::VOLTAGE) { char buf[8]; snprintf(buf, sizeof(buf),"%u.%02uV", batteryMilliVolts / 1000, (batteryMilliVolts % 1000) / 10); battLeftX = display.width() - display.getTextWidth(buf) - 1; - display.setCursor(battLeftX, 0); + display.setCursor(battLeftX, text_y); display.print(buf); } else { // icon — scales with lh, same box height as the status icons beside it (ind_h) const int iconH = ind_h; - const int iconW = lh * 2; + const int iconW = (bar_h > lh) ? ind_h * 2 : lh * 2; const int bm = display.pixelScale() > 1 ? 3 : 2; // inner margin: 3px on a doubled panel, else 2px battLeftX = display.width() - iconW - 3; // The outline with its corner pixels left out, softly rounded. @@ -809,7 +812,8 @@ class HomeScreen : public UIScreen { if (!s.active) continue; int ix = x - ind - ind_gap; if (ix < name_min) break; // out of room — drop this + all lower priority - drawSlotIcon(display, ix, ind, ind_h, *s.icon); + if (STATUS_ICONS_SCALED) drawSlotIconScaled(display, ix, ind, ind_h, *s.icon, sis); + else drawSlotIcon(display, ix, ind, ind_h, *s.icon); x = ix; } return x; @@ -1240,7 +1244,7 @@ public: const int W = d.width(), H = d.height(); d.setColor(DisplayDriver::LIGHT); d.setTextSize(1); - const int lh = d.getLineHeight(), top = lh + 3; + const int lh = d.getLineHeight(), top = statusBarH(d) + 3; const bool hint = Features::IS_EINK || _task->lockHintShown(); const int pill_y = H - lh - 3; // the bottom pill, clear of what's above const int unread = _task->getDMUnreadTotal() + _task->getChannelUnreadCount() + _task->getRoomUnreadCount(); @@ -1476,8 +1480,8 @@ public: // Page-indicator row: small page icons (PAGE_ICON_PX) replace the old dots. Centre and // gap scale with the font so the band clears the header above and content // below (identical to the old lh+4 / +6 dots layout at 1x). - const int pg_half = (PAGE_ICON_PX * miniIconScale(display) + 1) / 2; - const int dots_y = lh + pg_half + 1; // icon-row centre, below the header + const int pg_half = (PAGE_ICON_PX * statusIconScale(display) + 1) / 2; // page icons follow STATUS_ICON_SCALE + const int dots_y = statusBarH(display) + pg_half + 1; // icon-row centre, below the header const int content_y = contentTop(display); // first content row, below the icons const float slide = slideProgress(); // -1, or how far a page turn has got @@ -1492,6 +1496,7 @@ public: display.setColor(DisplayDriver::LIGHT); if (_page != LOCK) { + const int bar_ty = (statusBarH(display) - lh) / 2; // text centred in a tall bar (STATUS_ICON_SCALE) // The time, once the clock is set (the node name before that): the // name is what others see, the time is what you glance at here. char filtered_name[sizeof(_node_prefs->node_name)]; @@ -1517,10 +1522,10 @@ public: char pwr_buf[8]; snprintf(pwr_buf, sizeof(pwr_buf), "%ddB", (int)radio_driver.getTxPower()); int pwr_w = display.getTextWidth(pwr_buf); - display.drawTextEllipsized(0, 0, rightEdge - 2 - pwr_w - 2, filtered_name); - display.drawTextRightAlign(rightEdge - 2, 0, pwr_buf); + display.drawTextEllipsized(0, bar_ty, rightEdge - 2 - pwr_w - 2, filtered_name); + display.drawTextRightAlign(rightEdge - 2, bar_ty, pwr_buf); } else { - display.drawTextEllipsized(0, 0, rightEdge - 2, filtered_name); + display.drawTextEllipsized(0, bar_ty, rightEdge - 2, filtered_name); } } } @@ -1535,7 +1540,7 @@ public: int order[(int)Count]; int n = buildVisibleOrder(order); int curr_vis = 0; for (int i = 0; i < n; i++) if (order[i] == _page) { curr_vis = i; break; } - const int s = miniIconScale(display); + const int s = statusIconScale(display); // STATUS_ICON_SCALE, or the font's const int icon_w = PAGE_ICON_PX * s; int pitch = icon_w + 5 * s; // comfortable spacing if (n > 1) { // shrink to fit if many pages @@ -1560,7 +1565,7 @@ public: if (d2 <= pw - icon_w) display.setColor(DisplayDriver::DARK); // inside the pill else if (d2 < pw + icon_w) { x += pitch; continue; } // half under the gliding pill else display.setColor(DisplayDriver::LIGHT); - if (ic) miniIconDrawCentered(display, x, dots_y, *ic); + if (ic) miniIconDrawCenteredScaled(display, x, dots_y, *ic, s); x += pitch; } display.setColor(DisplayDriver::LIGHT); @@ -2042,6 +2047,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no applyBrightness(); applyRotation(); applyFullRefreshInterval(); + loadUiScale(); // Settings > Display > Text size / Icon size applyAllGpioModes(); // restore persisted pin modes to hardware before any UI/bot use // Locked above (a PIN, or the cover closed) before Home existed to be told. if (_locked) syncLockToHome(); @@ -4047,6 +4053,59 @@ void UITask::applyFullRefreshInterval() { } } +// Text size and Icon size (Settings > Display), in their own /ui_scale file -- +// [text pixel scale (0 = build default), icon scale (0 = Auto)] -- so the +// NodePrefs schema is untouched. +static const char* UI_SCALE_FILE = "/ui_scale"; +static const uint8_t UI_TEXT_SCALE_MAX = 5; + +void UITask::loadUiScale() { + uint8_t b[2] = { 0, statusIconScaleSetting() }; + DataStore* ds = the_mesh.getDataStore(); + if (ds) { + File f = ds->openRead(UI_SCALE_FILE); + if (f) { + f.read(b, 2); + f.close(); + } + } + _ui_font_scale = b[0] <= UI_TEXT_SCALE_MAX ? b[0] : 0; + if (b[1] <= STATUS_ICON_SCALE_MAX) statusIconScaleSetting() = b[1]; + if (_display != NULL && _ui_font_scale) _display->setUiScale(_ui_font_scale); +} + +void UITask::saveUiScale() { + DataStore* ds = the_mesh.getDataStore(); + if (!ds) return; + File f = ds->openWrite(UI_SCALE_FILE); + if (f) { + uint8_t b[2] = { _ui_font_scale, statusIconScaleSetting() }; + f.write(b, 2); + f.close(); + } +} + +uint8_t UITask::getUiFontScale() const { + return _display != NULL ? (uint8_t)_display->pixelScale() : 1; +} + +void UITask::setUiFontScale(uint8_t s) { + if (s < 1 || s > UI_TEXT_SCALE_MAX) s = 1; + _ui_font_scale = s; + if (_display != NULL) _display->setUiScale(s); + _next_refresh = 0; + saveUiScale(); +} + +uint8_t UITask::getStatusIconScale() const { return statusIconScaleSetting(); } + +void UITask::setStatusIconScale(uint8_t s) { + if (s > STATUS_ICON_SCALE_MAX) s = 0; + statusIconScaleSetting() = s; + _next_refresh = 0; // the frame differs, so it's redrawn + saveUiScale(); +} + void UITask::applySoundPrefs() { #ifdef PIN_BUZZER if (_node_prefs) setBuzzerVolumeLevel(_node_prefs->buzzer_volume); // with a sample at the new level diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 993ac5cd..608b00e7 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -62,6 +62,7 @@ class UITask : public UITaskBase, public UiCoreHost { unsigned long _next_refresh, _auto_off; bool _display_was_on = true; // loop(): messagesInView() last time round NodePrefs* _node_prefs; + uint8_t _ui_font_scale = 0; // Settings > Display > Text size (0 = build default) bool _locked; bool _tool_from_home = false; bool messagesInView() const; // Messages on a lit, unlocked display @@ -520,6 +521,14 @@ public: bool savePrefsIfDirty(bool& dirty); void applyRotation(); void applyFullRefreshInterval(); + // Settings > Display > Text size / Icon size. Kept in their own /ui_scale + // file rather than NodePrefs, so the prefs schema (and its size assert) is untouched. + uint8_t getUiFontScale() const; // the text's current pixel scale + void setUiFontScale(uint8_t s); // 1..5: applies + saves + uint8_t getStatusIconScale() const; // 0 = Auto (1.5x text), else n + void setStatusIconScale(uint8_t s); // 0..8: applies + saves + void loadUiScale(); // boot-time restore, from begin() + void saveUiScale(); uint32_t autoOffMillis() const { if (!_node_prefs || _node_prefs->auto_off_secs == 0) return 0; return (uint32_t)_node_prefs->auto_off_secs * 1000UL; diff --git a/examples/companion_radio/ui-new/icons.h b/examples/companion_radio/ui-new/icons.h index fd479f57..88509584 100644 --- a/examples/companion_radio/ui-new/icons.h +++ b/examples/companion_radio/ui-new/icons.h @@ -91,6 +91,55 @@ inline void miniIconDrawCentered(DisplayDriver& d, int cx, int cy, const MiniIco miniIconDrawTop(d, cx - (ic.w * s) / 2, cy - (ic.h * s) / 2, ic); } +// Home status bar icon scale. Defining STATUS_ICON_SCALE (build flag) turns on +// scalable status icons: the bar's icons, battery and the page-icon row below it +// are drawn at n x their pixel grid, and the bar grows tall enough to hold them -- +// for big panels whose 1-line bar leaves them tiny. Its value is the default for +// Settings > Display > Icon size: 0 = Auto (1.5x the text's pixel scale), else n. +// Unset: icons follow the font, as stock. +#ifdef STATUS_ICON_SCALE + #define STATUS_ICONS_SCALED 1 +#else + #define STATUS_ICONS_SCALED 0 + #define STATUS_ICON_SCALE 0 +#endif +static constexpr int STATUS_ICON_SCALE_MAX = 8; +static constexpr int STATUS_ICON_ROWS = MINI_ICONS_LARGE == 2 ? 9 : 7; // tallest status icon +// The Icon size setting (0 = Auto). One instance across translation units. +inline uint8_t& statusIconScaleSetting() { + static uint8_t v = (STATUS_ICON_SCALE >= 0 && STATUS_ICON_SCALE <= STATUS_ICON_SCALE_MAX) ? STATUS_ICON_SCALE : 0; + return v; +} +inline int statusIconScale(DisplayDriver& d) { + if (!STATUS_ICONS_SCALED) return miniIconScale(d); + int s = statusIconScaleSetting(); + if (s == 0) s = (3 * d.pixelScale() + 1) / 2; // Auto: 1.5x the text + return s < 1 ? 1 : s; +} +// Height of the home status bar: one text line, or the scaled icons if taller. +inline int statusBarH(DisplayDriver& d) { + const int lh = d.getLineHeight(); + const int ih = STATUS_ICON_ROWS * statusIconScale(d) + 4; + return (STATUS_ICONS_SCALED && ih > lh) ? ih : lh; +} +// miniIconDrawCentered at an explicit scale (the home page-icon row's). +inline void miniIconDrawCenteredScaled(DisplayDriver& d, int cx, int cy, const MiniIcon& ic, int s) { + const int x = cx - (ic.w * s) / 2, y = cy - (ic.h * s) / 2; + for (int r = 0; r < ic.h; r++) + for (int c = 0; c < ic.w; c++) + if (ic.rows[r] & (1 << c)) d.fillRect(x + c * s, y + r * s, s, s); +} +// drawSlotIcon at an explicit scale (the status bar's). +inline void drawSlotIconScaled(DisplayDriver& d, int x, int box_w, int box_h, const MiniIcon& ic, int s) { + int ix = x + (box_w - ic.w * s) / 2; + int iy = (box_h - ic.h * s) / 2; + if (ix < x) ix = x; + if (iy < 0) iy = 0; + for (int r = 0; r < ic.h; r++) + for (int c = 0; c < ic.w; c++) + if (ic.rows[r] & (1 << c)) d.fillRect(ix + c * s, iy + r * s, s, s); +} + // Centre a mini-icon inside the slot [x, 0, box_w, box_h] using the current ink // colour (no background). Centres on the box itself, not the text line, so the // glyph sits dead-centre regardless of font line height. diff --git a/solo/wio-tracker-l1-gdeq0426/platformio.ini b/solo/wio-tracker-l1-gdeq0426/platformio.ini index 7f90a27d..9ab3b0ef 100644 --- a/solo/wio-tracker-l1-gdeq0426/platformio.ini +++ b/solo/wio-tracker-l1-gdeq0426/platformio.ini @@ -12,6 +12,12 @@ ; a failed allocation there crashes into a boot loop. So: ; OFFLINE_QUEUE_SIZE 32 (stock e-ink: 128) frees ~17 KB ; MAX_CONTACTS 250 (stock: 350) frees ~18 KB +; - EINK_FONT_SCALE 4: text at 4x the 6x9 font (24x36 cells, ~33 characters +; x 13 lines) -- twice the stock 2x. The default for Settings > Display > +; Text size (1x-5x), which overrides it once changed. +; - STATUS_ICON_SCALE 0: turns on scalable Home status/page icons; 0 = Auto +; (1.5x the text: 6x at 4x text). The default for Settings > Display > +; Icon size (Auto, 1x-8x). ; ; Build: pio run -e WioTrackerL1_GDEQ0426T82_companion_solo_dual [env:WioTrackerL1_GDEQ0426T82_companion_solo_dual] @@ -35,6 +41,8 @@ build_flags = ${nrf52_base.build_flags} -D EINK_DISPLAY_MODEL=GxEPD2_426_GDEQ0426T82 -D DISPLAY_ROTATION=0 -D JOYSTICK_UPRIGHT_ROTATION=0 + -D EINK_FONT_SCALE=4 + -D STATUS_ICON_SCALE=0 -D GPS_BAUD_RATE=9600 -D ENV_PIN_SDA=PIN_WIRE1_SDA -D ENV_PIN_SCL=PIN_WIRE1_SCL diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index 0cb3709b..e2578ae6 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -536,6 +536,7 @@ public: virtual void setBrightness(uint8_t level) { } // level 0-4 (min to max), no-op default virtual void setDisplayRotation(uint8_t rot) { } // 0-3, no-op for fixed-orientation displays virtual void setFullRefreshInterval(uint8_t n) { } // e-ink: do full refresh every n partial refreshes (0=never) + virtual void setUiScale(uint8_t font_scale) { } // e-ink: text pixel multiplier, 0 = build default // Sideways slide between two frames, for screens that page left/right. // slideBegin() keeps rows y0.. of the frame on screen now; call it before the // next frame is drawn. slideCompose(dx), at the end of drawing that frame, diff --git a/src/helpers/ui/EinkGfxDisplay.h b/src/helpers/ui/EinkGfxDisplay.h index e52fb1a1..6e983559 100644 --- a/src/helpers/ui/EinkGfxDisplay.h +++ b/src/helpers/ui/EinkGfxDisplay.h @@ -30,6 +30,7 @@ class EinkGfxDisplay : public DisplayDriver { const GFXfont* _font; int _cell_w = 6, _cell_h = 9; // the font's cell, before _px and the text size int _px = 1; // pixel doubling (landscape without EINK_LARGE_FONT) + uint8_t _px_override = 0; // Settings > Display > Text size (0 = build default) int _text_sz = 1; int _cx = 0, _cy = 0; // cursor, top of the text row uint32_t _hash = 0, _shown_hash = 0; @@ -74,6 +75,9 @@ public: dest[dest_size - 1] = '\0'; } bool isEink() override { return true; } + // Runtime text scale (Settings > Display > Text size): re-lays out and forces + // a full redraw. 0 = the build default (EINK_FONT_SCALE). + void setUiScale(uint8_t font_scale) override { _px_override = font_scale; updateLayout(); } void startFrame(Color bkg = DARK) override; void setTextSize(int sz) override; diff --git a/src/helpers/ui/EinkGfxDisplayImpl.h b/src/helpers/ui/EinkGfxDisplayImpl.h index 884e05a9..26457b97 100644 --- a/src/helpers/ui/EinkGfxDisplayImpl.h +++ b/src/helpers/ui/EinkGfxDisplayImpl.h @@ -3,6 +3,12 @@ // the font tables below are static arrays. #include "EinkGfxDisplay.h" +// EINK_FONT_SCALE=: pixel multiplier for the 6x9 font on a landscape panel +// without EINK_LARGE_FONT (default 2). 4 on an 800x480 panel = 24x36 cells, +// ~33 characters x 13 lines. Settings > Display > Text size overrides it at run time. +#ifndef EINK_FONT_SCALE + #define EINK_FONT_SCALE 2 +#endif #include "MiscFixedRenderer.h" #if EINK_LARGE_FONT == 2 #include "MiscFixed9x15Font.h" @@ -20,7 +26,8 @@ EinkGfxDisplay::EinkGfxDisplay(int w, int h, Adafruit_GFX& gfx, uint16_t ink, ui void EinkGfxDisplay::updateLayout() { const bool landscape = width() >= height(); _font = (EINK_LARGE_FONT && landscape) ? &EINK_LARGE_GFXFONT : &MiscFixed; - _px = (!EINK_LARGE_FONT && landscape) ? 2 : 1; + _px = (!EINK_LARGE_FONT && landscape) ? EINK_FONT_SCALE : 1; + if (_px_override) _px = _px_override; _cell_w = miscFixedCellW(*_font); _cell_h = _font->yAdvance; _vw_dirty = true; @@ -87,7 +94,7 @@ void EinkGfxDisplay::fillRect(int x, int y, int w, int h) { void EinkGfxDisplay::drawRect(int x, int y, int w, int h) { mix(x); mix(y); mix(w); mix(h); _gfx.drawRect(x, y, w, h, _curr); - if (_px == 2 && w > 2 && h > 2) _gfx.drawRect(x + 1, y + 1, w - 2, h - 2, _curr); + if (_px >= 2 && w > 2 && h > 2) _gfx.drawRect(x + 1, y + 1, w - 2, h - 2, _curr); } void EinkGfxDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { From beae8de734d711beae5c6617fa0bd97e8f025672 Mon Sep 17 00:00:00 2001 From: pmow Date: Sat, 10 Oct 2026 16:14:18 -1000 Subject: [PATCH 4/6] docs(eink): name the GxEPD2 version GxEPD2_FastWrite.h copies from setRamArea() and fastWrite() follow GxEPD2 1.6.2's private _setPartialRamArea() and _writeImage() for the GDEQ0426T82; say so, so whoever bumps the library knows to re-check them. --- src/helpers/ui/GxEPD2_FastWrite.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/helpers/ui/GxEPD2_FastWrite.h b/src/helpers/ui/GxEPD2_FastWrite.h index a4bdf703..8bc74838 100644 --- a/src/helpers/ui/GxEPD2_FastWrite.h +++ b/src/helpers/ui/GxEPD2_FastWrite.h @@ -31,6 +31,8 @@ class GxEPD2_426_GDEQ0426T82_Fast : public GxEPD2_426_GDEQ0426T82 { // Same RAM window as the stock driver's (private) _setPartialRamArea(): // the gates are reversed on this panel, so y is mirrored and entered decreasing. + // Copied from GxEPD2 1.6.2 (src/gdeq/GxEPD2_426_GDEQ0426T82.cpp) -- when + // bumping GxEPD2, check the library's version still matches this one. void setRamArea(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { y = HEIGHT - y - h; _writeCommand(0x11); @@ -54,7 +56,7 @@ class GxEPD2_426_GDEQ0426T82_Fast : public GxEPD2_426_GDEQ0426T82 { } // The stock _writeImage(), with the per-byte loop replaced by one bulk - // transfer per row (clipping and indexing unchanged). + // transfer per row (clipping and indexing unchanged). Also from GxEPD2 1.6.2. void fastWrite(uint8_t command, const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert) { delay(1); // as the stock driver: yield int16_t wb = (w + 7) / 8; From 13531dd2ce4ac17d06c95a449c79ce983cd584ab Mon Sep 17 00:00:00 2001 From: pmow Date: Sat, 10 Oct 2026 16:36:39 -1000 Subject: [PATCH 5/6] refactor(eink): Text size and Icon size as schema settings, on opt-in builds only Review follow-up: - The rows exist only where the build asks for them: Text size with EINK_FONT_SCALE, Icon size with STATUS_ICON_SCALE. The stock 2.13", the portrait builds and the L2 don't get them -- their screens are laid out for one cell size. The text override also applies in landscape only. - Stored in NodePrefs (ui_text_scale, ui_icon_scale) instead of a /ui_scale file, as rows in the schema's Display section: the usual row, save path and factory reset, and console keys display.text.size / display.icon.size. The hand-written Settings rows and the load/save code are gone; UITask::applyUiScale() applies the prefs at boot and on change (applyDisplayPrefs). On opt-in builds ui_text_scale starts at the build's EINK_FONT_SCALE (DataStore sanitize), so the row shows the scale in use. - EINK_FONT_SCALE, when set, now scales whichever landscape font the build picks (the 6x9, or the EINK_LARGE_FONT one); unset, it is stock as before. - setUiScale() skips the re-layout and full redraw when nothing changed. Co-Authored-By: Claude Opus 5.5 --- examples/companion_radio/DataStore.cpp | 5 ++ examples/companion_radio/NodePrefs.h | 9 +++ .../companion_radio/ui-core/SettingsCli.h | 1 + .../companion_radio/ui-core/SettingsSchema.h | 18 ++++++ .../companion_radio/ui-new/SettingsScreen.h | 31 --------- examples/companion_radio/ui-new/UITask.cpp | 64 ++++--------------- examples/companion_radio/ui-new/UITask.h | 12 +--- examples/companion_radio/ui-new/icons.h | 13 ++-- solo/wio-tracker-l1-gdeq0426/platformio.ini | 9 ++- src/helpers/ui/EinkGfxDisplay.h | 8 ++- src/helpers/ui/EinkGfxDisplayImpl.h | 18 ++++-- 11 files changed, 74 insertions(+), 114 deletions(-) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 6ce4d7ec..aff4d46c 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -334,6 +334,11 @@ static void sanitize(NodePrefs& p) { flag(p.lock_compact, 0); flag(p.ble_off, 0); if (p.msg_alert > NodePrefs::MSG_ALERT_OFF) p.msg_alert = NodePrefs::MSG_ALERT_NORMAL; + if (p.ui_text_scale > NodePrefs::UI_TEXT_SCALE_MAX) p.ui_text_scale = 0; +#ifdef EINK_FONT_SCALE // the Text size row shows the scale in use: start at the build's + if (p.ui_text_scale == 0) p.ui_text_scale = EINK_FONT_SCALE; +#endif + if (p.ui_icon_scale > NodePrefs::UI_ICON_SCALE_MAX) p.ui_icon_scale = 0; if (!p.ch_scope_follow) { for (uint8_t i = 0; i < NodePrefs::MAX_SCOPED_CHANNELS; i++) if (p.ch_scope_idx[i] == 0) p.ch_scope_idx[i] = NodePrefs::CH_SCOPE_DEFAULT; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index b656d9a0..e42a5232 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -538,6 +538,15 @@ struct NodePrefs { // persisted to file static const uint8_t MSG_ALERT_NORMAL = 0, MSG_ALERT_COMPACT = 1, MSG_ALERT_OFF = 2; uint8_t msg_alert; + // ── Text and icon size (ui-new, big e-ink panels) ────────────────────── + // Settings > Display, only on builds that opt in (EINK_FONT_SCALE / + // STATUS_ICON_SCALE). ui_text_scale: the font's pixel multiplier, 1-5; 0 is + // the build's own (DataStore sanitize sets EINK_FONT_SCALE there). + // ui_icon_scale: Home's status icons, 0 = Auto (1.5x the text), else 1-8. + static const uint8_t UI_TEXT_SCALE_MAX = 5, UI_ICON_SCALE_MAX = 8; + uint8_t ui_text_scale; + uint8_t ui_icon_scale; + // Single source of truth for the live-share option tables (shared by the Map // UI labels and the auto-send engine in UITask). static const uint8_t LOC_SHARE_MOVE_COUNT = 4; diff --git a/examples/companion_radio/ui-core/SettingsCli.h b/examples/companion_radio/ui-core/SettingsCli.h index e6006876..0f79c493 100644 --- a/examples/companion_radio/ui-core/SettingsCli.h +++ b/examples/companion_radio/ui-core/SettingsCli.h @@ -23,6 +23,7 @@ static const Key KEYS[] = { K("contacts.expiry", contact_expiry_idx), K("contacts.fav.first", fav_sort), K("dm.resend", dm_resend_count), K("display.brightness", display_brightness), K("display.off", auto_off_secs), K("display.wake", msg_wake), K("display.alert", msg_alert), K("lock", auto_lock), K("lock.clock", lock_compact), + K("display.text.size", ui_text_scale), K("display.icon.size", ui_icon_scale), K("batt.display", batt_display_mode), K("batt.shutdown", low_batt_mv), K("gps.interval", gps_interval), K("clock.12h", clock_12h), K("clock.seconds", clock_hide_seconds), K("units.imperial", units_imperial), K("sound.volume", buzzer_volume), K("quiet", quiet_hours), K("quiet.from", quiet_from), K("quiet.until", quiet_to), diff --git a/examples/companion_radio/ui-core/SettingsSchema.h b/examples/companion_radio/ui-core/SettingsSchema.h index e8f865d8..d2efff6b 100644 --- a/examples/companion_radio/ui-core/SettingsSchema.h +++ b/examples/companion_radio/ui-core/SettingsSchema.h @@ -144,6 +144,15 @@ static void optMsgAlert(uint8_t v, char* b, int n, const NodePrefs&) { static const char* const L[] = { "Normal", "Compact", "Off" }; snprintf(b, n, "%s", L[v < 3 ? v : 0]); } +#ifdef EINK_FONT_SCALE +static const int32_t TEXT_SCALE[] = { 1, 2, 3, 4, 5 }; +static void optTextScale(uint8_t v, char* b, int n, const NodePrefs&) { snprintf(b, n, "%ldx", (long)TEXT_SCALE[v]); } +#endif +#ifdef STATUS_ICON_SCALE +static void optIconScale(uint8_t v, char* b, int n, const NodePrefs&) { + if (v == 0) snprintf(b, n, "Auto"); else snprintf(b, n, "%ux", (unsigned)v); +} +#endif static void optLockLook(uint8_t v, char* b, int n, const NodePrefs&) { snprintf(b, n, "%s", v ? "Compact" : "Big"); } static void optExpiry(uint8_t v, char* b, int n, const NodePrefs&) { snprintf(b, n, "%s", NodePrefs::contactExpiryLabel(v)); @@ -243,6 +252,15 @@ static const Setting ALL[] = { SW("Lock screen", "Slide to unlock after sleep", SEC_DISPLAY, auto_lock, nullptr), IDX("Lock clock", "Big, or compact with the clock fields", SEC_DISPLAY, lock_compact, 2, optLockLook, nullptr), IDX("Battery display", "In the status bar", SEC_DISPLAY, batt_display_mode, 3, optBattDisplay, nullptr), + // Only the builds that ask for them (big e-ink panels): each screen elsewhere + // is laid out for one cell size. +#ifdef EINK_FONT_SCALE + MAP("Text size", "Bigger letters on a big panel", SEC_DISPLAY, ui_text_scale, TEXT_SCALE, optTextScale, applyDisplay), +#endif +#ifdef STATUS_ICON_SCALE + IDX("Icon size", "Home's status icons", SEC_DISPLAY, ui_icon_scale, NodePrefs::UI_ICON_SCALE_MAX + 1, + optIconScale, applyDisplay), +#endif MAP("Battery shutdown", "Power off below this voltage", SEC_POWER, low_batt_mv, LOW_BATT, optLowBatt, nullptr), MAP("GPS power saving", "Sleep between fixes", SEC_POWER, gps_interval, GPS_DUTY, optGpsDuty, applyGpsDuty), diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 965fa2aa..bcb7dccf 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -27,10 +27,6 @@ class SettingsScreen : public UIScreen { #endif #if FEAT_FULL_REFRESH_SETTING EINK_FULL_REFRESH, - UI_TEXT_SCALE, // Text size: 1x..5x the 6x9 font -#if STATUS_ICONS_SCALED - UI_ICON_SCALE, // Icon size: Auto, 1x..8x -#endif #endif // Sound section SECTION_SOUND, @@ -559,18 +555,6 @@ class SettingsScreen : public UIScreen { uint8_t idx = p ? p->eink_full_refresh_every : 0; if (idx >= EINK_FULL_REFRESH_COUNT) idx = 0; val("Full rfsh", EINK_FULL_REFRESH_LABELS[idx]); - } else if (item == UI_TEXT_SCALE) { - char b[4]; - snprintf(b, sizeof(b), "%ux", (unsigned)_task->getUiFontScale()); - val("Text size", b); -#if STATUS_ICONS_SCALED - } else if (item == UI_ICON_SCALE) { - char b[12]; - const uint8_t v = _task->getStatusIconScale(); - if (v == 0) snprintf(b, sizeof(b), "Auto"); - else snprintf(b, sizeof(b), "%ux", (unsigned)v); - val("Icon size", b); -#endif #endif } else if (item == DM_FILTER) { val("DMs", (p && p->dm_show_all) ? "All" : "Fav"); @@ -1202,21 +1186,6 @@ public: _dirty = true; return true; } - if (_selected == UI_TEXT_SCALE && (left || right || enter)) { - int v = _task->getUiFontScale(); // 1..5, wrapping - v = left ? (v <= 1 ? 5 : v - 1) : (v >= 5 ? 1 : v + 1); - _task->setUiFontScale(v); // saved to /ui_scale immediately - return true; - } -#if STATUS_ICONS_SCALED - if (_selected == UI_ICON_SCALE && (left || right || enter)) { - const int n = STATUS_ICON_SCALE_MAX + 1; // Auto, 1x..8x - int v = _task->getStatusIconScale(); - v = (v + (left ? n - 1 : 1)) % n; - _task->setStatusIconScale(v); // saved to /ui_scale immediately - return true; - } -#endif #endif if (_selected == DM_FILTER && p && (left || right || enter)) { p->dm_show_all = p->dm_show_all ? 0 : 1; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index d4632718..fdb11d16 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2063,7 +2063,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no applyBrightness(); applyRotation(); applyFullRefreshInterval(); - loadUiScale(); // Settings > Display > Text size / Icon size + applyUiScale(); // Settings > Display > Text size / Icon size applyAllGpioModes(); // restore persisted pin modes to hardware before any UI/bot use // Locked above (a PIN, or the cover closed) before Home existed to be told. if (_locked) syncLockToHome(); @@ -4163,57 +4163,17 @@ void UITask::applyFullRefreshInterval() { } } -// Text size and Icon size (Settings > Display), in their own /ui_scale file -- -// [text pixel scale (0 = build default), icon scale (0 = Auto)] -- so the -// NodePrefs schema is untouched. -static const char* UI_SCALE_FILE = "/ui_scale"; -static const uint8_t UI_TEXT_SCALE_MAX = 5; - -void UITask::loadUiScale() { - uint8_t b[2] = { 0, statusIconScaleSetting() }; - DataStore* ds = the_mesh.getDataStore(); - if (ds) { - File f = ds->openRead(UI_SCALE_FILE); - if (f) { - f.read(b, 2); - f.close(); - } - } - _ui_font_scale = b[0] <= UI_TEXT_SCALE_MAX ? b[0] : 0; - if (b[1] <= STATUS_ICON_SCALE_MAX) statusIconScaleSetting() = b[1]; - if (_display != NULL && _ui_font_scale) _display->setUiScale(_ui_font_scale); -} - -void UITask::saveUiScale() { - DataStore* ds = the_mesh.getDataStore(); - if (!ds) return; - File f = ds->openWrite(UI_SCALE_FILE); - if (f) { - uint8_t b[2] = { _ui_font_scale, statusIconScaleSetting() }; - f.write(b, 2); - f.close(); - } -} - -uint8_t UITask::getUiFontScale() const { - return _display != NULL ? (uint8_t)_display->pixelScale() : 1; -} - -void UITask::setUiFontScale(uint8_t s) { - if (s < 1 || s > UI_TEXT_SCALE_MAX) s = 1; - _ui_font_scale = s; - if (_display != NULL) _display->setUiScale(s); - _next_refresh = 0; - saveUiScale(); -} - -uint8_t UITask::getStatusIconScale() const { return statusIconScaleSetting(); } - -void UITask::setStatusIconScale(uint8_t s) { - if (s > STATUS_ICON_SCALE_MAX) s = 0; - statusIconScaleSetting() = s; - _next_refresh = 0; // the frame differs, so it's redrawn - saveUiScale(); +// Text size and Icon size (Settings > Display, NodePrefs ui_text_scale / +// ui_icon_scale), on the builds that offer them. Elsewhere the fields stay 0: +// the build's font scale and Auto icons, as stock. +void UITask::applyUiScale() { + if (!_node_prefs) return; +#ifdef EINK_FONT_SCALE + if (_display != NULL) _display->setUiScale(_node_prefs->ui_text_scale); +#endif +#if STATUS_ICONS_SCALED + statusIconScaleSetting() = _node_prefs->ui_icon_scale; +#endif } void UITask::applySoundPrefs() { diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 4b678e1e..86fe04e5 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -62,7 +62,6 @@ class UITask : public UITaskBase, public UiCoreHost { unsigned long _next_refresh, _auto_off; bool _display_was_on = true; // loop(): messagesInView() last time round NodePrefs* _node_prefs; - uint8_t _ui_font_scale = 0; // Settings > Display > Text size (0 = build default) bool _locked; bool _tool_from_home = false; bool messagesInView() const; // Messages on a lit, unlocked display @@ -529,7 +528,7 @@ public: void applyAllGpioModes(); // boot-time restore from NodePrefs, called from begin() void applyBrightness(); // Settings changed through the schema (ui-core/SettingsSchema.h). - void applyDisplayPrefs() override { applyBrightness(); _next_refresh = 0; } + void applyDisplayPrefs() override { applyBrightness(); applyUiScale(); _next_refresh = 0; } void applySoundPrefs() override; bool cliCommand(const char* command, char* reply, int n) override; void setBuzzerVolumeLevel(uint8_t level); @@ -544,14 +543,7 @@ public: bool savePrefsIfDirty(bool& dirty); void applyRotation(); void applyFullRefreshInterval(); - // Settings > Display > Text size / Icon size. Kept in their own /ui_scale - // file rather than NodePrefs, so the prefs schema (and its size assert) is untouched. - uint8_t getUiFontScale() const; // the text's current pixel scale - void setUiFontScale(uint8_t s); // 1..5: applies + saves - uint8_t getStatusIconScale() const; // 0 = Auto (1.5x text), else n - void setStatusIconScale(uint8_t s); // 0..8: applies + saves - void loadUiScale(); // boot-time restore, from begin() - void saveUiScale(); + void applyUiScale(); // Settings > Display > Text size / Icon size, from the prefs uint32_t autoOffMillis() const { if (!_node_prefs || _node_prefs->auto_off_secs == 0) return 0; return (uint32_t)_node_prefs->auto_off_secs * 1000UL; diff --git a/examples/companion_radio/ui-new/icons.h b/examples/companion_radio/ui-new/icons.h index 88509584..d9433bf3 100644 --- a/examples/companion_radio/ui-new/icons.h +++ b/examples/companion_radio/ui-new/icons.h @@ -94,20 +94,19 @@ inline void miniIconDrawCentered(DisplayDriver& d, int cx, int cy, const MiniIco // Home status bar icon scale. Defining STATUS_ICON_SCALE (build flag) turns on // scalable status icons: the bar's icons, battery and the page-icon row below it // are drawn at n x their pixel grid, and the bar grows tall enough to hold them -- -// for big panels whose 1-line bar leaves them tiny. Its value is the default for -// Settings > Display > Icon size: 0 = Auto (1.5x the text's pixel scale), else n. -// Unset: icons follow the font, as stock. +// for big panels whose 1-line bar leaves them tiny -- and adds Settings > Display > +// Icon size (NodePrefs::ui_icon_scale): 0 = Auto (1.5x the text's pixel scale), +// else n. Unset: icons follow the font, as stock. #ifdef STATUS_ICON_SCALE #define STATUS_ICONS_SCALED 1 #else #define STATUS_ICONS_SCALED 0 - #define STATUS_ICON_SCALE 0 #endif -static constexpr int STATUS_ICON_SCALE_MAX = 8; static constexpr int STATUS_ICON_ROWS = MINI_ICONS_LARGE == 2 ? 9 : 7; // tallest status icon -// The Icon size setting (0 = Auto). One instance across translation units. +// The Icon size setting (0 = Auto), copied from the prefs by UITask::applyUiScale(). +// One instance across translation units. inline uint8_t& statusIconScaleSetting() { - static uint8_t v = (STATUS_ICON_SCALE >= 0 && STATUS_ICON_SCALE <= STATUS_ICON_SCALE_MAX) ? STATUS_ICON_SCALE : 0; + static uint8_t v = 0; return v; } inline int statusIconScale(DisplayDriver& d) { diff --git a/solo/wio-tracker-l1-gdeq0426/platformio.ini b/solo/wio-tracker-l1-gdeq0426/platformio.ini index 9ab3b0ef..7238ecaa 100644 --- a/solo/wio-tracker-l1-gdeq0426/platformio.ini +++ b/solo/wio-tracker-l1-gdeq0426/platformio.ini @@ -13,11 +13,10 @@ ; OFFLINE_QUEUE_SIZE 32 (stock e-ink: 128) frees ~17 KB ; MAX_CONTACTS 250 (stock: 350) frees ~18 KB ; - EINK_FONT_SCALE 4: text at 4x the 6x9 font (24x36 cells, ~33 characters -; x 13 lines) -- twice the stock 2x. The default for Settings > Display > -; Text size (1x-5x), which overrides it once changed. -; - STATUS_ICON_SCALE 0: turns on scalable Home status/page icons; 0 = Auto -; (1.5x the text: 6x at 4x text). The default for Settings > Display > -; Icon size (Auto, 1x-8x). +; x 13 lines) -- twice the stock 2x. Also adds Settings > Display > Text +; size (1x-5x), which starts here. +; - STATUS_ICON_SCALE turns on scalable Home status/page icons and adds +; Settings > Display > Icon size (Auto = 1.5x the text: 6x at 4x text, 1x-8x). ; ; Build: pio run -e WioTrackerL1_GDEQ0426T82_companion_solo_dual [env:WioTrackerL1_GDEQ0426T82_companion_solo_dual] diff --git a/src/helpers/ui/EinkGfxDisplay.h b/src/helpers/ui/EinkGfxDisplay.h index 6e983559..822a61f1 100644 --- a/src/helpers/ui/EinkGfxDisplay.h +++ b/src/helpers/ui/EinkGfxDisplay.h @@ -29,7 +29,7 @@ class EinkGfxDisplay : public DisplayDriver { uint16_t _curr; const GFXfont* _font; int _cell_w = 6, _cell_h = 9; // the font's cell, before _px and the text size - int _px = 1; // pixel doubling (landscape without EINK_LARGE_FONT) + int _px = 1; // pixel multiplier (landscape: EINK_FONT_PX, or Text size) uint8_t _px_override = 0; // Settings > Display > Text size (0 = build default) int _text_sz = 1; int _cx = 0, _cy = 0; // cursor, top of the text row @@ -77,7 +77,11 @@ public: bool isEink() override { return true; } // Runtime text scale (Settings > Display > Text size): re-lays out and forces // a full redraw. 0 = the build default (EINK_FONT_SCALE). - void setUiScale(uint8_t font_scale) override { _px_override = font_scale; updateLayout(); } + void setUiScale(uint8_t font_scale) override { + if (font_scale == _px_override) return; // unchanged: no full redraw + _px_override = font_scale; + updateLayout(); + } void startFrame(Color bkg = DARK) override; void setTextSize(int sz) override; diff --git a/src/helpers/ui/EinkGfxDisplayImpl.h b/src/helpers/ui/EinkGfxDisplayImpl.h index 26457b97..16b3aee4 100644 --- a/src/helpers/ui/EinkGfxDisplayImpl.h +++ b/src/helpers/ui/EinkGfxDisplayImpl.h @@ -3,11 +3,15 @@ // the font tables below are static arrays. #include "EinkGfxDisplay.h" -// EINK_FONT_SCALE=: pixel multiplier for the 6x9 font on a landscape panel -// without EINK_LARGE_FONT (default 2). 4 on an 800x480 panel = 24x36 cells, -// ~33 characters x 13 lines. Settings > Display > Text size overrides it at run time. -#ifndef EINK_FONT_SCALE - #define EINK_FONT_SCALE 2 +// EINK_FONT_SCALE=: pixel multiplier for the landscape font -- the 6x9, or +// the EINK_LARGE_FONT one if the build picks it. 4 on an 800x480 panel with the +// 6x9 = 24x36 cells, ~33 characters x 13 lines. Setting it also adds Settings > +// Display > Text size, which overrides it at run time. Unset: stock, the 6x9 at +// 2x or the large font at its own size. +#ifdef EINK_FONT_SCALE + #define EINK_FONT_PX EINK_FONT_SCALE +#else + #define EINK_FONT_PX (EINK_LARGE_FONT ? 1 : 2) #endif #include "MiscFixedRenderer.h" #if EINK_LARGE_FONT == 2 @@ -26,8 +30,8 @@ EinkGfxDisplay::EinkGfxDisplay(int w, int h, Adafruit_GFX& gfx, uint16_t ink, ui void EinkGfxDisplay::updateLayout() { const bool landscape = width() >= height(); _font = (EINK_LARGE_FONT && landscape) ? &EINK_LARGE_GFXFONT : &MiscFixed; - _px = (!EINK_LARGE_FONT && landscape) ? EINK_FONT_SCALE : 1; - if (_px_override) _px = _px_override; + _px = landscape ? EINK_FONT_PX : 1; + if (_px_override && landscape) _px = _px_override; // portrait screens are laid out for 1x _cell_w = miscFixedCellW(*_font); _cell_h = _font->yAdvance; _vw_dirty = true; From 95a58a55320eff6018f6d9d1f90793bef536c899 Mon Sep 17 00:00:00 2001 From: pmow Date: Sat, 10 Oct 2026 17:10:06 -1000 Subject: [PATCH 6/6] feat(solo): GDEQ0426T82 in the 9x15 font at 2x The 4.2" build's 9x15 font (EINK_LARGE_FONT=2) at 2x -- 18x30 cells, ~44 characters x 16 lines -- instead of the 6x9 at 4x, which goes blocky. Compared side by side on the panel, as suggested in review. Co-Authored-By: Claude Opus 5.5 --- solo/wio-tracker-l1-gdeq0426/platformio.ini | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/solo/wio-tracker-l1-gdeq0426/platformio.ini b/solo/wio-tracker-l1-gdeq0426/platformio.ini index 7238ecaa..7bf26f25 100644 --- a/solo/wio-tracker-l1-gdeq0426/platformio.ini +++ b/solo/wio-tracker-l1-gdeq0426/platformio.ini @@ -12,11 +12,13 @@ ; a failed allocation there crashes into a boot loop. So: ; OFFLINE_QUEUE_SIZE 32 (stock e-ink: 128) frees ~17 KB ; MAX_CONTACTS 250 (stock: 350) frees ~18 KB -; - EINK_FONT_SCALE 4: text at 4x the 6x9 font (24x36 cells, ~33 characters -; x 13 lines) -- twice the stock 2x. Also adds Settings > Display > Text -; size (1x-5x), which starts here. +; - EINK_LARGE_FONT 2: the 4.2" build's 9x15 font (and its 9 px icons) -- +; on hardware it reads much better here than the 6x9 scaled up, which goes +; blocky at 4x. +; - EINK_FONT_SCALE 2: that font at 2x (18x30 cells, ~44 characters x 16 +; lines). Also adds Settings > Display > Text size (1x-5x), which starts here. ; - STATUS_ICON_SCALE turns on scalable Home status/page icons and adds -; Settings > Display > Icon size (Auto = 1.5x the text: 6x at 4x text, 1x-8x). +; Settings > Display > Icon size (Auto = 1.5x the text: 3x at 2x text, 1x-8x). ; ; Build: pio run -e WioTrackerL1_GDEQ0426T82_companion_solo_dual [env:WioTrackerL1_GDEQ0426T82_companion_solo_dual] @@ -40,7 +42,8 @@ build_flags = ${nrf52_base.build_flags} -D EINK_DISPLAY_MODEL=GxEPD2_426_GDEQ0426T82 -D DISPLAY_ROTATION=0 -D JOYSTICK_UPRIGHT_ROTATION=0 - -D EINK_FONT_SCALE=4 + -D EINK_LARGE_FONT=2 + -D EINK_FONT_SCALE=2 -D STATUS_ICON_SCALE=0 -D GPS_BAUD_RATE=9600 -D ENV_PIN_SDA=PIN_WIRE1_SDA