refactor(ui-new): Home, splash, CardKB and the perf walk out of UITask.cpp

A pure move: each block now sits in its own header, included where its
code was, so the order of definitions is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-10-11 21:26:58 +02:00
co-authored by Claude Opus 5.5
parent a5423d42c1
commit e896e6af60
7 changed files with 2232 additions and 2217 deletions
+2 -2
View File
@@ -65,7 +65,7 @@ public:
4. Reach it from somewhere — usually a row in `ToolsScreen.h` (add an `Action` 4. Reach it from somewhere — usually a row in `ToolsScreen.h` (add an `Action`
enum value, a row in the right section table, and a `run()` case). The enum value, a row in the right section table, and a `run()` case). The
Tools home page lists the last two tools used; give the new one a state Tools home page lists the last two tools used; give the new one a state
in `HomeScreen::toolState()` (`UITask.cpp`) if it has one worth showing. in `HomeScreen::toolState()` (`HomeScreen.h`) if it has one worth showing.
That's the whole contract. Steps 1, 3 and 4 are compiler-checked (a mismatch That's the whole contract. Steps 1, 3 and 4 are compiler-checked (a mismatch
won't link); only a forgotten step 2 can slip through — every screen pointer is won't link); only a forgotten step 2 can slip through — every screen pointer is
@@ -296,7 +296,7 @@ Draw with `miniIconDraw(display, x, topY, ICON_FOO)` (auto-scaled & centered),
(centred in a status-bar slot). Use icons sparingly: headers and the Tools (centred in a status-bar slot). Use icons sparingly: headers and the Tools
list have none, the Home page row is the main place for them. list have none, the Home page row is the main place for them.
The home status bar (`HomeScreen::renderBatteryIndicator()`, `UITask.cpp`) The home status bar (`HomeScreen::renderBatteryIndicator()`, `HomeScreen.h`)
draws plain glyphs right to left from a fixed, priority-ordered table, with draws plain glyphs right to left from a fixed, priority-ordered table, with
no boxes and no blinking: each icon tells a state by its shape (Bluetooth only no boxes and no blinking: each icon tells a state by its shape (Bluetooth only
while a phone is connected, the GPS reticle broken until a fix, a pin while while a phone is connected, the GPS reticle broken until a fix, a pin while
+1 -1
View File
@@ -1,6 +1,6 @@
#pragma once #pragma once
// The MeshCore wordmark and its lettering, shared by both boot splashes // The MeshCore wordmark and its lettering, shared by both boot splashes
// (ui-new/UITask.cpp on the OLED/e-ink boards, ui-lvgl/Splash.h on the L2) // (ui-new/SplashScreen.h on the OLED/e-ink boards, ui-lvgl/Splash.h on the L2)
// and ui-new's big clock (Home clock page, lock screen): // and ui-new's big clock (Home clock page, lock screen):
// the 128x13 wordmark bitmap, its own letters, the extra characters drawn in // the 128x13 wordmark bitmap, its own letters, the extra characters drawn in
// the same strokes (digits, '.', ':', '-', v, l, d, space), and the Solo version / // the same strokes (digits, '.', ':', '-', v, l, d, space), and the Solo version /
+230
View File
@@ -0,0 +1,230 @@
#pragma once
// Included into UITask.cpp only: the CardKB (I2C keyboard) side of UITask.
// The CardKB background poll task (see UITask::cardkbTask) shares its I2C bus
// with the environment sensors, and some screens query those sensors while
// rendering -- exactly when that task is active. Holding this lock around a
// sensor query makes the two take turns. No-op on builds without the task.
#if defined(CARDKB_I2C) && defined(NRF52_PLATFORM)
static void* g_cardkb_bus_mutex = nullptr; // SemaphoreHandle_t, set by startCardKBCapture()
struct CardKBBusLock {
CardKBBusLock() { if (g_cardkb_bus_mutex) xSemaphoreTake((SemaphoreHandle_t)g_cardkb_bus_mutex, portMAX_DELAY); }
~CardKBBusLock() { if (g_cardkb_bus_mutex) xSemaphoreGive((SemaphoreHandle_t)g_cardkb_bus_mutex); }
};
#else
struct CardKBBusLock { };
#endif
#if defined(CARDKB_I2C)
// CardKB's "fn" column (key_map in M5Stack's unit_CardKB.cpp): Fn+<physical
// key> sends 0x80 + that key's row index, entirely disjoint from every other
// code this UI recognises. Indexed by (raw - 0x80); non-letter slots (digits,
// arrows, enter, tab, bs, space -- handled separately or unused) are 0.
static const char CARDKB_FN_BASE[48] = {
0,0,0,0,0,0,0,0,0,0,0,0,0, // esc,1-0,bs,tab
'q','w','e','r','t','y','u','i','o','p', 0, 0,0, // q-p, (unused), LEFT,UP
'a','s','d','f','g','h','j','k','l', 0, 0,0, // a-l, enter, DOWN,RIGHT
'z','x','c','v','b','n','m', 0,0,0, // z-m, comma,period,space
};
#endif
// Poll an optional CardKB (I2C keyboard, addr 0x5F) on CARDKB_I2C, feeding
// the same key queue as every physical button. Most of its output needs no
// translation at all: CardKB's own arrow/Enter/Esc byte codes are already
// identical to this UI's KEY_LEFT/UP/DOWN/RIGHT/ENTER/CANCEL (0xB4-0xB7, 13,
// 27), and Backspace (0x08) / printable ASCII (0x20-0x7E) collide with
// nothing that existed before. Plain Enter/arrows act like the physical
// centre button/joystick (grid commit/navigate) -- except in Compact mode's
// plain grid state (see below), which is designed to need no joystick at all.
// Tab (0x09, otherwise unused) is the Hold-Enter equivalent everywhere,
// including the ~30 non-keyboard Hold-Enter menus (message reply/navigate,
// Bot/Admin/Repeater, ...) and inside the on-screen keyboard itself (shift-
// lock, clear-all, accent popup on whatever cell is selected) -- it used to
// need a separate Fn+Tab for the latter, but that was pure redundancy: plain
// Tab already covered every case Fn+Tab did, just not while the keyboard was
// showing, so the carve-out was dropped instead of the shortcut. In Compact
// mode's plain grid state Tab means something more useful instead (opens the
// placeholder picker directly -- see below). Fn still gives two other clean,
// stateless modifiers:
// - Fn+Enter (0xA3) submits the field (KEY_KB_ENTER) without needing to
// navigate to the special row's DONE cell. The placeholder/accent popups
// are modal and consume it first (dismiss them with Enter/Esc), same as
// they consume every other key.
// - Fn+<letter> opens the accent popup for that base letter directly
// (KeyboardWidget::openAccentFor()) -- no arrow-hunting across the grid.
// CardKB is level-triggered (it keeps returning the held key's byte, not just
// once), so _cardkb_last_raw debounces it into one press per physical
// keypress, same as a MomentaryButton's CLICK event.
void UITask::pollCardKB() {
#if defined(CARDKB_I2C)
if (!_has_cardkb) return;
// No artificial throttle: unlike a MomentaryButton (BUTTON_USE_INTERRUPTS
// latches every edge in an ISR ring buffer, so it survives a blocking e-ink
// refresh untouched), CardKB is plain I2C polling with no interrupt line on
// the Grove cable and no onboard queue -- it only ever reports "what's held
// right now". A press that starts and fully releases while curr->render()
// is blocked is physically unobservable, no software fix can recover it.
// Polling every loop() iteration (same as a digital button's check(), which
// has no throttle either) just shrinks that miss window down to exactly the
// render() duration instead of render()+30ms.
// First, keys the background task captured while the display blocked, in order.
while (_cardkb_q_tail != _cardkb_q_head) {
uint8_t q = _cardkb_q[_cardkb_q_tail];
_cardkb_q_tail = (uint8_t)((_cardkb_q_tail + 1) % CARDKB_Q_LEN);
handleCardKBByte(q);
}
uint8_t raw;
if (!readCardKBEdge(raw)) return;
handleCardKBByte(raw);
#endif
}
#if defined(CARDKB_I2C)
bool UITask::readCardKBEdge(uint8_t& raw) {
CARDKB_I2C.requestFrom(0x5F, 1);
if (!CARDKB_I2C.available()) return false;
raw = CARDKB_I2C.read();
if (raw == _cardkb_last_raw) return false; // still held (or still released) -- no new edge
_cardkb_last_raw = raw;
return raw != 0; // 0 = key just released, nothing to enqueue
}
// Single producer (the poll task) / single consumer (pollCardKB() in the
// loop) ring -- byte-sized indices, so no locking needed.
void UITask::cardkbQueuePush(uint8_t raw) {
uint8_t next = (uint8_t)((_cardkb_q_head + 1) % CARDKB_Q_LEN);
if (next == _cardkb_q_tail) return; // full: drop (16 keys within one refresh)
_cardkb_q[_cardkb_q_head] = raw;
_cardkb_q_head = next;
}
#if defined(NRF52_PLATFORM)
void UITask::cardkbTask(void* self) {
UITask* t = (UITask*)self;
SemaphoreHandle_t m = (SemaphoreHandle_t)t->_cardkb_mutex;
for (;;) {
if (t->_cardkb_bg_active && xSemaphoreTake(m, portMAX_DELAY) == pdTRUE) {
if (t->_cardkb_bg_active) { // re-check: loop may have just finished
uint8_t raw;
if (t->readCardKBEdge(raw)) t->cardkbQueuePush(raw);
}
xSemaphoreGive(m);
}
vTaskDelay(ms2tick(10)); // a keypress lasts ~50+ ms, so 10 ms never misses one
}
}
#endif
#endif
// Keep sampling the CardKB while the display blocks the loop, so keys typed
// during a (long, on big panels) refresh aren't lost. Called LAST in begin():
// the heap is tight on large-panel builds, and every screen is allocated with
// `new` -- a failed allocation leaves that screen silently unopenable. Creating
// the optional poll task afterwards means a short heap costs the task (the
// CardKB is then only read between frames, as without it), never a screen.
void UITask::startCardKBCapture() {
#if defined(CARDKB_I2C) && defined(NRF52_PLATFORM)
if (!_has_cardkb) return;
SemaphoreHandle_t m = xSemaphoreCreateMutex();
if (!m) return;
// Publish the mutex BEFORE creating the task: at higher priority it starts
// running inside xTaskCreate() and reads _cardkb_mutex straight away.
_cardkb_mutex = (void*)m;
// TASK_PRIO_NORMAL (2) preempts the loop task (TASK_PRIO_LOW, 1); BLE runs higher.
if (xTaskCreate(&UITask::cardkbTask, "cardkb", 512, this, TASK_PRIO_NORMAL, NULL) == pdPASS) {
g_cardkb_bus_mutex = m; // shared with CardKBBusLock (sensor queries)
return;
}
_cardkb_mutex = nullptr; // no task -> setCardKBBackground() stays a no-op
vSemaphoreDelete(m);
#endif
}
// Hand CardKB sampling to the background task (on = loop is about to block on
// the display) or take it back (off). Taking the mutex on the way out waits for
// any in-flight background I2C read to finish before the loop touches the bus.
void UITask::setCardKBBackground(bool on) {
#if defined(CARDKB_I2C) && defined(NRF52_PLATFORM)
if (!_cardkb_mutex) return;
if (on) {
_cardkb_bg_active = true;
} else {
SemaphoreHandle_t m = (SemaphoreHandle_t)_cardkb_mutex;
xSemaphoreTake(m, portMAX_DELAY);
_cardkb_bg_active = false;
xSemaphoreGive(m);
}
#else
(void)on;
#endif
}
void UITask::handleCardKBByte(uint8_t raw) {
#if defined(CARDKB_I2C)
// Compact mode (Settings > Keyboard's "Ext. KB" row) hides the letter grid
// entirely, and is meant to guarantee joystick-free operation: while it's
// the active surface (KeyboardWidget::inPlainGridState() -- showing, no
// popup open, not already mid cursor-move) arrows drive the text cursor
// directly instead of a grid selection nobody could see anyway, and plain
// Tab opens the placeholder picker directly instead of the row/col-dependent
// Hold-Enter dispatch (which would be meaningless here -- row/col are never
// deliberately navigated to in this mode). Cursor mode / the accent /
// placeholder popups all render their own visible feedback regardless of
// Compact, so none of this applies once inPlainGridState() is false --
// arrows/Tab fall through to their normal meaning there (e.g. arrows drive
// the placeholder/accent popup's own selection).
bool compact_grid = _node_prefs && _node_prefs->keyboard_cardkb_compact && _kb.inPlainGridState();
char key;
if (raw == 0xA3) { // Fn+Enter -- submit the field
key = KEY_KB_ENTER;
} else if (compact_grid && (raw == (uint8_t)KEY_LEFT || raw == (uint8_t)KEY_UP ||
raw == (uint8_t)KEY_DOWN || raw == (uint8_t)KEY_RIGHT)) {
char woke = checkDisplayOn((char)raw); // already sets _next_refresh=0 when the display was on
if (woke && !_locked) _kb.moveCursorDirect((char)raw);
return;
} else if (raw == 0x09) { // Tab -- Hold-Enter equivalent, always (single shortcut: there used to
if (compact_grid) { // also be a separate Fn+Tab for this, but plain Tab already covers every
char woke = checkDisplayOn((char)raw); // case Fn+Tab did -- outside the keyboard, and now inside it
if (woke && !_locked) _kb.openPlaceholders(); // too -- so the modifier was pure redundancy)
return;
}
key = KEY_CONTEXT_MENU;
} else if (raw == 0x80) {
// Fn+Esc -- CardKB's lock/unlock gesture: a single press toggles _locked
// directly (unlike the physical Hold-Back+3xEnter combo's 3-press
// sequence), so it works to unlock a locked device too, where every
// other CardKB key is correctly discarded (see the Fn+<letter> branch
// below). Esc, not the adjacent Fn+Backspace, on purpose: Fn and
// Backspace sit right next to each other on CardKB's layout, making that
// combo too easy to hit by accident; Esc is on the opposite side of the
// keyboard. One press is enough -- Fn+Esc is already a deliberate
// two-key combo, so it doesn't need the physical combo's extra 3x
// repetition to guard against accidental triggering.
if (_display && !_display->isOn()) _display->turnOn();
toggleLock();
return;
} else if (raw >= 0x80 && raw <= 0xAF) { // Fn+<letter> -- open its accent popup
char base = CARDKB_FN_BASE[raw - 0x80];
if (base == 0) return; // Fn+digit/symbol/arrow -- not used by this UI
char woke = checkDisplayOn(base);
// Every other key here goes through enqueueKey(), so it's naturally eaten
// while locked (see the dequeue-time "if (!_locked && curr)" gate in
// loop()). This path calls into the keyboard widget directly instead, so
// it needs its own _locked check -- otherwise a stray Fn+letter (e.g. the
// keyboard was left open before the device locked, or brushed against in
// a pocket) could pop the accent popup while the screen is supposed to
// ignore all input.
if (woke && !_locked) _kb.openAccentFor(base);
return;
} else {
// Plain Enter would otherwise commit whatever grid cell row/col happen to
// be frozen at (there's no grid navigation to have deliberately landed on
// one in Compact) -- submit instead, same as Fn+Enter. Backspace/ASCII
// passthrough is unaffected by Compact either way.
key = (compact_grid && raw == (uint8_t)KEY_ENTER) ? KEY_KB_ENTER : (char)raw;
}
enqueueKey(checkDisplayOn(key));
#endif
}
File diff suppressed because it is too large Load Diff
+138
View File
@@ -0,0 +1,138 @@
#pragma once
// Included into UITask.cpp only.
#if defined(UI_PERF_L1) && defined(NRF52_PLATFORM)
// -D UI_PERF_L1: walks the main screens by itself (keys fed into the queue) and
// prints, per step over USB serial: frames drawn, frames the display skipped
// as unchanged, render/flush time (avg/max us), heap (used/free/largest
// block) and every FreeRTOS task's stack headroom. Measurement only.
#include "FreeRTOS.h"
#include "task.h"
#include <malloc.h>
extern unsigned char __HeapBase[];
extern unsigned char __HeapLimit[];
extern uint32_t g_disp_flushes;
namespace perfl1 {
// Each step opens a screen directly (so the walk doesn't depend on which
// Home pages are enabled), then types keys: R/L/U/D/E/X, H = hold Enter.
enum Open : uint8_t { NONE, HOME, SETTINGS, MESSAGES, TOOLS, NEARBY, TRAIL, MAP, COMPASS, STATUS,
LIVE, LOCATOR, ADVERT, REPEATER, BOT, CLOCKTOOLS, CHANNEL0 };
struct Step { const char* name; Open open; const char* keys; uint16_t dwell_ms; };
static const Step STEPS[] = {
{"boot-idle", NONE, "", 3000},
{"home", HOME, "", 2000},
{"home-R1", NONE, "R", 1500}, {"home-R2", NONE, "R", 1500}, {"home-R3", NONE, "R", 1500},
{"home-R4", NONE, "R", 1500}, {"home-R5", NONE, "R", 1500}, {"home-R6", NONE, "R", 1500},
{"home-R7", NONE, "R", 1500}, {"home-R8", NONE, "R", 1500},
{"home-R9", NONE, "R", 1500}, {"home-R10", NONE, "R", 1500}, {"home-R11", NONE, "R", 1500},
{"home-R12", NONE, "R", 1500}, {"home-R13", NONE, "R", 1500},
{"settings", SETTINGS, "", 2000}, {"set-display", NONE, "E", 2000},
{"set-scroll", NONE, "DDDDDDDDDDDD", 2000},
{"messages", MESSAGES, "", 2000}, {"dm-list", NONE, "E", 2000},
{"dm-scroll", NONE, "DDDDDDDDDDDDDDDDDDDD", 2000},
{"dm-thread", NONE, "E", 3000}, {"dm-thread-up", NONE, "UUUU", 2000},
{"channel", CHANNEL0, "", 3000}, {"channel-up", NONE, "UUUUUU", 2000},
{"compose", NONE, "EE", 2000}, {"kbd-type", NONE, "RRDRRDRRE", 2000},
{"kbd-close", NONE, "XXX", 1500},
{"nearby", NEARBY, "", 3000}, {"nearby-scroll", NONE, "DDDDDDDDDDDDDDDDDDDD", 2000},
{"tools", TOOLS, "", 1500}, {"trail", TRAIL, "", 2000}, {"map", MAP, "", 3000},
{"compass", COMPASS, "", 3000},
{"st-radio", STATUS, "", 3000}, {"st-radio-dn", NONE, "DD", 2000}, {"st-gps", NONE, "UUR", 3000},
{"st-gps-dn", NONE, "DDD", 2000}, {"st-sky", NONE, "UUUR", 4000}, {"st-signal", NONE, "E", 3000},
{"st-power", NONE, "R", 2000}, {"st-mesh", NONE, "R", 2000}, {"st-mesh-dn", NONE, "DDD", 2000},
{"st-system", NONE, "UUUR", 2000}, {"st-system-dn", NONE, "DDDDDD", 2000},
{"liveshare", LIVE, "", 1500}, {"locator", LOCATOR, "", 1500}, {"autoadvert", ADVERT, "", 1500},
{"repeater", REPEATER, "", 1500}, {"bot", BOT, "", 1500}, {"clocktools", CLOCKTOOLS, "", 1500},
{"home-end", HOME, "", 3000},
{"done", NONE, "", 500},
};
static int step = -1;
static uint32_t step_at = 0, frames = 0, r_sum = 0, r_max = 0, f_sum = 0, f_max = 0, fl0 = 0;
static uint32_t heap_min_free = 0xFFFFFFFF;
static uint32_t largestBlock() { // bisect the biggest malloc that succeeds
uint32_t lo = 0, hi = (uint32_t)(__HeapLimit - __HeapBase);
while (hi - lo > 64) {
uint32_t mid = (lo + hi) / 2;
void* q = malloc(mid);
if (q) { free(q); lo = mid; } else hi = mid;
}
return lo;
}
static void heap(uint32_t& used, uint32_t& freeb) {
uint32_t total = (uint32_t)(__HeapLimit - __HeapBase);
used = (uint32_t)mallinfo().uordblks;
freeb = used < total ? total - used : 0;
}
void frame(uint32_t render_us, uint32_t flush_us) {
frames++; r_sum += render_us; f_sum += flush_us;
if (render_us > r_max) r_max = render_us;
if (flush_us > f_max) f_max = flush_us;
}
static void report(const char* name) {
uint32_t used, freeb; heap(used, freeb);
if (freeb < heap_min_free) heap_min_free = freeb;
uint32_t fl = g_disp_flushes - fl0;
Serial.printf("PERF %-12s frames %3lu sent %3lu render avg %5lu max %6lu flush avg %5lu max %6lu us | heap used %lu free %lu largest %lu min %lu\n",
name, frames, fl, frames ? r_sum / frames : 0, r_max, frames ? f_sum / frames : 0, f_max,
used, freeb, largestBlock(), heap_min_free);
}
static void tasks() {
TaskStatus_t st[16];
UBaseType_t n = uxTaskGetSystemState(st, 16, NULL);
for (UBaseType_t i = 0; i < n; i++)
Serial.printf("TASK %-10s stack free %5lu B\n", st[i].pcTaskName, (unsigned long)st[i].usStackHighWaterMark * sizeof(StackType_t));
}
static DisplayDriver* disp = nullptr;
static void shot(const char* name) { // the 1 KB page buffer as hex, for a PNG on the host
if (!disp || !disp->getBuffer()) return;
const uint8_t* b = disp->getBuffer();
Serial.printf("SHOT %s ", name);
for (uint16_t i = 0; i < disp->getBufferSize(); i++) Serial.printf("%02x", b[i]);
Serial.println();
}
static char keyOf(char c) {
switch (c) { case 'R': return KEY_RIGHT; case 'L': return KEY_LEFT; case 'U': return KEY_UP;
case 'D': return KEY_DOWN; case 'E': return KEY_ENTER; case 'X': return KEY_CANCEL; case 'H': return KEY_CONTEXT_MENU; }
return 0;
}
static const char* pending = nullptr;
static uint32_t next_key_at = 0;
// Returns a key to enqueue (0 = none).
Open open_now = NONE;
char tick() {
uint32_t now = millis();
if (step >= (int)(sizeof(STEPS) / sizeof(STEPS[0]))) return 0;
if (pending && *pending) {
if ((int32_t)(now - next_key_at) < 0) return 0;
next_key_at = now + 300;
char k = keyOf(*pending++);
if (!*pending) { step_at = now; frames = r_sum = r_max = f_sum = f_max = 0; fl0 = g_disp_flushes; }
return k;
}
if (step >= 0 && (int32_t)(now - step_at) < (int32_t)STEPS[step].dwell_ms) return 0;
if (step >= 0) { report(STEPS[step].name); shot(STEPS[step].name); }
step++;
if (step >= (int)(sizeof(STEPS) / sizeof(STEPS[0]))) { tasks(); Serial.println("PERF end"); return 0; }
if (step == 0) tasks();
pending = STEPS[step].keys;
open_now = STEPS[step].open;
step_at = now; frames = r_sum = r_max = f_sum = f_max = 0; fl0 = g_disp_flushes;
next_key_at = now;
return 0;
}
}
// micros() ticks in ~1 ms steps here (RTC-based), so time with the CPU cycle counter.
static inline uint32_t perfUs() {
static bool on = false;
if (!on) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; on = true; }
return DWT->CYCCNT; // cycles; differences stay right across the 67 s wrap
}
#define PERF_T0() uint32_t _pf0 = perfUs()
#define PERF_T1() uint32_t _pf1 = perfUs()
#define PERF_T2() perfl1::frame((_pf1 - _pf0) / (SystemCoreClock / 1000000), (perfUs() - _pf1) / (SystemCoreClock / 1000000))
#else
#define PERF_T0()
#define PERF_T1()
#define PERF_T2()
#endif
@@ -0,0 +1,98 @@
#pragma once
// Included into UITask.cpp only (see the note on screen fragments there).
// Boot splash, as on the L2 (ui-lvgl/Splash.h, same lettering from
// ui-core/Lettering.h): the MeshCore wordmark rising into place, "solo" in its
// lettering once it has landed, the Solo version, upstream version + build date, and three dots
// rising in turn while the device starts. E-ink gets the still final frame.
class SplashScreen : public UIScreen {
UITask* _task;
unsigned long _start, dismiss_after;
char _solo_ver[24];
char _line2[32];
// A word in the wordmark's lettering at `sc` px a pixel, top-left at (x, y).
static void drawLettering(DisplayDriver& d, const char* text, int x, int y, int gap, int sc) {
for (const char* p = text; *p; p++) {
char c = (char)tolower((unsigned char)*p);
int cw = lettering::charW(c);
if (cw < 0) continue;
for (int r = 0; r < lettering::LOGO_H; r++)
for (int col = 0; col < cw; ) { // runs of inked pixels as one rect
if (!lettering::inked(c, col, r)) { col++; continue; }
int e = col;
while (e + 1 < cw && lettering::inked(c, e + 1, r)) e++;
d.fillRect(x + col * sc, y + r * sc, (e - col + 1) * sc, sc);
col = e + 1;
}
x += cw * sc + gap;
}
}
public:
SplashScreen(UITask* task) : _task(task) {
lettering::soloVersion(_solo_ver, sizeof(_solo_ver));
char date[16];
lettering::buildDate(date, sizeof(date));
#ifdef MESHCORE_VERSION
snprintf(_line2, sizeof(_line2), "%s %s", MESHCORE_VERSION, date);
#else
snprintf(_line2, sizeof(_line2), "%s", date);
#endif
_start = millis();
dismiss_after = _start + BOOT_SCREEN_MILLIS;
}
int render(DisplayDriver& display) override {
const bool anim = !display.isEink();
const unsigned long t = anim ? millis() - _start : 100000UL; // e-ink: the settled frame
display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT);
const int lh = display.getLineHeight();
const int cx = display.width() / 2;
// The wordmark and "solo" grow in whole pixels with the panel (1x on 128 px,
// 3x on the 4.2"), as far as the block still fits over the dots.
const int dots_h = 8;
int sc = display.width() / lettering::LOGO_W;
while (sc > 1 && (lettering::LOGO_H * 2 + 5) * sc + lh * 2 + 2 > display.height() - dots_h) sc--;
if (sc < 1) sc = 1;
// Block from the wordmark down to the second text line, centred above the dots.
const int logo_h = lettering::LOGO_H * sc;
const int block_h = logo_h * 2 + 5 * sc + lh * 2 + 2;
int top = (display.height() - dots_h - block_h) / 2;
if (top < 0) top = 0;
// Wordmark: rises 6 px into place over the first 400 ms (ease-out).
int rise = 0;
if (t < 400) { int k = 400 - (int)t; rise = (6 * sc * k * k) / (400 * 400); }
const int lx = (display.width() - lettering::LOGO_W * sc) / 2;
for (int r = 0; r < lettering::LOGO_H; r++)
for (int col = 0; col < lettering::LOGO_W; ) { // runs of inked pixels as one rect
if (!lettering::logoBit(col, r)) { col++; continue; }
int e = col;
while (e + 1 < lettering::LOGO_W && lettering::logoBit(e + 1, r)) e++;
display.fillRect(lx + col * sc, top + rise + r * sc, (e - col + 1) * sc, sc);
col = e + 1;
}
int y = top + logo_h + 3 * sc;
const int solo_w = lettering::textW("solo", 0) * sc + 2 * sc * 3;
if (t >= 400) drawLettering(display, "solo", cx - solo_w / 2, y, 2 * sc, sc); // once the wordmark has landed
y += logo_h + 2 * sc;
display.drawTextCentered(cx, y, _solo_ver[0] ? _solo_ver : "dev");
y += lh + 2;
display.drawTextCentered(cx, y, _line2);
drawLoadingDots(display, cx, display.height() - 2); // as the L2's
return anim ? 40 : 1000;
}
void poll() override {
if ((int32_t)(millis() - dismiss_after) >= 0) {
_task->gotoHomeScreen();
}
}
};
File diff suppressed because it is too large Load Diff