mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 02:11:57 +00:00
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:
@@ -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,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 /
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user