feat(ui-lvgl): splash text in the MeshCore wordmark's lettering

"solo", our version, upstream's and the build date (as digits) are drawn
in the wordmark's letters, the missing ones (l, v, d, digits, '.', '-')
made to match; a line with any other character falls back to Noto.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-26 22:58:53 +02:00
co-authored by Claude Opus 5.5
parent c7c150da58
commit 44fea2fa6a
2 changed files with 160 additions and 43 deletions
+157 -41
View File
@@ -1,7 +1,9 @@
// Boot splash -- included into UITask.cpp only.
//
// The MeshCore wordmark (ui-new's 128x13 bitmap, drawn 2x in amber), "SOLO",
// the Solo and upstream versions and the build date, with three dots rising
// The MeshCore wordmark (ui-new's 128x13 bitmap, drawn 2x in amber), "solo",
// the Solo and upstream versions and the build date -- all in the wordmark's
// lettering (its own letters, plus l, v, d, digits, '.', '-' drawn to match;
// a line with anything else falls back to Noto) -- with three dots rising
// in turn while the rest of the device starts. Covers the whole display
// (status bar too) from lv_layer_top, fades out after SPLASH_MS; a tap skips
// it. Home -- and the PIN lock, if set -- are already built underneath.
@@ -27,17 +29,59 @@ static const uint8_t LOGO[] = {
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8,
};
// The wordmark's own letters: columns of LOGO.
struct LogoLetter { char c; uint8_t x0, w; };
static const LogoLetter LOGO_LETTERS[] = {
{ 'm', 0, 18 }, { 'e', 19, 15 }, { 's', 35, 15 }, { 'h', 50, 16 }, { 'c', 66, 14 }, { 'o', 80, 16 }, { 'r', 96, 16 },
};
// The rest, drawn upright in the same strokes (3 px bars at the top, middle
// and bottom); slanted like the wordmark when drawn (SLANT).
struct PixGlyph { char c; const char* rows[LOGO_H]; };
#define F10 "##########"
#define L10 "###......."
#define R10 ".......###"
#define B10 "###....###"
static const PixGlyph GLYPHS[] = {
{ '0', { F10, F10, F10, B10, B10, B10, B10, B10, B10, B10, F10, F10, F10 } },
{ '1', { "....####..", "...#####..", "..######..", "....####..", "....####..", "....####..", "....####..",
"....####..", "....####..", "....####..", "....####..", "....####..", "....####.." } },
{ '2', { F10, F10, F10, R10, R10, F10, F10, F10, L10, L10, F10, F10, F10 } },
{ '3', { F10, F10, F10, R10, R10, F10, F10, F10, R10, R10, F10, F10, F10 } },
{ '4', { B10, B10, B10, B10, B10, F10, F10, F10, R10, R10, R10, R10, R10 } },
{ '5', { F10, F10, F10, L10, L10, F10, F10, F10, R10, R10, F10, F10, F10 } },
{ '6', { F10, F10, F10, L10, L10, F10, F10, F10, B10, B10, F10, F10, F10 } },
{ '7', { F10, F10, F10, R10, R10, R10, R10, R10, R10, R10, R10, R10, R10 } },
{ '8', { F10, F10, F10, B10, B10, F10, F10, F10, B10, B10, F10, F10, F10 } },
{ '9', { F10, F10, F10, B10, B10, F10, F10, F10, R10, R10, F10, F10, F10 } },
{ '.', { "....", "....", "....", "....", "....", "....", "....", "....", "....", "....", "###.", "###.", "###." } },
{ '-', { ".......", ".......", ".......", ".......", ".......", "#######", "#######", "#######",
".......", ".......", ".......", ".......", "......." } },
{ 'v', { "###.....###", "###.....###", "###.....###", "###.....###", "###.....###", "###.....###", "###.....###",
"###.....###", ".###...###.", ".###...###.", "..###.###..", "...#####...", "....###...." } },
{ 'l', { "###......", "###......", "###......", "###......", "###......", "###......", "###......",
"###......", "###......", "###......", "#########", "#########", "#########" } },
{ 'd', { "#########.", F10, F10, B10, B10, B10, B10, B10, B10, B10, F10, F10, "#########." } },
{ ' ', { ".....", ".....", ".....", ".....", ".....", ".....", ".....", ".....", ".....", ".....", ".....", ".....", "....." } },
};
#undef F10
#undef L10
#undef R10
#undef B10
static const uint8_t SLANT[LOGO_H] = { 2, 2, 2, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0 }; // px right, per row
static lv_obj_t* s_root = nullptr;
static uint32_t* s_px = nullptr; // ARGB8888 pixels of the scaled logo
static lv_image_dsc_t s_img;
static const int MAX_IMGS = 6;
static uint32_t* s_px[MAX_IMGS]; // ARGB8888 pixels of each drawn line
static lv_image_dsc_t s_img[MAX_IMGS];
static int s_imgs = 0;
static lv_timer_t* s_timer = nullptr;
static void freeAll(lv_anim_t* a) {
(void)a;
if (s_root) lv_obj_delete(s_root);
s_root = nullptr;
if (s_px) lv_free(s_px);
s_px = nullptr;
for (int i = 0; i < s_imgs; i++) lv_free(s_px[i]);
s_imgs = 0;
}
static void dismiss() {
@@ -50,27 +94,83 @@ static void dismiss() {
static void onTimer(lv_timer_t* t) { (void)t; dismiss(); }
static void onTap(lv_event_t* e) { (void)e; dismiss(); }
// The logo bitmap scaled up into an ARGB8888 image in `col`.
static bool buildLogo(uint32_t col) {
const int W = LOGO_W * LOGO_SCALE, H = LOGO_H * LOGO_SCALE;
s_px = (uint32_t*)lv_malloc((size_t)W * H * 4);
if (!s_px) return false;
// One character of the lettering: its width, and whether (x, y) is inked.
static const LogoLetter* logoLetter(char c) {
for (const LogoLetter& l : LOGO_LETTERS) if (l.c == c) return &l;
return nullptr;
}
static const PixGlyph* pixGlyph(char c) {
for (const PixGlyph& g : GLYPHS) if (g.c == c) return &g;
return nullptr;
}
static int charW(char c) {
if (const LogoLetter* l = logoLetter(c)) return l->w;
if (const PixGlyph* g = pixGlyph(c)) return (int)strlen(g->rows[0]) + 2; // + the slant
return -1;
}
static bool inked(char c, int x, int y) {
if (const LogoLetter* l = logoLetter(c)) {
int sx = l->x0 + x;
return LOGO[y * (LOGO_W / 8) + sx / 8] & (0x80 >> (sx % 8));
}
const PixGlyph* g = pixGlyph(c);
int gx = x - SLANT[y];
return g && gx >= 0 && gx < (int)strlen(g->rows[y]) && g->rows[y][gx] == '#';
}
// `text` in the lettering (lower case), -1 if it has a character that isn't drawn.
static int textW(const char* text, int gap) {
int w = 0;
for (const char* p = text; *p; p++) {
int cw = charW((char)tolower((unsigned char)*p));
if (cw < 0) return -1;
w += cw + (p[1] ? gap : 0);
}
return w;
}
// A line in the lettering, `scale` x, in `col`: an image, or a Noto label when
// the text has a character the lettering lacks. `text` null = the wordmark.
static lv_obj_t* letters(lv_obj_t* parent, const char* text, int scale, int gap, uint32_t col, const lv_font_t* fallback) {
int w = text ? textW(text, gap) : LOGO_W;
if (w <= 0 || s_imgs >= MAX_IMGS) return label(parent, text ? text : "MeshCore", fallback, col);
const int W = w * scale, H = LOGO_H * scale;
uint32_t* px = (uint32_t*)lv_malloc((size_t)W * H * 4);
if (!px) return label(parent, text ? text : "MeshCore", fallback, col);
memset(px, 0, (size_t)W * H * 4);
uint32_t on = 0xFF000000u | (col & 0xFFFFFFu);
for (int y = 0; y < H; y++)
for (int x = 0; x < W; x++) {
int sx = x / LOGO_SCALE, sy = y / LOGO_SCALE;
bool bit = LOGO[sy * (LOGO_W / 8) + sx / 8] & (0x80 >> (sx % 8));
s_px[y * W + x] = bit ? on : 0;
auto plot = [&](int x, int y) {
for (int dy = 0; dy < scale; dy++)
for (int dx = 0; dx < scale; dx++) px[(y * scale + dy) * W + x * scale + dx] = on;
};
if (!text) {
for (int y = 0; y < LOGO_H; y++)
for (int x = 0; x < LOGO_W; x++)
if (LOGO[y * (LOGO_W / 8) + x / 8] & (0x80 >> (x % 8))) plot(x, y);
} else {
int ox = 0;
for (const char* p = text; *p; p++) {
char c = (char)tolower((unsigned char)*p);
int cw = charW(c);
for (int y = 0; y < LOGO_H; y++)
for (int x = 0; x < cw; x++)
if (inked(c, x, y)) plot(ox + x, y);
ox += cw + gap;
}
memset(&s_img, 0, sizeof(s_img));
s_img.header.magic = LV_IMAGE_HEADER_MAGIC;
s_img.header.cf = LV_COLOR_FORMAT_ARGB8888;
s_img.header.w = W;
s_img.header.h = H;
s_img.header.stride = W * 4;
s_img.data_size = (uint32_t)W * H * 4;
s_img.data = (const uint8_t*)s_px;
return true;
}
lv_image_dsc_t& d = s_img[s_imgs];
s_px[s_imgs++] = px;
memset(&d, 0, sizeof(d));
d.header.magic = LV_IMAGE_HEADER_MAGIC;
d.header.cf = LV_COLOR_FORMAT_ARGB8888;
d.header.w = W;
d.header.h = H;
d.header.stride = W * 4;
d.data_size = (uint32_t)W * H * 4;
d.data = (const uint8_t*)px;
lv_obj_t* img = lv_image_create(parent);
lv_image_set_src(img, &d);
return img;
}
// One of the loading dots: rises 6 px and back, each a beat after the last.
@@ -100,6 +200,23 @@ static void soloVersion(char* out, size_t n) {
memcpy(out, ver, len);
out[len] = '\0';
}
// The build date as digits, "2026-09-26": build.sh gives "26-Sep-2026",
// __DATE__ "Sep 26 2026". Anything else is passed through.
static void buildDate(char* out, size_t n) {
static const char MON[] = "JanFebMarAprMayJunJulAugSepOctNovDec";
const char* d = FIRMWARE_BUILD_DATE;
int day = 0, year = 0, mon = 0;
char m[4] = "";
if (sscanf(d, "%d-%3s-%d", &day, m, &year) != 3 && sscanf(d, "%3s %d %d", m, &day, &year) != 3) {
snprintf(out, n, "%s", d);
return;
}
const char* f = strstr(MON, m);
mon = f && m[0] ? (int)(f - MON) / 3 + 1 : 0;
if (mon) snprintf(out, n, "%04d-%02d-%02d", year, mon, day);
else snprintf(out, n, "%s", d);
}
} // namespace splash
void UITask::showSplash() {
@@ -113,26 +230,25 @@ void UITask::showSplash() {
lv_obj_add_flag(s_root, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(s_root, onTap, LV_EVENT_CLICKED, NULL);
if (buildLogo(theme::ACCENT)) {
lv_obj_t* logo = lv_image_create(s_root);
lv_image_set_src(logo, &s_img);
lv_obj_align(logo, LV_ALIGN_CENTER, 0, -42);
anim::rise(logo, 10, 400);
}
lv_obj_t* solo = label(s_root, "S O L O", THEME_FONT_TITLE, theme::TEXT);
lv_obj_align(solo, LV_ALIGN_CENTER, 0, -8);
lv_obj_t* logo = letters(s_root, nullptr, LOGO_SCALE, 0, theme::ACCENT, THEME_FONT_LARGE);
lv_obj_align(logo, LV_ALIGN_CENTER, 0, -50);
anim::rise(logo, 10, 400);
lv_obj_t* solo = letters(s_root, "solo", LOGO_SCALE, 3, theme::TEXT, THEME_FONT_LARGE);
lv_obj_align(solo, LV_ALIGN_CENTER, 0, -14);
char ver[24], line[64];
// Ours, then upstream's, then the build date.
char ver[24], line[48], date[24];
soloVersion(ver, sizeof(ver));
lv_obj_t* v = label(s_root, ver[0] ? ver : "dev", THEME_FONT_BODY, theme::ACCENT);
lv_obj_align(v, LV_ALIGN_CENTER, 0, 22);
lv_obj_t* v = letters(s_root, ver[0] ? ver : "dev", 1, 2, theme::ACCENT, THEME_FONT_BODY);
lv_obj_align(v, LV_ALIGN_CENTER, 0, 18);
int y = 38;
#ifdef MESHCORE_VERSION
snprintf(line, sizeof(line), "MeshCore %s - built %s", MESHCORE_VERSION, FIRMWARE_BUILD_DATE);
#else
snprintf(line, sizeof(line), "Built %s", FIRMWARE_BUILD_DATE);
snprintf(line, sizeof(line), "meshcore %s", MESHCORE_VERSION);
lv_obj_align(letters(s_root, line, 1, 2, theme::TEXT_MUTED, THEME_FONT_SMALL), LV_ALIGN_CENTER, 0, y);
y += 20;
#endif
lv_obj_t* up = label(s_root, line, THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_obj_align(up, LV_ALIGN_CENTER, 0, 42);
buildDate(date, sizeof(date));
lv_obj_align(letters(s_root, date, 1, 2, theme::TEXT_MUTED, THEME_FONT_SMALL), LV_ALIGN_CENTER, 0, y);
lv_obj_t* dots = lv_obj_create(s_root);
lv_obj_remove_style_all(dots);