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
+10
View File
@@ -0,0 +1,10 @@
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
file(GLOB_RECURSE UNIT_MODULE_FILES ../../../Libraries/M5UnitModules/Source/*.c*)
idf_component_register(
SRCS ${SOURCE_FILES} ${UNIT_MODULE_FILES}
# Library headers must be included directly,
# because all regular dependencies get stripped by elf_loader's cmake script
INCLUDE_DIRS ../../../Libraries/TactilityCpp/Include ../../../Libraries/M5UnitModules/Include
REQUIRES TactilitySDK
)
+121
View File
@@ -0,0 +1,121 @@
#include "M5UnitTest.h"
#include "TestListView.h"
#include "TestViewBase.h"
#include "TestUnit8Encoder.h"
#include "TestUnitByteButton.h"
#include "TestUnitJoystick2.h"
#include "TestUnitScroll.h"
#include "TestUnitPaHub.h"
#include "TestUnitLcd.h"
#include "TestUnitDualButton.h"
#include "TestUnitCardKB2.h"
#include "TestUnitMidi.h"
#include "TestUnitRfid2.h"
#include "TestUnitLcdGfx.h"
#include <tactility/device.h>
#include <tt_lvgl_toolbar.h>
#include <esp_log.h>
constexpr auto* TAG = "M5UnitTest";
M5UnitTest* M5UnitTest::s_instance = nullptr;
// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------
void M5UnitTest::onShow(AppHandle handle, lv_obj_t* parent) {
s_instance = this;
appHandle_ = handle;
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
createWrapper(parent);
if (!listView_) {
listView_ = new TestListView();
listView_->onStart(wrapper_, handle, this);
}
}
void M5UnitTest::onHide(AppHandle handle) {
if (activeTestView_) {
activeTestView_->onStop();
delete activeTestView_;
activeTestView_ = nullptr;
}
if (listView_) listView_->onStop();
delete listView_;
listView_ = nullptr;
wrapper_ = nullptr;
appHandle_ = nullptr;
s_instance = nullptr;
}
// ---------------------------------------------------------------------------
// View switching
// ---------------------------------------------------------------------------
template<typename T>
static TestViewBase* makeTestView(lv_obj_t* wrapper, AppHandle handle, M5UnitTest* app) {
auto* v = new T();
v->onStart(wrapper, handle, app);
return v;
}
void M5UnitTest::createWrapper(lv_obj_t* parent) {
wrapper_ = lv_obj_create(parent);
lv_obj_set_width(wrapper_, LV_PCT(100));
lv_obj_set_flex_grow(wrapper_, 1);
lv_obj_set_layout(wrapper_, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(wrapper_, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_all(wrapper_, 0, 0);
lv_obj_set_style_bg_opa(wrapper_, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(wrapper_, 0, 0);
}
void M5UnitTest::showTest(int unitIndex) {
// Tear down any active test view and the list view, then clean the wrapper
if (activeTestView_) {
activeTestView_->onStop();
delete activeTestView_;
activeTestView_ = nullptr;
}
if (listView_) {
listView_->onStop();
delete listView_;
listView_ = nullptr;
}
lv_obj_clean(wrapper_);
ESP_LOGI(TAG, "Opening test for unit %d", unitIndex);
switch (unitIndex) {
case 0: activeTestView_ = makeTestView<TestUnit8Encoder> (wrapper_, appHandle_, this); break;
case 1: activeTestView_ = makeTestView<TestUnitByteButton>(wrapper_, appHandle_, this); break;
case 2: activeTestView_ = makeTestView<TestUnitJoystick2> (wrapper_, appHandle_, this); break;
case 3: activeTestView_ = makeTestView<TestUnitScroll> (wrapper_, appHandle_, this); break;
case 4: activeTestView_ = makeTestView<TestUnitPaHub> (wrapper_, appHandle_, this); break;
case 5: activeTestView_ = makeTestView<TestUnitLcd> (wrapper_, appHandle_, this); break;
case 6: activeTestView_ = makeTestView<TestUnitLcdGfx> (wrapper_, appHandle_, this); break;
case 7: activeTestView_ = makeTestView<TestUnitDualButton>(wrapper_, appHandle_, this); break;
case 8: activeTestView_ = makeTestView<TestUnitCardKB2> (wrapper_, appHandle_, this); break;
case 9: activeTestView_ = makeTestView<TestUnitMidi> (wrapper_, appHandle_, this); break;
case 10: activeTestView_ = makeTestView<TestUnitRfid2> (wrapper_, appHandle_, this); break;
default: showList(); return;
}
}
void M5UnitTest::showList() {
if (activeTestView_) {
activeTestView_->onStop();
delete activeTestView_;
activeTestView_ = nullptr;
}
lv_obj_clean(wrapper_);
listView_ = new TestListView();
listView_->onStart(wrapper_, appHandle_, this);
}
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include <tt_app.h>
#include <lvgl.h>
#include <TactilityCpp/App.h>
// Forward declarations for test views
class TestListView;
class TestViewBase;
class M5UnitTest final : public App {
public:
void onShow(AppHandle handle, lv_obj_t* parent) override;
void onHide(AppHandle handle) override;
// Called by TestListView when user selects a unit to test
void showTest(int unitIndex);
// Called by test views when user presses back
void showList();
// Called by the Back button path after the view has already been deleted
void clearActiveTestView() { activeTestView_ = nullptr; }
AppHandle getAppHandle() const { return appHandle_; }
private:
AppHandle appHandle_ = nullptr;
lv_obj_t* wrapper_ = nullptr; // full-screen container, cleaned between views
TestListView* listView_ = nullptr;
TestViewBase* activeTestView_ = nullptr;
static M5UnitTest* s_instance;
void createWrapper(lv_obj_t* parent);
};
@@ -0,0 +1,44 @@
#include "TestListView.h"
#include "M5UnitTest.h"
#include "UiScale.h"
#include <tt_lvgl_toolbar.h>
#include <tactility/lvgl_fonts.h>
void TestListView::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
app_ = app;
tt_lvgl_toolbar_create_for_app(parent, handle);
list_ = lv_list_create(parent);
lv_obj_set_width(list_, LV_PCT(100));
lv_obj_set_flex_grow(list_, 1);
lv_obj_set_style_pad_all(list_, uiPad(), 0);
lv_obj_set_style_pad_row(list_, uiRowGap(), 0);
lv_obj_set_style_border_width(list_, 0, 0);
lv_obj_set_style_bg_opa(list_, LV_OPA_TRANSP, 0);
const lv_font_t* font = lvgl_get_text_font(uiFont());
for (int i = 0; i < UNIT_COUNT; i++) {
lv_obj_t* btn = lv_list_add_button(list_, UNIT_ICONS[i], UNIT_NAMES[i]);
lv_obj_set_user_data(btn, (void*)(intptr_t)i);
lv_obj_add_event_cb(btn, onBtnClicked, LV_EVENT_CLICKED, this);
// lv_list_add_button creates: child 0 = icon label, child 1 = text label
lv_obj_t* textLbl = lv_obj_get_child(btn, 1);
if (textLbl) lv_obj_set_style_text_font(textLbl, font, 0);
lv_obj_t* iconLbl = lv_obj_get_child(btn, 0);
if (iconLbl) lv_obj_set_style_text_font(iconLbl, lvgl_get_shared_icon_font(), 0);
}
}
void TestListView::onStop() {
list_ = nullptr;
app_ = nullptr;
}
void TestListView::onBtnClicked(lv_event_t* e) {
auto* self = static_cast<TestListView*>(lv_event_get_user_data(e));
lv_obj_t* btn = static_cast<lv_obj_t*>(lv_event_get_target(e));
int idx = (int)(intptr_t)lv_obj_get_user_data(btn);
if (self && self->app_) self->app_->showTest(idx);
}
@@ -0,0 +1,49 @@
#pragma once
#include <array>
#include <lvgl.h>
#include <tactility/lvgl_icon_shared.h>
#include <tt_app.h>
class M5UnitTest;
class TestListView {
public:
void onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app);
void onStop();
private:
lv_obj_t* list_ = nullptr;
M5UnitTest* app_ = nullptr;
static constexpr std::array<const char*, 11> UNIT_NAMES = {{
"8Encoder",
"ByteButton",
"Joystick2",
"Scroll",
"PaHub",
"Color LCD",
"LCD Gfx Test",
"Dual-Button",
"CardKB2",
"MIDI / Synth",
"RFID 2",
}};
// Interface icons from shared Material icon font
static constexpr std::array<const char*, 11> UNIT_ICONS = {{
LVGL_ICON_SHARED_SETTINGS, // 8Encoder - I2C
LVGL_ICON_SHARED_SETTINGS, // ByteButton - I2C
LVGL_ICON_SHARED_GAMEPAD, // Joystick2 - I2C
LVGL_ICON_SHARED_SETTINGS, // Scroll - I2C
LVGL_ICON_SHARED_HUB, // PaHub - I2C
LVGL_ICON_SHARED_DEVICES, // Color LCD - I2C
LVGL_ICON_SHARED_AREA_CHART, // LCD Gfx Test - I2C
LVGL_ICON_SHARED_ELECTRIC_BOLT, // Dual-Button - GPIO
LVGL_ICON_SHARED_KEYBOARD_ALT, // CardKB2 - I2C
LVGL_ICON_SHARED_MUSIC_NOTE, // MIDI / Synth - UART
LVGL_ICON_SHARED_WIFI, // RFID 2 - I2C
}};
static constexpr int UNIT_COUNT = UNIT_NAMES.size();
static void onBtnClicked(lv_event_t* e);
};
@@ -0,0 +1,175 @@
#include "TestUnit8Encoder.h"
#include "UiScale.h"
#include <tactility/device.h>
#include <tactility/lvgl_fonts.h>
#include <cstring>
void TestUnit8Encoder::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
app_ = app;
memset(counters_, 0, sizeof(counters_));
memset(ledColors_, 0, sizeof(ledColors_));
createToolbar(parent, handle, "8Encoder");
createBanner(parent, "8Encoder", "I2C", COLOR_I2C);
int numCols = uiW() >= 800 ? 4 : (uiW() >= 200 ? 2 : 1);
int dotSz = (int)(uiShortSide() / 60);
if (dotSz < 8) dotSz = 8;
if (dotSz > 20) dotSz = 20;
const lv_font_t* fnt = lvgl_get_text_font(uiFont());
// Status label shown when not connected — sits above the grid at full width
lblStatus_ = lv_label_create(parent);
lv_obj_set_style_text_font(lblStatus_, fnt, 0);
lv_obj_set_width(lblStatus_, LV_PCT(100));
lv_obj_set_style_pad_hor(lblStatus_, uiPad(), 0);
lv_label_set_text(lblStatus_, "");
lv_obj_t* grid = lv_obj_create(parent);
lv_obj_set_width(grid, LV_PCT(100));
lv_obj_set_flex_grow(grid, 1);
lv_obj_set_layout(grid, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(grid, LV_FLEX_FLOW_ROW_WRAP);
lv_obj_set_flex_align(grid, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
lv_obj_set_style_pad_all(grid, uiPad(), 0);
lv_obj_set_style_pad_row(grid, uiRowGap(), 0);
lv_obj_set_style_pad_column(grid, uiPad(), 0);
lv_obj_set_style_bg_opa(grid, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(grid, 0, 0);
auto makeCard = [&](lv_obj_t* parent) -> lv_obj_t* {
lv_obj_t* card = lv_obj_create(parent);
lv_obj_set_width(card, numCols == 4 ? LV_PCT(23) : (numCols == 2 ? LV_PCT(48) : LV_PCT(100)));
lv_obj_set_height(card, LV_SIZE_CONTENT);
lv_obj_set_layout(card, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(card, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_all(card, uiPad() / 2, 0);
lv_obj_set_style_pad_column(card, uiRowGap(), 0);
lv_obj_set_style_bg_opa(card, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(card, 0, 0);
lv_obj_remove_flag(card, LV_OBJ_FLAG_SCROLLABLE);
return card;
};
auto makeDot = [&](lv_obj_t* parent) -> lv_obj_t* {
lv_obj_t* dot = lv_obj_create(parent);
lv_obj_set_size(dot, dotSz, dotSz);
lv_obj_set_style_radius(dot, dotSz / 2, 0);
lv_obj_set_style_bg_color(dot, lv_color_hex(0x333333), 0);
lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(dot, 0, 0);
lv_obj_remove_flag(dot, LV_OBJ_FLAG_SCROLLABLE);
return dot;
};
for (int i = 0; i < 8; i++) {
lv_obj_t* card = makeCard(grid);
lv_obj_t* num = lv_label_create(card);
lv_label_set_text_fmt(num, "E%d:", i + 1);
lv_obj_set_style_text_font(num, fnt, 0);
lv_obj_set_width(num, LV_SIZE_CONTENT);
lblCounters_[i] = lv_label_create(card);
lv_label_set_text(lblCounters_[i], "0");
lv_obj_set_style_text_font(lblCounters_[i], fnt, 0);
lv_obj_set_flex_grow(lblCounters_[i], 1);
dotButtons_[i] = makeDot(card);
}
// Switch row
lv_obj_t* swCard = makeCard(grid);
lv_obj_set_width(swCard, LV_PCT(100));
lv_obj_t* swLbl = lv_label_create(swCard);
lv_label_set_text(swLbl, "SW:");
lv_obj_set_style_text_font(swLbl, fnt, 0);
lv_obj_set_width(swLbl, LV_SIZE_CONTENT);
lblSwitch_ = lv_label_create(swCard);
lv_label_set_text(lblSwitch_, "off");
lv_obj_set_style_text_font(lblSwitch_, fnt, 0);
lv_obj_set_flex_grow(lblSwitch_, 1);
dotSwitch_ = makeDot(swCard);
Device* i2c = device_find_by_name("i2c1");
if (!i2c) {
lv_label_set_text(lblStatus_, "i2c1 not found");
return;
}
if (enc_.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 (enc_.begin(i2c)) found = true;
}
if (!found) {
hub_.deselect();
lv_label_set_text(lblStatus_, "8Encoder not found");
return;
}
} else {
lv_label_set_text(lblStatus_, "8Encoder not found");
return;
}
timer_ = lv_timer_create(onTimer, 50, this);
update();
}
void TestUnit8Encoder::onStop() {
if (timer_) { lv_timer_delete(timer_); timer_ = nullptr; }
selectIfNeeded();
if (enc_.isPresent()) enc_.setAllLeds(0x000000);
if (usingPaHub_ && hub_.isPresent()) hub_.deselect();
lblStatus_ = nullptr;
memset(lblCounters_, 0, sizeof(lblCounters_));
memset(dotButtons_, 0, sizeof(dotButtons_));
lblSwitch_ = dotSwitch_ = nullptr;
}
void TestUnit8Encoder::onTimer(lv_timer_t* t) {
static_cast<TestUnit8Encoder*>(lv_timer_get_user_data(t))->update();
}
void TestUnit8Encoder::selectIfNeeded() {
if (usingPaHub_ && hub_.isPresent())
hub_.select(hub_.currentChannel());
}
void TestUnit8Encoder::update() {
selectIfNeeded();
if (!enc_.isPresent()) return;
int32_t deltas[8];
uint8_t buttons[8];
if (!enc_.readAll(deltas, buttons)) return;
for (int i = 0; i < 8; i++) {
counters_[i] += deltas[i];
lv_label_set_text_fmt(lblCounters_[i], "%ld", (long)counters_[i]);
lv_color_t dotCol = buttons[i] ? lv_color_hex(0x00DD44) : lv_color_hex(0x333333);
lv_obj_set_style_bg_color(dotButtons_[i], dotCol, 0);
// Encoder LED: hue cycles with counter position
uint32_t hue = (uint32_t)((counters_[i] % 360 + 360) % 360);
lv_color_t c = lv_color_hsv_to_rgb((uint16_t)hue, 100, 78);
lv_color32_t c32 = lv_color_to_32(c, LV_OPA_COVER);
ledColors_[i] = ((uint32_t)c32.red << 16) | ((uint32_t)c32.green << 8) | c32.blue;
}
// Switch (index 8): green=on, dark gray=off; skip update on I2C error
bool sw = false;
if (enc_.readSwitch(sw)) {
lv_label_set_text(lblSwitch_, sw ? "on" : "off");
lv_obj_set_style_bg_color(dotSwitch_, lv_color_hex(sw ? 0x00DD44 : 0x333333), 0);
ledColors_[8] = sw ? 0x00DD44 : 0x000000;
}
enc_.flushLeds(ledColors_);
}
@@ -0,0 +1,27 @@
#pragma once
#include "TestViewBase.h"
#include <UnitPaHub.h>
#include <Unit8Encoder.h>
class TestUnit8Encoder final : public TestViewBase {
public:
void onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) override;
void onStop() override;
private:
UnitPaHub hub_;
Unit8Encoder enc_;
lv_obj_t* lblStatus_ = nullptr;
lv_obj_t* lblCounters_[8] = {};
lv_obj_t* dotButtons_[8] = {};
lv_obj_t* lblSwitch_ = nullptr;
lv_obj_t* dotSwitch_ = nullptr;
lv_timer_t* timer_ = nullptr;
int32_t counters_[8] = {};
uint32_t ledColors_[Unit8Encoder::LED_COUNT] = {};
bool usingPaHub_ = false;
static void onTimer(lv_timer_t* t);
void update();
void selectIfNeeded();
};
@@ -0,0 +1,125 @@
#include "TestUnitByteButton.h"
#include "UiScale.h"
#include <tactility/device.h>
#include <tactility/lvgl_fonts.h>
#include <cstring>
void TestUnitByteButton::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
app_ = app;
memset(ledColors_, 0, sizeof(ledColors_));
memset(prevPressed_, 0, sizeof(prevPressed_));
createToolbar(parent, handle, "ByteButton");
createBanner(parent, "ByteButton", "I2C", COLOR_I2C);
int dotSz = (int)(uiShortSide() / 14);
if (dotSz < 20) dotSz = 20;
if (dotSz > 64) dotSz = 64;
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_flex_align(cont, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_row(cont, uiRowGap() * 2, 0);
lv_obj_set_style_pad_all(cont, uiPad(), 0);
lv_obj_set_style_bg_opa(cont, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(cont, 0, 0);
// Dot grid - flex wrap row so it auto-adjusts columns
lv_obj_t* dotGrid = lv_obj_create(cont);
lv_obj_set_width(dotGrid, LV_PCT(100));
lv_obj_set_height(dotGrid, LV_SIZE_CONTENT);
lv_obj_set_layout(dotGrid, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(dotGrid, LV_FLEX_FLOW_ROW_WRAP);
lv_obj_set_flex_align(dotGrid, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_row(dotGrid, uiRowGap(), 0);
lv_obj_set_style_pad_column(dotGrid, uiRowGap(), 0);
lv_obj_set_style_bg_opa(dotGrid, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(dotGrid, 0, 0);
lv_obj_set_style_pad_all(dotGrid, 0, 0);
for (int i = 0; i < BTN_COUNT; i++) {
lv_obj_t* sq = lv_obj_create(dotGrid);
lv_obj_set_size(sq, dotSz, dotSz);
lv_obj_set_style_radius(sq, 4, 0);
lv_obj_set_style_bg_color(sq, lv_color_hex(COLOR_OFF), 0);
lv_obj_set_style_bg_opa(sq, LV_OPA_COVER, 0);
lv_obj_set_style_border_color(sq, lv_color_hex(0x444444), 0);
lv_obj_set_style_border_width(sq, 1, 0);
lv_obj_remove_flag(sq, LV_OBJ_FLAG_SCROLLABLE);
indicators_[i] = sq;
}
lv_obj_t* hint = lv_label_create(cont);
lv_obj_set_style_text_font(hint, lvgl_get_text_font(FONT_SIZE_SMALL), 0);
lv_label_set_text(hint, "Press buttons - LEDs toggle");
Device* i2c = device_find_by_name("i2c1");
if (!i2c) {
for (int i = 0; i < BTN_COUNT; i++) lv_obj_set_style_bg_color(indicators_[i], lv_color_hex(COLOR_ERROR), 0);
lv_label_set_text(hint, "i2c1 not found");
return;
}
if (unit_.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 (unit_.begin(i2c)) found = true;
}
if (!found) {
hub_.deselect();
for (int i = 0; i < BTN_COUNT; i++) lv_obj_set_style_bg_color(indicators_[i], lv_color_hex(COLOR_ERROR), 0);
lv_label_set_text(hint, "ByteButton not found");
return;
}
} else {
for (int i = 0; i < BTN_COUNT; i++) lv_obj_set_style_bg_color(indicators_[i], lv_color_hex(COLOR_ERROR), 0);
lv_label_set_text(hint, "ByteButton not found");
return;
}
timer_ = lv_timer_create(onTimer, 50, this);
update();
}
void TestUnitByteButton::onStop() {
if (timer_) { lv_timer_delete(timer_); timer_ = nullptr; }
selectIfNeeded();
if (unit_.isPresent()) {
for (int i = 0; i < BTN_COUNT; i++) unit_.setLed((uint8_t)i, 0x000000);
}
if (usingPaHub_ && hub_.isPresent()) hub_.deselect();
memset(indicators_, 0, sizeof(indicators_));
}
void TestUnitByteButton::selectIfNeeded() {
if (usingPaHub_ && hub_.isPresent())
hub_.select(hub_.currentChannel());
}
void TestUnitByteButton::onTimer(lv_timer_t* t) {
static_cast<TestUnitByteButton*>(lv_timer_get_user_data(t))->update();
}
void TestUnitByteButton::update() {
selectIfNeeded();
if (!unit_.isPresent()) return;
uint8_t mask = unit_.readButtons();
for (int i = 0; i < BTN_COUNT; i++) {
bool pressed = (mask >> i) & 0x01;
// Toggle LED only on rising edge (press, not hold)
if (pressed && !prevPressed_[i]) {
ledColors_[i] = (ledColors_[i] == 0) ? COLOR_ON : 0;
unit_.setLed((uint8_t)i, ledColors_[i]);
}
prevPressed_[i] = pressed;
lv_color_t col = pressed ? lv_color_hex(COLOR_PRESSED) : lv_color_hex(COLOR_OFF);
lv_obj_set_style_bg_color(indicators_[i], col, 0);
}
}
@@ -0,0 +1,29 @@
#pragma once
#include "TestViewBase.h"
#include <UnitByteButton.h>
#include <UnitPaHub.h>
class TestUnitByteButton final : public TestViewBase {
public:
void onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) override;
void onStop() override;
private:
static constexpr int BTN_COUNT = UnitByteButton::BUTTON_COUNT;
static constexpr uint32_t COLOR_OFF = 0x001100;
static constexpr uint32_t COLOR_ON = 0x00FF44;
static constexpr uint32_t COLOR_ERROR = 0x440000;
static constexpr uint32_t COLOR_PRESSED = 0xFFFF00;
UnitPaHub hub_;
UnitByteButton unit_;
lv_obj_t* indicators_[BTN_COUNT] = {};
lv_timer_t* timer_ = nullptr;
uint32_t ledColors_[BTN_COUNT] = {};
bool prevPressed_[BTN_COUNT]= {};
bool usingPaHub_ = false;
void selectIfNeeded();
static void onTimer(lv_timer_t* t);
void update();
};
@@ -0,0 +1,283 @@
#include "TestUnitCardKB2.h"
#include "UiScale.h"
#include <tactility/device.h>
#include <tactility/drivers/uart_controller.h>
#include <tactility/lvgl_fonts.h>
#include <cstring>
// ---------------------------------------------------------------------------
// Physical key layout - 5 rows (4 real + arrow row).
// matchChar=0 = modifier key (no highlight target).
// Arrow keys produced by Fn+Z/X/D/C in I2C mode; by key-id in UART mode.
// ---------------------------------------------------------------------------
static const struct { const char* label; uint8_t matchChar; int grow; } LAYOUT[][12] = {
// Row 0: 1 2 3 4 5 6 7 8 9 0 Del (+ Esc shown but Esc = Fn+1, no direct key-id)
{ {"1",'1',1},{"2",'2',1},{"3",'3',1},{"4",'4',1},{"5",'5',1},{"6",'6',1},
{"7",'7',1},{"8",'8',1},{"9",'9',1},{"0",'0',1},{"Del",0x08,1},{nullptr,0,0} },
// Row 1: q w e r t y u i o p Del
{ {"q",'q',1},{"w",'w',1},{"e",'e',1},{"r",'r',1},{"t",'t',1},{"y",'y',1},
{"u",'u',1},{"i",'i',1},{"o",'o',1},{"p",'p',1},{"Del",0x08,1},{nullptr,0,0} },
// Row 2: Aa a s d f g h j k l Enter
{ {"Aa",0,1},{"a",'a',1},{"s",'s',1},{"d",'d',1},{"f",'f',1},{"g",'g',1},
{"h",'h',1},{"j",'j',1},{"k",'k',1},{"l",'l',1},{"Ent",0x0A,1},{nullptr,0,0} },
// Row 3: Fn Sym z x c v b n m Spc
{ {"Fn",0,1},{"Sym",0,1},{"z",'z',1},{"x",'x',1},{"c",'c',1},{"v",'v',1},
{"b",'b',1},{"n",'n',1},{"m",'m',1},{"Spc",0x20,3},{nullptr,0,0},{nullptr,0,0} },
// Row 4: arrow keys (ASCII from Fn combos in I2C; produced by Fn key-ids in UART)
{ {"<",0x1D,1},{"v",0x1F,1},{"^",0x1E,1},{">",0x1C,1},
{nullptr,0,0},{nullptr,0,0},{nullptr,0,0},{nullptr,0,0},{nullptr,0,0},{nullptr,0,0},{nullptr,0,0},{nullptr,0,0} },
};
static constexpr int ROW_COUNT = 5;
static constexpr int COL_COUNT = 12;
// ---------------------------------------------------------------------------
// Grid construction
// ---------------------------------------------------------------------------
void TestUnitCardKB2::buildGrid(lv_obj_t* parent) {
gridCount_ = 0;
for (int row = 0; row < ROW_COUNT; row++) {
lv_obj_t* rowCont = lv_obj_create(parent);
lv_obj_set_width(rowCont, LV_PCT(100));
lv_obj_set_height(rowCont, LV_SIZE_CONTENT);
lv_obj_set_layout(rowCont, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(rowCont, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(rowCont, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_column(rowCont, uiPad() / 4 + 1, 0);
lv_obj_set_style_pad_all(rowCont, 0, 0);
lv_obj_set_style_bg_opa(rowCont, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(rowCont, 0, 0);
for (int col = 0; col < COL_COUNT; col++) {
if (LAYOUT[row][col].label == nullptr) break;
lv_obj_t* btn = lv_button_create(rowCont);
lv_obj_set_style_pad_hor(btn, uiPad() / 2, 0);
lv_obj_set_style_pad_ver(btn, uiRowGap(), 0);
lv_obj_set_style_radius(btn, 4, 0);
lv_obj_set_flex_grow(btn, LAYOUT[row][col].grow);
lv_obj_t* lbl = lv_label_create(btn);
lv_obj_set_style_text_font(lbl, lvgl_get_text_font(FONT_SIZE_SMALL), 0);
lv_label_set_text(lbl, LAYOUT[row][col].label);
lv_obj_center(lbl);
if (gridCount_ < GRID_KEY_COUNT) {
grid_[gridCount_++] = { LAYOUT[row][col].label, LAYOUT[row][col].matchChar, btn, lbl };
}
}
}
}
// ---------------------------------------------------------------------------
// Connection overlay
// ---------------------------------------------------------------------------
void TestUnitCardKB2::showConnectOverlay() {
int pad = uiPad();
int rowGap = uiRowGap();
const lv_font_t* fnt = lvgl_get_text_font(uiFont());
connectOverlay_ = lv_obj_create(parentRef_);
lv_obj_set_size(connectOverlay_, LV_PCT(100), LV_PCT(100));
lv_obj_set_pos(connectOverlay_, 0, 0);
lv_obj_set_layout(connectOverlay_, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(connectOverlay_, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(connectOverlay_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_all(connectOverlay_, pad, 0);
lv_obj_set_style_pad_row(connectOverlay_, rowGap * 2, 0);
lv_obj_t* title = lv_label_create(connectOverlay_);
lv_obj_set_style_text_font(title, fnt, 0);
lv_label_set_text(title, "Select connection mode");
lv_obj_t* hint = lv_label_create(connectOverlay_);
lv_obj_set_style_text_font(hint, lvgl_get_text_font(FONT_SIZE_SMALL), 0);
lv_obj_set_style_text_color(hint, lv_color_hex(0x888888), 0);
lv_label_set_text(hint, "Fn+Sym+1 = I2C | Fn+Sym+2 = UART");
auto makeBtn = [&](const char* label, lv_event_cb_t cb) {
lv_obj_t* btn = lv_button_create(connectOverlay_);
lv_obj_set_width(btn, LV_PCT(60));
lv_obj_add_event_cb(btn, cb, LV_EVENT_CLICKED, this);
lv_obj_t* lbl = lv_label_create(btn);
lv_obj_set_style_text_font(lbl, fnt, 0);
lv_label_set_text(lbl, label);
lv_obj_center(lbl);
};
makeBtn("I2C (Grove port)", onI2CBtn);
makeBtn("UART (Grove port)", onUartBtn);
}
// ---------------------------------------------------------------------------
// Connect handlers
// ---------------------------------------------------------------------------
void TestUnitCardKB2::connectI2C() {
lv_obj_delete(connectOverlay_);
connectOverlay_ = nullptr;
Device* i2c = device_find_by_name("i2c1");
if (!i2c) {
buildMainUI();
lv_label_set_text(lblHistory_, "i2c1 not found");
return;
}
bool ok = false;
if (unit_.begin(i2c)) {
usingPaHub_ = false;
ok = true;
} else if (hub_.begin(i2c)) {
usingPaHub_ = true;
for (uint8_t ch = 0; ch < UnitPaHub::NUM_CHANNELS && !ok; ch++) {
hub_.select(ch);
if (unit_.begin(i2c)) ok = true;
}
if (!ok) hub_.deselect();
}
buildMainUI();
if (!ok) lv_label_set_text(lblHistory_, "CardKB2 not found");
else timer_ = lv_timer_create(onTimer, 50, this);
}
void TestUnitCardKB2::connectUart() {
lv_obj_delete(connectOverlay_);
connectOverlay_ = nullptr;
Device* uart = device_find_by_name("uart1");
buildMainUI();
if (!uart) {
lv_label_set_text(lblHistory_, "uart1 not found");
return;
}
if (!unit_.beginUart(uart)) {
lv_label_set_text(lblHistory_, "UART open failed");
return;
}
timer_ = lv_timer_create(onTimer, 50, this);
}
// ---------------------------------------------------------------------------
// Main content UI (built after connection type selected)
// ---------------------------------------------------------------------------
void TestUnitCardKB2::buildMainUI() {
memset(history_, 0, sizeof(history_));
histLen_ = 0;
gridCount_ = 0;
activeBtn_ = nullptr;
lv_obj_t* cont = lv_obj_create(parentRef_);
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_flex_align(cont, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
lv_obj_set_style_pad_row(cont, 3, 0);
lv_obj_set_style_pad_all(cont, uiPad(), 0);
lv_obj_set_style_bg_opa(cont, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(cont, 0, 0);
if (uiW() >= 200) buildGrid(cont);
lblHistory_ = lv_label_create(cont);
lv_obj_set_style_text_font(lblHistory_, lvgl_get_text_font(uiFont()), 0);
lv_label_set_text(lblHistory_, "");
lv_obj_set_width(lblHistory_, LV_PCT(100));
lv_label_set_long_mode(lblHistory_, LV_LABEL_LONG_WRAP);
lv_obj_set_flex_grow(lblHistory_, 1);
}
// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------
void TestUnitCardKB2::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
app_ = app;
parentRef_ = parent;
handleRef_ = handle;
createToolbar(parent, handle, "CardKB2");
createBanner(parent, "CardKB2", "I2C/UART", COLOR_I2C);
showConnectOverlay();
}
void TestUnitCardKB2::onStop() {
if (timer_) { lv_timer_delete(timer_); timer_ = nullptr; }
if (usingPaHub_ && hub_.isPresent()) hub_.deselect();
unit_.end();
lblHistory_ = nullptr;
connectOverlay_ = nullptr;
activeBtn_ = nullptr;
gridCount_ = 0;
}
// ---------------------------------------------------------------------------
// Static callbacks
// ---------------------------------------------------------------------------
void TestUnitCardKB2::onI2CBtn(lv_event_t* e) {
static_cast<TestUnitCardKB2*>(lv_event_get_user_data(e))->connectI2C();
}
void TestUnitCardKB2::onUartBtn(lv_event_t* e) {
static_cast<TestUnitCardKB2*>(lv_event_get_user_data(e))->connectUart();
}
void TestUnitCardKB2::onTimer(lv_timer_t* t) {
static_cast<TestUnitCardKB2*>(lv_timer_get_user_data(t))->update();
}
// ---------------------------------------------------------------------------
// PaHub helper
// ---------------------------------------------------------------------------
void TestUnitCardKB2::selectIfNeeded() {
if (usingPaHub_ && hub_.isPresent())
hub_.select(hub_.currentChannel());
}
// ---------------------------------------------------------------------------
// Update (called from timer)
// ---------------------------------------------------------------------------
void TestUnitCardKB2::update() {
if (!unit_.isPresent()) return;
if (unit_.mode() == UnitCardKB2::Mode::I2C) selectIfNeeded();
char c = unit_.getKey();
// Grid highlight
if (gridCount_ > 0) {
lv_obj_t* newActive = nullptr;
if (c != 0) {
for (int i = 0; i < gridCount_; i++) {
if (grid_[i].matchChar == 0 || grid_[i].btn == nullptr) continue;
uint8_t mc = grid_[i].matchChar;
// Match lower and uppercase variants of letter keys
bool match = (mc == (uint8_t)c) ||
(mc >= 'a' && mc <= 'z' && mc == (uint8_t)(c | 0x20));
if (match) { newActive = grid_[i].btn; break; }
}
}
if (newActive != activeBtn_) {
if (activeBtn_) lv_obj_remove_state(activeBtn_, LV_STATE_PRESSED);
if (newActive) lv_obj_add_state(newActive, LV_STATE_PRESSED);
activeBtn_ = newActive;
}
}
// History strip - printable chars only
if (c != 0 && c >= 0x20 && c < 0x7F) {
if (histLen_ < sizeof(history_) - 1) {
history_[histLen_++] = c;
} else {
memmove(history_, history_ + 1, histLen_ - 1);
history_[histLen_ - 1] = c;
}
history_[histLen_] = '\0';
lv_label_set_text(lblHistory_, history_);
}
}
@@ -0,0 +1,51 @@
#pragma once
#include "TestViewBase.h"
#include <UnitCardKB2.h>
#include <UnitPaHub.h>
class TestUnitCardKB2 final : public TestViewBase {
public:
void onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) override;
void onStop() override;
private:
UnitPaHub hub_;
UnitCardKB2 unit_;
lv_timer_t* timer_ = nullptr;
bool usingPaHub_ = false;
// Connection selection overlay (shown before connecting)
lv_obj_t* connectOverlay_ = nullptr;
// Main content (shown after connecting)
lv_obj_t* lblHistory_ = nullptr;
char history_[64] = {};
uint8_t histLen_ = 0;
// Keyboard grid (only built on screens >= 200px wide)
struct KeyCell {
const char* label;
uint8_t matchChar; // ASCII to highlight; 0 = not matchable
lv_obj_t* btn = nullptr;
lv_obj_t* lbl = nullptr;
};
static constexpr int GRID_KEY_COUNT = 52;
KeyCell grid_[GRID_KEY_COUNT] = {};
int gridCount_ = 0;
lv_obj_t* activeBtn_ = nullptr;
lv_obj_t* parentRef_ = nullptr;
AppHandle handleRef_ = nullptr;
void showConnectOverlay();
void connectI2C();
void connectUart();
void buildMainUI();
void buildGrid(lv_obj_t* parent);
void selectIfNeeded();
static void onTimer(lv_timer_t* t);
static void onI2CBtn(lv_event_t* e);
static void onUartBtn(lv_event_t* e);
void update();
};
@@ -0,0 +1,305 @@
#include "TestUnitDualButton.h"
#include "UiScale.h"
#include <tactility/device.h>
#include <tactility/lvgl_fonts.h>
static constexpr gpio_pin_t PIN_MIN = 0;
static constexpr gpio_pin_t PIN_MAX = 57;
static constexpr lv_color_t COLOR_A_ACTIVE = LV_COLOR_MAKE(0xE0, 0x30, 0x30);
static constexpr lv_color_t COLOR_A_DIM = LV_COLOR_MAKE(0x40, 0x10, 0x10);
static constexpr lv_color_t COLOR_B_ACTIVE = LV_COLOR_MAKE(0x30, 0x60, 0xE0);
static constexpr lv_color_t COLOR_B_DIM = LV_COLOR_MAKE(0x10, 0x20, 0x50);
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
// Scale UI elements off the shorter display side so config looks good in both orientations.
static lv_coord_t uiShort() {
lv_coord_t w = lv_display_get_horizontal_resolution(nullptr);
lv_coord_t h = lv_display_get_vertical_resolution(nullptr);
return w < h ? w : h;
}
// Button size and value label width proportional to short side, clamped to reasonable range.
static lv_coord_t uiBtnSize() { lv_coord_t s = uiShort() / 10; return s < 36 ? 36 : (s > 80 ? 80 : s); }
static lv_coord_t uiValWidth() { lv_coord_t s = uiShort() / 8; return s < 48 ? 48 : (s > 100 ? 100 : s); }
static lv_obj_t* makePinRow(lv_obj_t* parent, const char* label,
lv_event_cb_t cbDown, lv_event_cb_t cbUp,
lv_obj_t** outLbl, void* userData) {
int pad = uiPad();
int gap = uiRowGap() * 2;
lv_coord_t btnSz = uiBtnSize();
lv_coord_t valW = uiValWidth();
// Use LARGE font on larger screens (short side >= 400), DEFAULT otherwise
enum LvglFontSize fnt = uiShort() >= 400 ? FONT_SIZE_LARGE : FONT_SIZE_DEFAULT;
lv_obj_t* row = lv_obj_create(parent);
lv_obj_set_width(row, LV_PCT(100));
lv_obj_set_height(row, LV_SIZE_CONTENT);
lv_obj_set_layout(row, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_column(row, gap, 0);
lv_obj_set_style_pad_ver(row, pad, 0);
lv_obj_set_style_pad_hor(row, pad, 0);
lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(row, 0, 0);
lv_obj_t* lbl = lv_label_create(row);
lv_label_set_text(lbl, label);
lv_obj_set_style_text_font(lbl, lvgl_get_text_font(fnt), 0);
lv_obj_t* btnMinus = lv_button_create(row);
lv_obj_set_size(btnMinus, btnSz, btnSz);
lv_obj_add_event_cb(btnMinus, cbDown, LV_EVENT_CLICKED, userData);
lv_obj_t* lblMinus = lv_label_create(btnMinus);
lv_obj_set_style_text_font(lblMinus, lvgl_get_text_font(fnt), 0);
lv_label_set_text(lblMinus, "-");
lv_obj_center(lblMinus);
lv_obj_t* valLbl = lv_label_create(row);
lv_obj_set_style_text_font(valLbl, lvgl_get_text_font(fnt), 0);
lv_obj_set_width(valLbl, valW);
lv_obj_set_style_text_align(valLbl, LV_TEXT_ALIGN_CENTER, 0);
*outLbl = valLbl;
lv_obj_t* btnPlus = lv_button_create(row);
lv_obj_set_size(btnPlus, btnSz, btnSz);
lv_obj_add_event_cb(btnPlus, cbUp, LV_EVENT_CLICKED, userData);
lv_obj_t* lblPlus = lv_label_create(btnPlus);
lv_obj_set_style_text_font(lblPlus, lvgl_get_text_font(fnt), 0);
lv_label_set_text(lblPlus, "+");
lv_obj_center(lblPlus);
return row;
}
// ---------------------------------------------------------------------------
// onStart
// ---------------------------------------------------------------------------
void TestUnitDualButton::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
app_ = app;
createToolbar(parent, handle, "Dual-Button");
createBanner(parent, "Dual-Button", "GPIO", COLOR_GPIO);
buildConfigScreen(parent);
}
void TestUnitDualButton::buildConfigScreen(lv_obj_t* parent) {
int pad = uiPad();
int gap = uiRowGap();
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_flex_align(cont, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_row(cont, gap, 0);
lv_obj_set_style_pad_all(cont, pad, 0);
lv_obj_set_style_bg_opa(cont, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(cont, 0, 0);
makePinRow(cont, "Pin A:", onPinADown, onPinAUp, &lblPinA_, this);
lv_label_set_text_fmt(lblPinA_, "%d", (int)pinA_);
makePinRow(cont, "Pin B:", onPinBDown, onPinBUp, &lblPinB_, this);
lv_label_set_text_fmt(lblPinB_, "%d", (int)pinB_);
enum LvglFontSize fnt = uiShort() >= 400 ? FONT_SIZE_LARGE : FONT_SIZE_DEFAULT;
lv_coord_t btnH = uiBtnSize() * 3 / 2;
lv_obj_t* btnConnect = lv_button_create(cont);
lv_obj_set_width(btnConnect, LV_PCT(60));
lv_obj_set_height(btnConnect, btnH);
lv_obj_add_event_cb(btnConnect, onConnect, LV_EVENT_CLICKED, this);
lv_obj_t* lbl = lv_label_create(btnConnect);
lv_obj_set_style_text_font(lbl, lvgl_get_text_font(fnt), 0);
lv_label_set_text(lbl, "Connect");
lv_obj_center(lbl);
lblError_ = lv_label_create(cont);
lv_obj_set_style_text_color(lblError_, lv_color_make(0xE0, 0x40, 0x40), 0);
lv_obj_set_style_text_font(lblError_, lvgl_get_text_font(fnt), 0);
lv_label_set_text(lblError_, "");
}
void TestUnitDualButton::buildTestScreen(lv_obj_t* parent) {
// Remove config screen (last child added - the cont with flex_grow=1)
// We clean the parent area by deleting children after toolbar+banner (first 2),
// so instead we track via the cont pointer approach: just clean parent and rebuild
// toolbar+banner are already created; we delete everything after them by cleaning
// what was added by buildConfigScreen. The simplest approach: delete the cont we
// created. Since we don't store it, clean children from index 2 onward.
uint32_t childCnt = lv_obj_get_child_count(parent);
for (uint32_t i = childCnt; i > 2; --i) {
lv_obj_delete(lv_obj_get_child(parent, i - 1));
}
lblPinA_ = lblPinB_ = lblError_ = nullptr;
int pad = uiPad();
lv_coord_t dispW = lv_display_get_horizontal_resolution(nullptr);
lv_coord_t dispH = lv_display_get_vertical_resolution(nullptr);
bool landscape = dispW > dispH;
// Diameter: fit two circles side-by-side in landscape, stacked in portrait.
// Base off the shorter display dimension to avoid blowing up on wide screens.
lv_coord_t shortSide = dispW < dispH ? dispW : dispH;
lv_coord_t overhead = 40 + 28 + pad * 2; // toolbar + banner + padding
lv_coord_t diam;
if (landscape) {
// Side by side: diameter limited by available height and half the width
lv_coord_t byH = dispH - overhead - pad * 2;
lv_coord_t byW = (dispW - pad * 3) / 2;
diam = byH < byW ? byH : byW;
} else {
// Stacked: diameter limited by width and half available height
lv_coord_t byW = dispW - pad * 2;
lv_coord_t byH = (dispH - overhead - pad * 3) / 2;
diam = byW < byH ? byW : byH;
}
// Cap at 40% of short side so circles never dominate the whole screen
lv_coord_t cap = shortSide * 2 / 5;
if (diam > cap) diam = cap;
if (diam < 20) diam = 20;
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, landscape ? LV_FLEX_FLOW_ROW : LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(cont, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_all(cont, pad, 0);
lv_obj_set_style_bg_opa(cont, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(cont, 0, 0);
// Circle A (red)
circleA_ = lv_obj_create(cont);
lv_obj_set_size(circleA_, diam, diam);
lv_obj_set_style_radius(circleA_, LV_RADIUS_CIRCLE, 0);
lv_obj_set_style_bg_color(circleA_, COLOR_A_DIM, 0);
lv_obj_set_style_bg_opa(circleA_, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(circleA_, 0, 0);
lv_obj_set_style_pad_all(circleA_, 0, 0);
circleLblA_ = lv_label_create(circleA_);
lv_label_set_text(circleLblA_, "A");
lv_obj_set_style_text_font(circleLblA_, lvgl_get_text_font(FONT_SIZE_LARGE), 0);
lv_obj_set_style_text_color(circleLblA_, lv_color_make(0x80, 0x40, 0x40), 0);
lv_obj_set_style_text_align(circleLblA_, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(circleLblA_, LV_ALIGN_CENTER, 0, 0);
// Circle B (blue)
circleB_ = lv_obj_create(cont);
lv_obj_set_size(circleB_, diam, diam);
lv_obj_set_style_radius(circleB_, LV_RADIUS_CIRCLE, 0);
lv_obj_set_style_bg_color(circleB_, COLOR_B_DIM, 0);
lv_obj_set_style_bg_opa(circleB_, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(circleB_, 0, 0);
lv_obj_set_style_pad_all(circleB_, 0, 0);
circleLblB_ = lv_label_create(circleB_);
lv_label_set_text(circleLblB_, "B");
lv_obj_set_style_text_font(circleLblB_, lvgl_get_text_font(FONT_SIZE_LARGE), 0);
lv_obj_set_style_text_color(circleLblB_, lv_color_make(0x40, 0x50, 0x80), 0);
lv_obj_set_style_text_align(circleLblB_, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(circleLblB_, LV_ALIGN_CENTER, 0, 0);
timer_ = lv_timer_create(onTimer, 50, this);
update();
}
// ---------------------------------------------------------------------------
// onStop
// ---------------------------------------------------------------------------
void TestUnitDualButton::onStop() {
if (timer_) { lv_timer_delete(timer_); timer_ = nullptr; }
if (connected_) { unit_.end(); connected_ = false; }
lblPinA_ = lblPinB_ = lblError_ = nullptr;
circleA_ = circleB_ = nullptr;
circleLblA_ = circleLblB_ = nullptr;
pinA_ = 0;
pinB_ = 49;
}
// ---------------------------------------------------------------------------
// update (polls hardware, refreshes circles)
// ---------------------------------------------------------------------------
void TestUnitDualButton::update() {
if (!unit_.isPresent() || !circleA_) return;
bool pressA = unit_.isButtonAPressed();
lv_obj_set_style_bg_color(circleA_, pressA ? COLOR_A_ACTIVE : COLOR_A_DIM, 0);
lv_label_set_text(circleLblA_, pressA ? "PRESSED" : "A");
lv_obj_set_style_text_color(circleLblA_,
pressA ? lv_color_white() : lv_color_make(0x80, 0x40, 0x40), 0);
bool pressB = unit_.isButtonBPressed();
lv_obj_set_style_bg_color(circleB_, pressB ? COLOR_B_ACTIVE : COLOR_B_DIM, 0);
lv_label_set_text(circleLblB_, pressB ? "PRESSED" : "B");
lv_obj_set_style_text_color(circleLblB_,
pressB ? lv_color_white() : lv_color_make(0x40, 0x50, 0x80), 0);
}
// ---------------------------------------------------------------------------
// Static callbacks
// ---------------------------------------------------------------------------
void TestUnitDualButton::onTimer(lv_timer_t* t) {
static_cast<TestUnitDualButton*>(lv_timer_get_user_data(t))->update();
}
void TestUnitDualButton::onPinADown(lv_event_t* e) {
auto* self = static_cast<TestUnitDualButton*>(lv_event_get_user_data(e));
if (self->pinA_ > PIN_MIN) {
self->pinA_--;
lv_label_set_text_fmt(self->lblPinA_, "%d", (int)self->pinA_);
}
}
void TestUnitDualButton::onPinAUp(lv_event_t* e) {
auto* self = static_cast<TestUnitDualButton*>(lv_event_get_user_data(e));
if (self->pinA_ < PIN_MAX) {
self->pinA_++;
lv_label_set_text_fmt(self->lblPinA_, "%d", (int)self->pinA_);
}
}
void TestUnitDualButton::onPinBDown(lv_event_t* e) {
auto* self = static_cast<TestUnitDualButton*>(lv_event_get_user_data(e));
if (self->pinB_ > PIN_MIN) {
self->pinB_--;
lv_label_set_text_fmt(self->lblPinB_, "%d", (int)self->pinB_);
}
}
void TestUnitDualButton::onPinBUp(lv_event_t* e) {
auto* self = static_cast<TestUnitDualButton*>(lv_event_get_user_data(e));
if (self->pinB_ < PIN_MAX) {
self->pinB_++;
lv_label_set_text_fmt(self->lblPinB_, "%d", (int)self->pinB_);
}
}
void TestUnitDualButton::onConnect(lv_event_t* e) {
auto* self = static_cast<TestUnitDualButton*>(lv_event_get_user_data(e));
Device* gpio = device_find_by_name("gpio0");
if (!gpio || !self->unit_.begin(gpio, self->pinA_, self->pinB_)) {
if (self->lblError_) {
lv_label_set_text(self->lblError_, "GPIO init failed - check pins");
}
return;
}
self->connected_ = true;
// Obtain the parent (grandparent of the button's container)
lv_obj_t* btn = lv_event_get_target_obj(e);
lv_obj_t* cont = lv_obj_get_parent(btn);
lv_obj_t* parent = lv_obj_get_parent(cont);
self->buildTestScreen(parent);
}
@@ -0,0 +1,40 @@
#pragma once
#include "TestViewBase.h"
#include <UnitDualButton.h>
#include <tactility/drivers/gpio_controller.h>
class TestUnitDualButton final : public TestViewBase {
public:
void onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) override;
void onStop() override;
private:
UnitDualButton unit_;
bool connected_ = false;
// Config screen
gpio_pin_t pinA_ = 0;
gpio_pin_t pinB_ = 49;
lv_obj_t* lblPinA_ = nullptr;
lv_obj_t* lblPinB_ = nullptr;
lv_obj_t* lblError_ = nullptr;
// Test screen
lv_obj_t* circleA_ = nullptr;
lv_obj_t* circleB_ = nullptr;
lv_obj_t* circleLblA_ = nullptr;
lv_obj_t* circleLblB_ = nullptr;
lv_timer_t* timer_ = nullptr;
void buildConfigScreen(lv_obj_t* parent);
void buildTestScreen(lv_obj_t* parent);
void update();
static void onTimer(lv_timer_t* t);
static void onPinADown(lv_event_t* e);
static void onPinAUp(lv_event_t* e);
static void onPinBDown(lv_event_t* e);
static void onPinBUp(lv_event_t* e);
static void onConnect(lv_event_t* e);
};
@@ -0,0 +1,143 @@
#include "TestUnitJoystick2.h"
#include "UiScale.h"
#include <tactility/device.h>
#include <tactility/lvgl_fonts.h>
#include <algorithm>
#include <cmath>
void TestUnitJoystick2::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
app_ = app;
createToolbar(parent, handle, "Joystick2");
createBanner(parent, "Joystick2", "I2C", COLOR_I2C);
// Scale joystick area to shorter display dimension, clamped 80..300px
lv_coord_t minDim = std::min(uiW(), uiH());
int JOY_AREA = (int)(minDim * 3 / 10);
if (JOY_AREA < 80) JOY_AREA = 80;
if (JOY_AREA > 300) JOY_AREA = 300;
int DOT_SIZE = JOY_AREA / 8;
if (DOT_SIZE < 8) DOT_SIZE = 8;
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_flex_align(cont, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
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());
lblXY_ = lv_label_create(cont);
lv_obj_set_style_text_font(lblXY_, fnt, 0);
lblButton_ = lv_label_create(cont);
lv_obj_set_style_text_font(lblButton_, fnt, 0);
joyArea_ = JOY_AREA;
dotSize_ = DOT_SIZE;
// Visual joystick area
joyCont_ = lv_obj_create(cont);
lv_obj_set_size(joyCont_, JOY_AREA, JOY_AREA);
lv_obj_set_style_bg_color(joyCont_, lv_color_hex(0x222222), 0);
lv_obj_set_style_radius(joyCont_, JOY_AREA / 2, 0);
lv_obj_set_style_border_width(joyCont_, 2, 0);
lv_obj_set_style_pad_all(joyCont_, 0, 0);
lv_obj_remove_flag(joyCont_, LV_OBJ_FLAG_SCROLLABLE);
dot_ = lv_obj_create(joyCont_);
lv_obj_set_size(dot_, DOT_SIZE, DOT_SIZE);
lv_obj_set_style_radius(dot_, DOT_SIZE / 2, 0);
lv_obj_set_style_bg_color(dot_, lv_color_hex(0x00FF00), 0);
lv_obj_set_style_border_width(dot_, 0, 0);
lv_obj_set_pos(dot_, (JOY_AREA - DOT_SIZE) / 2, (JOY_AREA - DOT_SIZE) / 2);
Device* i2c = device_find_by_name("i2c1");
if (!i2c) {
lv_label_set_text(lblXY_, "i2c1 not found");
return;
}
if (unit_.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 (unit_.begin(i2c)) found = true;
}
if (!found) {
hub_.deselect();
lv_label_set_text(lblXY_, "Joystick2 not found");
return;
}
} else {
lv_label_set_text(lblXY_, "Joystick2 not found");
return;
}
timer_ = lv_timer_create(onTimer, 50, this);
update();
}
void TestUnitJoystick2::onStop() {
if (timer_) { lv_timer_delete(timer_); timer_ = nullptr; }
selectIfNeeded();
if (unit_.isPresent()) unit_.setLed(0x000000);
if (usingPaHub_ && hub_.isPresent()) hub_.deselect();
lblXY_ = lblButton_ = dot_ = joyCont_ = nullptr;
}
void TestUnitJoystick2::selectIfNeeded() {
if (usingPaHub_ && hub_.isPresent())
hub_.select(hub_.currentChannel());
}
void TestUnitJoystick2::onTimer(lv_timer_t* t) {
static_cast<TestUnitJoystick2*>(lv_timer_get_user_data(t))->update();
}
void TestUnitJoystick2::update() {
selectIfNeeded();
if (!unit_.isPresent()) return;
int16_t x = 0, y = 0;
unit_.readXY12(&x, &y);
bool pressed = unit_.isPressed();
lv_label_set_text_fmt(lblXY_, "X: %d Y: %d", (int)x, (int)y);
lv_label_set_text_fmt(lblButton_, "Button: %s", pressed ? "PRESSED" : "released");
// Map ±2048 joystick range to dot position within a circle.
// Work in float to do circular clamping, then snap back to int pixels.
// Negate both axes to match LVGL screen coordinates and joystick orientation.
// Grove connector facing away from the user.
float radius = (float)(joyArea_ - dotSize_) / 2.0f;
float nx = -(float)x / 2048.0f; // normalised -1..1
float ny = -(float)y / 2048.0f; // normalised -1..1
float dist2 = nx * nx + ny * ny;
if (dist2 > 1.0f) {
float inv = 1.0f / std::sqrt(dist2);
nx *= inv;
ny *= inv;
}
int cx = (int)(radius + nx * radius);
int cy = (int)(radius + ny * radius);
lv_obj_set_pos(dot_, cx, cy);
// LED: hue cycles with X position, blue when pressed
if (pressed) {
unit_.setLed(0x0000FF);
} else {
uint16_t hue = (uint16_t)((int)x * 360 / 4096 + 180); // map -2048..2048 -> 0..360
lv_color_t c = lv_color_hsv_to_rgb(hue % 360, 100, 78);
lv_color32_t c32 = lv_color_to_32(c, LV_OPA_COVER);
unit_.setLed(((uint32_t)c32.red << 16) | ((uint32_t)c32.green << 8) | c32.blue);
}
lv_obj_set_style_bg_color(dot_, pressed ? lv_color_hex(0xFF4400) : lv_color_hex(0x00FF00), 0);
}
@@ -0,0 +1,26 @@
#pragma once
#include "TestViewBase.h"
#include <UnitJoystick2.h>
#include <UnitPaHub.h>
class TestUnitJoystick2 final : public TestViewBase {
public:
void onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) override;
void onStop() override;
private:
UnitPaHub hub_;
UnitJoystick2 unit_;
lv_obj_t* lblXY_ = nullptr;
lv_obj_t* lblButton_ = nullptr;
lv_obj_t* dot_ = nullptr;
lv_obj_t* joyCont_ = nullptr;
lv_timer_t* timer_ = nullptr;
bool usingPaHub_ = false;
int joyArea_ = 120;
int dotSize_ = 16;
void selectIfNeeded();
static void onTimer(lv_timer_t* t);
void update();
};
+192
View File
@@ -0,0 +1,192 @@
#include "TestUnitLcd.h"
#include "UiScale.h"
#include <tactility/device.h>
#include <tactility/lvgl_fonts.h>
void TestUnitLcd::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
app_ = app;
rotation_ = 0;
usingPaHub_ = false;
createToolbar(parent, handle, "Color LCD");
createBanner(parent, "Color LCD", "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());
lblStatus_ = lv_label_create(cont);
lv_obj_set_style_text_font(lblStatus_, fnt, 0);
// Brightness row
lv_obj_t* brRow = lv_obj_create(cont);
lv_obj_set_width(brRow, LV_PCT(100));
lv_obj_set_height(brRow, LV_SIZE_CONTENT);
lv_obj_set_layout(brRow, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(brRow, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(brRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_all(brRow, 0, 0);
lv_obj_set_style_bg_opa(brRow, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(brRow, 0, 0);
lv_obj_t* brLbl = lv_label_create(brRow);
lv_label_set_text(brLbl, "Bright:");
lv_obj_set_style_text_font(brLbl, fnt, 0);
lv_obj_set_width(brLbl, LV_SIZE_CONTENT);
sliderBr_ = lv_slider_create(brRow);
lv_slider_set_range(sliderBr_, 0, 255);
lv_slider_set_value(sliderBr_, 128, LV_ANIM_OFF);
lv_obj_set_flex_grow(sliderBr_, 1);
lv_obj_add_event_cb(sliderBr_, onBrightnessChanged, LV_EVENT_VALUE_CHANGED, this);
// Rotation
lblRotation_ = lv_label_create(cont);
lv_obj_set_style_text_font(lblRotation_, fnt, 0);
lv_obj_t* btnRot = lv_button_create(cont);
lv_obj_add_event_cb(btnRot, onRotateClicked, LV_EVENT_CLICKED, this);
lv_obj_t* lbl = lv_label_create(btnRot);
lv_label_set_text(lbl, "Rotate 90");
lv_obj_set_style_text_font(lbl, fnt, 0);
// Fill buttons
lv_obj_t* fillRow = lv_obj_create(cont);
lv_obj_set_width(fillRow, LV_PCT(100));
lv_obj_set_height(fillRow, LV_SIZE_CONTENT);
lv_obj_set_layout(fillRow, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(fillRow, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(fillRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_column(fillRow, 8, 0);
lv_obj_set_style_bg_opa(fillRow, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(fillRow, 0, 0);
lv_obj_set_style_pad_all(fillRow, 0, 0);
lv_obj_t* btnRed = lv_button_create(fillRow);
lv_obj_add_event_cb(btnRed, onFillRedClicked, LV_EVENT_CLICKED, this);
lv_obj_t* lRed = lv_label_create(btnRed);
lv_label_set_text(lRed, "Fill Red");
lv_obj_set_style_text_font(lRed, fnt, 0);
lv_obj_t* btnBlue = lv_button_create(fillRow);
lv_obj_add_event_cb(btnBlue, onFillBlueClicked, LV_EVENT_CLICKED, this);
lv_obj_t* lBlue = lv_label_create(btnBlue);
lv_label_set_text(lBlue, "Fill Blue");
lv_obj_set_style_text_font(lBlue, fnt, 0);
// Text test button
lv_obj_t* btnText = lv_button_create(cont);
lv_obj_add_event_cb(btnText, onWriteTextClicked, LV_EVENT_CLICKED, this);
lv_obj_t* lText = lv_label_create(btnText);
lv_label_set_text(lText, "Write Text");
lv_obj_set_style_text_font(lText, fnt, 0);
Device* i2c = device_find_by_name("i2c1");
if (!i2c) {
lv_label_set_text(lblStatus_, "i2c1 not found");
return;
}
// Try standalone first, then scan PaHub channels
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; lcdChannel_ = ch; }
}
if (!found) {
hub_.deselect();
lv_label_set_text(lblStatus_, "LCD Unit not found");
return;
}
} else {
lv_label_set_text(lblStatus_, "LCD Unit not found");
return;
}
lv_label_set_text(lblStatus_, "LCD ready");
lv_label_set_text_fmt(lblRotation_, "Rotation: %d (portrait)", (int)rotation_);
lcd_.setBrightness(128);
lcd_.fillScreen(0x0000);
}
void TestUnitLcd::selectIfNeeded() {
if (usingPaHub_ && hub_.isPresent())
hub_.select(lcdChannel_);
}
void TestUnitLcd::onStop() {
selectIfNeeded();
if (lcd_.isPresent()) lcd_.setBrightness(0);
if (usingPaHub_ && hub_.isPresent()) hub_.deselect();
lblStatus_ = sliderBr_ = lblRotation_ = nullptr;
}
void TestUnitLcd::onBrightnessChanged(lv_event_t* e) {
auto* self = static_cast<TestUnitLcd*>(lv_event_get_user_data(e));
self->selectIfNeeded();
if (!self->lcd_.isPresent()) return;
self->lcd_.setBrightness((uint8_t)lv_slider_get_value(self->sliderBr_));
}
void TestUnitLcd::onRotateClicked(lv_event_t* e) {
auto* self = static_cast<TestUnitLcd*>(lv_event_get_user_data(e));
self->selectIfNeeded();
if (!self->lcd_.isPresent()) return;
self->rotation_ = (self->rotation_ + 1) & 0x03;
self->lcd_.setRotation(self->rotation_);
const char* names[] = { "0 (portrait)", "1 (landscape)", "2 (portrait flip)", "3 (landscape flip)" };
lv_label_set_text_fmt(self->lblRotation_, "Rotation: %s", names[self->rotation_]);
}
void TestUnitLcd::onFillRedClicked(lv_event_t* e) {
auto* self = static_cast<TestUnitLcd*>(lv_event_get_user_data(e));
self->selectIfNeeded();
if (!self->lcd_.isPresent()) return;
self->lcd_.fillScreen(UnitLcd::rgb888to565(0xFF0000));
}
void TestUnitLcd::onFillBlueClicked(lv_event_t* e) {
auto* self = static_cast<TestUnitLcd*>(lv_event_get_user_data(e));
self->selectIfNeeded();
if (!self->lcd_.isPresent()) return;
self->lcd_.fillScreen(UnitLcd::rgb888to565(0x0000FF));
}
void TestUnitLcd::onWriteTextClicked(lv_event_t* e) {
auto* self = static_cast<TestUnitLcd*>(lv_event_get_user_data(e));
self->selectIfNeeded();
if (!self->lcd_.isPresent()) return;
self->lcd_.fillScreen(0x0000);
uint16_t white = UnitLcd::rgb888to565(0xFFFFFF);
uint16_t yellow = UnitLcd::rgb888to565(0xFFFF00);
uint16_t cyan = UnitLcd::rgb888to565(0x00FFFF);
uint16_t red = UnitLcd::rgb888to565(0xFF4444);
uint16_t black = 0x0000;
uint16_t h = self->lcd_.height();
uint16_t w = self->lcd_.width();
// 5x7 bitmap font: 1 char = 7px tall, 6px wide; scale-1 row pitch = 9px (7+2 gap)
static constexpr uint16_t FONT1_ROW_H = 9; // row height at scale 1
static constexpr uint16_t FONT1_CHAR_W = 6; // char advance at scale 1
static constexpr uint16_t FONT1_COL_W = FONT1_CHAR_W * 2 + 1; // "R" + margin
int16_t bottomY = (h > FONT1_ROW_H) ? (int16_t)(h - FONT1_ROW_H) : 0;
int16_t middleY = (int16_t)(h / 2);
int16_t rightX = (w > FONT1_COL_W) ? (int16_t)(w - FONT1_COL_W) : 0;
// Scale-2 text: 14px tall, 12px pitch
self->lcd_.drawText(4, 8, "HELLO", yellow, black, 2);
self->lcd_.drawText(4, 32, "WORLD", cyan, black, 2);
self->lcd_.drawText(4, 4, "TOP-LEFT", white, black, 1);
self->lcd_.drawText(4, bottomY, "BOTTOM", red, black, 1);
self->lcd_.drawText(4, middleY, "MIDDLE", white, black, 1);
// Right-side marker so portrait/landscape are visually distinct
self->lcd_.drawText(rightX, 4, "R", white, black, 1);
}
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include "TestViewBase.h"
#include <UnitLcd.h>
#include <UnitPaHub.h>
class TestUnitLcd final : public TestViewBase {
public:
void onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) override;
void onStop() override;
private:
UnitPaHub hub_;
UnitLcd lcd_;
lv_obj_t* lblStatus_ = nullptr;
lv_obj_t* sliderBr_ = nullptr;
lv_obj_t* lblRotation_ = nullptr;
uint8_t rotation_ = 0;
bool usingPaHub_ = false;
uint8_t lcdChannel_ = 0;
void selectIfNeeded();
static void onBrightnessChanged(lv_event_t* e);
static void onRotateClicked(lv_event_t* e);
static void onFillRedClicked(lv_event_t* e);
static void onFillBlueClicked(lv_event_t* e);
static void onWriteTextClicked(lv_event_t* e);
};
@@ -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;
}
@@ -0,0 +1,58 @@
#pragma once
#include "TestViewBase.h"
#include <UnitLcd.h>
#include <UnitPaHub.h>
// PDQ graphics benchmark test, matching the Arduino_GFX PDQgraphicstest sketch.
// Phases run sequentially via one-shot LVGL timers; results appear in the LVGL
// log and are also drawn back onto the LCD unit at the end.
class TestUnitLcdGfx final : public TestViewBase {
public:
void onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) override;
void onStop() override;
private:
UnitPaHub hub_;
UnitLcd lcd_;
bool usingPaHub_ = false;
lv_obj_t* lblPhase_ = nullptr;
lv_obj_t* lblLog_ = nullptr;
lv_timer_t* timer_ = nullptr;
static constexpr int RESULT_COUNT = 15;
int phase_ = 0;
char logBuf_[768] = {};
uint32_t results_[RESULT_COUNT] = {};
// Pre-computed layout constants (set in onStart after lcd_.begin)
int16_t w_ = 0, h_ = 0;
int16_t minDim_ = 0, minDim1_ = 0; // min(w,h) and min(w,h)-1
int16_t cx_ = 0, cy_ = 0, cx1_ = 0, cy1_ = 0;
int16_t cMin_ = 0, cMin1_ = 0; // min(cx1,cy1) and min(cx1,cy1)-1
void selectIfNeeded();
void runNextPhase();
void appendLog(const char* name, uint32_t us);
void drawResultsOnLcd();
static void onTimer(lv_timer_t* t);
// Benchmark phases - matching PDQ order exactly
uint32_t testFillScreen();
uint32_t testText();
uint32_t testPixels();
uint32_t testLines();
uint32_t testFastLines();
uint32_t testFilledRects();
uint32_t testRects();
uint32_t testFilledTriangles();
uint32_t testTriangles();
uint32_t testFilledCircles();
uint32_t testCircles();
uint32_t testFillArcs();
uint32_t testArcs();
uint32_t testFilledRoundRects();
uint32_t testRoundRects();
};
@@ -0,0 +1,167 @@
#include "TestUnitMidi.h"
#include "UiScale.h"
#include <tactility/device.h>
#include <tactility/lvgl_fonts.h>
void TestUnitMidi::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
app_ = app;
createToolbar(parent, handle, "MIDI / Synth");
createBanner(parent, "MIDI", "UART", COLOR_UART);
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);
lblStatus_ = lv_label_create(cont);
lv_obj_set_style_text_font(lblStatus_, fnt, 0);
lblChannel_ = lv_label_create(cont);
lv_obj_set_style_text_font(lblChannel_, fntS, 0);
lblProgram_ = lv_label_create(cont);
lv_obj_set_style_text_font(lblProgram_, fntS, 0);
// Channel row
auto makeAdjRow = [&](const char* name, lv_event_cb_t down, lv_event_cb_t up) {
lv_obj_t* row = lv_obj_create(cont);
lv_obj_set_width(row, LV_PCT(100));
lv_obj_set_height(row, LV_SIZE_CONTENT);
lv_obj_set_layout(row, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_all(row, 0, 0);
lv_obj_set_style_pad_column(row, 8, 0);
lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(row, 0, 0);
lv_obj_t* lbl = lv_label_create(row);
lv_label_set_text(lbl, name);
lv_obj_set_style_text_font(lbl, fntS, 0);
lv_obj_set_width(lbl, LV_SIZE_CONTENT);
lv_obj_t* bDown = lv_button_create(row);
lv_obj_add_event_cb(bDown, down, LV_EVENT_CLICKED, this);
lv_obj_t* lD = lv_label_create(bDown); lv_label_set_text(lD, "-");
lv_obj_set_style_text_font(lD, fntS, 0);
lv_obj_t* bUp = lv_button_create(row);
lv_obj_add_event_cb(bUp, up, LV_EVENT_CLICKED, this);
lv_obj_t* lU = lv_label_create(bUp); lv_label_set_text(lU, "+");
lv_obj_set_style_text_font(lU, fntS, 0);
};
makeAdjRow("Channel:", onChDown, onChUp);
makeAdjRow("Program:", onProgDown, onProgUp);
// Note on/off
lv_obj_t* noteRow = lv_obj_create(cont);
lv_obj_set_width(noteRow, LV_PCT(100));
lv_obj_set_height(noteRow, LV_SIZE_CONTENT);
lv_obj_set_layout(noteRow, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(noteRow, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(noteRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_all(noteRow, 0, 0);
lv_obj_set_style_pad_column(noteRow, 8, 0);
lv_obj_set_style_bg_opa(noteRow, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(noteRow, 0, 0);
lv_obj_t* btnOn = lv_button_create(noteRow);
lv_obj_add_event_cb(btnOn, onNoteOnClicked, LV_EVENT_CLICKED, this);
lv_obj_t* lOn = lv_label_create(btnOn); lv_label_set_text(lOn, "Note On (C4)");
lv_obj_set_style_text_font(lOn, fntS, 0);
lv_obj_t* btnOff = lv_button_create(noteRow);
lv_obj_add_event_cb(btnOff, onNoteOffClicked, LV_EVENT_CLICKED, this);
lv_obj_t* lOff = lv_label_create(btnOff); lv_label_set_text(lOff, "Note Off");
lv_obj_set_style_text_font(lOff, fntS, 0);
Device* uart = device_find_by_name(UART_DEVICE);
if (!uart || !device_is_ready(uart) || !unit_.begin(uart)) {
lv_label_set_text(lblStatus_, "MIDI UART not available");
updateLabels();
return;
}
lv_label_set_text(lblStatus_, "MIDI ready (31250 bps)");
unit_.programChange(channel_, program_);
updateLabels();
}
void TestUnitMidi::onStop() {
if (notePlaying_ && unit_.isPresent()) {
unit_.noteOff(channel_, note_);
notePlaying_ = false;
}
unit_.end();
lblStatus_ = lblChannel_ = lblProgram_ = nullptr;
}
void TestUnitMidi::updateLabels() {
lv_label_set_text_fmt(lblChannel_, "Channel: %d", (int)channel_ + 1);
lv_label_set_text_fmt(lblProgram_, "Program: %d", (int)program_ + 1);
}
void TestUnitMidi::onNoteOnClicked(lv_event_t* e) {
auto* self = static_cast<TestUnitMidi*>(lv_event_get_user_data(e));
if (!self->unit_.isPresent()) return;
self->unit_.noteOn(self->channel_, self->note_, 100);
self->notePlaying_ = true;
}
void TestUnitMidi::onNoteOffClicked(lv_event_t* e) {
auto* self = static_cast<TestUnitMidi*>(lv_event_get_user_data(e));
if (!self->unit_.isPresent()) return;
self->unit_.noteOff(self->channel_, self->note_);
self->notePlaying_ = false;
}
void TestUnitMidi::onChDown(lv_event_t* e) {
auto* self = static_cast<TestUnitMidi*>(lv_event_get_user_data(e));
if (self->channel_ > 0) {
if (self->notePlaying_ && self->unit_.isPresent()) {
self->unit_.noteOff(self->channel_, self->note_);
self->notePlaying_ = false;
}
self->channel_--;
self->updateLabels();
}
}
void TestUnitMidi::onChUp(lv_event_t* e) {
auto* self = static_cast<TestUnitMidi*>(lv_event_get_user_data(e));
if (self->channel_ < 15) {
if (self->notePlaying_ && self->unit_.isPresent()) {
self->unit_.noteOff(self->channel_, self->note_);
self->notePlaying_ = false;
}
self->channel_++;
self->updateLabels();
}
}
void TestUnitMidi::onProgDown(lv_event_t* e) {
auto* self = static_cast<TestUnitMidi*>(lv_event_get_user_data(e));
if (self->program_ > 0) {
self->program_--;
if (self->unit_.isPresent()) self->unit_.programChange(self->channel_, self->program_);
self->updateLabels();
}
}
void TestUnitMidi::onProgUp(lv_event_t* e) {
auto* self = static_cast<TestUnitMidi*>(lv_event_get_user_data(e));
if (self->program_ < 127) {
self->program_++;
if (self->unit_.isPresent()) self->unit_.programChange(self->channel_, self->program_);
self->updateLabels();
}
}
@@ -0,0 +1,30 @@
#pragma once
#include "TestViewBase.h"
#include <UnitMidi.h>
class TestUnitMidi final : public TestViewBase {
public:
void onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) override;
void onStop() override;
private:
UnitMidi unit_;
lv_obj_t* lblStatus_ = nullptr;
lv_obj_t* lblChannel_ = nullptr;
lv_obj_t* lblProgram_ = nullptr;
uint8_t channel_ = 0;
uint8_t program_ = 0;
uint8_t note_ = 60; // middle C
bool notePlaying_= false;
// UART device name for MIDI unit (adjust to match your board wiring)
static constexpr const char* UART_DEVICE = "uart1";
static void onNoteOnClicked(lv_event_t* e);
static void onNoteOffClicked(lv_event_t* e);
static void onChUp(lv_event_t* e);
static void onChDown(lv_event_t* e);
static void onProgUp(lv_event_t* e);
static void onProgDown(lv_event_t* e);
void updateLabels();
};
@@ -0,0 +1,127 @@
#include "TestUnitPaHub.h"
#include "UiScale.h"
#include <tactility/device.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/lvgl_fonts.h>
void TestUnitPaHub::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
app_ = app;
createToolbar(parent, handle, "PaHub");
createBanner(parent, "PaHub", "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());
int pad = uiPad();
int gap = uiRowGap();
// Row of channel buttons — flex-grow so they share space evenly at any width
lv_obj_t* btnRow = lv_obj_create(cont);
lv_obj_set_width(btnRow, LV_PCT(100));
lv_obj_set_height(btnRow, LV_SIZE_CONTENT);
lv_obj_set_layout(btnRow, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(btnRow, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(btnRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_column(btnRow, gap, 0);
lv_obj_set_style_bg_opa(btnRow, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(btnRow, 0, 0);
lv_obj_set_style_pad_all(btnRow, 0, 0);
for (int i = 0; i < CH_COUNT; i++) {
lv_obj_t* btn = lv_button_create(btnRow);
lv_obj_set_flex_grow(btn, 1);
lv_obj_set_style_pad_hor(btn, pad, 0);
lv_obj_set_style_pad_ver(btn, gap, 0);
lv_obj_set_user_data(btn, (void*)(intptr_t)i);
lv_obj_add_event_cb(btn, onChannelBtn, LV_EVENT_CLICKED, this);
lv_obj_t* lbl = lv_label_create(btn);
lv_label_set_text_fmt(lbl, "CH%d", i);
lv_obj_set_style_text_font(lbl, fnt, 0);
lv_obj_align(lbl, LV_ALIGN_CENTER, 0, 0);
btnCh_[i] = btn;
}
lblStatus_ = lv_label_create(cont);
lv_obj_set_style_text_font(lblStatus_, fnt, 0);
lv_label_set_text(lblStatus_, "Select a channel to probe");
for (int i = 0; i < CH_COUNT; i++) {
lblCh_[i] = lv_label_create(cont);
lv_obj_set_style_text_font(lblCh_[i], fnt, 0);
lv_label_set_text_fmt(lblCh_[i], "CH%d: -", i);
}
Device* i2c = device_find_by_name("i2c1");
if (!i2c || !hub_.begin(i2c)) {
lv_label_set_text(lblStatus_, "PaHub not found");
return;
}
lv_label_set_text(lblStatus_, "PaHub ready - tap channel to probe");
timer_ = lv_timer_create(onTimer, 1000, this);
}
void TestUnitPaHub::onStop() {
if (timer_) { lv_timer_delete(timer_); timer_ = nullptr; }
if (hub_.isPresent()) hub_.deselect();
lblStatus_ = nullptr;
for (int i = 0; i < CH_COUNT; i++) { btnCh_[i] = nullptr; lblCh_[i] = nullptr; }
}
void TestUnitPaHub::onChannelBtn(lv_event_t* e) {
auto* self = static_cast<TestUnitPaHub*>(lv_event_get_user_data(e));
int ch = (int)(intptr_t)lv_obj_get_user_data(lv_event_get_target_obj(e));
self->selChannel_ = ch;
self->probeSelected();
}
void TestUnitPaHub::onTimer(lv_timer_t* t) {
auto* self = static_cast<TestUnitPaHub*>(lv_timer_get_user_data(t));
if (self->selChannel_ >= 0) self->probeSelected();
}
void TestUnitPaHub::probeSelected() {
if (!hub_.isPresent() || selChannel_ < 0 || selChannel_ >= CH_COUNT) return;
hub_.select((uint8_t)selChannel_);
// Scan I2C addresses 0x08-0x77 on this channel
Device* i2c = device_find_by_name("i2c1");
if (!i2c) {
lv_label_set_text_fmt(lblCh_[selChannel_], "CH%d: i2c1 not found", selChannel_);
hub_.deselect();
return;
}
char found[256] = "Found: ";
bool any = false;
for (uint8_t addr = 0x08; addr < 0x78; addr++) {
if (i2c_controller_has_device_at_address(i2c, addr,
pdMS_TO_TICKS(10)) == ERROR_NONE) {
size_t remaining = sizeof(found) - strlen(found) - 1;
if (remaining < 7) {
strncat(found, "...", remaining);
break;
}
char hex[8];
snprintf(hex, sizeof(hex), "0x%02X ", addr);
strncat(found, hex, remaining);
any = true;
}
}
if (!any) strcpy(found, "No devices found");
lv_label_set_text_fmt(lblCh_[selChannel_], "CH%d: %s", selChannel_, found);
lv_label_set_text_fmt(lblStatus_, "Probed CH%d", selChannel_);
hub_.deselect();
}
@@ -0,0 +1,23 @@
#pragma once
#include "TestViewBase.h"
#include <UnitPaHub.h>
class TestUnitPaHub final : public TestViewBase {
public:
void onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) override;
void onStop() override;
private:
static constexpr int CH_COUNT = UnitPaHub::NUM_CHANNELS;
UnitPaHub hub_;
lv_obj_t* lblStatus_ = nullptr;
lv_obj_t* btnCh_[CH_COUNT] = {};
lv_obj_t* lblCh_[CH_COUNT] = {};
lv_timer_t* timer_ = nullptr;
int selChannel_ = -1;
static void onChannelBtn(lv_event_t* e);
static void onTimer(lv_timer_t* t);
void probeSelected();
};
@@ -0,0 +1,222 @@
#include "TestUnitRfid2.h"
#include "UiScale.h"
#include <tactility/device.h>
#include <tactility/lvgl_fonts.h>
#include <tt_lvgl_toolbar.h>
#include <algorithm>
#include <cstdio>
#include <cstring>
// ---------------------------------------------------------------------------
// onStart
// ---------------------------------------------------------------------------
void TestUnitRfid2::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
app_ = app;
createToolbar(parent, handle, "RFID 2");
createBanner(parent, "RFID 2", "I2C", COLOR_I2C);
int pad = uiPad();
int rowGap = uiRowGap();
auto font = uiFont();
bool wide = uiW() >= 240;
// Compute circle diameter: min(availW, availH - toolbar) * 2/3, clamped to 300
lv_coord_t availW = uiW();
lv_coord_t availH = uiH() - 50;
lv_coord_t diam = static_cast<lv_coord_t>(std::min(availW, availH) * 2 / 3);
if (diam > 300) diam = 300;
// Content container - flex column, fills remaining space, centered
lv_obj_t* content = lv_obj_create(parent);
lv_obj_set_width(content, LV_PCT(100));
lv_obj_set_flex_grow(content, 1);
lv_obj_set_layout(content, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(content, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_all(content, pad, 0);
lv_obj_set_style_pad_row(content, rowGap, 0);
lv_obj_set_style_bg_opa(content, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(content, 0, 0);
// Idle group
idleGroup_ = lv_obj_create(content);
lv_obj_set_width(idleGroup_, LV_SIZE_CONTENT);
lv_obj_set_height(idleGroup_, LV_SIZE_CONTENT);
lv_obj_set_layout(idleGroup_, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(idleGroup_, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(idleGroup_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_all(idleGroup_, 0, 0);
lv_obj_set_style_pad_row(idleGroup_, rowGap, 0);
lv_obj_set_style_bg_opa(idleGroup_, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(idleGroup_, 0, 0);
// Green circle
circle_ = lv_obj_create(idleGroup_);
lv_obj_set_size(circle_, diam, diam);
lv_obj_set_style_radius(circle_, LV_RADIUS_CIRCLE, 0);
lv_obj_set_style_bg_color(circle_, LV_COLOR_MAKE(0x20, 0xC0, 0x50), 0);
lv_obj_set_style_bg_opa(circle_, pulseOpa_, 0);
lv_obj_set_style_border_width(circle_, 0, 0);
lv_obj_remove_flag(circle_, LV_OBJ_FLAG_SCROLLABLE);
// "Tap a tag/card..." label
lv_obj_t* tapLabel = lv_label_create(idleGroup_);
lv_obj_set_style_text_font(tapLabel, lvgl_get_text_font(font), 0);
lv_label_set_text(tapLabel, "Tap a tag/card...");
// ── Card info group ───────────────────────────────────────────────────────
cardGroup_ = lv_obj_create(content);
lv_obj_set_width(cardGroup_, LV_PCT(100));
lv_obj_set_height(cardGroup_, LV_SIZE_CONTENT);
lv_obj_set_layout(cardGroup_, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(cardGroup_, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(cardGroup_, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
lv_obj_set_style_pad_all(cardGroup_, pad, 0);
lv_obj_set_style_pad_row(cardGroup_, rowGap, 0);
lv_obj_set_style_bg_opa(cardGroup_, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(cardGroup_, 0, 0);
lv_obj_add_flag(cardGroup_, LV_OBJ_FLAG_HIDDEN);
lblUid_ = lv_label_create(cardGroup_);
lv_obj_set_style_text_font(lblUid_,
lvgl_get_text_font(wide ? FONT_SIZE_LARGE : FONT_SIZE_DEFAULT), 0);
lv_label_set_text(lblUid_, "");
lblType_ = lv_label_create(cardGroup_);
lv_obj_set_style_text_font(lblType_, lvgl_get_text_font(FONT_SIZE_DEFAULT), 0);
lv_label_set_text(lblType_, "");
lblSak_ = lv_label_create(cardGroup_);
lv_obj_set_style_text_font(lblSak_, lvgl_get_text_font(FONT_SIZE_SMALL), 0);
lv_label_set_text(lblSak_, "");
lv_obj_t* btnClear = lv_button_create(cardGroup_);
lv_obj_set_style_pad_hor(btnClear, pad * 2, 0);
lv_obj_set_style_pad_ver(btnClear, rowGap, 0);
lv_obj_add_event_cb(btnClear, onClear, LV_EVENT_CLICKED, this);
lv_obj_t* btnLbl = lv_label_create(btnClear);
lv_obj_set_style_text_font(btnLbl, lvgl_get_text_font(font), 0);
lv_label_set_text(btnLbl, "Clear");
// ── Device discovery ─────────────────────────────────────────────────────
Device* i2c = device_find_by_name("i2c1");
if (!i2c) return;
if (unit_.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 (unit_.begin(i2c)) found = true;
}
if (!found) {
hub_.deselect();
return;
}
} else {
return;
}
timer_ = lv_timer_create(onTimer, 100, this);
pulseTimer_ = lv_timer_create(onPulseTimer, 60, this);
}
// ---------------------------------------------------------------------------
// onStop
// ---------------------------------------------------------------------------
void TestUnitRfid2::onStop() {
if (timer_) { lv_timer_delete(timer_); timer_ = nullptr; }
if (pulseTimer_) { lv_timer_delete(pulseTimer_); pulseTimer_ = nullptr; }
if (usingPaHub_ && hub_.isPresent()) hub_.deselect();
cardShown_ = false;
idleGroup_ = circle_ = nullptr;
cardGroup_ = lblUid_ = lblType_ = lblSak_ = nullptr;
}
// ---------------------------------------------------------------------------
// PaHub helper
// ---------------------------------------------------------------------------
void TestUnitRfid2::selectIfNeeded() {
if (usingPaHub_ && hub_.isPresent())
hub_.select(hub_.currentChannel());
}
// ---------------------------------------------------------------------------
// showCard
// ---------------------------------------------------------------------------
void TestUnitRfid2::showCard(const UnitRfid2::Uid& uid) {
lastUid_ = uid;
cardType_ = unit_.getCardType(uid);
cardShown_ = true;
// UID string
char uidBuf[40] = "UID: ";
int pos = 5;
uint8_t size = (uid.size <= 10) ? uid.size : 10;
for (uint8_t i = 0; i < size; i++)
pos += snprintf(uidBuf + pos, sizeof(uidBuf) - (size_t)pos, "%02X ", uid.bytes[i]);
lv_label_set_text(lblUid_, uidBuf);
char typeBuf[64];
snprintf(typeBuf, sizeof(typeBuf), "Type: %s", unit_.cardTypeName(cardType_));
lv_label_set_text(lblType_, typeBuf);
char sakBuf[40];
snprintf(sakBuf, sizeof(sakBuf), "SAK: %02X ATQA: %02X %02X",
uid.sak, uid.atqa[0], uid.atqa[1]);
lv_label_set_text(lblSak_, sakBuf);
lv_obj_add_flag(idleGroup_, LV_OBJ_FLAG_HIDDEN);
lv_obj_remove_flag(cardGroup_, LV_OBJ_FLAG_HIDDEN);
unit_.haltCard();
}
// ---------------------------------------------------------------------------
// Timer callbacks
// ---------------------------------------------------------------------------
void TestUnitRfid2::onTimer(lv_timer_t* t) {
auto* self = static_cast<TestUnitRfid2*>(lv_timer_get_user_data(t));
if (self->cardShown_) return;
self->selectIfNeeded();
if (!self->unit_.isPresent()) return;
UnitRfid2::Uid uid = {};
if (self->unit_.readCard(&uid))
self->showCard(uid);
}
void TestUnitRfid2::onPulseTimer(lv_timer_t* t) {
auto* self = static_cast<TestUnitRfid2*>(lv_timer_get_user_data(t));
if (self->cardShown_ || !self->circle_) return;
self->pulseOpa_ = static_cast<uint8_t>(self->pulseOpa_ + self->pulseDir_);
if (self->pulseOpa_ >= 255) {
self->pulseOpa_ = 255;
self->pulseDir_ = -8;
} else if (self->pulseOpa_ <= 180) {
self->pulseOpa_ = 180;
self->pulseDir_ = 8;
}
lv_obj_set_style_bg_opa(self->circle_, self->pulseOpa_, 0);
}
// ---------------------------------------------------------------------------
// Clear button callback
// ---------------------------------------------------------------------------
void TestUnitRfid2::onClear(lv_event_t* e) {
auto* self = static_cast<TestUnitRfid2*>(lv_event_get_user_data(e));
self->cardShown_ = false;
lv_obj_remove_flag(self->idleGroup_, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(self->cardGroup_, LV_OBJ_FLAG_HIDDEN);
}
@@ -0,0 +1,44 @@
#pragma once
#include "TestViewBase.h"
#include <UnitRfid2.h>
#include <UnitPaHub.h>
#include <tt_app.h>
class TestUnitRfid2 final : public TestViewBase {
public:
void onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) override;
void onStop() override;
private:
UnitPaHub hub_;
UnitRfid2 unit_;
lv_timer_t* timer_ = nullptr;
lv_timer_t* pulseTimer_ = nullptr;
bool usingPaHub_ = false;
bool cardShown_ = false;
// Idle group
lv_obj_t* idleGroup_ = nullptr;
lv_obj_t* circle_ = nullptr;
// Card info group
lv_obj_t* cardGroup_ = nullptr;
lv_obj_t* lblUid_ = nullptr;
lv_obj_t* lblType_ = nullptr;
lv_obj_t* lblSak_ = nullptr;
// Pulse state
uint8_t pulseOpa_ = 220;
int8_t pulseDir_ = 8;
// Stored card info
UnitRfid2::Uid lastUid_ = {};
UnitRfid2::CardType cardType_ = UnitRfid2::CardType::Unknown;
void selectIfNeeded();
void showCard(const UnitRfid2::Uid& uid);
static void onTimer(lv_timer_t* t);
static void onPulseTimer(lv_timer_t* t);
static void onClear(lv_event_t* e);
};
@@ -0,0 +1,128 @@
#include "TestUnitScroll.h"
#include "UiScale.h"
#include <tactility/device.h>
#include <tactility/lvgl_fonts.h>
void TestUnitScroll::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
app_ = app;
createToolbar(parent, handle, "Scroll");
createBanner(parent, "Scroll", "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());
lblCounter_ = lv_label_create(cont);
lv_obj_set_style_text_font(lblCounter_, lvgl_get_text_font(uiW() < 200 ? FONT_SIZE_DEFAULT : FONT_SIZE_LARGE), 0);
lblButton_ = lv_label_create(cont);
lv_obj_set_style_text_font(lblButton_, fnt, 0);
lblLed_ = lv_label_create(cont);
lv_obj_set_style_text_font(lblLed_, fnt, 0);
// RGB sliders
auto makeSlider = [&](const char* name) -> lv_obj_t* {
lv_obj_t* row = lv_obj_create(cont);
lv_obj_set_width(row, LV_PCT(100));
lv_obj_set_height(row, LV_SIZE_CONTENT);
lv_obj_set_layout(row, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_all(row, 0, 0);
lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(row, 0, 0);
lv_obj_t* lbl = lv_label_create(row);
lv_label_set_text(lbl, name);
lv_obj_set_style_text_font(lbl, fnt, 0);
lv_obj_set_width(lbl, LV_SIZE_CONTENT);
lv_obj_t* sl = lv_slider_create(row);
lv_slider_set_range(sl, 0, 255);
lv_slider_set_value(sl, 0, LV_ANIM_OFF);
lv_obj_set_flex_grow(sl, 1);
lv_obj_add_event_cb(sl, onSliderChanged, LV_EVENT_VALUE_CHANGED, this);
return sl;
};
sliderR_ = makeSlider("R");
sliderG_ = makeSlider("G");
sliderB_ = makeSlider("B");
lv_label_set_text(lblLed_, "LED: #000000");
Device* i2c = device_find_by_name("i2c1");
if (!i2c) {
lv_label_set_text(lblCounter_, "i2c1 not found");
return;
}
if (unit_.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 (unit_.begin(i2c)) found = true;
}
if (!found) {
hub_.deselect();
lv_label_set_text(lblCounter_, "Scroll not found");
return;
}
} else {
lv_label_set_text(lblCounter_, "Scroll not found");
return;
}
timer_ = lv_timer_create(onTimer, 50, this);
update();
}
void TestUnitScroll::onStop() {
if (timer_) { lv_timer_delete(timer_); timer_ = nullptr; }
selectIfNeeded();
if (unit_.isPresent()) unit_.setLed(0x000000);
if (usingPaHub_ && hub_.isPresent()) hub_.deselect();
lblCounter_ = lblButton_ = lblLed_ = nullptr;
sliderR_ = sliderG_ = sliderB_ = nullptr;
}
void TestUnitScroll::selectIfNeeded() {
if (usingPaHub_ && hub_.isPresent())
hub_.select(hub_.currentChannel());
}
void TestUnitScroll::onTimer(lv_timer_t* t) {
static_cast<TestUnitScroll*>(lv_timer_get_user_data(t))->update();
}
void TestUnitScroll::onSliderChanged(lv_event_t* e) {
static_cast<TestUnitScroll*>(lv_event_get_user_data(e))->updateLedFromSliders();
}
void TestUnitScroll::update() {
selectIfNeeded();
if (!unit_.isPresent()) return;
counter_ -= unit_.readDelta();
lv_label_set_text_fmt(lblCounter_, "Counter: %ld", (long)counter_);
lv_label_set_text_fmt(lblButton_, "Button: %s", unit_.isPressed() ? "PRESSED" : "released");
}
void TestUnitScroll::updateLedFromSliders() {
selectIfNeeded();
if (!unit_.isPresent()) return;
uint32_t r = (uint32_t)lv_slider_get_value(sliderR_);
uint32_t g = (uint32_t)lv_slider_get_value(sliderG_);
uint32_t b = (uint32_t)lv_slider_get_value(sliderB_);
uint32_t rgb = (r << 16) | (g << 8) | b;
unit_.setLed(rgb);
lv_label_set_text_fmt(lblLed_, "LED: #%06lX", (unsigned long)rgb);
}
@@ -0,0 +1,29 @@
#pragma once
#include "TestViewBase.h"
#include <UnitScroll.h>
#include <UnitPaHub.h>
class TestUnitScroll final : public TestViewBase {
public:
void onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) override;
void onStop() override;
private:
UnitPaHub hub_;
UnitScroll unit_;
lv_obj_t* lblCounter_ = nullptr;
lv_obj_t* lblButton_ = nullptr;
lv_obj_t* lblLed_ = nullptr;
lv_obj_t* sliderR_ = nullptr;
lv_obj_t* sliderG_ = nullptr;
lv_obj_t* sliderB_ = nullptr;
lv_timer_t* timer_ = nullptr;
int32_t counter_ = 0;
bool usingPaHub_ = false;
void selectIfNeeded();
static void onTimer(lv_timer_t* t);
static void onSliderChanged(lv_event_t* e);
void update();
void updateLedFromSliders();
};
@@ -0,0 +1,67 @@
#include "TestViewBase.h"
#include "M5UnitTest.h"
#include "UiScale.h"
#include <tt_lvgl_toolbar.h>
#include <tactility/lvgl_fonts.h>
lv_obj_t* TestViewBase::createToolbar(lv_obj_t* parent, AppHandle handle, const char* title) {
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, handle);
tt_lvgl_toolbar_set_title(toolbar, title);
tt_lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_LEFT, onBackClicked, this);
return toolbar;
}
void TestViewBase::createBanner(lv_obj_t* parent, const char* unitName,
const char* ifaceBadge, lv_color_t accentColor) {
lv_coord_t bannerH = uiH() < 200 ? 16 : 22;
lv_obj_t* banner = lv_obj_create(parent);
lv_obj_set_width(banner, LV_PCT(100));
lv_obj_set_height(banner, bannerH);
lv_obj_set_style_bg_color(banner, accentColor, 0);
lv_obj_set_style_bg_opa(banner, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(banner, 0, 0);
lv_obj_set_style_radius(banner, 0, 0);
lv_obj_set_style_pad_hor(banner, 6, 0);
lv_obj_set_style_pad_ver(banner, 0, 0);
lv_obj_set_layout(banner, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(banner, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(banner, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_remove_flag(banner, LV_OBJ_FLAG_SCROLLABLE);
// Unit name label (left-aligned)
lv_obj_t* nameLabel = lv_label_create(banner);
lv_label_set_text(nameLabel, unitName);
lv_obj_set_style_text_color(nameLabel, lv_color_white(), 0);
lv_obj_set_style_text_font(nameLabel, lvgl_get_text_font(uiFont()), 0);
lv_obj_set_width(nameLabel, LV_SIZE_CONTENT);
// Interface badge pill (right-aligned)
lv_obj_t* pill = lv_obj_create(banner);
lv_obj_set_size(pill, LV_SIZE_CONTENT, bannerH - 4);
lv_obj_set_style_bg_color(pill, lv_color_white(), 0);
lv_obj_set_style_bg_opa(pill, LV_OPA_30, 0);
lv_obj_set_style_border_width(pill, 1, 0);
lv_obj_set_style_border_color(pill, lv_color_white(), 0);
lv_obj_set_style_border_opa(pill, LV_OPA_60, 0);
lv_obj_set_style_radius(pill, 4, 0);
lv_obj_set_style_pad_hor(pill, 4, 0);
lv_obj_set_style_pad_ver(pill, 0, 0);
lv_obj_remove_flag(pill, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* badgeLabel = lv_label_create(pill);
lv_label_set_text(badgeLabel, ifaceBadge);
lv_obj_set_style_text_color(badgeLabel, lv_color_white(), 0);
lv_obj_set_style_text_font(badgeLabel, lvgl_get_text_font(FONT_SIZE_SMALL), 0);
lv_obj_center(badgeLabel);
}
void TestViewBase::onBackClicked(lv_event_t* e) {
auto* self = static_cast<TestViewBase*>(lv_event_get_user_data(e));
if (!self || !self->app_) return;
self->onStop();
M5UnitTest* app = self->app_;
app->clearActiveTestView();
delete self;
app->showList();
}
@@ -0,0 +1,34 @@
#pragma once
#include <tt_app.h>
#include <lvgl.h>
class M5UnitTest;
// Minimal interface shared by all test views.
// To return to the list, concrete views call showList() on the owning M5UnitTest.
// M5UnitTest calls onStop() then deletes the view after the Back button is tapped.
class TestViewBase {
public:
virtual ~TestViewBase() = default;
virtual void onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) = 0;
virtual void onStop() = 0;
protected:
M5UnitTest* app_ = nullptr;
// Accent colors matching M5Stack product image palette
static constexpr lv_color_t COLOR_I2C = LV_COLOR_MAKE(0x1A, 0x6E, 0xC8); // M5 blue
static constexpr lv_color_t COLOR_GPIO = LV_COLOR_MAKE(0xC0, 0x20, 0x20); // red
static constexpr lv_color_t COLOR_UART = LV_COLOR_MAKE(0x20, 0x90, 0x50); // green
// Creates a standard toolbar with a Back button that returns to the list.
lv_obj_t* createToolbar(lv_obj_t* parent, AppHandle handle, const char* title);
// Creates a colored identity banner strip below the toolbar.
// ifaceBadge: short string e.g. "I2C", "GPIO", "UART"
void createBanner(lv_obj_t* parent, const char* unitName,
const char* ifaceBadge, lv_color_t accentColor);
static void onBackClicked(lv_event_t* e);
};
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <lvgl.h>
#include <tactility/lvgl_fonts.h>
// Device screen widths in default (portrait) orientation:
// tiny < 200 : small OLEDs, custom breadboard devices
// medium 200-399: M5Stack Core2, Cardputer (~240w)
// large 400-539: 800x480 devices (~480w portrait)
// large 540-719: M5PaperS3 (540w), 1024x600 (600w portrait)
// xlarge 720+ : Tab5 (720w portrait, 1280w landscape)
// uiW() reflects the actual rendered orientation (landscape flips w/h).
static constexpr lv_coord_t UI_TINY_THRESHOLD = 200;
static constexpr lv_coord_t UI_MEDIUM_THRESHOLD = 400;
static constexpr lv_coord_t UI_LARGE_THRESHOLD = 540;
inline lv_coord_t uiW() { return lv_display_get_horizontal_resolution(nullptr); }
inline lv_coord_t uiH() { return lv_display_get_vertical_resolution(nullptr); }
inline lv_coord_t uiShortSide() { lv_coord_t w = uiW(), h = uiH(); return w < h ? w : h; }
inline int uiPad() {
lv_coord_t w = uiW();
if (w < UI_TINY_THRESHOLD) return 4;
if (w < UI_MEDIUM_THRESHOLD) return 8;
if (w < UI_LARGE_THRESHOLD) return 12;
return 16;
}
inline int uiRowGap() {
lv_coord_t w = uiW();
if (w < UI_TINY_THRESHOLD) return 3;
if (w < UI_MEDIUM_THRESHOLD) return 6;
if (w < UI_LARGE_THRESHOLD) return 8;
return 12;
}
inline int uiCols() { return uiW() >= UI_TINY_THRESHOLD ? 2 : 1; }
inline enum LvglFontSize uiFont() {
lv_coord_t w = uiW();
return w < UI_TINY_THRESHOLD ? FONT_SIZE_SMALL : (w < UI_LARGE_THRESHOLD ? FONT_SIZE_DEFAULT : FONT_SIZE_LARGE);
}
+11
View File
@@ -0,0 +1,11 @@
#include "M5UnitTest.h"
#include <TactilityCpp/App.h>
extern "C" {
int main(int argc, char* argv[]) {
registerApp<M5UnitTest>();
return 0;
}
}