Fixes and new apps (#30)

- M5 Unit Modules library + M5 Unit Test app
- Minor fixes for TodoList, TwoEleven, Snake, Brainfuck and Breakout
- Fixed SerialConsole to use the uart controller as it was broken in one of the many updates
- Fixed keyboard input in TwoEleven, Breakout, Magic8Ball and Snake
- Bluetooth Media Keys app, supports pressing physical keys to trigger the corresponding buttonmatrix button
- Epub Reader app
This commit is contained in:
Shadowtrance
2026-06-07 23:56:32 +10:00
committed by GitHub
parent 1f959c8bbf
commit dbf850c434
127 changed files with 11156 additions and 174 deletions
@@ -0,0 +1,428 @@
#include "TestUnitLcdGfx.h"
#include "UiScale.h"
#include <tactility/device.h>
#include <tactility/lvgl_fonts.h>
#include <esp_timer.h>
#include <cstring>
#include <cstdio>
#include <algorithm>
static inline uint32_t usNow() {
return (uint32_t)esp_timer_get_time();
}
static const char* PHASE_NAMES[] = {
"Screen fill",
"Text",
"Pixels",
"Lines",
"H/V Lines",
"Rectangles (filled)",
"Rectangles (outline)",
"Triangles (filled)",
"Triangles (outline)",
"Circles (filled)",
"Circles (outline)",
"Arcs (filled)",
"Arcs (outline)",
"Round rects (filled)",
"Round rects (outline)",
"Results",
};
static constexpr int PHASE_COUNT = (int)(sizeof(PHASE_NAMES) / sizeof(PHASE_NAMES[0]));
// ---------------------------------------------------------------------------
void TestUnitLcdGfx::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
app_ = app;
phase_ = 0;
logBuf_[0] = '\0';
memset(results_, 0, sizeof(results_));
createToolbar(parent, handle, "LCD Gfx Test");
createBanner(parent, "LCD Gfx", "I2C", COLOR_I2C);
lv_obj_t* cont = lv_obj_create(parent);
lv_obj_set_width(cont, LV_PCT(100));
lv_obj_set_flex_grow(cont, 1);
lv_obj_set_layout(cont, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_all(cont, uiPad(), 0);
lv_obj_set_style_pad_row(cont, uiRowGap(), 0);
lv_obj_set_style_bg_opa(cont, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(cont, 0, 0);
const lv_font_t* fnt = lvgl_get_text_font(uiFont());
const lv_font_t* fntS = lvgl_get_text_font(FONT_SIZE_SMALL);
lblPhase_ = lv_label_create(cont);
lv_obj_set_style_text_font(lblPhase_, fnt, 0);
lv_label_set_text(lblPhase_, "Searching...");
lblLog_ = lv_label_create(cont);
lv_obj_set_style_text_font(lblLog_, fntS, 0);
lv_label_set_long_mode(lblLog_, LV_LABEL_LONG_WRAP);
lv_obj_set_width(lblLog_, LV_PCT(100));
lv_label_set_text(lblLog_, "");
Device* i2c = device_find_by_name("i2c1");
if (!i2c) { lv_label_set_text(lblPhase_, "i2c1 not found"); return; }
if (lcd_.begin(i2c)) {
usingPaHub_ = false;
} else if (hub_.begin(i2c)) {
usingPaHub_ = true;
bool found = false;
for (uint8_t ch = 0; ch < UnitPaHub::NUM_CHANNELS && !found; ch++) {
hub_.select(ch);
if (lcd_.begin(i2c)) found = true;
}
if (!found) {
hub_.deselect();
lv_label_set_text(lblPhase_, "LCD not found");
return;
}
} else {
lv_label_set_text(lblPhase_, "LCD not found");
return;
}
lcd_.setBrightness(180);
lcd_.setRotation(0);
// Pre-compute layout constants
w_ = (int16_t)lcd_.width();
h_ = (int16_t)lcd_.height();
minDim_ = std::min(w_, h_);
minDim1_= minDim_ - 1;
cx_ = w_ / 2;
cy_ = h_ / 2;
cx1_ = cx_ - 1;
cy1_ = cy_ - 1;
cMin_ = std::min(cx1_, cy1_);
cMin1_ = cMin_ - 1;
lv_label_set_text(lblPhase_, PHASE_NAMES[0]);
timer_ = lv_timer_create(onTimer, 200, this);
lv_timer_set_repeat_count(timer_, 1);
}
void TestUnitLcdGfx::onStop() {
if (timer_) { lv_timer_delete(timer_); timer_ = nullptr; }
selectIfNeeded();
if (lcd_.isPresent()) lcd_.setBrightness(0);
if (usingPaHub_ && hub_.isPresent()) hub_.deselect();
lblPhase_ = lblLog_ = nullptr;
}
void TestUnitLcdGfx::selectIfNeeded() {
if (usingPaHub_ && hub_.isPresent())
hub_.select(hub_.currentChannel());
}
void TestUnitLcdGfx::onTimer(lv_timer_t* t) {
static_cast<TestUnitLcdGfx*>(lv_timer_get_user_data(t))->runNextPhase();
}
void TestUnitLcdGfx::appendLog(const char* name, uint32_t us) {
size_t len = strlen(logBuf_);
size_t rem = sizeof(logBuf_) - len;
snprintf(logBuf_ + len, rem, "%-20s %lu\n", name, (unsigned long)us);
lv_label_set_text(lblLog_, logBuf_);
}
void TestUnitLcdGfx::runNextPhase() {
timer_ = nullptr;
if (!lcd_.isPresent()) return;
selectIfNeeded();
if (phase_ >= PHASE_COUNT) { lv_label_set_text(lblPhase_, "Done!"); return; }
lv_label_set_text(lblPhase_, PHASE_NAMES[phase_]);
if (phase_ < PHASE_COUNT - 1) {
// Benchmark phase
uint32_t us = 0;
switch (phase_) {
case 0: us = testFillScreen(); break;
case 1: us = testText(); break;
case 2: us = testPixels(); break;
case 3: us = testLines(); break;
case 4: us = testFastLines(); break;
case 5: us = testFilledRects(); break;
case 6: us = testRects(); break;
case 7: us = testFilledTriangles(); break;
case 8: us = testTriangles(); break;
case 9: us = testFilledCircles(); break;
case 10: us = testCircles(); break;
case 11: us = testFillArcs(); break;
case 12: us = testArcs(); break;
case 13: us = testFilledRoundRects(); break;
case 14: us = testRoundRects(); break;
}
results_[phase_] = us;
appendLog(PHASE_NAMES[phase_], us);
} else {
// Results screen on the LCD itself
drawResultsOnLcd();
lv_label_set_text(lblPhase_, "Done!");
phase_++;
return;
}
phase_++;
// Short pause between phases so the LCD buffer drains
timer_ = lv_timer_create(onTimer, 80, this);
lv_timer_set_repeat_count(timer_, 1);
}
// Draw the timing summary on the LCD unit itself, matching PDQ's results screen.
// PDQ background: c cycles 4..11 and is used directly as an RGB565 value
// (these are near-black blues: 0x0004..0x000B). The subtle banding effect is
// identical to the Arduino PDQ sketch's final loop.
void TestUnitLcdGfx::drawResultsOnLcd() {
uint16_t cyan = UnitLcd::rgb888to565(0x00FFFF);
uint16_t yellow = UnitLcd::rgb888to565(0xFFFF00);
uint16_t green = UnitLcd::rgb888to565(0x00FF00);
uint16_t magenta = UnitLcd::rgb888to565(0xFF00FF);
uint16_t W = lcd_.width(), H = lcd_.height();
// PDQ blue-band background - c is a raw RGB565 value cycling 4..11
{
uint16_t c = 4;
int8_t d = 1;
for (uint16_t i = 0; i < H; i++) {
lcd_.drawHLine(0, (uint8_t)i, (uint8_t)W, c);
c = (uint16_t)(c + d);
if (c <= 4 || c >= 11) d = -d;
}
}
// Title - "LCD GFX PDQ" in magenta (PDQ uses "Arduino GFX PDQ")
uint8_t y = 2;
lcd_.drawText(2, y, "LCD GFX PDQ", magenta, 0x0006, 1); y += 10;
// Header line - green, matching PDQ's "\nBenchmark micro-secs"
lcd_.drawText(2, y, "Benchmark micro-secs", green, 0x0006, 1); y += 10;
// Results - cyan label + yellow number, one per row, 9px line height
// Names padded to 12 chars; number right-aligned in 9 chars (matches PDQ comma style)
static const char* SHORT_NAMES[] = {
"Screen fill",
"Text ",
"Pixels ",
"Lines ",
"H/V Lines ",
"Rectangles F",
"Rectangles ",
"Triangles F",
"Triangles ",
"Circles F ",
"Circles ",
"Arcs F ",
"Arcs ",
"RoundRects F",
"RoundRects ",
};
for (int i = 0; i < RESULT_COUNT; i++) {
if ((int)y + 9 > (int)H - 9) break;
// Label in cyan
lcd_.drawText(2, y, SHORT_NAMES[i], cyan, 0x0006, 1);
// Number in yellow, formatted with commas like PDQ's printnice()
char num[14];
snprintf(num, sizeof(num), "%lu", (unsigned long)results_[i]);
// Insert commas right-to-left (PDQ style)
for (char* p = (num + strlen(num)) - 3; p > num; p -= 3) {
memmove(p + 1, p, strlen(p) + 1);
*p = ',';
}
// Right-align in the remaining width (screen is 135px, label ~72px, 63px left)
// Draw at fixed x so numbers line up
lcd_.drawText(74, y, num, yellow, 0x0006, 1);
y += 9;
}
lcd_.drawText(2, (uint8_t)(H - 9), "Benchmark Complete!", green, 0x0006, 1);
}
// ---------------------------------------------------------------------------
// Benchmark phases - matching PDQ exactly
// ---------------------------------------------------------------------------
uint32_t TestUnitLcdGfx::testFillScreen() {
uint32_t s = usNow();
lcd_.fillScreen(0xFFFF);
lcd_.fillScreen(0xF800);
lcd_.fillScreen(0x07E0);
lcd_.fillScreen(0x001F);
lcd_.fillScreen(0x0000);
return usNow() - s;
}
uint32_t TestUnitLcdGfx::testText() {
// Mirror PDQ testText() - for a 135px wide screen tsa/tsb/tsc all = 1.
// Scale 2 is used once at the end (fits: "Size 2" = 6 chars × 12px = 72px).
uint16_t black = 0x0000;
lcd_.fillScreen(black);
uint32_t s = usNow();
uint8_t y = 0;
lcd_.drawText(0, y, "Hello World!", 0xFFFF, black, 1); y += 9;
lcd_.drawText(0, y, "RED GREEN BLUE", UnitLcd::color565(255,0,0), black, 1); y += 9;
lcd_.drawText(0, y, "1234.56", UnitLcd::color565(255,255,0), black, 1); y += 9;
lcd_.drawText(0, y, "0xDEADBEEF", 0xFFFF, black, 1); y += 9;
lcd_.drawText(0, y, "Groop,", UnitLcd::color565(0,255,255), black, 1); y += 9;
lcd_.drawText(0, y, "I implore thee,", UnitLcd::color565(255,0,255), black, 1); y += 9;
lcd_.drawText(0, y, "my foonting", UnitLcd::color565(0,0,200), black, 1); y += 9;
lcd_.drawText(0, y, "turlingdromes.", UnitLcd::color565(0,128,0), black, 1); y += 9;
lcd_.drawText(0, y, "crinkly bindlewurdles",UnitLcd::color565(0,128,128), black, 1); y += 9;
lcd_.drawText(0, y, "Or I will rend thee", UnitLcd::color565(128,0,0), black, 1); y += 9;
lcd_.drawText(0, y, "gobberwartsb", UnitLcd::color565(128,0,128), black, 1); y += 9;
lcd_.drawText(0, y, "blurglecruncheon,", UnitLcd::color565(128,128,0), black, 1); y += 9;
lcd_.drawText(0, y, "see if I don't!", UnitLcd::color565(64,64,64), black, 1); y += 9;
lcd_.drawText(0, y, "Size 2", UnitLcd::color565(255,0,0), black, 2); y += 18;
lcd_.drawText(0, y, "Size 3", UnitLcd::color565(255,165,0), black, 2); // capped at 2
return usNow() - s;
}
uint32_t TestUnitLcdGfx::testPixels() {
lcd_.fillScreen(0x0000);
uint32_t s = usNow();
for (int16_t y = 0; y < h_; y++) {
for (int16_t x = 0; x < w_; x++) {
lcd_.drawPixel((uint8_t)x, (uint8_t)y,
UnitLcd::color565((uint8_t)(x << 3), (uint8_t)(y << 3),
(uint8_t)((x * y) & 0xFF)));
}
}
return usNow() - s;
}
uint32_t TestUnitLcdGfx::testLines() {
uint16_t blue = 0x001F;
lcd_.fillScreen(0x0000);
uint32_t s = usNow();
// All 4 corners x 2 sweeps each (matching PDQ exactly)
for (int16_t x = 0; x < w_; x += 6) lcd_.drawLine(0, 0, x, h_-1, blue);
for (int16_t y = 0; y < h_; y += 6) lcd_.drawLine(0, 0, w_-1, y, blue);
lcd_.fillScreen(0x0000);
for (int16_t x = 0; x < w_; x += 6) lcd_.drawLine(w_-1, 0, x, h_-1, blue);
for (int16_t y = 0; y < h_; y += 6) lcd_.drawLine(w_-1, 0, 0, y, blue);
lcd_.fillScreen(0x0000);
for (int16_t x = 0; x < w_; x += 6) lcd_.drawLine(0, h_-1, x, 0, blue);
for (int16_t y = 0; y < h_; y += 6) lcd_.drawLine(0, h_-1, w_-1, y, blue);
lcd_.fillScreen(0x0000);
for (int16_t x = 0; x < w_; x += 6) lcd_.drawLine(w_-1, h_-1, x, 0, blue);
for (int16_t y = 0; y < h_; y += 6) lcd_.drawLine(w_-1, h_-1, 0, y, blue);
return usNow() - s;
}
uint32_t TestUnitLcdGfx::testFastLines() {
lcd_.fillScreen(0x0000);
uint32_t s = usNow();
for (int16_t y = 0; y < h_; y += 5) lcd_.drawHLine(0, (uint8_t)y, (uint8_t)w_, 0xF800);
for (int16_t x = 0; x < w_; x += 5) lcd_.drawVLine((uint8_t)x, 0, (uint8_t)h_, 0x001F);
return usNow() - s;
}
uint32_t TestUnitLcdGfx::testFilledRects() {
lcd_.fillScreen(0x0000);
uint32_t s = usNow();
for (int16_t i = minDim_; i > 0; i -= 6) {
int16_t half = i / 2;
lcd_.fillRect((uint8_t)(cx_ - half), (uint8_t)(cy_ - half),
(uint8_t)(cx_ + half - 1), (uint8_t)(cy_ + half - 1),
UnitLcd::color565((uint8_t)std::min((int)i, 255),
(uint8_t)std::min((int)i, 255), 0));
}
return usNow() - s;
}
uint32_t TestUnitLcdGfx::testRects() {
// Don't clear - runs on top of filled rects (matches PDQ)
uint32_t s = usNow();
for (int16_t i = 2; i < minDim_; i += 6) {
int16_t half = i / 2;
lcd_.drawRect((uint8_t)(cx_ - half), (uint8_t)(cy_ - half),
(uint8_t)i, (uint8_t)i, 0x07E0);
}
return usNow() - s;
}
uint32_t TestUnitLcdGfx::testFilledTriangles() {
lcd_.fillScreen(0x0000);
uint32_t s = usNow();
for (int16_t i = cMin1_; i > 10; i -= 5) {
lcd_.fillTriangle(cx1_, cy1_ - i, cx1_ - i, cy1_ + i, cx1_ + i, cy1_ + i,
UnitLcd::color565(0, (uint8_t)std::min(i*2, 255), (uint8_t)std::min(i*2, 255)));
}
return usNow() - s;
}
uint32_t TestUnitLcdGfx::testTriangles() {
// Don't clear - runs on top (matches PDQ)
uint32_t s = usNow();
for (int16_t i = 0; i < cMin_; i += 5) {
lcd_.drawTriangle(cx1_, cy1_ - i, cx1_ - i, cy1_ + i, cx1_ + i, cy1_ + i,
UnitLcd::color565(0, 0, (uint8_t)std::min(i*4, 255)));
}
return usNow() - s;
}
uint32_t TestUnitLcdGfx::testFilledCircles() {
lcd_.fillScreen(0x0000);
uint32_t s = usNow();
for (int16_t x = 10; x < (int16_t)w_; x += 20)
for (int16_t y = 10; y < (int16_t)h_; y += 20)
lcd_.fillCircle(x, y, 10, 0xF81F);
return usNow() - s;
}
uint32_t TestUnitLcdGfx::testCircles() {
// Don't clear (matches PDQ)
uint32_t s = usNow();
for (int16_t x = 0; x <= (int16_t)w_ + 10; x += 20)
for (int16_t y = 0; y <= (int16_t)h_ + 10; y += 20)
lcd_.drawCircle(x, y, 10, 0xFFFF);
return usNow() - s;
}
uint32_t TestUnitLcdGfx::testFillArcs() {
lcd_.fillScreen(0x0000);
int16_t r = (cMin_ > 0) ? (360 / cMin_) : 6;
uint32_t s = usNow();
for (int16_t i = 6; i < cMin_; i += 6)
lcd_.fillArc(cx1_, cy1_, i, i - 3, 0.0f, (float)(i * r), 0xF800);
return usNow() - s;
}
uint32_t TestUnitLcdGfx::testArcs() {
// Don't clear (matches PDQ)
int16_t r = (cMin_ > 0) ? (360 / cMin_) : 6;
uint32_t s = usNow();
for (int16_t i = 6; i < cMin_; i += 6)
lcd_.drawArc(cx1_, cy1_, i, i - 3, 0.0f, (float)(i * r), 0xFFFF);
return usNow() - s;
}
uint32_t TestUnitLcdGfx::testFilledRoundRects() {
lcd_.fillScreen(0x0000);
uint32_t s = usNow();
for (int16_t i = minDim1_; i > 20; i -= 6) {
int16_t half = i / 2;
lcd_.fillRoundRect(cx_ - half, cy_ - half, i, i, i / 8,
UnitLcd::color565(0, (uint8_t)std::min(i*2, 255), 0));
}
return usNow() - s;
}
uint32_t TestUnitLcdGfx::testRoundRects() {
// Don't clear (matches PDQ)
uint32_t s = usNow();
for (int16_t i = 20; i < minDim1_; i += 6) {
int16_t half = i / 2;
lcd_.drawRoundRect(cx_ - half, cy_ - half, i, i, i / 8,
UnitLcd::color565((uint8_t)std::min(i*2, 255), 0, 0));
}
return usNow() - s;
}