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`
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
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
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
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
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
+1 -1
View File
@@ -1,6 +1,6 @@
#pragma once
// 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):
// the 128x13 wordmark bitmap, its own letters, the extra characters drawn in
// 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