Compare commits

..

7 Commits

32 changed files with 10270 additions and 3 deletions

View File

@ -0,0 +1,26 @@
# ESP32 UI-прототип сабсервера SHiNE
- краткое описание фичи:
для `Waveshare ESP32-S3-Touch-AMOLED-2.16` добавлен новый интерактивный UI-скетч сабсервера `SHiNE` с хранением данных в `NVS`, настройками `Wi-Fi`, настройками серверов, кошельком, экраном `QR/URI`, живой Solana-регистрацией и экраном входящих запросов. Логика PIN в коде сохранена, но вход по PIN во временной сборке отключён, чтобы не блокировать проверку остальных экранов. В текущей версии `Wi-Fi` подключается реально, адреса `API/RPC/WS` проверяются реально, баланс кошелька читается из `Solana RPC`, а регистрация отправляет `create_user_pda` в `shine_users`.
- что именно проверять:
1. Прошить режим `subserver-ui` и дождаться открытия главного экрана без PIN.
2. Проверить, что текст в заголовках, кнопках и статусах отображается читаемо; в текущей временной сборке допускается ASCII-транслитерация русского текста.
3. Открыть `Настройки` и убедиться, что показывается пометка о временно отключённом входе по PIN.
4. Открыть `Подключение -> Wi-Fi`, ввести `SSID` и пароль, нажать `Проверить`, дождаться реального подключения, затем перезагрузить устройство и проверить, что значения сохранились.
5. Открыть `Подключение -> Серверы`, проверить или изменить `API/RPC/WS`, нажать `Проверить` и убедиться, что показываются реальные статусы доступности, затем перезагрузить устройство и проверить сохранение значений.
6. Открыть `Аккаунт`, ввести логин, имя сабсервера и нажать `Сгенерировать`; проверить, что появились секрет и адрес кошелька, а после перезагрузки они не исчезают.
7. Открыть `Кошелёк`, нажать `Проверить` и убедиться, что баланс реально читается из `Solana RPC`; затем открыть `QR и URI` и проверить, что QR-код отрисовывается и сканируется как `solana:`-ссылка.
8. При необходимости отдельно проверить тестовые кнопки `+/- SOL`: они меняют локальный баланс для UX-сценариев, но после следующей реальной RPC-проверки баланс должен вернуться к сетевому значению.
9. Вернуться на главный экран и проверить, что до выполнения всех условий кнопка регистрации недоступна, а после выполнения становится доступной.
10. Выполнить регистрацию и убедиться, что статус меняется на `Сабсервер активен`, онлайн-статус становится активным, а на экране появляются краткие отпечатки `PDA/TX`.
11. После регистрации проверить через `Solana`/UI проекта, что `user_pda` для этого логина реально создана и соответствует `device`-адресу устройства.
12. Открыть `Запросы`, поочерёдно открыть оба демонстрационных запроса и проверить, что кнопки `Разрешить` и `Отклонить` меняют их статус.
13. При необходимости открыть `Настройки -> Сменить PIN` и убедиться, что новый PIN сохраняется, хотя вход по PIN временно не используется на старте.
14. Выполнить `Полный сброс` и убедиться, что все поля, секрет, баланс, онлайн и регистрация очищаются.
- ожидаемый результат:
новый `ESP32`-скетч стабильно запускается, показывает читаемый интерфейс хотя бы в ASCII-транслитерации, сохраняет данные во внутренней памяти устройства, реально подключается к `Wi-Fi`, реально проверяет `API/RPC/WS`, реально читает баланс из `Solana RPC`, рисует рабочий `QR` для `solana:`-URI и позволяет вручную пройти полный сценарий on-chain регистрации сабсервера.
- статус:
pending

View File

@ -0,0 +1,13 @@
# ESP32 авто-прошивка shine_subserver_ui
- краткое описание фичи:
добавлен исполняемый скрипт `flash_shine_subserver_ui.sh`, который автоматически ищет USB-порт `ESP32` и запускает заливку прошивки `shine_subserver_ui` без ручного указания `PORT`.
- что именно проверять:
1. Подключить плату `ESP32` по USB.
2. Перейти в папку `ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/`.
3. Запустить `./flash_shine_subserver_ui.sh`.
4. Убедиться, что скрипт сам показывает найденный порт и успешно запускает compile/upload.
- ожидаемый результат:
скрипт без ручного ввода порта находит `ESP32`, печатает найденный `/dev/ttyACM*` или `/dev/ttyUSB*` и заливает `shine_subserver_ui`.
- статус:
pending

View File

@ -0,0 +1,14 @@
# ESP32 тест рендера текста
- краткое описание фичи:
добавлен отдельный диагностический скетч `text_render_test`, который показывает один экран с несколькими вариантами вывода текста: встроенный шрифт `Arduino_GFX`, `U8g2` ASCII, `U8g2` кириллица и кнопки с подписями. Скрипт нужен для изоляции проблемы, когда на экране видны только цветные кнопки и блоки, но не видно ни одной буквы.
- что именно проверять:
1. Прошить режим `text-test`.
2. Проверить, виден ли заголовок `TEXT TEST 123`.
3. Проверить, видны ли строки `A`, `B`, `C`, `D`.
4. Проверить, видны ли подписи на трёх нижних кнопках: `BTN 1`, `abc123`, `Русский`.
5. Сравнить, какой из способов вывода реально отображается, а какой нет.
- ожидаемый результат:
хотя бы один вариант вывода текста становится видим на экране, что позволяет локализовать проблему до конкретного шрифта или способа рендера.
- статус:
pending

View File

@ -0,0 +1,12 @@
# ESP32 PIN-клавиатура: подписи кнопок
- краткое описание фичи:
в UI-скетче `shine_subserver_ui` изменена отрисовка подписей кнопок. Вместо малого шрифта теперь используется более стабильный шрифт с явным центрированием текста внутри кнопок, чтобы на экране ввода PIN и других экранах не пропадали цифры и надписи.
- что именно проверять:
1. Включить устройство и дождаться экрана ввода PIN.
2. Убедиться, что на всех серых кнопках видны цифры `0-9`, `Отмена` и `OK`.
3. Открыть другие экраны с кнопками (`Главный экран`, `Wi-Fi`, `Серверы`, `Настройки`) и убедиться, что подписи отображаются и не уезжают за границы кнопок.
- ожидаемый результат:
подписи кнопок стабильно видны сразу после старта, текст визуально центрирован, пустых серых кнопок без цифр и названий нет.
- статус:
pending

View File

@ -0,0 +1,13 @@
# ESP32 папка тестовых скетчей
- краткое описание фичи:
добавлена отдельная папка `test_sketches/` с изолированными диагностическими скетчами для экрана `ESP32-S3-Touch-AMOLED-2.16`: тест рендера текста через `Arduino_GFX`, тест геометрии кнопок и минимальный тест `LVGL`.
- что именно проверять:
1. Запустить `./burn.sh gfx-text-test` и убедиться, что прошивается тест текста из новой папки.
2. Запустить `./burn.sh gfx-layout-test` и проверить нижние ряды кнопок.
3. Запустить `./burn.sh lvgl-basic-test` и проверить, что `LVGL` показывает текст и кнопки.
4. Убедиться, что новая папка не мешает сборке `subserver-ui`.
- ожидаемый результат:
тестовые скетчи лежат отдельно от основного UI, шьются отдельными режимами и позволяют быстро проверять разные гипотезы по экрану без правок в `shine_subserver_ui`.
- статус:
pending

View File

@ -0,0 +1,14 @@
# ESP32 LVGL interaction test
- краткое описание фичи:
добавлен отдельный скетч `lvgl_interaction_test` на `LVGL`: экран с 9 кнопками, touch-вводом и нижней статусной строкой. При нажатии на кнопку на экране и в `Serial` показывается, какая именно кнопка нажата и сколько нажатий уже было.
- что именно проверять:
1. Прошить режим `lvgl-interaction-test`.
2. Убедиться, что виден заголовок, подзаголовок, 9 кнопок и нижняя статусная панель.
3. Поочерёдно нажать разные кнопки.
4. Проверить, что нижняя строка меняется на `Pressed: <button> (#N)`.
5. Проверить, что touch устойчиво работает по всей сетке кнопок.
- ожидаемый результат:
`LVGL` стабильно рисует плотный экран с множеством кнопок, а нажатия корректно обрабатываются и визуально подтверждаются без глюков позиционирования.
- статус:
pending

View File

@ -0,0 +1,14 @@
# ESP32 LVGL touch debug test
- краткое описание фичи:
добавлен отдельный диагностический скетч `lvgl_touch_debug_test`, который одновременно показывает сырые координаты touch, маркер точки касания и одну большую кнопку `LVGL`. Он нужен, чтобы отделить проблему raw-touch от проблемы доставки событий в `LVGL`.
- что именно проверять:
1. Прошить режим `lvgl-touch-debug-test`.
2. Коснуться экрана в разных местах.
3. Проверить, меняется ли текст `RAW pressed` и координаты `x/y`.
4. Проверить, появляется ли розовый маркер точки касания.
5. Проверить, срабатывает ли большая кнопка `Tap Here` и меняется ли строка `LVGL button clicked`.
- ожидаемый результат:
становится ясно, работает ли сам touch-драйвер, правильно ли приходят координаты и доходит ли нажатие до кнопки `LVGL`.
- статус:
pending

View File

@ -0,0 +1,13 @@
# ESP32 LVGL official based test
- краткое описание фичи:
добавлен отдельный скетч `lvgl_official_based_test`, который строится на максимально близкой к официальному `05_LVGL_Widgets` инициализации `display + touch + LVGL`, но вместо официального demo рисует наш компактный экран с кнопками и статусом нажатия.
- что именно проверять:
1. Прошить режим `lvgl-official-based-test`.
2. Убедиться, что экран отображается без артефактов по краям.
3. Нажать разные кнопки и проверить, меняется ли нижняя строка `Pressed: ...`.
4. Проверить, идут ли координаты touch в `Serial`.
- ожидаемый результат:
если официальный каркас инициализации действительно является рабочей базой, то на этом тесте должны заработать и touch, и кнопки, и исчезнуть визуальные артефакты, которые были в наших самодельных `LVGL`-тестах.
- статус:
pending

View File

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

View File

@ -0,0 +1,64 @@
# ESP32 nav minimal test
- Краткое описание: минимальный UI-прототип для сабсервера на базе `LVGL + subserver touch`, с Wi-Fi flow, серверными адресами и общим экраном редактирования текста.
- Что проверять:
- стартует экран `HOME`;
- на `HOME` видны реальное значение логина или `login not set`, реальное значение сабсервера или `subserver not set`, при отсутствии секрета строка `secret not set`, а также `W BAT`, `STATUS`, Wi-Fi статус и кнопка `SETTINGS`;
- Wi-Fi статус на `HOME` корректно показывает одно из состояний:
- `Wi-Fi not configured`
- `Wi-Fi disconnected`
- `Wi-Fi connected`
- кнопка `SETTINGS` открывает `SETTINGS_MENU`;
- свайп влево на `HOME` открывает `SETTINGS_MENU`;
- в `SETTINGS_MENU` сначала видны только `Wi-Fi` и `Server`;
- обе видимые карточки меню одного цвета;
- свайп вверх показывает `Server` и `Account`;
- свайп вниз возвращает `Wi-Fi` и `Server`;
- свайп вправо из `SETTINGS_MENU` возвращает на `HOME`;
- нажатие `Wi-Fi` открывает `WIFI_SCREEN`;
- `SELECT NETWORK` запускает скан;
- после скана показывается список доступных SSID;
- выбор SSID открывает общий экран редактирования текста для пароля;
- на этом экране видно старое значение, курсор стоит в конце;
- две верхние служебные строки над полем ввода отсутствуют;
- при вводе пароля Wi-Fi текст показывается открыто, без точек;
- большая клавиатура реально видна на экране и занимает большую часть высоты;
- буквы разбиты на 2 страницы;
- режим символов тоже разбит на 2 страницы;
- на правой странице кнопки стоят в ровных вертикальных колонках;
- свайп влево/вправо на экране ввода переключает страницы клавиатуры;
- при этом свайп страниц клавиатуры срабатывает только из нижней клавиатурной зоны, а не из верхней части экрана;
- при переключении `ABC/123` и `SHIFT` уже введённый текст не пропадает;
- при свайпе между левой и правой половиной клавиатуры уже введённый текст тоже не пропадает, в том числе для цифр, символов и заглавных букв;
- визуальный курсор в поле ввода не показывается;
- новые символы всегда дописываются только в конец строки;
- основные 3 ряда клавиш и нижний служебный ряд стали выше;
- внизу остаётся отдельная тёмная полоса с версией `NAV v7`, а рамка клавиатурного блока заканчивается выше неё;
- `ABC/123`, `SHIFT`, `DEL`, `SAVE`, `CANCEL` работают;
- при успехе SSID и пароль сохраняются, а `HOME` показывает `Wi-Fi connected`;
- при ошибке показывается `Connection failed`;
- `CLEAR SAVED WI-FI` очищает сохранённые настройки;
- если сеть была ранее успешно сохранена, после потери связи устройство автоматически пытается переподключиться;
- первые повторные попытки идут раз в `10` секунд, а после долгого отсутствия связи интервал увеличивается до `30` секунд;
- нажатие `Server` открывает `SERVER_SCREEN`;
- в `SERVER_SCREEN` видны и редактируются два значения:
- `https://api.devnet.solana.com`
- `https://shineup.me`
- нажатие `SOLANA RPC` открывает общий экран редактирования;
- нажатие `SHINE SERVER` открывает общий экран редактирования;
- после `SAVE` новые адреса сохраняются в NVS;
- нажатие `Account` открывает `ACCOUNT_SCREEN`;
- `ACCOUNT_SCREEN` показывает 3 кнопки:
- `Login (<value|not set>)`
- `Subserver (<value|not set>)`
- `Secret (<*****|not set>)`
- `Login` открывает общий экран редактирования и сохраняется в NVS;
- `Subserver` открывает промежуточный экран с `USE SUBSERVER1` и `EDIT MANUALLY`;
- `USE SUBSERVER1` возвращает стандартное значение `subserver1`;
- `EDIT MANUALLY` открывает общий экран редактирования и сохраняет значение в NVS;
- `Secret` открывает экран-заглушку, где сказано, что настройка ещё не реализована;
- свайп вправо из внутренних экранов возвращает в `SETTINGS_MENU`;
- свайп вправо из `ACCOUNT_SUBSERVER_SCREEN` и `ACCOUNT_SECRET_SCREEN` возвращает в `ACCOUNT_SCREEN`;
- если во время реального свайпа палец проходит по кнопке, это не должно открывать кнопку как обычный `click`.
- Ожидаемый результат: новый скетч даёт чистый навигационный каркас и уже умеет настраивать Wi-Fi и серверные адреса на самой ESP32.
- Статус: pending

View File

@ -0,0 +1,277 @@
# SHiNE ESP32 Subserver UI Nav Minimal Spec
Минимальный навигационный прототип для `Waveshare ESP32-S3-Touch-AMOLED-2.16`.
## Цель
Этот прототип проверяет базовую механику экранов, крупных кнопок, свайпов, первичную настройку Wi-Fi и настройку серверных адресов через общий экран редактирования текста.
На этом этапе отсутствуют:
- логика серверной проверки доступности;
- логин/пароль учётной записи SHiNE;
- PIN;
- кошелёк;
- QR;
- баланс;
- регистрация;
- PDA и транзакции;
- входящие запросы.
## Экраны
Прототип содержит 8 экранов:
- `HOME`
- `SETTINGS_MENU`
- `WIFI_SCREEN`
- `SERVER_SCREEN`
- `ACCOUNT_SCREEN`
- `ACCOUNT_SUBSERVER_SCREEN`
- `ACCOUNT_SECRET_SCREEN`
- `TEXT_EDIT_SCREEN`
## HOME
Показывает:
- сверху слева значение логина или `login not set`;
- ниже значение сабсервера или `subserver not set`;
- третьей строкой `secret not set`, если секрет ещё не помечен как установленный;
- сверху справа простые индикаторы `W BAT`;
- по центру крупный текст `STATUS`;
- статус Wi-Fi;
- сохранённый SSID или пометку, что он не сохранён;
- снизу большую кнопку `SETTINGS`.
Статусы Wi-Fi на `HOME`:
- `Wi-Fi not configured`
- `Wi-Fi disconnected`
- `Wi-Fi connected`
Переходы:
- кнопка `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
Экран содержит 2 внутренних режима.
### 1. Overview
Показывает:
- текущий Wi-Fi статус;
- сохранённый SSID;
- статусное сообщение;
- кнопку `SELECT NETWORK`;
- кнопку `CLEAR SAVED WI-FI`;
- кнопку `BACK`.
### 2. Scan Results
После `SELECT NETWORK` выполняется скан доступных Wi-Fi сетей.
Показывает:
- заголовок `SELECT NETWORK`;
- количество или результат сканирования;
- список найденных SSID как крупные кнопки;
- кнопку `SCAN AGAIN`;
- кнопку `BACK`.
Нажатие на SSID открывает `TEXT_EDIT_SCREEN` для ввода пароля.
Переходы:
- свайп вправо из любого режима `WIFI_SCREEN` -> `SETTINGS_MENU`
- кнопка `BACK` -> `SETTINGS_MENU`
## SERVER_SCREEN
Показывает:
- статусное сообщение;
- текущий `Solana RPC` адрес;
- кнопку `SOLANA RPC`;
- текущий `Shine server` адрес;
- кнопку `SHINE SERVER`.
Значения по умолчанию:
- Solana RPC: `https://api.devnet.solana.com`
- Shine server: `https://shineup.me`
Нажатие на любую из двух кнопок открывает `TEXT_EDIT_SCREEN`.
Переходы:
- свайп вправо -> `SETTINGS_MENU`
## ACCOUNT_SCREEN
Показывает:
- заголовок `ACCOUNT`;
- статусное сообщение;
- кнопку `Login (<value|not set>)`;
- кнопку `Subserver (<value|not set>)`;
- кнопку `Secret (<*****|not set>)`.
Переходы:
- свайп вправо -> `SETTINGS_MENU`
- `Login` -> `TEXT_EDIT_SCREEN`
- `Subserver` -> `ACCOUNT_SUBSERVER_SCREEN`
- `Secret` -> `ACCOUNT_SECRET_SCREEN`
## ACCOUNT_SUBSERVER_SCREEN
Показывает:
- текущий `subserver`;
- рекомендацию оставить `subserver1`, если устройство одно;
- кнопку `USE SUBSERVER1`;
- кнопку `EDIT MANUALLY`;
- кнопку `BACK`.
Переходы:
- `USE SUBSERVER1` -> сохраняет `subserver1` и возвращает в `ACCOUNT_SCREEN`
- `EDIT MANUALLY` -> `TEXT_EDIT_SCREEN`
- свайп вправо -> `ACCOUNT_SCREEN`
## ACCOUNT_SECRET_SCREEN
Пока это заглушка.
Показывает:
- текущий статус секрета `set/not set`;
- сообщение, что настройка секрета пока не реализована;
- кнопку `BACK`.
Переходы:
- свайп вправо -> `ACCOUNT_SCREEN`
- `BACK` -> `ACCOUNT_SCREEN`
## TEXT_EDIT_SCREEN
Общий экран редактирования строковых значений.
Используется для:
- пароля Wi-Fi;
- Solana RPC;
- Shine server.
Показывает:
- заголовок;
- поле ввода, уже заполненное старым значением;
- новые символы всегда добавляются только в конец текста;
- визуальный курсор не показывается;
- пароль Wi-Fi показывается открыто, без маскировки точками;
- кнопки `SAVE`, `CANCEL`, `DEL`, `CLR`;
- большую экранную клавиатуру.
## Клавиатура
Клавиатура единая для всех текстовых вводов.
Особенности:
- занимает нижнюю часть экрана;
- разбита на 2 страницы;
- переключение страниц выполняется свайпом влево/вправо внутри `TEXT_EDIT_SCREEN`;
- страница 1 содержит в основном буквы и базовые URL-символы;
- страница 2 содержит цифры и дополнительные URL/символьные кнопки;
- переключение `ABC/123` и `SHIFT` не должно очищать уже введённый текст;
- переключение страниц клавиатуры свайпом тоже не должно очищать уже введённый текст, включая цифры, символы и уже набранные заглавные буквы;
- есть специальные действия `DEL` и `CLR`.
## Хранение Wi-Fi
Используется `Preferences` (NVS памяти ESP32):
- `wifi_ssid`
- `wifi_pass`
- `wifi_known_good`
При старте устройства, если сохранён SSID, выполняется попытка подключения к сохранённой сети.
Если сеть раньше уже была успешно подключена и помечена как валидная:
- после потери связи устройство автоматически пытается переподключиться;
- первые повторные попытки идут раз в `10` секунд;
- если связи нет долго, интервал увеличивается до `30` секунд.
## Хранение серверов
Используется `Preferences` (NVS памяти ESP32):
- `solana_rpc`
- `shine_server`
## Хранение аккаунта
Используется `Preferences` (NVS памяти ESP32):
- `login`
- `subserver`
- `secret_set`
## Детали клавиатуры
- клавиатура занимает примерно `2/3`-`3/4` высоты экрана;
- сверху остаются только заголовок и поле ввода;
- буквы занимают 3 ряда;
- половина букв находится на левой странице, половина на правой;
- на правой странице кнопки тоже стоят в ровных колонках, без сдвига рядов вправо;
- отдельный режим `symbols` тоже разделён на 2 страницы;
- 3 основных ряда клавиш и нижний служебный ряд увеличены по высоте;
- клавиши дополнительно увеличены по высоте по сравнению с предыдущим промежуточным вариантом;
- четвёртый ряд содержит:
- переключение `ABC/123`
- `SHIFT`
- `DEL`
- `SAVE`
- `CANCEL`
- ниже рамки клавиатурного блока остаётся отдельная тёмная полоса с версией `NAV v7`.
## Жесты
Поддерживаются направления:
- `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`
- `ACCOUNT_SUBSERVER_SCREEN`: свайп вправо -> `ACCOUNT_SCREEN`
- `ACCOUNT_SECRET_SCREEN`: свайп вправо -> `ACCOUNT_SCREEN`
- `TEXT_EDIT_SCREEN`: свайп влево/вправо -> переключение страниц клавиатуры
- переключение страниц клавиатуры срабатывает только если свайп начался в зоне самой клавиатуры, а не по всему экрану редактора
## Особенность обработки жестов
- если палец ушёл из зоны обычного тапа и жест определяется как свайп, случайное попадание на кнопку по пути не должно превращать свайп в нажатие;
- приоритет у свайпа, а не у `click`.
## Язык
Пока интерфейс отображается в ASCII/English, чтобы использовать подтверждённый рабочий шрифтовой путь на устройстве.

View File

@ -0,0 +1,477 @@
# SHiNE ESP32 Subserver UI Spec
## Назначение
Этот документ описывает актуальный UI-прототип сабсервера `SHiNE` для платы `Waveshare ESP32-S3-Touch-AMOLED-2.16`.
Документ является источником истины для Arduino-скетча:
- если меняется этот документ, должен меняться и скетч;
- если меняется скетч, должен обновляться и этот документ;
- экраны, кнопки, поля, статусы, переходы и тексты не должны расходиться.
## Текущий объём реализации
Текущая реализация является интерактивным прототипом экрана устройства, пригодным для ручной проверки на железе.
Что уже входит в прототип:
- локальный UI на тач-экране;
- хранение настроек и секретов во внутренней памяти `ESP32` через `NVS`;
- русский текст в логике интерфейса; в текущей временной сборке отображение на экране идёт через ASCII-транслитерацию, потому что `U8g2`-шрифты на устройстве временно не рисуются;
- экран пополнения с реальным `solana:` URI и рисованием QR-кода;
- реальное подключение к `Wi-Fi` по сохранённым `SSID/паролю`;
- реальная проверка доступности `API`, `RPC` и `WS`-адресов;
- реальное чтение баланса кошелька из `Solana RPC`;
- проверка обязательных условий перед регистрацией;
- живая on-chain регистрация серверного `user_pda` в `shine_users` через `device key` устройства;
- прототип входящих запросов с подтверждением и отклонением;
- PIN-блокировка (в текущей временной сборке вход по PIN отключён, устройство открывает `HOME` сразу после старта);
- базовые настройки, статус и главный экран;
- сохранение `PDA` и `tx signature` после успешной регистрации.
Что пока считается именно прототипом, а не финальной интеграцией:
- приём реальных входящих запросов на вход/подпись пока не подключён к живой сети;
- входящие запросы пока демонстрационные, чтобы можно было проверить UX и логику подтверждения.
## Основная идея устройства
Устройство работает как отдельный сабсервер:
- хранит секрет на самом устройстве;
- позволяет ввести логин, секрет и имя сабсервера;
- показывает адрес кошелька устройства;
- позволяет пополнить баланс перед регистрацией;
- после выполнения условий даёт зарегистрировать устройство как сабсервер;
- после регистрации может принимать входящие запросы на вход и на подпись.
`SD`-карта не нужна для постоянного хранения секрета в этом прототипе.
Основное сохранение идёт во внутреннюю flash-память через `NVS`.
## Данные, которые хранятся на устройстве
Прототип хранит:
- `PIN`;
- `Wi-Fi SSID`;
- `Wi-Fi password`;
- `login`;
- `session/subserver name`;
- `master secret`;
- `wallet address`;
- `user pda address`;
- `registration signature`;
- `balance`;
- `server api url`;
- `server rpc url`;
- `server ws url`;
- флаги:
`wifiReady`, `serversReady`, `secretReady`, `registered`, `online`.
## Правила готовности к регистрации
Кнопка регистрации доступна только если одновременно выполнены условия:
1. настроен и подтверждён `Wi-Fi`;
2. заполнены и подтверждены серверные адреса;
3. задан логин;
4. сгенерирован или введён секрет;
5. баланс кошелька не меньше `0.20 SOL`;
6. устройство ещё не зарегистрировано.
Если хотя бы одно условие не выполнено, главный экран показывает, чего именно не хватает.
## Экранная модель
В прототипе используются следующие экраны:
1. `LOCK`
2. `HOME`
3. `STATUS`
4. `CONNECTION`
5. `WIFI_EDIT`
6. `SERVERS`
7. `ACCOUNT`
8. `WALLET`
9. `WALLET_QR`
10. `REQUESTS`
11. `REQUEST_DETAIL`
12. `SETTINGS`
13. `PIN_EDIT`
14. `TEXT_INPUT`
15. `CONFIRM`
## Общие правила интерфейса
- Верхняя строка всегда показывает краткий статус устройства:
`PIN`, `Wi-Fi`, `сервер`, `регистрация`.
- Основной язык прототипа: русский.
- Для вывода текста в текущей временной сборке используется стандартный шрифт `Arduino_GFX`.
- Русские строки на экране временно показываются в ASCII-транслитерации.
- Кнопки крупные, с тач-ориентированным размером.
- Опасные действия подтверждаются отдельным диалогом.
- После изменения данных конфигурация сразу сохраняется в `NVS`.
## Экран LOCK
Назначение:
- экран блокировки предусмотрен в UI, но в текущей временной сборке после запуска пропускается;
- вход по PIN временно отключён для отладки остальных экранов.
Отображается:
- заголовок `SHiNE Device`;
- статус `Устройство заблокировано`;
- поле ввода PIN в виде маски;
- кнопки цифровой клавиатуры;
- кнопки `Стереть` и `Открыть`.
Поведение:
- если PIN введён верно, открывается `HOME`;
- если PIN неверный, показывается ошибка `Неверный PIN`;
- в текущей временной сборке этот экран не показывается автоматически при старте.
## Экран HOME
Это основной экран устройства.
Показывает:
- крупный статус регистрации;
- имя логина;
- имя сабсервера;
- короткий статус Wi-Fi;
- короткий статус сервера;
- короткий статус баланса.
Нижние кнопки:
- `Статус`
- `Подключение`
- `Аккаунт`
- `Кошелёк`
- `Запросы`
- `Настройки`
Дополнительная большая кнопка:
- `Зарегистрировать`
Если регистрация уже сделана:
- вместо призыва к регистрации показывается статус `Сабсервер активен`.
## Экран STATUS
Показывает сводку:
- логин;
- сабсервер;
- есть ли секрет;
- зарегистрировано ли устройство;
- подключён ли Wi-Fi;
- доступны ли серверы;
- хватает ли баланса;
- находится ли устройство онлайн;
- краткий отпечаток `PDA` или `tx`.
Кнопки:
- `Назад`
- `Обновить статус`
`Обновить статус` в прототипе не делает сеть, а просто перерисовывает текущие вычисленные состояния.
## Экран CONNECTION
Показывает:
- `Wi-Fi`: готов / не готов;
- `Серверы`: готовы / не готовы;
- `Онлайн`: да / нет.
Кнопки:
- `Wi-Fi`
- `Серверы`
- `Подключить`
- `Отключить`
- `Назад`
Поведение:
- `Подключить` переводит устройство в `online=true`, если `Wi-Fi` реально подключён и серверы реально проверены;
- `Отключить` переводит устройство в `online=false`.
## Экран WIFI_EDIT
Поля:
- `SSID`
- `Пароль`
Кнопки:
- `Изменить SSID`
- `Изменить пароль`
- `Проверить`
- `Сбросить`
- `Назад`
Поведение:
- `Проверить` делает реальную попытку подключения к `Wi-Fi`;
- `Сбросить` очищает обе строки и ставит `wifiReady=false`.
## Экран SERVERS
Поля:
- `API URL`
- `RPC URL`
- `WS URL`
Кнопки:
- `Изменить API`
- `Изменить RPC`
- `Изменить WS`
- `Проверить`
- `Тестовые`
- `Назад`
Поведение:
- `Тестовые` подставляет дефолтные тестовые значения;
- `Проверить` делает реальные сетевые проверки:
- `API URL` должен отвечать по `HTTP/HTTPS`;
- `RPC URL` должен отвечать на `Solana JSON-RPC`;
- `WS URL` должен принимать `TCP/TLS`-соединение.
## Экран ACCOUNT
Показывает:
- логин;
- имя сабсервера;
- статус секрета;
- короткий отпечаток секрета;
- статус регистрации;
- короткий отпечаток `PDA` или `tx`.
Кнопки:
- `Изменить логин`
- `Секрет`
- `Имя сабсервера`
- `Сгенерировать`
- `Очистить`
- `Назад`
Поведение:
- `Сгенерировать` создаёт новый `master secret` и пересчитывает из него `device`-кошелёк;
- `Очистить` удаляет секрет, адрес кошелька, `PDA`, `tx`, регистрацию и онлайн-статус;
- логин приводится к нижнему регистру и trim.
## Экран WALLET
Показывает:
- адрес кошелька устройства;
- баланс в `SOL`;
- минимально рекомендуемую сумму для регистрации;
- статус `Хватает / Не хватает`.
Кнопки:
- `QR и URI`
- `+0.10 SOL`
- `+0.25 SOL`
- `-0.10 SOL`
- `Проверить`
- `Назад`
Поведение:
- кнопки пополнения/уменьшения нужны для теста сценариев;
- `Проверить` читает реальный баланс из `Solana RPC`;
- адрес кошелька должен совпадать с `device key`, вычисленным из сохранённого `master secret`;
- отрицательный баланс не допускается.
## Экран WALLET_QR
Показывает:
- QR-код для строки вида:
`solana:<wallet>?amount=0.20&label=SHiNE%20Register`;
- адрес кошелька;
- сумму;
- текст URI.
Кнопки:
- `Назад`
QR должен быть сканируемым, а не декоративным.
## Экран REQUESTS
Показывает список демонстрационных запросов:
- `Вход в сессию`
- `Подпись сообщения`
Для каждого запроса:
- тип;
- источник;
- короткий статус.
Кнопки:
- `Открыть запрос 1`
- `Открыть запрос 2`
- `Назад`
## Экран REQUEST_DETAIL
Показывает детали выбранного запроса:
- тип запроса;
- кто запросил;
- время;
- описание;
- отпечаток/идентификатор.
Кнопки:
- `Разрешить`
- `Отклонить`
- `Назад`
Поведение:
- после разрешения или отклонения запрос помечается обработанным;
- экран списка отражает новый статус.
## Экран SETTINGS
Показывает:
- текущий PIN;
- базовые флаги безопасности;
- технические действия прототипа.
Кнопки:
- `Сменить PIN`
- `Сбросить онлайн`
- `Полный сброс`
- `Назад`
`Полный сброс` очищает весь локальный конфиг и возвращает устройство к стартовому состоянию.
## Экран PIN_EDIT
Используется для ввода нового PIN.
Правила:
- допустимы только цифры;
- длина PIN: 4..8 символов;
- после сохранения новый PIN немедленно пишется в `NVS`.
## Экран TEXT_INPUT
Это общий экран редактирования текстовых полей.
Используется для:
- `SSID`
- `Пароль Wi-Fi`
- `Логин`
- `Имя сабсервера`
- `API URL`
- `RPC URL`
- `WS URL`
Состав экрана:
- заголовок поля;
- текущее значение;
- программная клавиатура;
- кнопки `Стереть`, `OK`, `Отмена`.
## Экран CONFIRM
Это модальный экран подтверждения.
Используется для:
- регистрации;
- очистки секрета;
- полного сброса.
Кнопки:
- `Подтвердить`
- `Отмена`
## Сценарий первой настройки
Ожидаемый путь пользователя:
1. открыть устройство сразу после старта; экран PIN временно не требуется;
2. открыть `Подключение -> Wi-Fi`;
3. ввести `SSID` и пароль, нажать `Проверить`;
4. открыть `Подключение -> Серверы`;
5. проверить или задать серверные адреса;
6. открыть `Аккаунт`;
7. ввести логин;
8. задать имя сабсервера;
9. сгенерировать секрет;
10. открыть `Кошелёк`;
11. при необходимости пополнить баланс;
12. вернуться на `HOME`;
13. нажать `Зарегистрировать`;
14. после подтверждения увидеть статус `Сабсервер активен`.
Примечание:
- устройство реально отправляет `create_user_pda` в `shine_users`, а после подтверждения сохраняет `PDA` и `tx signature`.
## Сценарий входящего запроса
1. открыть `Запросы`;
2. выбрать один из запросов;
3. прочитать детали;
4. нажать `Разрешить` или `Отклонить`;
5. убедиться, что статус запроса изменился.
## Временный режим текста
В текущей диагностической версии:
- строковые литералы в коде остаются русскими и в `UTF-8`;
- перед выводом на экран они временно транслитерируются в ASCII;
- рендер выполняется стандартным шрифтом `Arduino_GFX`;
- это обходной режим, пока `U8g2`-шрифты на устройстве не начнут рисоваться стабильно.
## Критерии ручной проверки
Минимально нужно проверить:
1. устройство загружается и сразу открывает `HOME`; экран блокировки временно отключён;
2. текст отображается читаемо хотя бы в ASCII-транслитерации;
3. ввод по экранной клавиатуре работает;
4. после перезагрузки сохранённые поля остаются в памяти;
5. секрет и адрес кошелька сохраняются на устройстве;
6. экран `QR и URI` рисует читаемый QR-код;
7. регистрация блокируется, пока условия не выполнены;
8. после выполнения условий регистрация становится доступной;
9. список запросов и экран подтверждения работают;
10. полный сброс действительно очищает сохранённое состояние.

View File

@ -10,6 +10,17 @@
- `hello` — базовый тест экрана (пример `01_HelloWorld`)
- `simple` — простой кастомный тест: экран + touch + запись/проигрывание + наклон (IMU)
- `argon2` — генерация masterSecret через Argon2id с SD-картой как памятью (тест скорости)
- `subserver-ui` — основной UI-прототип сабсервера SHiNE: NVS, PIN, Wi-Fi, серверы, кошелёк, QR, запросы
- `text-test` — диагностический экран рендера текста: default font, U8g2 ASCII, U8g2 кириллица, кнопки с подписями
- `gfx-text-test` — тот же тест рендера текста, но уже внутри новой папки `test_sketches/`
- `gfx-layout-test` — тест геометрии и нижних рядов кнопок
- `lvgl-basic-test` — минимальный экран на `LVGL` с текстом и кнопками
- `lvgl-interaction-test` — экран на `LVGL` с большим числом кнопок и сообщением о нажатой кнопке
- `lvgl-touch-debug-test` — точечная диагностика touch: сырые координаты, маркер точки и большая тест-кнопка `LVGL`
- `lvgl-official-based-test` — наш минимальный экран, но на максимально близкой к официальному `LVGL_Widgets` инициализации
- `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`, свайпы, крупные кнопки и реальная настройка Wi-Fi с сохранением в NVS
Запуск:
@ -17,3 +28,15 @@
- `./burn.sh audio`
- `./burn.sh hello`
- `./burn.sh simple`
- `./burn.sh subserver-ui`
- `./burn.sh text-test`
- `./burn.sh gfx-text-test`
- `./burn.sh gfx-layout-test`
- `./burn.sh lvgl-basic-test`
- `./burn.sh lvgl-interaction-test`
- `./burn.sh lvgl-touch-debug-test`
- `./burn.sh lvgl-official-based-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`

View File

@ -16,9 +16,20 @@ case "${MODE}" in
audio) SKETCH_DIR="${DEMO_BASE}/examples/07_ES8311" ;;
simple) SKETCH_DIR="${ROOT_DIR}/simple_av_test" ;;
argon2) SKETCH_DIR="${ROOT_DIR}/argon2_sd_test" ;;
subserver-ui) SKETCH_DIR="${ROOT_DIR}/shine_subserver_ui" ;;
text-test) SKETCH_DIR="${ROOT_DIR}/text_render_test" ;;
gfx-text-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/gfx_text_render_test" ;;
gfx-layout-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/gfx_button_layout_test" ;;
lvgl-basic-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_basic_test" ;;
lvgl-interaction-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_interaction_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-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 "Use one of: hello, widgets, audio, simple, argon2" >&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
;;
esac

View File

@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
detect_port_from_arduino_cli() {
local line
while IFS= read -r line; do
[[ -z "${line}" ]] && continue
[[ "${line}" == Port* ]] && continue
if [[ "${line}" == /dev/* ]]; then
awk '{print $1}' <<<"${line}"
return 0
fi
done < <(arduino-cli board list 2>/dev/null || true)
return 1
}
detect_port_from_dev() {
local candidates=()
local path
for path in /dev/ttyACM* /dev/ttyUSB*; do
[[ -e "${path}" ]] || continue
candidates+=("${path}")
done
if [[ "${#candidates[@]}" -eq 1 ]]; then
printf '%s\n' "${candidates[0]}"
return 0
fi
return 1
}
PORT="${PORT:-}"
if [[ -z "${PORT}" ]]; then
PORT="$(detect_port_from_arduino_cli || true)"
fi
if [[ -z "${PORT}" ]]; then
PORT="$(detect_port_from_dev || true)"
fi
if [[ -z "${PORT}" ]]; then
echo "Не удалось автоматически найти USB-порт ESP32." >&2
echo "Подключите плату и проверьте 'arduino-cli board list'." >&2
echo "Либо укажите порт вручную: PORT=/dev/ttyACM0 ./flash_shine_subserver_ui.sh" >&2
exit 1
fi
echo "== Найден порт: ${PORT}"
PORT="${PORT}" "${ROOT_DIR}/burn.sh" subserver-ui

View File

@ -0,0 +1 @@
#include "../../official-demo/examples/Arduino-v3.3.5/libraries/lvgl/src/extra/libs/qrcode/qrcodegen.c"

View File

@ -0,0 +1,31 @@
# Test Sketches
Набор отдельных диагностических скетчей для `Waveshare ESP32-S3-Touch-AMOLED-2.16`.
Скетчи в этой папке нужны для быстрой проверки конкретных гипотез без влияния на основной `shine_subserver_ui`.
## Список
- `gfx_text_render_test/` - проверка рендера текста через `Arduino_GFX` и сравнение с `U8g2`
- `gfx_button_layout_test/` - проверка геометрии кнопок, особенно нижних рядов и широких кнопок
- `lvgl_basic_test/` - минимальный тест `LVGL` с заголовком, текстом и кнопками
- `lvgl_interaction_test/` - расширенный тест `LVGL` с 9 кнопками, touch-вводом и статусом нажатия
- `lvgl_touch_debug_test/` - диагностика touch: сырые координаты, точка касания и одна большая кнопка `LVGL`
- `lvgl_official_based_test/` - минимальный наш экран поверх максимально близкой к официальному `05_LVGL_Widgets` инициализации
- `lvgl_subserver_touch_test/` - гибридный тест: `LVGL`-экран с инициализацией дисплея и чтением touch из `shine_subserver_ui`; подтверждён на реальном устройстве
- `lvgl_russian_font_test/` - тест кастомного кириллического `LVGL`-шрифта с русскими кнопками, длинными строками и рабочим touch
- `lvgl_nav_minimal_test/` - новый минимальный навигационный каркас сабсервера на рабочем `LVGL + subserver touch`, расширенный настройкой Wi-Fi и сохранением в NVS
## Запуск
Использовать через `burn.sh`:
- `./burn.sh gfx-text-test`
- `./burn.sh gfx-layout-test`
- `./burn.sh lvgl-basic-test`
- `./burn.sh lvgl-interaction-test`
- `./burn.sh lvgl-touch-debug-test`
- `./burn.sh lvgl-official-based-test`
- `./burn.sh lvgl-subserver-touch-test`
- `./burn.sh lvgl-russian-font-test`
- `./burn.sh lvgl-nav-minimal-test`

View File

@ -0,0 +1,93 @@
#include <Arduino.h>
#include <Wire.h>
#include <Arduino_GFX_Library.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 DISP_W 480
#define DISP_H 480
#define C_BG 0x0841u
#define C_PANEL 0x1082u
#define C_CARD 0x18C3u
#define C_BORDER 0x39C7u
#define C_TEXT 0xFFFFu
#define C_OK 0x3666u
#define C_WARN 0xECA0u
#define C_BUTTON 0x2145u
#define C_BUTTON2 0x3186u
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);
static void drawPanel(int x, int y, int w, int h, uint16_t fill, uint16_t border, int radius = 10) {
gfx->fillRoundRect(x, y, w, h, radius, fill);
gfx->drawRoundRect(x, y, w, h, radius, border);
}
static void drawText(int x, int y, const char *text, uint16_t color, uint8_t size = 2) {
gfx->setFont();
gfx->setTextSize(size);
gfx->setTextColor(color);
gfx->setCursor(x, y);
gfx->print(text);
}
static void drawButton(int x, int y, int w, int h, uint16_t fill, const char *label, uint8_t size = 2) {
drawPanel(x, y, w, h, fill, C_BORDER, 12);
gfx->setFont();
gfx->setTextSize(size);
gfx->setTextColor(C_TEXT);
int textW = strlen(label) * 6 * size;
int textX = x + (w - textW) / 2;
if (textX < x + 8) {
textX = x + 8;
}
int textY = y + (h + 8 * size) / 2 - 2;
gfx->setCursor(textX, textY);
gfx->print(label);
}
static void drawScreen() {
gfx->fillScreen(C_BG);
drawPanel(12, 12, 456, 456, C_PANEL, C_BORDER, 16);
drawText(24, 42, "BUTTON LAYOUT TEST", C_TEXT, 2);
drawText(24, 70, "Check bottom and wide buttons", C_WARN, 1);
drawButton(20, 110, 136, 52, C_BUTTON, "STATUS");
drawButton(172, 110, 136, 52, C_BUTTON, "CONNECT");
drawButton(324, 110, 136, 52, C_BUTTON, "ACCOUNT");
drawButton(20, 180, 136, 52, C_BUTTON, "WALLET");
drawButton(172, 180, 136, 52, C_BUTTON, "REQUESTS");
drawButton(324, 180, 136, 52, C_BUTTON, "SETTINGS");
drawButton(20, 270, 212, 48, C_BUTTON2, "BACK");
drawButton(248, 270, 212, 48, C_OK, "REFRESH");
drawButton(20, 338, 440, 40, C_BUTTON2, "FULL WIDTH BUTTON", 2);
drawButton(20, 390, 440, 56, C_OK, "REGISTER DEVICE", 2);
}
void setup() {
Serial.begin(115200);
Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL);
gfx->begin();
gBus->writeC8D8(0x36, 0xA0);
gfx->setBrightness(220);
drawScreen();
}
void loop() {
delay(1000);
}

View File

@ -0,0 +1,104 @@
#include <Arduino.h>
#include <Wire.h>
#include <Arduino_GFX_Library.h>
#include <U8g2lib.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 DISP_W 480
#define DISP_H 480
#define C_BG 0x0841u
#define C_PANEL 0x1082u
#define C_CARD 0x18C3u
#define C_BORDER 0x39C7u
#define C_TEXT 0xFFFFu
#define C_MUTE 0xBDF7u
#define C_OK 0x3666u
#define C_WARN 0xECA0u
#define C_BUTTON 0x2145u
#define C_BUTTON2 0x3186u
#define FONT_BODY u8g2_font_9x15_t_cyrillic
#define FONT_SMALL u8g2_font_6x13_t_cyrillic
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);
static void drawPanel(int x, int y, int w, int h, uint16_t fill, uint16_t border, int radius = 10) {
gfx->fillRoundRect(x, y, w, h, radius, fill);
gfx->drawRoundRect(x, y, w, h, radius, border);
}
static void drawDefaultText(int x, int y, const char *text, uint16_t color, uint8_t size = 1) {
gfx->setFont();
gfx->setTextSize(size);
gfx->setTextColor(color);
gfx->setCursor(x, y);
gfx->print(text);
}
static void drawU8Text(int x, int y, const String &text, uint16_t color, const uint8_t *font) {
gfx->setTextSize(1);
gfx->setFont(font);
gfx->setTextColor(color);
gfx->setCursor(x, y);
gfx->print(text);
}
static void drawButton(int x, int y, int w, int h, uint16_t fill, const String &label, const uint8_t *font, bool useDefaultFont = false) {
drawPanel(x, y, w, h, fill, C_BORDER, 12);
if (useDefaultFont) {
drawDefaultText(x + 14, y + 30, label.c_str(), C_TEXT, 2);
return;
}
drawU8Text(x + 14, y + 32, label, C_TEXT, font);
}
static void drawScreen() {
gfx->fillScreen(C_BG);
drawPanel(12, 12, 456, 456, C_PANEL, C_BORDER, 16);
drawDefaultText(28, 42, "TEXT TEST 123", C_TEXT, 2);
drawU8Text(28, 72, "Default ASCII above", C_MUTE, (const uint8_t *)FONT_SMALL);
drawPanel(24, 90, 432, 72, C_CARD, C_BORDER, 12);
drawDefaultText(38, 118, "A: Default font ABC123", C_TEXT, 2);
drawDefaultText(38, 146, "Visible? then base path OK", C_WARN, 1);
drawPanel(24, 174, 432, 84, C_CARD, C_BORDER, 12);
drawU8Text(38, 204, "B: U8g2 ASCII abc123", C_TEXT, (const uint8_t *)FONT_BODY);
drawU8Text(38, 230, "C: Русский текст 123", C_OK, (const uint8_t *)FONT_BODY);
drawPanel(24, 270, 432, 84, C_CARD, C_BORDER, 12);
drawU8Text(38, 298, "D: Мелкий шрифт кнопок", C_TEXT, (const uint8_t *)FONT_SMALL);
drawU8Text(38, 320, "Если это видно, FONT_SMALL жив", C_MUTE, (const uint8_t *)FONT_SMALL);
drawButton(24, 368, 128, 72, C_BUTTON, "BTN 1", nullptr, true);
drawButton(176, 368, 128, 72, C_OK, "abc123", (const uint8_t *)FONT_BODY);
drawButton(328, 368, 128, 72, C_BUTTON2, "Русский", (const uint8_t *)FONT_BODY);
}
void setup() {
Serial.begin(115200);
Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL);
gfx->begin();
gBus->writeC8D8(0x36, 0xA0);
gfx->setBrightness(220);
gfx->setUTF8Print(true);
drawScreen();
}
void loop() {
delay(1000);
}

View File

@ -0,0 +1,125 @@
#include <Arduino.h>
#include <Wire.h>
#include <lvgl.h>
#include <Arduino_GFX_Library.h>
#include "pin_config.h"
#define EXAMPLE_LVGL_TICK_PERIOD_MS 2
static lv_disp_draw_buf_t gDrawBuf;
static lv_color_t *gBuf1 = nullptr;
static lv_color_t *gBuf2 = nullptr;
Arduino_DataBus *gBus = new Arduino_ESP32QSPI(
LCD_CS, LCD_SCLK, LCD_SDIO0, LCD_SDIO1, LCD_SDIO2, LCD_SDIO3);
Arduino_CO5300 *gfx = new Arduino_CO5300(
gBus, LCD_RESET, 0, LCD_WIDTH, LCD_HEIGHT, 0, 0, 0, 0);
static void lvglRounderCb(lv_disp_drv_t *dispDrv, lv_area_t *area) {
LV_UNUSED(dispDrv);
if (area->x1 % 2 != 0) area->x1--;
if (area->y1 % 2 != 0) area->y1--;
if (area->x2 % 2 == 0) area->x2++;
if (area->y2 % 2 == 0) area->y2++;
}
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(EXAMPLE_LVGL_TICK_PERIOD_MS);
}
static void createUi() {
lv_obj_set_style_bg_color(lv_scr_act(), lv_color_hex(0x0B1320), 0);
lv_obj_set_style_bg_opa(lv_scr_act(), LV_OPA_COVER, 0);
lv_obj_t *title = lv_label_create(lv_scr_act());
lv_label_set_text(title, "LVGL BASIC TEST");
lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0);
lv_obj_set_style_text_font(title, &lv_font_montserrat_22, 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 18);
lv_obj_t *subtitle = lv_label_create(lv_scr_act());
lv_label_set_text(subtitle, "If this text is visible, LVGL path works.");
lv_obj_set_width(subtitle, 420);
lv_label_set_long_mode(subtitle, LV_LABEL_LONG_WRAP);
lv_obj_set_style_text_color(subtitle, lv_color_hex(0xD5DCE5), 0);
lv_obj_set_style_text_font(subtitle, &lv_font_montserrat_16, 0);
lv_obj_align(subtitle, LV_ALIGN_TOP_MID, 0, 58);
const char *labels[3] = {"Button One", "Second Button", "Bottom Action"};
const lv_color_t colors[3] = {
lv_color_hex(0x2B4C7E),
lv_color_hex(0x2E8B57),
lv_color_hex(0xB45F06),
};
const lv_coord_t ys[3] = {150, 236, 340};
const lv_coord_t hs[3] = {58, 58, 64};
for (int i = 0; i < 3; i++) {
lv_obj_t *btn = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn, 360, hs[i]);
lv_obj_align(btn, LV_ALIGN_TOP_MID, 0, ys[i]);
lv_obj_set_style_radius(btn, 14, 0);
lv_obj_set_style_bg_color(btn, colors[i], 0);
lv_obj_set_style_border_width(btn, 2, 0);
lv_obj_set_style_border_color(btn, lv_color_hex(0x7F96B0), 0);
lv_obj_t *label = lv_label_create(btn);
lv_label_set_text(label, labels[i]);
lv_obj_set_style_text_font(label, &lv_font_montserrat_20, 0);
lv_obj_set_style_text_color(label, lv_color_hex(0xFFFFFF), 0);
lv_obj_center(label);
}
}
void setup() {
Serial.begin(115200);
Wire.begin(IIC_SDA, IIC_SCL);
gfx->begin();
gBus->writeC8D8(0x36, 0xA0);
gfx->setBrightness(220);
lv_init();
uint32_t screenWidth = gfx->width();
uint32_t screenHeight = gfx->height();
gBuf1 = (lv_color_t *)heap_caps_malloc(screenWidth * screenHeight / 4 * sizeof(lv_color_t), MALLOC_CAP_DMA);
gBuf2 = (lv_color_t *)heap_caps_malloc(screenWidth * screenHeight / 4 * sizeof(lv_color_t), MALLOC_CAP_DMA);
lv_disp_draw_buf_init(&gDrawBuf, gBuf1, gBuf2, screenWidth * screenHeight / 4);
static lv_disp_drv_t dispDrv;
lv_disp_drv_init(&dispDrv);
dispDrv.hor_res = screenWidth;
dispDrv.ver_res = screenHeight;
dispDrv.flush_cb = lvglFlushCb;
dispDrv.rounder_cb = lvglRounderCb;
dispDrv.draw_buf = &gDrawBuf;
lv_disp_drv_register(&dispDrv);
const esp_timer_create_args_t lvglTickTimerArgs = {
.callback = &lvglTick,
.name = "lvgl_tick"};
esp_timer_handle_t lvglTickTimer = nullptr;
esp_timer_create(&lvglTickTimerArgs, &lvglTickTimer);
esp_timer_start_periodic(lvglTickTimer, EXAMPLE_LVGL_TICK_PERIOD_MS * 1000);
createUi();
Serial.println("LVGL basic test ready");
}
void loop() {
lv_timer_handler();
delay(5);
}

View File

@ -0,0 +1,214 @@
#include <Arduino.h>
#include <Wire.h>
#include <lvgl.h>
#include <Arduino_GFX_Library.h>
#include "pin_config.h"
#include "TouchDrvCSTXXX.hpp"
#define EXAMPLE_LVGL_TICK_PERIOD_MS 2
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 uint32_t gPressCount = 0;
static TouchDrvCST92xx gTouch;
static int16_t gTouchX[5];
static int16_t gTouchY[5];
Arduino_DataBus *gBus = new Arduino_ESP32QSPI(
LCD_CS, LCD_SCLK, LCD_SDIO0, LCD_SDIO1, LCD_SDIO2, LCD_SDIO3);
Arduino_CO5300 *gfx = new Arduino_CO5300(
gBus, LCD_RESET, 0, LCD_WIDTH, LCD_HEIGHT, 0, 0, 0, 0);
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(EXAMPLE_LVGL_TICK_PERIOD_MS);
}
static void lvglTouchRead(lv_indev_drv_t *indevDrv, lv_indev_data_t *data) {
LV_UNUSED(indevDrv);
uint8_t touched = gTouch.getPoint(gTouchX, gTouchY, gTouch.getSupportTouchPoint());
if (touched > 0) {
data->state = LV_INDEV_STATE_PR;
data->point.x = gTouchX[0];
data->point.y = gTouchY[0];
return;
}
data->state = LV_INDEV_STATE_REL;
}
static void buttonEventCb(lv_event_t *event) {
if (lv_event_get_code(event) != LV_EVENT_CLICKED) {
return;
}
lv_obj_t *btn = lv_event_get_target(event);
const char *name = (const char *)lv_event_get_user_data(event);
gPressCount++;
lv_obj_set_style_outline_width(btn, 4, 0);
lv_obj_set_style_outline_color(btn, lv_color_hex(0xFFF3B0), 0);
lv_obj_set_style_outline_pad(btn, 2, 0);
String status = "Pressed: ";
status += name;
status += " (#";
status += String(gPressCount);
status += ")";
lv_label_set_text(gStatusLabel, status.c_str());
Serial.println(status);
}
static void createButton(lv_obj_t *parent, const char *labelText, lv_coord_t col, lv_coord_t row, uint32_t colorHex) {
static const lv_coord_t kStartX = 22;
static const lv_coord_t kStartY = 122;
static const lv_coord_t kButtonW = 134;
static const lv_coord_t kButtonH = 64;
static const lv_coord_t kGapX = 18;
static const lv_coord_t kGapY = 16;
lv_obj_t *btn = lv_btn_create(parent);
lv_obj_set_size(btn, kButtonW, kButtonH);
lv_obj_set_pos(btn, kStartX + col * (kButtonW + kGapX), kStartY + row * (kButtonH + kGapY));
lv_obj_set_style_radius(btn, 16, 0);
lv_obj_set_style_bg_color(btn, lv_color_hex(colorHex), 0);
lv_obj_set_style_border_width(btn, 2, 0);
lv_obj_set_style_border_color(btn, lv_color_hex(0x7F96B0), 0);
lv_obj_set_style_shadow_width(btn, 0, 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_style_text_font(label, &lv_font_montserrat_18, 0);
lv_obj_set_style_text_color(label, lv_color_hex(0xFFFFFF), 0);
lv_obj_center(label);
}
static void createUi() {
lv_obj_t *screen = lv_scr_act();
lv_obj_set_style_bg_color(screen, lv_color_hex(0x0B1320), 0);
lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, 0);
lv_obj_t *title = lv_label_create(screen);
lv_label_set_text(title, "LVGL INTERACTION TEST");
lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0);
lv_obj_set_style_text_font(title, &lv_font_montserrat_22, 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 14);
lv_obj_t *subtitle = lv_label_create(screen);
lv_label_set_text(subtitle, "Tap buttons. Bottom label must change on every press.");
lv_obj_set_width(subtitle, 420);
lv_label_set_long_mode(subtitle, LV_LABEL_LONG_WRAP);
lv_obj_set_style_text_color(subtitle, lv_color_hex(0xD5DCE5), 0);
lv_obj_set_style_text_font(subtitle, &lv_font_montserrat_14, 0);
lv_obj_align(subtitle, LV_ALIGN_TOP_MID, 0, 48);
createButton(screen, "Status", 0, 0, 0x355C7D);
createButton(screen, "Connect", 1, 0, 0x2A9D8F);
createButton(screen, "Wallet", 2, 0, 0x457B9D);
createButton(screen, "Requests", 0, 1, 0xE76F51);
createButton(screen, "Settings", 1, 1, 0x8D5A97);
createButton(screen, "Register", 2, 1, 0x6A994E);
createButton(screen, "Approve", 0, 2, 0xBC6C25);
createButton(screen, "Reject", 1, 2, 0xB56576);
createButton(screen, "Back", 2, 2, 0x6C757D);
lv_obj_t *statusPanel = lv_obj_create(screen);
lv_obj_set_size(statusPanel, 436, 56);
lv_obj_align(statusPanel, LV_ALIGN_BOTTOM_MID, 0, -16);
lv_obj_set_style_radius(statusPanel, 14, 0);
lv_obj_set_style_bg_color(statusPanel, lv_color_hex(0x162033), 0);
lv_obj_set_style_border_width(statusPanel, 2, 0);
lv_obj_set_style_border_color(statusPanel, lv_color_hex(0x415A77), 0);
lv_obj_set_style_pad_all(statusPanel, 10, 0);
gStatusLabel = lv_label_create(statusPanel);
lv_label_set_text(gStatusLabel, "Pressed: none");
lv_obj_set_style_text_font(gStatusLabel, &lv_font_montserrat_18, 0);
lv_obj_set_style_text_color(gStatusLabel, lv_color_hex(0xFFFFFF), 0);
lv_obj_center(gStatusLabel);
}
void setup() {
Serial.begin(115200);
Wire.begin(IIC_SDA, IIC_SCL);
pinMode(TP_RST, OUTPUT);
pinMode(TP_INT, INPUT);
digitalWrite(TP_RST, LOW);
delay(30);
digitalWrite(TP_RST, HIGH);
delay(50);
delay(1000);
gTouch.setPins(TP_RST, TP_INT);
bool touchOk = gTouch.begin(Wire, 0x5A, IIC_SDA, IIC_SCL);
if (!touchOk) {
Serial.println("Touch init failed");
} else {
Serial.print("Touch model: ");
Serial.println(gTouch.getModelName());
gTouch.sleep();
gTouch.reset();
gTouch.setMaxCoordinates(480, 480);
gTouch.setSwapXY(true);
gTouch.setMirrorXY(true, false);
}
gfx->begin();
gBus->writeC8D8(0x36, 0xA0);
gfx->setBrightness(220);
gfx->fillScreen(0x0000);
lv_init();
uint32_t screenWidth = gfx->width();
uint32_t screenHeight = gfx->height();
gBuf1 = (lv_color_t *)heap_caps_malloc(screenWidth * screenHeight / 4 * sizeof(lv_color_t), MALLOC_CAP_DMA);
gBuf2 = (lv_color_t *)heap_caps_malloc(screenWidth * screenHeight / 4 * sizeof(lv_color_t), MALLOC_CAP_DMA);
lv_disp_draw_buf_init(&gDrawBuf, gBuf1, gBuf2, screenWidth * screenHeight / 4);
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 lvglTickTimerArgs = {
.callback = &lvglTick,
.name = "lvgl_tick"};
esp_timer_handle_t lvglTickTimer = nullptr;
esp_timer_create(&lvglTickTimerArgs, &lvglTickTimer);
esp_timer_start_periodic(lvglTickTimer, EXAMPLE_LVGL_TICK_PERIOD_MS * 1000);
createUi();
Serial.println("LVGL interaction test ready");
}
void loop() {
lv_timer_handler();
delay(5);
}

View File

@ -0,0 +1,252 @@
#include <lvgl.h>
#include "Arduino_GFX_Library.h"
#include "pin_config.h"
#include "TouchDrvCSTXXX.hpp"
#include "lv_conf.h"
#include "HWCDC.h"
#include <Wire.h>
HWCDC USBSerial;
#define EXAMPLE_LVGL_TICK_PERIOD_MS 2
static lv_disp_draw_buf_t draw_buf;
static lv_color_t *buf1 = nullptr;
static lv_color_t *buf2 = nullptr;
static lv_obj_t *status_label = nullptr;
static uint32_t click_count = 0;
Arduino_DataBus *bus = new Arduino_ESP32QSPI(
LCD_CS, LCD_SCLK, LCD_SDIO0, LCD_SDIO1, LCD_SDIO2, LCD_SDIO3);
Arduino_CO5300 *gfx = new Arduino_CO5300(
bus, LCD_RESET, 0, LCD_WIDTH, LCD_HEIGHT, 0, 0, 0, 0);
TouchDrvCST92xx touch;
int16_t touch_x[5], touch_y[5];
bool isPressed = false;
uint32_t screenWidth = 0;
uint32_t screenHeight = 0;
#if LV_USE_LOG != 0
void my_print(const char *buf) {
USBSerial.printf(buf);
USBSerial.flush();
}
#endif
static void example_lvgl_rounder_cb(struct _lv_disp_drv_t *disp_drv, lv_area_t *area) {
LV_UNUSED(disp_drv);
if (area->x1 % 2 != 0) area->x1--;
if (area->y1 % 2 != 0) area->y1--;
if (area->x2 % 2 == 0) area->x2++;
if (area->y2 % 2 == 0) area->y2++;
}
static void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) {
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 *)&color_p->full, w, h);
#else
gfx->draw16bitRGBBitmap(area->x1, area->y1, (uint16_t *)&color_p->full, w, h);
#endif
lv_disp_flush_ready(disp);
}
static void example_increase_lvgl_tick(void *arg) {
LV_UNUSED(arg);
lv_tick_inc(EXAMPLE_LVGL_TICK_PERIOD_MS);
}
static void my_touchpad_read(lv_indev_drv_t *indev_driver, lv_indev_data_t *data) {
LV_UNUSED(indev_driver);
if (isPressed) {
uint8_t touched = touch.getPoint(touch_x, touch_y, touch.getSupportTouchPoint());
if (touched) {
isPressed = false;
data->state = LV_INDEV_STATE_PR;
data->point.x = touch_x[0];
data->point.y = touch_y[0];
USBSerial.print("Touch x=");
USBSerial.print(touch_x[0]);
USBSerial.print(" y=");
USBSerial.println(touch_y[0]);
} else {
data->state = LV_INDEV_STATE_REL;
}
} else {
data->state = LV_INDEV_STATE_REL;
}
}
static void button_event_cb(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);
click_count++;
String text = "Pressed: ";
text += name;
text += " (#";
text += String(click_count);
text += ")";
lv_label_set_text(status_label, text.c_str());
USBSerial.println(text);
}
static lv_obj_t *make_button(lv_obj_t *parent, const char *label_text, lv_coord_t x, lv_coord_t y, lv_coord_t w, lv_coord_t h, lv_color_t bg) {
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, bg, 0);
lv_obj_set_style_border_width(btn, 2, 0);
lv_obj_set_style_border_color(btn, lv_color_hex(0x8AA4BF), 0);
lv_obj_set_style_shadow_width(btn, 0, 0);
lv_obj_add_event_cb(btn, button_event_cb, LV_EVENT_CLICKED, (void *)label_text);
lv_obj_t *label = lv_label_create(btn);
lv_label_set_text(label, label_text);
lv_obj_set_style_text_font(label, &lv_font_montserrat_18, 0);
lv_obj_set_style_text_color(label, lv_color_hex(0xFFFFFF), 0);
lv_obj_center(label);
return btn;
}
static void create_ui() {
lv_obj_t *screen = lv_scr_act();
lv_obj_set_style_bg_color(screen, lv_color_hex(0x0B1320), 0);
lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, 0);
lv_obj_t *title = lv_label_create(screen);
lv_label_set_text(title, "LVGL OFFICIAL BASED TEST");
lv_obj_set_style_text_font(title, &lv_font_montserrat_20, 0);
lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 14);
lv_obj_t *hint = lv_label_create(screen);
lv_label_set_text(hint, "Based on official LVGL_Widgets init. Tap buttons and watch the status.");
lv_obj_set_width(hint, 430);
lv_label_set_long_mode(hint, LV_LABEL_LONG_WRAP);
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(hint, lv_color_hex(0xD4DDE7), 0);
lv_obj_align(hint, LV_ALIGN_TOP_MID, 0, 44);
make_button(screen, "Status", 22, 120, 136, 60, lv_color_hex(0x355C7D));
make_button(screen, "Connect", 174, 120, 136, 60, lv_color_hex(0x2A9D8F));
make_button(screen, "Wallet", 326, 120, 132, 60, lv_color_hex(0x457B9D));
make_button(screen, "Requests", 22, 196, 136, 60, lv_color_hex(0xE76F51));
make_button(screen, "Settings",174, 196, 136, 60, lv_color_hex(0x8D5A97));
make_button(screen, "Register",326, 196, 132, 60, lv_color_hex(0x6A994E));
make_button(screen, "Approve", 22, 272, 136, 60, lv_color_hex(0xBC6C25));
make_button(screen, "Reject", 174, 272, 136, 60, lv_color_hex(0xB56576));
make_button(screen, "Back", 326, 272, 132, 60, lv_color_hex(0x6C757D));
lv_obj_t *bottom_btn = make_button(screen, "Bottom Action", 22, 350, 436, 64, lv_color_hex(0x1D6F42));
lv_obj_set_style_text_font(lv_obj_get_child(bottom_btn, 0), &lv_font_montserrat_20, 0);
lv_obj_t *status_panel = lv_obj_create(screen);
lv_obj_set_size(status_panel, 436, 42);
lv_obj_set_pos(status_panel, 22, 426);
lv_obj_set_style_radius(status_panel, 12, 0);
lv_obj_set_style_bg_color(status_panel, lv_color_hex(0x17263A), 0);
lv_obj_set_style_border_width(status_panel, 2, 0);
lv_obj_set_style_border_color(status_panel, lv_color_hex(0x496582), 0);
lv_obj_set_style_pad_all(status_panel, 6, 0);
status_label = lv_label_create(status_panel);
lv_label_set_text(status_label, "Pressed: none");
lv_obj_set_style_text_font(status_label, &lv_font_montserrat_16, 0);
lv_obj_set_style_text_color(status_label, lv_color_hex(0xFFFFFF), 0);
lv_obj_center(status_label);
}
void setup() {
USBSerial.begin(115200);
Wire.begin(IIC_SDA, IIC_SCL);
digitalWrite(TP_RST, LOW);
delay(30);
digitalWrite(TP_RST, HIGH);
delay(50);
delay(1000);
Wire.begin(IIC_SDA, IIC_SCL);
touch.setPins(TP_RST, TP_INT);
bool result = touch.begin(Wire, 0x5A, IIC_SDA, IIC_SCL);
if (result == false) {
USBSerial.println("touch is not online...");
while (1) delay(1000);
}
USBSerial.print("Touch model: ");
USBSerial.println(touch.getModelName());
touch.sleep();
touch.reset();
touch.setMaxCoordinates(480, 480);
touch.setSwapXY(true);
touch.setMirrorXY(true, false);
attachInterrupt(
TP_INT, []() {
isPressed = true;
},
FALLING);
gfx->begin();
gfx->setBrightness(200);
bus->writeC8D8(0x36, 0xA0);
screenWidth = gfx->width();
screenHeight = gfx->height();
lv_init();
buf1 = (lv_color_t *)heap_caps_malloc(screenWidth * screenHeight / 4 * sizeof(lv_color_t), MALLOC_CAP_DMA);
buf2 = (lv_color_t *)heap_caps_malloc(screenWidth * screenHeight / 4 * sizeof(lv_color_t), MALLOC_CAP_DMA);
#if LV_USE_LOG != 0
lv_log_register_print_cb(my_print);
#endif
lv_disp_draw_buf_init(&draw_buf, buf1, buf2, screenWidth * screenHeight / 4);
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.hor_res = screenWidth;
disp_drv.ver_res = screenHeight;
disp_drv.flush_cb = my_disp_flush;
disp_drv.rounder_cb = example_lvgl_rounder_cb;
disp_drv.draw_buf = &draw_buf;
disp_drv.sw_rotate = 1;
lv_disp_drv_register(&disp_drv);
static lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = my_touchpad_read;
lv_indev_drv_register(&indev_drv);
const esp_timer_create_args_t lvgl_tick_timer_args = {
.callback = &example_increase_lvgl_tick,
.name = "lvgl_tick"
};
esp_timer_handle_t lvgl_tick_timer = NULL;
esp_timer_create(&lvgl_tick_timer_args, &lvgl_tick_timer);
esp_timer_start_periodic(lvgl_tick_timer, EXAMPLE_LVGL_TICK_PERIOD_MS * 1000);
create_ui();
USBSerial.println("LVGL official based test ready");
}
void loop() {
lv_timer_handler();
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

@ -0,0 +1,256 @@
#include <Arduino.h>
#include <Wire.h>
#include <lvgl.h>
#include <Arduino_GFX_Library.h>
#include <TouchDrvCSTXXX.hpp>
// Подтверждено на устройстве: LVGL-рендер работает вместе с touch-путём из shine_subserver_ui.
#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-subtouch"
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 lv_obj_t *gVersionLabel = 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), "Touch: pressed x=%d y=%d", gLastX, gLastY);
} else {
snprintf(text, sizeof(text), "Touch: released 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[96];
snprintf(text, sizeof(text), "Pressed: %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_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_style_text_color(label, lv_color_hex(0xFFFFFF), 0);
lv_obj_set_style_text_font(label, &lv_font_montserrat_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(0x07111C), 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);
gVersionLabel = lv_label_create(screen);
lv_label_set_text(gVersionLabel, "LVGL + subserver touch " TEST_VERSION);
lv_obj_set_style_text_font(gVersionLabel, &lv_font_montserrat_18, 0);
lv_obj_set_style_text_color(gVersionLabel, lv_color_hex(0xFFFFFF), 0);
lv_obj_align(gVersionLabel, LV_ALIGN_TOP_MID, 0, 12);
lv_obj_t *subtitle = lv_label_create(screen);
lv_label_set_text(subtitle, "Touch path comes from shine_subserver_ui. Tap buttons and watch status.");
lv_obj_set_width(subtitle, 436);
lv_label_set_long_mode(subtitle, LV_LABEL_LONG_WRAP);
lv_obj_set_style_text_font(subtitle, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(subtitle, lv_color_hex(0xD6DEE7), 0);
lv_obj_align(subtitle, LV_ALIGN_TOP_MID, 0, 42);
makeButton(screen, "Status", 22, 110, 136, 62, 0x355C7D);
makeButton(screen, "Connect", 172, 110, 136, 62, 0x2A9D8F);
makeButton(screen, "Wallet", 322, 110, 136, 62, 0x457B9D);
makeButton(screen, "Requests", 22, 188, 136, 62, 0xE76F51);
makeButton(screen, "Settings", 172, 188, 136, 62, 0x8D5A97);
makeButton(screen, "Register", 322, 188, 136, 62, 0x6A994E);
makeButton(screen, "Approve", 22, 266, 136, 62, 0xBC6C25);
makeButton(screen, "Reject", 172, 266, 136, 62, 0xB56576);
makeButton(screen, "Back", 322, 266, 136, 62, 0x6C757D);
makeButton(screen, "Bottom Action", 22, 346, 436, 68, 0x1D6F42);
lv_obj_t *statusPanel = lv_obj_create(screen);
lv_obj_set_size(statusPanel, 436, 26);
lv_obj_set_pos(statusPanel, 22, 420);
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, "Pressed: none");
lv_obj_set_style_text_font(gStatusLabel, &lv_font_montserrat_14, 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, 26);
lv_obj_set_pos(touchPanel, 22, 450);
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, "Touch: released x=-1 y=-1");
lv_obj_set_style_text_font(gTouchLabel, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(gTouchLabel, lv_color_hex(0xD6DEE7), 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 + subserver touch test ready: " TEST_VERSION);
}
void loop() {
lv_timer_handler();
delay(5);
}

View File

@ -0,0 +1,218 @@
#include <Arduino.h>
#include <Wire.h>
#include <lvgl.h>
#include <Arduino_GFX_Library.h>
#include "pin_config.h"
#include "TouchDrvCSTXXX.hpp"
#define EXAMPLE_LVGL_TICK_PERIOD_MS 2
static lv_disp_draw_buf_t gDrawBuf;
static lv_color_t *gBuf1 = nullptr;
static lv_color_t *gBuf2 = nullptr;
static lv_obj_t *gRawLabel = nullptr;
static lv_obj_t *gEventLabel = nullptr;
static lv_obj_t *gMarker = nullptr;
static TouchDrvCST92xx gTouch;
static int16_t gTouchX[5];
static int16_t gTouchY[5];
static bool gTouchPressed = false;
static uint32_t gRawTouchCount = 0;
static uint32_t gLvglButtonCount = 0;
Arduino_DataBus *gBus = new Arduino_ESP32QSPI(
LCD_CS, LCD_SCLK, LCD_SDIO0, LCD_SDIO1, LCD_SDIO2, LCD_SDIO3);
Arduino_CO5300 *gfx = new Arduino_CO5300(
gBus, LCD_RESET, 0, LCD_WIDTH, LCD_HEIGHT, 0, 0, 0, 0);
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(EXAMPLE_LVGL_TICK_PERIOD_MS);
}
static void lvglTouchRead(lv_indev_drv_t *indevDrv, lv_indev_data_t *data) {
LV_UNUSED(indevDrv);
uint8_t touched = gTouch.getPoint(gTouchX, gTouchY, gTouch.getSupportTouchPoint());
if (touched > 0) {
data->state = LV_INDEV_STATE_PR;
data->point.x = gTouchX[0];
data->point.y = gTouchY[0];
return;
}
data->state = LV_INDEV_STATE_REL;
}
static void bigButtonEventCb(lv_event_t *event) {
if (lv_event_get_code(event) != LV_EVENT_CLICKED) {
return;
}
gLvglButtonCount++;
String status = "LVGL button clicked: ";
status += String(gLvglButtonCount);
lv_label_set_text(gEventLabel, status.c_str());
Serial.println(status);
}
static void updateRawTouchState() {
uint8_t touched = gTouch.getPoint(gTouchX, gTouchY, gTouch.getSupportTouchPoint());
if (touched > 0) {
gTouchPressed = true;
gRawTouchCount++;
String line1 = "RAW pressed";
String line2 = "x=" + String(gTouchX[0]) + " y=" + String(gTouchY[0]) + " n=" + String(gRawTouchCount);
String text = line1 + "\n" + line2;
lv_label_set_text(gRawLabel, text.c_str());
int markerX = gTouchX[0] - 8;
int markerY = gTouchY[0] - 8;
if (markerX < 0) markerX = 0;
if (markerY < 0) markerY = 0;
lv_obj_set_pos(gMarker, markerX, markerY);
lv_obj_clear_flag(gMarker, LV_OBJ_FLAG_HIDDEN);
return;
}
if (gTouchPressed) {
gTouchPressed = false;
lv_label_set_text(gRawLabel, "RAW released\nTouch ended");
lv_obj_add_flag(gMarker, LV_OBJ_FLAG_HIDDEN);
}
}
static void createUi() {
lv_obj_t *screen = lv_scr_act();
lv_obj_set_style_bg_color(screen, lv_color_hex(0x000000), 0);
lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, 0);
lv_obj_t *title = lv_label_create(screen);
lv_label_set_text(title, "LVGL TOUCH DEBUG");
lv_obj_set_style_text_font(title, &lv_font_montserrat_22, 0);
lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 12);
gRawLabel = lv_label_create(screen);
lv_label_set_text(gRawLabel, "RAW released\nTouch not seen yet");
lv_obj_set_style_text_font(gRawLabel, &lv_font_montserrat_18, 0);
lv_obj_set_style_text_color(gRawLabel, lv_color_hex(0x9EE493), 0);
lv_obj_align(gRawLabel, LV_ALIGN_TOP_LEFT, 16, 56);
gEventLabel = lv_label_create(screen);
lv_label_set_text(gEventLabel, "LVGL button clicked: 0");
lv_obj_set_style_text_font(gEventLabel, &lv_font_montserrat_18, 0);
lv_obj_set_style_text_color(gEventLabel, lv_color_hex(0xFFD166), 0);
lv_obj_align(gEventLabel, LV_ALIGN_TOP_LEFT, 16, 118);
lv_obj_t *btn = lv_btn_create(screen);
lv_obj_set_size(btn, 360, 110);
lv_obj_align(btn, LV_ALIGN_CENTER, 0, 36);
lv_obj_set_style_radius(btn, 18, 0);
lv_obj_set_style_bg_color(btn, lv_color_hex(0x2A9D8F), 0);
lv_obj_set_style_border_width(btn, 3, 0);
lv_obj_set_style_border_color(btn, lv_color_hex(0xBDE0FE), 0);
lv_obj_add_event_cb(btn, bigButtonEventCb, LV_EVENT_CLICKED, nullptr);
lv_obj_t *btnLabel = lv_label_create(btn);
lv_label_set_text(btnLabel, "Tap Here");
lv_obj_set_style_text_font(btnLabel, &lv_font_montserrat_24, 0);
lv_obj_set_style_text_color(btnLabel, lv_color_hex(0xFFFFFF), 0);
lv_obj_center(btnLabel);
lv_obj_t *hint = lv_label_create(screen);
lv_label_set_text(hint, "Need both: RAW coords change and LVGL button click.");
lv_obj_set_width(hint, 430);
lv_label_set_long_mode(hint, LV_LABEL_LONG_WRAP);
lv_obj_set_style_text_font(hint, &lv_font_montserrat_16, 0);
lv_obj_set_style_text_color(hint, lv_color_hex(0xD9D9D9), 0);
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -34);
gMarker = lv_obj_create(screen);
lv_obj_set_size(gMarker, 16, 16);
lv_obj_set_style_radius(gMarker, LV_RADIUS_CIRCLE, 0);
lv_obj_set_style_bg_color(gMarker, lv_color_hex(0xFF4D6D), 0);
lv_obj_set_style_border_width(gMarker, 2, 0);
lv_obj_set_style_border_color(gMarker, lv_color_hex(0xFFFFFF), 0);
lv_obj_add_flag(gMarker, LV_OBJ_FLAG_HIDDEN);
}
void setup() {
Serial.begin(115200);
Wire.begin(IIC_SDA, IIC_SCL);
pinMode(TP_RST, OUTPUT);
pinMode(TP_INT, INPUT);
digitalWrite(TP_RST, LOW);
delay(30);
digitalWrite(TP_RST, HIGH);
delay(50);
delay(1000);
gTouch.setPins(TP_RST, TP_INT);
bool touchOk = gTouch.begin(Wire, 0x5A, IIC_SDA, IIC_SCL);
if (!touchOk) {
Serial.println("Touch init failed");
} else {
Serial.print("Touch model: ");
Serial.println(gTouch.getModelName());
gTouch.sleep();
gTouch.reset();
gTouch.setMaxCoordinates(480, 480);
gTouch.setSwapXY(true);
gTouch.setMirrorXY(true, false);
}
gfx->begin();
gBus->writeC8D8(0x36, 0xA0);
gfx->setBrightness(220);
gfx->fillScreen(0x0000);
lv_init();
uint32_t screenWidth = gfx->width();
uint32_t screenHeight = gfx->height();
gBuf1 = (lv_color_t *)heap_caps_malloc(screenWidth * screenHeight / 4 * sizeof(lv_color_t), MALLOC_CAP_DMA);
gBuf2 = (lv_color_t *)heap_caps_malloc(screenWidth * screenHeight / 4 * sizeof(lv_color_t), MALLOC_CAP_DMA);
lv_disp_draw_buf_init(&gDrawBuf, gBuf1, gBuf2, screenWidth * screenHeight / 4);
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 lvglTickTimerArgs = {
.callback = &lvglTick,
.name = "lvgl_tick"};
esp_timer_handle_t lvglTickTimer = nullptr;
esp_timer_create(&lvglTickTimerArgs, &lvglTickTimer);
esp_timer_start_periodic(lvglTickTimer, EXAMPLE_LVGL_TICK_PERIOD_MS * 1000);
createUi();
Serial.println("LVGL touch debug test ready");
}
void loop() {
updateRawTouchState();
lv_timer_handler();
delay(5);
}

View File

@ -0,0 +1,115 @@
#include <Arduino.h>
#include <Wire.h>
#include <Arduino_GFX_Library.h>
#include <U8g2lib.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 DISP_W 480
#define DISP_H 480
#define C_BG 0x0841u
#define C_PANEL 0x1082u
#define C_CARD 0x18C3u
#define C_BORDER 0x39C7u
#define C_TEXT 0xFFFFu
#define C_MUTE 0xBDF7u
#define C_OK 0x3666u
#define C_WARN 0xECA0u
#define C_BAD 0xD145u
#define C_BUTTON 0x2145u
#define C_BUTTON2 0x3186u
#define FONT_HEAD u8g2_font_10x20_t_cyrillic
#define FONT_BODY u8g2_font_9x15_t_cyrillic
#define FONT_SMALL u8g2_font_6x13_t_cyrillic
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);
static void drawPanel(int x, int y, int w, int h, uint16_t fill, uint16_t border, int radius = 10) {
gfx->fillRoundRect(x, y, w, h, radius, fill);
gfx->drawRoundRect(x, y, w, h, radius, border);
}
static void drawDefaultText(int x, int y, const char *text, uint16_t color, uint8_t size = 1) {
gfx->setFont();
gfx->setTextSize(size);
gfx->setTextColor(color);
gfx->setCursor(x, y);
gfx->print(text);
}
static void drawU8Text(int x, int y, const String &text, uint16_t color, const uint8_t *font) {
gfx->setTextSize(1);
gfx->setFont(font);
gfx->setTextColor(color);
gfx->setCursor(x, y);
gfx->print(text);
}
static void drawButton(int x, int y, int w, int h, uint16_t fill, const String &label, const uint8_t *font, bool useDefaultFont = false) {
drawPanel(x, y, w, h, fill, C_BORDER, 12);
if (useDefaultFont) {
drawDefaultText(x + 14, y + 30, label.c_str(), C_TEXT, 2);
return;
}
drawU8Text(x + 14, y + 32, label, C_TEXT, font);
}
static void drawScreen() {
gfx->fillScreen(C_BG);
drawPanel(12, 12, 456, 456, C_PANEL, C_BORDER, 16);
drawDefaultText(28, 42, "TEXT TEST 123", C_TEXT, 2);
drawU8Text(28, 72, "Default ASCII above", C_MUTE, (const uint8_t *)FONT_SMALL);
drawPanel(24, 90, 432, 72, C_CARD, C_BORDER, 12);
drawDefaultText(38, 118, "A: Default font ABC123", C_TEXT, 2);
drawDefaultText(38, 146, "Visible? then base path OK", C_WARN, 1);
drawPanel(24, 174, 432, 84, C_CARD, C_BORDER, 12);
drawU8Text(38, 204, "B: U8g2 ASCII abc123", C_TEXT, (const uint8_t *)FONT_BODY);
drawU8Text(38, 230, "C: Русский текст 123", C_OK, (const uint8_t *)FONT_BODY);
drawPanel(24, 270, 432, 84, C_CARD, C_BORDER, 12);
drawU8Text(38, 298, "D: Мелкий шрифт кнопок", C_TEXT, (const uint8_t *)FONT_SMALL);
drawU8Text(38, 320, "Если это видно, FONT_SMALL жив", C_MUTE, (const uint8_t *)FONT_SMALL);
drawButton(24, 368, 128, 72, C_BUTTON, "BTN 1", nullptr, true);
drawButton(176, 368, 128, 72, C_OK, "abc123", (const uint8_t *)FONT_BODY);
drawButton(328, 368, 128, 72, C_BUTTON2, "Русский", (const uint8_t *)FONT_BODY);
}
void setup() {
Serial.begin(115200);
Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL);
gfx->begin();
gBus->writeC8D8(0x36, 0xA0);
gfx->setBrightness(220);
gfx->setUTF8Print(true);
drawScreen();
Serial.println();
Serial.println("=== text_render_test ===");
Serial.println("A: default Arduino_GFX font");
Serial.println("B: U8g2 ASCII");
Serial.println("C: U8g2 Cyrillic");
Serial.println("Buttons: default / ASCII / Cyrillic");
}
void loop() {
delay(1000);
}

View File

@ -1,2 +1,2 @@
client.version=1.2.135
server.version=1.2.127
client.version=1.2.144
server.version=1.2.136