ESP32: зафиксировать рабочий LVGL nav prototype и тесты

This commit is contained in:
AidarKC 2026-06-08 18:39:11 +04:00
parent a8734846a0
commit 32606fe1c2
12 changed files with 4533 additions and 3 deletions

View File

@ -0,0 +1,12 @@
# LVGL Russian font test
- Краткое описание: тест кастомного `LVGL`-шрифта с кириллицей на базе рабочего `LVGL + subserver touch` контура.
- Что проверять:
- на экране видны русские заголовки и подписи без транслита;
- отображаются буквы `Ё/ё`;
- видны кнопки `Статус`, `Подключение`, `Кошелёк`, `Запросы`, `Настройки`, `Регистрация`, `Разрешить`, `Отклонить`, `Назад`;
- длинная кнопка `Проверка переноса русского текста` отображается читаемо;
- строка `Нажато:` меняется при клике;
- строка `Касание:` меняется при касании.
- Ожидаемый результат: кириллица стабильно отображается на `LVGL`-экране и не ломает touch.
- Статус: pending

View File

@ -0,0 +1,17 @@
# ESP32 nav minimal test
- Краткое описание: новый минимальный навигационный UI-прототип для сабсервера на базе рабочего `LVGL + subserver touch`.
- Что проверять:
- стартует экран `HOME`;
- на `HOME` видны `login`, `subserver1`, `W BAT`, `STATUS`, кнопка `SETTINGS`;
- кнопка `SETTINGS` открывает `SETTINGS_MENU`;
- свайп влево на `HOME` открывает `SETTINGS_MENU`;
- в `SETTINGS_MENU` сначала видны только `Wi-Fi` и `Server`;
- свайп вверх показывает `Server` и `Account`;
- свайп вниз возвращает `Wi-Fi` и `Server`;
- свайп вправо из `SETTINGS_MENU` возвращает на `HOME`;
- нажатия по `Wi-Fi`, `Server`, `Account` открывают соответствующие экраны;
- свайп вправо из внутренних экранов возвращает в `SETTINGS_MENU`;
- если во время реального свайпа палец проходит по кнопке, это не должно открывать кнопку как обычный `click`.
- Ожидаемый результат: новый скетч даёт чистый навигационный каркас без старой перегруженной логики.
- Статус: pending

View File

@ -0,0 +1,123 @@
# SHiNE ESP32 Subserver UI Nav Minimal Spec
Минимальный навигационный прототип для `Waveshare ESP32-S3-Touch-AMOLED-2.16`.
## Цель
Этот прототип проверяет только базовую механику экранов, крупных кнопок и свайпов.
На этом этапе отсутствуют:
- реальный Wi-Fi;
- реальные серверные проверки;
- логин/пароль;
- PIN;
- кошелёк;
- QR;
- баланс;
- регистрация;
- PDA и транзакции;
- входящие запросы.
## Экраны
Прототип содержит 5 экранов:
- `HOME`
- `SETTINGS_MENU`
- `WIFI_SCREEN`
- `SERVER_SCREEN`
- `ACCOUNT_SCREEN`
## HOME
Показывает:
- сверху слева `login`;
- ниже `subserver1`;
- сверху справа простые индикаторы `W BAT`;
- по центру крупный текст `STATUS`;
- снизу большую кнопку `SETTINGS`.
Переходы:
- кнопка `SETTINGS` -> `SETTINGS_MENU`;
- свайп влево -> `SETTINGS_MENU`.
## SETTINGS_MENU
Показывает вертикальное меню из 3 пунктов:
- `Wi-Fi`
- `Server`
- `Account`
Одновременно видны только 2 пункта.
Начальное состояние:
- `Wi-Fi`
- `Server`
После свайпа вверх:
- `Server`
- `Account`
После свайпа вниз:
- `Wi-Fi`
- `Server`
Переходы:
- нажатие `Wi-Fi` -> `WIFI_SCREEN`;
- нажатие `Server` -> `SERVER_SCREEN`;
- нажатие `Account` -> `ACCOUNT_SCREEN`;
- свайп вправо -> `HOME`.
## WIFI_SCREEN
Показывает:
- `Wi-Fi`
- `Wi-Fi screen`
Переходы:
- свайп вправо -> `SETTINGS_MENU`
- кнопка `BACK` -> `SETTINGS_MENU`
## SERVER_SCREEN
Показывает:
- `Server`
- `Server screen`
Переходы:
- свайп вправо -> `SETTINGS_MENU`
- кнопка `BACK` -> `SETTINGS_MENU`
## ACCOUNT_SCREEN
Показывает:
- `Account`
- `Account screen`
Переходы:
- свайп вправо -> `SETTINGS_MENU`
- кнопка `BACK` -> `SETTINGS_MENU`
## Жесты
Поддерживаются направления:
- `SWIPE_LEFT`
- `SWIPE_RIGHT`
- `SWIPE_UP`
- `SWIPE_DOWN`
Маршруты:
- `HOME`: свайп влево -> `SETTINGS_MENU`
- `SETTINGS_MENU`: свайп вправо -> `HOME`
- `SETTINGS_MENU`: свайп вверх/вниз -> прокрутка списка
- `WIFI_SCREEN`: свайп вправо -> `SETTINGS_MENU`
- `SERVER_SCREEN`: свайп вправо -> `SETTINGS_MENU`
- `ACCOUNT_SCREEN`: свайп вправо -> `SETTINGS_MENU`
## Особенность обработки жестов
- если палец ушёл из зоны обычного тапа и жест определяется как свайп, случайное попадание на кнопку по пути не должно превращать свайп в нажатие;
- приоритет у свайпа, а не у `click`.
## Язык
Пока интерфейс отображается в ASCII/English, чтобы использовать подтверждённый рабочий шрифтовой путь на устройстве.

View File

@ -19,6 +19,8 @@
- `lvgl-touch-debug-test` — точечная диагностика touch: сырые координаты, маркер точки и большая тест-кнопка `LVGL` - `lvgl-touch-debug-test` — точечная диагностика touch: сырые координаты, маркер точки и большая тест-кнопка `LVGL`
- `lvgl-official-based-test` — наш минимальный экран, но на максимально близкой к официальному `LVGL_Widgets` инициализации - `lvgl-official-based-test` — наш минимальный экран, но на максимально близкой к официальному `LVGL_Widgets` инициализации
- `lvgl-subserver-touch-test` — гибрид: `LVGL`-интерфейс, но display/touch init и raw touch-read взяты из `shine_subserver_ui`; подтверждено на устройстве, touch работает, зелёных линий по краям нет - `lvgl-subserver-touch-test` — гибрид: `LVGL`-интерфейс, но display/touch init и raw touch-read взяты из `shine_subserver_ui`; подтверждено на устройстве, touch работает, зелёных линий по краям нет
- `lvgl-russian-font-test` — тест кастомного `LVGL`-шрифта с кириллицей: русские кнопки, длинные подписи и статусы
- `lvgl-nav-minimal-test` — новый минимальный UI-каркас сабсервера: `HOME`, `SETTINGS_MENU`, `Wi-Fi`, `Server`, `Account`, свайпы и крупные кнопки
Запуск: Запуск:
@ -35,4 +37,6 @@
- `./burn.sh lvgl-touch-debug-test` - `./burn.sh lvgl-touch-debug-test`
- `./burn.sh lvgl-official-based-test` - `./burn.sh lvgl-official-based-test`
- `./burn.sh lvgl-subserver-touch-test` - `./burn.sh lvgl-subserver-touch-test`
- `./burn.sh lvgl-russian-font-test`
- `./burn.sh lvgl-nav-minimal-test`
- `./flash_shine_subserver_ui.sh` - автоматически находит USB-порт и заливает `shine_subserver_ui` - `./flash_shine_subserver_ui.sh` - автоматически находит USB-порт и заливает `shine_subserver_ui`

View File

@ -25,9 +25,11 @@ case "${MODE}" in
lvgl-touch-debug-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_touch_debug_test" ;; lvgl-touch-debug-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_touch_debug_test" ;;
lvgl-official-based-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_official_based_test" ;; lvgl-official-based-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_official_based_test" ;;
lvgl-subserver-touch-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_subserver_touch_test" ;; lvgl-subserver-touch-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_subserver_touch_test" ;;
lvgl-russian-font-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_russian_font_test" ;;
lvgl-nav-minimal-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_nav_minimal_test" ;;
*) *)
echo "Unknown mode: ${MODE}" >&2 echo "Unknown mode: ${MODE}" >&2
echo "Use one of: hello, widgets, audio, simple, argon2, subserver-ui, text-test, gfx-text-test, gfx-layout-test, lvgl-basic-test, lvgl-interaction-test, lvgl-touch-debug-test, lvgl-official-based-test, lvgl-subserver-touch-test" >&2 echo "Use one of: hello, widgets, audio, simple, argon2, subserver-ui, text-test, gfx-text-test, gfx-layout-test, lvgl-basic-test, lvgl-interaction-test, lvgl-touch-debug-test, lvgl-official-based-test, lvgl-subserver-touch-test, lvgl-russian-font-test, lvgl-nav-minimal-test" >&2
exit 2 exit 2
;; ;;
esac esac

View File

@ -13,6 +13,8 @@
- `lvgl_touch_debug_test/` - диагностика touch: сырые координаты, точка касания и одна большая кнопка `LVGL` - `lvgl_touch_debug_test/` - диагностика touch: сырые координаты, точка касания и одна большая кнопка `LVGL`
- `lvgl_official_based_test/` - минимальный наш экран поверх максимально близкой к официальному `05_LVGL_Widgets` инициализации - `lvgl_official_based_test/` - минимальный наш экран поверх максимально близкой к официальному `05_LVGL_Widgets` инициализации
- `lvgl_subserver_touch_test/` - гибридный тест: `LVGL`-экран с инициализацией дисплея и чтением touch из `shine_subserver_ui`; подтверждён на реальном устройстве - `lvgl_subserver_touch_test/` - гибридный тест: `LVGL`-экран с инициализацией дисплея и чтением touch из `shine_subserver_ui`; подтверждён на реальном устройстве
- `lvgl_russian_font_test/` - тест кастомного кириллического `LVGL`-шрифта с русскими кнопками, длинными строками и рабочим touch
- `lvgl_nav_minimal_test/` - новый минимальный навигационный каркас сабсервера на рабочем `LVGL + subserver touch`
## Запуск ## Запуск
@ -25,3 +27,5 @@
- `./burn.sh lvgl-touch-debug-test` - `./burn.sh lvgl-touch-debug-test`
- `./burn.sh lvgl-official-based-test` - `./burn.sh lvgl-official-based-test`
- `./burn.sh lvgl-subserver-touch-test` - `./burn.sh lvgl-subserver-touch-test`
- `./burn.sh lvgl-russian-font-test`
- `./burn.sh lvgl-nav-minimal-test`

View File

@ -0,0 +1,499 @@
#include <Arduino.h>
#include <Wire.h>
#include <lvgl.h>
#include <Arduino_GFX_Library.h>
#include <TouchDrvCSTXXX.hpp>
#include <stdint.h>
#define PIN_LCD_CS 12
#define PIN_LCD_SCLK 38
#define PIN_LCD_D0 4
#define PIN_LCD_D1 5
#define PIN_LCD_D2 6
#define PIN_LCD_D3 7
#define PIN_LCD_RST 2
#define PIN_I2C_SDA 15
#define PIN_I2C_SCL 14
#define PIN_TP_INT 11
#define DISP_W 480
#define DISP_H 480
#define LVGL_TICK_MS 2
#define SWIPE_THRESHOLD 48
#define TAP_CANCEL_THRESHOLD 18
#define TEST_VERSION "NAV v2"
enum Screen {
SCREEN_HOME,
SCREEN_SETTINGS_MENU,
SCREEN_WIFI,
SCREEN_SERVER,
SCREEN_ACCOUNT,
};
enum SwipeDirection {
SWIPE_NONE,
SWIPE_LEFT,
SWIPE_RIGHT,
SWIPE_UP,
SWIPE_DOWN,
};
enum ActionId {
ACTION_NONE,
ACTION_OPEN_SETTINGS,
ACTION_OPEN_WIFI,
ACTION_OPEN_SERVER,
ACTION_OPEN_ACCOUNT,
ACTION_BACK_HOME,
ACTION_BACK_SETTINGS,
};
static const char *kMenuItems[] = {"Wi-Fi", "Server", "Account"};
static const size_t kMenuCount = sizeof(kMenuItems) / sizeof(kMenuItems[0]);
static lv_disp_draw_buf_t gDrawBuf;
static lv_color_t *gBuf1 = nullptr;
static lv_color_t *gBuf2 = nullptr;
static lv_obj_t *gRoot = nullptr;
Arduino_DataBus *gBus = new Arduino_ESP32QSPI(
PIN_LCD_CS, PIN_LCD_SCLK, PIN_LCD_D0, PIN_LCD_D1, PIN_LCD_D2, PIN_LCD_D3);
Arduino_CO5300 *gfx = new Arduino_CO5300(
gBus, PIN_LCD_RST, 0, DISP_W, DISP_H, 0, 0, 0, 0);
TouchDrvCST92xx gTouch;
static Screen gCurrentScreen = SCREEN_HOME;
static int gSettingsScrollIndex = 0;
static bool gTouchDown = false;
static int16_t gTouchStartX = 0;
static int16_t gTouchStartY = 0;
static int16_t gTouchLastX = 0;
static int16_t gTouchLastY = 0;
static SwipeDirection gPendingSwipe = SWIPE_NONE;
static bool gBlockClick = false;
static void rebuildScreen();
static void showScreen(Screen screen);
static void handleSwipe(SwipeDirection swipe);
static void lvglFlushCb(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *colorP) {
uint32_t w = area->x2 - area->x1 + 1;
uint32_t h = area->y2 - area->y1 + 1;
#if (LV_COLOR_16_SWAP != 0)
gfx->draw16bitBeRGBBitmap(area->x1, area->y1, (uint16_t *)&colorP->full, w, h);
#else
gfx->draw16bitRGBBitmap(area->x1, area->y1, (uint16_t *)&colorP->full, w, h);
#endif
lv_disp_flush_ready(disp);
}
static void lvglTick(void *arg) {
LV_UNUSED(arg);
lv_tick_inc(LVGL_TICK_MS);
}
static SwipeDirection detectSwipe(int16_t startX, int16_t startY, int16_t endX, int16_t endY) {
int16_t dx = endX - startX;
int16_t dy = endY - startY;
int16_t absDx = abs(dx);
int16_t absDy = abs(dy);
if (absDx < SWIPE_THRESHOLD && absDy < SWIPE_THRESHOLD) {
return SWIPE_NONE;
}
if (absDx >= absDy) {
return dx > 0 ? SWIPE_RIGHT : SWIPE_LEFT;
}
return dy > 0 ? SWIPE_DOWN : SWIPE_UP;
}
static bool movedTooFarForTap(int16_t startX, int16_t startY, int16_t endX, int16_t endY) {
return abs(endX - startX) >= TAP_CANCEL_THRESHOLD || abs(endY - startY) >= TAP_CANCEL_THRESHOLD;
}
static void lvglTouchRead(lv_indev_drv_t *indevDrv, lv_indev_data_t *data) {
LV_UNUSED(indevDrv);
int16_t x = 0;
int16_t y = 0;
bool touching = gTouch.getPoint(&x, &y, 1) > 0;
if (touching) {
if (!gTouchDown) {
gTouchDown = true;
gTouchStartX = x;
gTouchStartY = y;
}
gTouchLastX = x;
gTouchLastY = y;
if (movedTooFarForTap(gTouchStartX, gTouchStartY, gTouchLastX, gTouchLastY)) {
gBlockClick = true;
}
data->state = LV_INDEV_STATE_PR;
data->point.x = x;
data->point.y = y;
return;
}
if (gTouchDown) {
gTouchDown = false;
gPendingSwipe = detectSwipe(gTouchStartX, gTouchStartY, gTouchLastX, gTouchLastY);
if (gPendingSwipe == SWIPE_NONE && !movedTooFarForTap(gTouchStartX, gTouchStartY, gTouchLastX, gTouchLastY)) {
gBlockClick = false;
}
}
data->state = LV_INDEV_STATE_REL;
data->point.x = gTouchLastX;
data->point.y = gTouchLastY;
}
static void setRootStyle() {
lv_obj_set_style_bg_color(gRoot, lv_color_hex(0x08111B), 0);
lv_obj_set_style_bg_opa(gRoot, LV_OPA_COVER, 0);
lv_obj_set_style_pad_all(gRoot, 0, 0);
lv_obj_set_scrollbar_mode(gRoot, LV_SCROLLBAR_MODE_OFF);
}
static lv_obj_t *makeTitle(const char *text, lv_coord_t y, const lv_font_t *font) {
lv_obj_t *label = lv_label_create(gRoot);
lv_label_set_text(label, text);
lv_obj_set_style_text_font(label, font, 0);
lv_obj_set_style_text_color(label, lv_color_hex(0xFFFFFF), 0);
lv_obj_align(label, LV_ALIGN_TOP_MID, 0, y);
return label;
}
static lv_obj_t *makeBody(const char *text, lv_coord_t y, lv_coord_t width) {
lv_obj_t *label = lv_label_create(gRoot);
lv_label_set_text(label, text);
lv_obj_set_width(label, width);
lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP);
lv_obj_set_style_text_font(label, &lv_font_montserrat_18, 0);
lv_obj_set_style_text_color(label, lv_color_hex(0xD9E1EA), 0);
lv_obj_align(label, LV_ALIGN_TOP_MID, 0, y);
return label;
}
static void actionButtonCb(lv_event_t *event) {
if (lv_event_get_code(event) != LV_EVENT_CLICKED) {
return;
}
if (gBlockClick) {
return;
}
ActionId action = static_cast<ActionId>(reinterpret_cast<uintptr_t>(lv_event_get_user_data(event)));
switch (action) {
case ACTION_OPEN_SETTINGS:
showScreen(SCREEN_SETTINGS_MENU);
break;
case ACTION_OPEN_WIFI:
showScreen(SCREEN_WIFI);
break;
case ACTION_OPEN_SERVER:
showScreen(SCREEN_SERVER);
break;
case ACTION_OPEN_ACCOUNT:
showScreen(SCREEN_ACCOUNT);
break;
case ACTION_BACK_HOME:
showScreen(SCREEN_HOME);
break;
case ACTION_BACK_SETTINGS:
showScreen(SCREEN_SETTINGS_MENU);
break;
case ACTION_NONE:
default:
break;
}
}
static lv_obj_t *makeButton(const char *text,
lv_coord_t x,
lv_coord_t y,
lv_coord_t w,
lv_coord_t h,
uint32_t bgColor,
ActionId action,
const lv_font_t *font = &lv_font_montserrat_22) {
lv_obj_t *btn = lv_btn_create(gRoot);
lv_obj_set_size(btn, w, h);
lv_obj_set_pos(btn, x, y);
lv_obj_set_style_radius(btn, 18, 0);
lv_obj_set_style_bg_color(btn, lv_color_hex(bgColor), 0);
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(btn, 2, 0);
lv_obj_set_style_border_color(btn, lv_color_hex(0x6E8AA3), 0);
lv_obj_set_style_shadow_width(btn, 0, 0);
lv_obj_add_event_cb(btn, actionButtonCb, LV_EVENT_CLICKED, reinterpret_cast<void *>(static_cast<uintptr_t>(action)));
lv_obj_t *label = lv_label_create(btn);
lv_label_set_text(label, text);
lv_obj_set_style_text_font(label, font, 0);
lv_obj_set_style_text_color(label, lv_color_hex(0xFFFFFF), 0);
lv_obj_center(label);
return btn;
}
static void makeTopIcons() {
lv_obj_t *icons = lv_label_create(gRoot);
lv_label_set_text(icons, "W BAT");
lv_obj_set_style_text_font(icons, &lv_font_montserrat_18, 0);
lv_obj_set_style_text_color(icons, lv_color_hex(0xC9D3DE), 0);
lv_obj_align(icons, LV_ALIGN_TOP_RIGHT, -24, 18);
}
static void makeVersionTag() {
lv_obj_t *tag = lv_label_create(gRoot);
lv_label_set_text(tag, TEST_VERSION);
lv_obj_set_style_text_font(tag, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(tag, lv_color_hex(0x8398AD), 0);
lv_obj_align(tag, LV_ALIGN_BOTTOM_MID, 0, -10);
}
static void drawHome() {
setRootStyle();
lv_obj_t *login = lv_label_create(gRoot);
lv_label_set_text(login, "login");
lv_obj_set_style_text_font(login, &lv_font_montserrat_18, 0);
lv_obj_set_style_text_color(login, lv_color_hex(0xFFFFFF), 0);
lv_obj_align(login, LV_ALIGN_TOP_LEFT, 24, 18);
lv_obj_t *subserver = lv_label_create(gRoot);
lv_label_set_text(subserver, "subserver1");
lv_obj_set_style_text_font(subserver, &lv_font_montserrat_18, 0);
lv_obj_set_style_text_color(subserver, lv_color_hex(0xD5DEE7), 0);
lv_obj_align_to(subserver, login, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 6);
makeTopIcons();
makeTitle("STATUS", 165, &lv_font_montserrat_28);
makeBody("Swipe left or tap the button below.", 214, 360);
makeButton("SETTINGS", 22, 360, 436, 78, 0x2A6F97, ACTION_OPEN_SETTINGS, &lv_font_montserrat_24);
makeVersionTag();
}
static void drawSettingsMenu() {
setRootStyle();
makeTitle("SETTINGS", 18, &lv_font_montserrat_24);
makeBody("Swipe up/down to move. Swipe right to go home.", 56, 420);
for (int visibleIndex = 0; visibleIndex < 2; ++visibleIndex) {
int itemIndex = gSettingsScrollIndex + visibleIndex;
if (itemIndex >= static_cast<int>(kMenuCount)) {
break;
}
ActionId action = ACTION_NONE;
if (itemIndex == 0) action = ACTION_OPEN_WIFI;
if (itemIndex == 1) action = ACTION_OPEN_SERVER;
if (itemIndex == 2) action = ACTION_OPEN_ACCOUNT;
makeButton(kMenuItems[itemIndex], 22, 132 + visibleIndex * 126, 436, 104,
visibleIndex == 0 ? 0x355C7D : 0x2A9D8F, action, &lv_font_montserrat_24);
}
lv_obj_t *hint = lv_label_create(gRoot);
char hintText[48];
snprintf(hintText, sizeof(hintText), "Items %d-%d of %d",
gSettingsScrollIndex + 1,
min(gSettingsScrollIndex + 2, static_cast<int>(kMenuCount)),
static_cast<int>(kMenuCount));
lv_label_set_text(hint, hintText);
lv_obj_set_style_text_font(hint, &lv_font_montserrat_16, 0);
lv_obj_set_style_text_color(hint, lv_color_hex(0xA8B8C7), 0);
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -34);
makeVersionTag();
}
static void drawSimpleScreen(const char *title, const char *body) {
setRootStyle();
makeTitle(title, 56, &lv_font_montserrat_28);
makeBody(body, 132, 400);
makeBody("Swipe right to return to Settings.", 204, 400);
makeButton("BACK", 140, 344, 200, 72, 0x5A6570, ACTION_BACK_SETTINGS, &lv_font_montserrat_22);
makeVersionTag();
}
static void drawWifiScreen() {
drawSimpleScreen("Wi-Fi", "Wi-Fi screen");
}
static void drawServerScreen() {
drawSimpleScreen("Server", "Server screen");
}
static void drawAccountScreen() {
drawSimpleScreen("Account", "Account screen");
}
static void rebuildScreen() {
if (!gRoot) {
gRoot = lv_scr_act();
}
lv_obj_clean(gRoot);
switch (gCurrentScreen) {
case SCREEN_HOME:
drawHome();
break;
case SCREEN_SETTINGS_MENU:
drawSettingsMenu();
break;
case SCREEN_WIFI:
drawWifiScreen();
break;
case SCREEN_SERVER:
drawServerScreen();
break;
case SCREEN_ACCOUNT:
drawAccountScreen();
break;
}
}
static void showScreen(Screen screen) {
gCurrentScreen = screen;
rebuildScreen();
}
static void handleHomeSwipe(SwipeDirection swipe) {
if (swipe == SWIPE_LEFT) {
showScreen(SCREEN_SETTINGS_MENU);
}
}
static void handleSettingsSwipe(SwipeDirection swipe) {
if (swipe == SWIPE_RIGHT) {
showScreen(SCREEN_HOME);
return;
}
if (swipe == SWIPE_UP) {
gSettingsScrollIndex++;
if (gSettingsScrollIndex > static_cast<int>(kMenuCount) - 2) {
gSettingsScrollIndex = static_cast<int>(kMenuCount) - 2;
}
rebuildScreen();
return;
}
if (swipe == SWIPE_DOWN) {
gSettingsScrollIndex--;
if (gSettingsScrollIndex < 0) {
gSettingsScrollIndex = 0;
}
rebuildScreen();
}
}
static void handleWifiSwipe(SwipeDirection swipe) {
if (swipe == SWIPE_RIGHT) {
showScreen(SCREEN_SETTINGS_MENU);
}
}
static void handleServerSwipe(SwipeDirection swipe) {
if (swipe == SWIPE_RIGHT) {
showScreen(SCREEN_SETTINGS_MENU);
}
}
static void handleAccountSwipe(SwipeDirection swipe) {
if (swipe == SWIPE_RIGHT) {
showScreen(SCREEN_SETTINGS_MENU);
}
}
static void handleSwipe(SwipeDirection swipe) {
if (swipe == SWIPE_NONE) {
return;
}
switch (gCurrentScreen) {
case SCREEN_HOME:
handleHomeSwipe(swipe);
break;
case SCREEN_SETTINGS_MENU:
handleSettingsSwipe(swipe);
break;
case SCREEN_WIFI:
handleWifiSwipe(swipe);
break;
case SCREEN_SERVER:
handleServerSwipe(swipe);
break;
case SCREEN_ACCOUNT:
handleAccountSwipe(swipe);
break;
}
}
void setup() {
Serial.begin(115200);
Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL);
gfx->begin();
gBus->writeC8D8(0x36, 0xA0);
gfx->setBrightness(220);
gfx->fillScreen(0x0000);
gTouch.setPins(PIN_TP_INT, -1);
gTouch.begin(Wire, CST92XX_SLAVE_ADDRESS, PIN_I2C_SDA, PIN_I2C_SCL);
gTouch.setMaxCoordinates(DISP_W, DISP_H);
gTouch.setSwapXY(true);
gTouch.setMirrorXY(true, false);
lv_init();
uint32_t screenWidth = gfx->width();
uint32_t screenHeight = gfx->height();
uint32_t bufPixels = screenWidth * 40;
gBuf1 = (lv_color_t *)heap_caps_malloc(bufPixels * sizeof(lv_color_t), MALLOC_CAP_DMA);
gBuf2 = (lv_color_t *)heap_caps_malloc(bufPixels * sizeof(lv_color_t), MALLOC_CAP_DMA);
lv_disp_draw_buf_init(&gDrawBuf, gBuf1, gBuf2, bufPixels);
static lv_disp_drv_t dispDrv;
lv_disp_drv_init(&dispDrv);
dispDrv.hor_res = screenWidth;
dispDrv.ver_res = screenHeight;
dispDrv.flush_cb = lvglFlushCb;
dispDrv.draw_buf = &gDrawBuf;
lv_disp_drv_register(&dispDrv);
static lv_indev_drv_t indevDrv;
lv_indev_drv_init(&indevDrv);
indevDrv.type = LV_INDEV_TYPE_POINTER;
indevDrv.read_cb = lvglTouchRead;
lv_indev_drv_register(&indevDrv);
const esp_timer_create_args_t tickArgs = {
.callback = &lvglTick,
.name = "lvgl_tick"};
esp_timer_handle_t tickTimer = nullptr;
esp_timer_create(&tickArgs, &tickTimer);
esp_timer_start_periodic(tickTimer, LVGL_TICK_MS * 1000);
rebuildScreen();
Serial.println("Minimal nav test ready");
}
void loop() {
lv_timer_handler();
if (gPendingSwipe != SWIPE_NONE) {
SwipeDirection swipe = gPendingSwipe;
gPendingSwipe = SWIPE_NONE;
handleSwipe(swipe);
gBlockClick = false;
}
delay(5);
}

View File

@ -0,0 +1,4 @@
#pragma once
#include <lvgl.h>
LV_FONT_DECLARE(lv_font_ru_18);
LV_FONT_DECLARE(lv_font_ru_24);

View File

@ -0,0 +1,264 @@
#include <Arduino.h>
#include <Wire.h>
#include <lvgl.h>
#include <Arduino_GFX_Library.h>
#include <TouchDrvCSTXXX.hpp>
#include "lv_font_ru.h"
#define PIN_LCD_CS 12
#define PIN_LCD_SCLK 38
#define PIN_LCD_D0 4
#define PIN_LCD_D1 5
#define PIN_LCD_D2 6
#define PIN_LCD_D3 7
#define PIN_LCD_RST 2
#define PIN_I2C_SDA 15
#define PIN_I2C_SCL 14
#define PIN_TP_INT 11
#define DISP_W 480
#define DISP_H 480
#define LVGL_TICK_MS 2
#define TEST_VERSION "v1-ru-font"
static lv_disp_draw_buf_t gDrawBuf;
static lv_color_t *gBuf1 = nullptr;
static lv_color_t *gBuf2 = nullptr;
static lv_obj_t *gStatusLabel = nullptr;
static lv_obj_t *gTouchLabel = nullptr;
static uint32_t gClickCount = 0;
static bool gTouchActive = false;
static int16_t gLastX = -1;
static int16_t gLastY = -1;
Arduino_DataBus *gBus = new Arduino_ESP32QSPI(
PIN_LCD_CS, PIN_LCD_SCLK, PIN_LCD_D0, PIN_LCD_D1, PIN_LCD_D2, PIN_LCD_D3);
Arduino_CO5300 *gfx = new Arduino_CO5300(
gBus, PIN_LCD_RST, 0, DISP_W, DISP_H, 0, 0, 0, 0);
TouchDrvCST92xx gTouch;
static void lvglFlushCb(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *colorP) {
uint32_t w = area->x2 - area->x1 + 1;
uint32_t h = area->y2 - area->y1 + 1;
#if (LV_COLOR_16_SWAP != 0)
gfx->draw16bitBeRGBBitmap(area->x1, area->y1, (uint16_t *)&colorP->full, w, h);
#else
gfx->draw16bitRGBBitmap(area->x1, area->y1, (uint16_t *)&colorP->full, w, h);
#endif
lv_disp_flush_ready(disp);
}
static void lvglTick(void *arg) {
LV_UNUSED(arg);
lv_tick_inc(LVGL_TICK_MS);
}
static void updateTouchLabel() {
if (!gTouchLabel) {
return;
}
char text[96];
if (gTouchActive) {
snprintf(text, sizeof(text), "Касание: x=%d y=%d", gLastX, gLastY);
} else {
snprintf(text, sizeof(text), "Касание: нет x=%d y=%d", gLastX, gLastY);
}
lv_label_set_text(gTouchLabel, text);
}
static void lvglTouchRead(lv_indev_drv_t *indevDrv, lv_indev_data_t *data) {
LV_UNUSED(indevDrv);
int16_t x = 0;
int16_t y = 0;
bool touching = gTouch.getPoint(&x, &y, 1) > 0;
if (touching) {
gTouchActive = true;
gLastX = x;
gLastY = y;
data->state = LV_INDEV_STATE_PR;
data->point.x = x;
data->point.y = y;
} else {
gTouchActive = false;
data->state = LV_INDEV_STATE_REL;
data->point.x = gLastX >= 0 ? gLastX : 0;
data->point.y = gLastY >= 0 ? gLastY : 0;
}
updateTouchLabel();
}
static void buttonEventCb(lv_event_t *event) {
if (lv_event_get_code(event) != LV_EVENT_CLICKED) {
return;
}
const char *name = (const char *)lv_event_get_user_data(event);
gClickCount++;
char text[128];
snprintf(text, sizeof(text), "Нажато: %s (%lu)", name, (unsigned long)gClickCount);
lv_label_set_text(gStatusLabel, text);
Serial.println(text);
}
static lv_obj_t *makeButton(lv_obj_t *parent,
const char *labelText,
lv_coord_t x,
lv_coord_t y,
lv_coord_t w,
lv_coord_t h,
uint32_t bgColor) {
lv_obj_t *btn = lv_btn_create(parent);
lv_obj_set_size(btn, w, h);
lv_obj_set_pos(btn, x, y);
lv_obj_set_style_radius(btn, 16, 0);
lv_obj_set_style_bg_color(btn, lv_color_hex(bgColor), 0);
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(btn, 2, 0);
lv_obj_set_style_border_color(btn, lv_color_hex(0x6F8BA4), 0);
lv_obj_set_style_shadow_width(btn, 0, 0);
lv_obj_set_style_pad_all(btn, 4, 0);
lv_obj_add_event_cb(btn, buttonEventCb, LV_EVENT_CLICKED, (void *)labelText);
lv_obj_t *label = lv_label_create(btn);
lv_label_set_text(label, labelText);
lv_obj_set_width(label, w - 12);
lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP);
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_color(label, lv_color_hex(0xFFFFFF), 0);
lv_obj_set_style_text_font(label, &lv_font_ru_18, 0);
lv_obj_center(label);
return btn;
}
static void createUi() {
lv_obj_t *screen = lv_scr_act();
lv_obj_set_style_bg_color(screen, lv_color_hex(0x08131E), 0);
lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, 0);
lv_obj_set_style_pad_all(screen, 0, 0);
lv_obj_set_scrollbar_mode(screen, LV_SCROLLBAR_MODE_OFF);
lv_obj_t *title = lv_label_create(screen);
lv_label_set_text(title, "Русский тест LVGL");
lv_obj_set_style_text_font(title, &lv_font_ru_24, 0);
lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10);
lv_obj_t *version = lv_label_create(screen);
lv_label_set_text(version, "Версия: " TEST_VERSION);
lv_obj_set_style_text_font(version, &lv_font_ru_18, 0);
lv_obj_set_style_text_color(version, lv_color_hex(0xD8E0EA), 0);
lv_obj_align(version, LV_ALIGN_TOP_MID, 0, 42);
lv_obj_t *hint = lv_label_create(screen);
lv_label_set_text(hint, "Проверка кириллицы, длинных слов, кнопок и нажатий.");
lv_obj_set_width(hint, 436);
lv_label_set_long_mode(hint, LV_LABEL_LONG_WRAP);
lv_obj_set_style_text_font(hint, &lv_font_ru_18, 0);
lv_obj_set_style_text_color(hint, lv_color_hex(0xD8E0EA), 0);
lv_obj_align(hint, LV_ALIGN_TOP_MID, 0, 72);
makeButton(screen, "Статус", 22, 126, 136, 62, 0x355C7D);
makeButton(screen, "Подключение", 172, 126, 136, 62, 0x2A9D8F);
makeButton(screen, "Кошелёк", 322, 126, 136, 62, 0x457B9D);
makeButton(screen, "Запросы", 22, 204, 136, 62, 0xE76F51);
makeButton(screen, "Настройки", 172, 204, 136, 62, 0x8D5A97);
makeButton(screen, "Регистрация", 322, 204, 136, 62, 0x6A994E);
makeButton(screen, "Разрешить", 22, 282, 136, 62, 0xBC6C25);
makeButton(screen, "Отклонить", 172, 282, 136, 62, 0xB56576);
makeButton(screen, "Назад", 322, 282, 136, 62, 0x6C757D);
makeButton(screen, "Проверка переноса русского текста", 22, 360, 436, 64, 0x1D6F42);
lv_obj_t *statusPanel = lv_obj_create(screen);
lv_obj_set_size(statusPanel, 436, 24);
lv_obj_set_pos(statusPanel, 22, 428);
lv_obj_set_style_radius(statusPanel, 10, 0);
lv_obj_set_style_bg_color(statusPanel, lv_color_hex(0x162435), 0);
lv_obj_set_style_bg_opa(statusPanel, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(statusPanel, 1, 0);
lv_obj_set_style_border_color(statusPanel, lv_color_hex(0x4D6986), 0);
lv_obj_set_style_pad_all(statusPanel, 2, 0);
gStatusLabel = lv_label_create(statusPanel);
lv_label_set_text(gStatusLabel, "Нажато: ничего");
lv_obj_set_style_text_font(gStatusLabel, &lv_font_ru_18, 0);
lv_obj_set_style_text_color(gStatusLabel, lv_color_hex(0xFFFFFF), 0);
lv_obj_center(gStatusLabel);
lv_obj_t *touchPanel = lv_obj_create(screen);
lv_obj_set_size(touchPanel, 436, 24);
lv_obj_set_pos(touchPanel, 22, 454);
lv_obj_set_style_radius(touchPanel, 10, 0);
lv_obj_set_style_bg_color(touchPanel, lv_color_hex(0x162435), 0);
lv_obj_set_style_bg_opa(touchPanel, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(touchPanel, 1, 0);
lv_obj_set_style_border_color(touchPanel, lv_color_hex(0x4D6986), 0);
lv_obj_set_style_pad_all(touchPanel, 2, 0);
gTouchLabel = lv_label_create(touchPanel);
lv_label_set_text(gTouchLabel, "Касание: нет x=-1 y=-1");
lv_obj_set_style_text_font(gTouchLabel, &lv_font_ru_18, 0);
lv_obj_set_style_text_color(gTouchLabel, lv_color_hex(0xD8E0EA), 0);
lv_obj_center(gTouchLabel);
}
void setup() {
Serial.begin(115200);
Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL);
gfx->begin();
gBus->writeC8D8(0x36, 0xA0);
gfx->setBrightness(220);
gfx->fillScreen(0x0000);
gTouch.setPins(PIN_TP_INT, -1);
gTouch.begin(Wire, CST92XX_SLAVE_ADDRESS, PIN_I2C_SDA, PIN_I2C_SCL);
gTouch.setMaxCoordinates(DISP_W, DISP_H);
gTouch.setSwapXY(true);
gTouch.setMirrorXY(true, false);
lv_init();
uint32_t screenWidth = gfx->width();
uint32_t screenHeight = gfx->height();
uint32_t bufPixels = screenWidth * 40;
gBuf1 = (lv_color_t *)heap_caps_malloc(bufPixels * sizeof(lv_color_t), MALLOC_CAP_DMA);
gBuf2 = (lv_color_t *)heap_caps_malloc(bufPixels * sizeof(lv_color_t), MALLOC_CAP_DMA);
lv_disp_draw_buf_init(&gDrawBuf, gBuf1, gBuf2, bufPixels);
static lv_disp_drv_t dispDrv;
lv_disp_drv_init(&dispDrv);
dispDrv.hor_res = screenWidth;
dispDrv.ver_res = screenHeight;
dispDrv.flush_cb = lvglFlushCb;
dispDrv.draw_buf = &gDrawBuf;
lv_disp_drv_register(&dispDrv);
static lv_indev_drv_t indevDrv;
lv_indev_drv_init(&indevDrv);
indevDrv.type = LV_INDEV_TYPE_POINTER;
indevDrv.read_cb = lvglTouchRead;
lv_indev_drv_register(&indevDrv);
const esp_timer_create_args_t tickArgs = {
.callback = &lvglTick,
.name = "lvgl_tick"};
esp_timer_handle_t tickTimer = nullptr;
esp_timer_create(&tickArgs, &tickTimer);
esp_timer_start_periodic(tickTimer, LVGL_TICK_MS * 1000);
createUi();
Serial.println("Русский LVGL font test ready: " TEST_VERSION);
}
void loop() {
lv_timer_handler();
delay(5);
}

View File

@ -1,2 +1,2 @@
client.version=1.2.141 client.version=1.2.142
server.version=1.2.133 server.version=1.2.134