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:
@@ -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
|
||||
)
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#include "M5UnitTest.h"
|
||||
#include <TactilityCpp/App.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
registerApp<M5UnitTest>();
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user