Files
tactility_apps/Apps/M5UnitTest/main/Source/TestUnitLcdGfx.cpp
T
Shadowtrance dbf850c434 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
2026-06-07 15:56:32 +02:00

429 lines
15 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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;
}