From 0ebb71daf140fb72dcc602ae33b82748e4c74bde2d4bd20a8dbfacc2477dab62 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Sun, 14 Jun 2026 10:27:10 +0400 Subject: [PATCH] =?UTF-8?q?ESP32:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=80=D0=B5=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9?= =?UTF-8?q?=20wallet=20QR=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20LVGL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 9 + .../2026-06-14_1450_esp32_wallet_qr_lvgl.md | 26 ++ .../shine_homeserver_main.ino | 235 ++++++------------ .../reference/shine_homeserver_ui_spec.md | 4 +- VERSION.properties | 4 +- 5 files changed, 114 insertions(+), 164 deletions(-) create mode 100644 Dev_Docs/Pending_Features/2026-06-14_1450_esp32_wallet_qr_lvgl.md diff --git a/.gitignore b/.gitignore index 09b09a4..5a1214c 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,15 @@ ESP32/**/.idea/ ESP32-wallet/.idea/ ESP32/**/.arduino-build/ ESP32/**/official-demo/ +!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/ +ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/** +!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/ +ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/** +!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/ +ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/** +!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/libraries/ +ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/libraries/** +!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/libraries/lv_conf.h ESP32/**/original-firmware/*.bin ESP32/**/original-firmware/*.bin.sha256 ESP32/**/*.elf diff --git a/Dev_Docs/Pending_Features/2026-06-14_1450_esp32_wallet_qr_lvgl.md b/Dev_Docs/Pending_Features/2026-06-14_1450_esp32_wallet_qr_lvgl.md new file mode 100644 index 0000000..63d70bc --- /dev/null +++ b/Dev_Docs/Pending_Features/2026-06-14_1450_esp32_wallet_qr_lvgl.md @@ -0,0 +1,26 @@ +# ESP32 wallet QR через LVGL + +- Статус: `pending` + +## Что сделано + +- для `WALLET_QR` включён штатный `LVGL`-виджет `lv_qrcode`; +- кнопка на главном экране оставлена текстовой `QR`; +- экран должен показывать реальный `QR` для `solana:`; +- тап по экрану должен возвращать на `HOME`, без перезагрузки устройства и без потери `Wi-Fi`/`SHiNE`. + +## Что проверять + +1. На главном экране нажать кнопку `QR`. +2. Убедиться, что открывается экран `WALLET QR`. +3. Проверить, что виден настоящий QR-код. +4. Проверить, что внизу мелким текстом показан адрес кошелька. +5. Нажать в любое место экрана и убедиться, что устройство возвращается на `HOME`. +6. Убедиться, что после открытия и закрытия QR-экрана не рвутся `Wi-Fi` и подключение к `SHiNE`. + +## Ожидаемый результат + +- QR-экран открывается стабильно; +- QR-код читается приложением кошелька; +- возврат на главный экран работает обычным тапом; +- устройство не перезагружается, сетевые подключения не теряются. diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_main/shine_homeserver_main.ino b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_main/shine_homeserver_main.ino index de0841c..a04ac43 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_main/shine_homeserver_main.ino +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_main/shine_homeserver_main.ino @@ -18,7 +18,7 @@ #include extern "C" { -#include "../../official-demo/examples/Arduino-v3.3.5/libraries/lvgl/src/extra/libs/qrcode/qrcodegen.h" +#include "../../official-demo/examples/Arduino-v3.3.5/libraries/lvgl/src/extra/libs/qrcode/lv_qrcode.h" } #define PIN_LCD_CS 12 @@ -254,6 +254,7 @@ static bool gBlockClick = false; static bool gSuppressTouchUntilRelease = false; static uint32_t gTouchSequence = 0; static uint32_t gLastHandledTouchSequence = 0; +static bool gWalletQrTapReturnPending = false; static String gWifiSavedSsid; static String gWifiSavedPassword; @@ -409,7 +410,6 @@ static String shineHomeLine(); static String balanceHomeLine(); static String walletQrUri(); static String walletQrAddressLine(); -static void releaseTransientUiBuffers(); static uint64_t shineNowMs(); static bool loadWalletBalanceLamports(uint64_t &lamportsOut, String &messageOut); static void shortVecEncode(size_t value, std::vector &out); @@ -588,6 +588,11 @@ static void lvglTouchRead(lv_indev_drv_t *indevDrv, lv_indev_data_t *data) { if (gTouchDown) { gTouchDown = false; gPendingSwipe = detectSwipe(gTouchStartX, gTouchStartY, gTouchLastX, gTouchLastY); + if (gCurrentScreen == SCREEN_WALLET_QR + && gPendingSwipe == SWIPE_NONE + && !movedTooFarForTap(gTouchStartX, gTouchStartY, gTouchLastX, gTouchLastY)) { + gWalletQrTapReturnPending = true; + } } gSuppressTouchUntilRelease = false; @@ -1149,6 +1154,17 @@ static String walletQrAddressLine() { return gDevicePubB58.isEmpty() ? String("Wallet not set") : gDevicePubB58; } +static void releaseTransientUiBuffers() { + if (gWalletQrCanvasBuffer) { + heap_caps_free(gWalletQrCanvasBuffer); + gWalletQrCanvasBuffer = nullptr; + } +} + +static void saveUiErrorDiag(const String &summary, const String &details) { + saveRegisterDiag("error", summary, String("kind=ui_runtime\nscreen=wallet_qr\n") + details); +} + static String buildSessionKeyStringFromPublicBase64(const String &pubB64) { return String("ed25519/") + pubB64; } @@ -4401,148 +4417,6 @@ static void makeVersionTag() { lv_obj_align(tag, LV_ALIGN_BOTTOM_MID, 0, -10); } -static void releaseTransientUiBuffers() { - if (gWalletQrCanvasBuffer) { - heap_caps_free(gWalletQrCanvasBuffer); - gWalletQrCanvasBuffer = nullptr; - } -} - -static lv_obj_t *makeQrIconButton(lv_coord_t x, - lv_coord_t y, - lv_coord_t w, - lv_coord_t h, - uint32_t bgColor, - ActionId action) { - 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(static_cast(action))); - - auto makeSquare = [&](lv_coord_t sx, lv_coord_t sy, lv_coord_t size, bool filled) { - lv_obj_t *box = lv_obj_create(btn); - lv_obj_set_size(box, size, size); - lv_obj_set_pos(box, sx, sy); - lv_obj_set_style_radius(box, 2, 0); - lv_obj_set_style_border_width(box, 2, 0); - lv_obj_set_style_border_color(box, lv_color_hex(0xFFFFFF), 0); - lv_obj_set_style_bg_color(box, lv_color_hex(filled ? 0xFFFFFF : bgColor), 0); - lv_obj_set_style_bg_opa(box, filled ? LV_OPA_COVER : LV_OPA_TRANSP, 0); - lv_obj_set_style_shadow_width(box, 0, 0); - lv_obj_clear_flag(box, LV_OBJ_FLAG_SCROLLABLE); - }; - - makeSquare(16, 10, 18, false); - makeSquare(20, 14, 10, true); - makeSquare(48, 10, 18, false); - makeSquare(52, 14, 10, true); - makeSquare(16, 30, 18, false); - makeSquare(20, 34, 10, true); - - const lv_coord_t patternX[] = {40, 44, 48, 52, 40, 48, 56, 56, 48}; - const lv_coord_t patternY[] = {34, 34, 34, 34, 42, 42, 42, 50, 50}; - for (size_t i = 0; i < sizeof(patternX) / sizeof(patternX[0]); ++i) { - lv_obj_t *dot = lv_obj_create(btn); - lv_obj_set_size(dot, 4, 4); - lv_obj_set_pos(dot, patternX[i], patternY[i]); - lv_obj_set_style_radius(dot, 1, 0); - lv_obj_set_style_border_width(dot, 0, 0); - lv_obj_set_style_bg_color(dot, lv_color_hex(0xFFFFFF), 0); - lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0); - lv_obj_set_style_shadow_width(dot, 0, 0); - lv_obj_clear_flag(dot, LV_OBJ_FLAG_SCROLLABLE); - } - return btn; -} - -static void drawQrCanvasOnParent(lv_obj_t *parent, - lv_coord_t x, - lv_coord_t y, - lv_coord_t size, - const String &payload) { - lv_obj_t *panel = lv_obj_create(parent); - lv_obj_set_size(panel, size + 20, size + 20); - lv_obj_set_pos(panel, x - 10, y - 10); - lv_obj_set_style_radius(panel, 18, 0); - lv_obj_set_style_bg_color(panel, lv_color_hex(0xFFFFFF), 0); - lv_obj_set_style_bg_opa(panel, LV_OPA_COVER, 0); - lv_obj_set_style_border_width(panel, 0, 0); - lv_obj_set_style_shadow_width(panel, 0, 0); - lv_obj_clear_flag(panel, LV_OBJ_FLAG_SCROLLABLE); - - if (payload.isEmpty()) { - lv_obj_t *error = lv_label_create(parent); - lv_label_set_text(error, "Wallet not set"); - lv_obj_set_width(error, size); - lv_obj_set_style_text_align(error, LV_TEXT_ALIGN_CENTER, 0); - lv_obj_set_style_text_font(error, &lv_font_montserrat_18, 0); - lv_obj_set_style_text_color(error, lv_color_hex(0xD26969), 0); - lv_obj_set_pos(error, x, y + size / 2 - 12); - return; - } - - uint8_t qr[qrcodegen_BUFFER_LEN_MAX]; - uint8_t tmp[qrcodegen_BUFFER_LEN_MAX]; - memset(qr, 0, sizeof(qr)); - memset(tmp, 0, sizeof(tmp)); - bool ok = qrcodegen_encodeText(payload.c_str(), tmp, qr, qrcodegen_Ecc_MEDIUM, 1, 10, qrcodegen_Mask_AUTO, true); - if (!ok) { - lv_obj_t *error = lv_label_create(parent); - lv_label_set_text(error, "QR error"); - lv_obj_set_width(error, size); - lv_obj_set_style_text_align(error, LV_TEXT_ALIGN_CENTER, 0); - lv_obj_set_style_text_font(error, &lv_font_montserrat_18, 0); - lv_obj_set_style_text_color(error, lv_color_hex(0xD26969), 0); - lv_obj_set_pos(error, x, y + size / 2 - 12); - return; - } - - gWalletQrCanvasBuffer = static_cast( - heap_caps_malloc(LV_CANVAS_BUF_SIZE_TRUE_COLOR(size, size), MALLOC_CAP_8BIT)); - if (!gWalletQrCanvasBuffer) { - lv_obj_t *error = lv_label_create(parent); - lv_label_set_text(error, "QR memory error"); - lv_obj_set_width(error, size); - lv_obj_set_style_text_align(error, LV_TEXT_ALIGN_CENTER, 0); - lv_obj_set_style_text_font(error, &lv_font_montserrat_18, 0); - lv_obj_set_style_text_color(error, lv_color_hex(0xD26969), 0); - lv_obj_set_pos(error, x, y + size / 2 - 12); - return; - } - - lv_obj_t *canvas = lv_canvas_create(parent); - lv_obj_set_pos(canvas, x, y); - lv_canvas_set_buffer(canvas, gWalletQrCanvasBuffer, size, size, LV_IMG_CF_TRUE_COLOR); - lv_canvas_fill_bg(canvas, lv_color_hex(0xFFFFFF), LV_OPA_COVER); - - int qrSize = qrcodegen_getSize(qr); - int scale = (size - 16) / qrSize; - if (scale < 1) { - scale = 1; - } - int margin = (size - qrSize * scale) / 2; - for (int yy = 0; yy < qrSize; ++yy) { - for (int xx = 0; xx < qrSize; ++xx) { - if (!qrcodegen_getModule(qr, xx, yy)) { - continue; - } - for (int py = 0; py < scale; ++py) { - for (int px = 0; px < scale; ++px) { - lv_canvas_set_px(canvas, - margin + xx * scale + px, - margin + yy * scale + py, - lv_color_black()); - } - } - } - } -} static void drawHome() { setRootStyle(); @@ -4606,7 +4480,7 @@ static void drawHome() { makeTitle("STATUS", 138, &lv_font_montserrat_28); showMessageAt(wifiHomeSummary(), 214); makeButton(balanceHomeLine().c_str(), 22, 254, 340, 56, 0x355C7D, ACTION_REFRESH_BALANCE, &lv_font_montserrat_18); - makeQrIconButton(374, 254, 84, 56, 0x2A6F97, ACTION_OPEN_WALLET_QR); + makeButton("QR", 374, 254, 84, 56, 0x2A6F97, ACTION_OPEN_WALLET_QR, &lv_font_montserrat_20); showMessageAt(shineHomeLine(), 322); if (gShowRegisterAccountButton) { makeButton("REGISTER ACCOUNT", 20, 360, 210, 78, 0x2A9D8F, ACTION_REGISTER_ACCOUNT, &lv_font_montserrat_20); @@ -4622,27 +4496,64 @@ static void drawHome() { static void drawWalletQrScreen() { setRootStyle(); + gWalletQrTapReturnPending = false; - lv_obj_t *tapSurface = lv_obj_create(gRoot); - lv_obj_set_size(tapSurface, DISP_W, DISP_H); - lv_obj_set_pos(tapSurface, 0, 0); - lv_obj_set_style_radius(tapSurface, 0, 0); - lv_obj_set_style_bg_opa(tapSurface, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(tapSurface, 0, 0); - lv_obj_set_style_shadow_width(tapSurface, 0, 0); - lv_obj_clear_flag(tapSurface, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_add_flag(tapSurface, LV_OBJ_FLAG_CLICKABLE); - lv_obj_add_event_cb(tapSurface, actionButtonCb, LV_EVENT_CLICKED, reinterpret_cast(static_cast(ACTION_BACK_HOME))); - - lv_obj_t *title = lv_label_create(tapSurface); + lv_obj_t *title = lv_label_create(gRoot); lv_label_set_text(title, "WALLET QR"); lv_obj_set_style_text_font(title, &lv_font_montserrat_24, 0); lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0); lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 22); + String qrPayload = walletQrUri(); + if (qrPayload.isEmpty()) { + saveUiErrorDiag("Wallet QR unavailable", "wallet_address_empty=true\n"); + lv_obj_t *error = lv_label_create(gRoot); + lv_label_set_text(error, "Wallet not set"); + lv_obj_set_width(error, 380); + lv_obj_set_style_text_align(error, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_text_font(error, &lv_font_montserrat_20, 0); + lv_obj_set_style_text_color(error, lv_color_hex(0xD26969), 0); + lv_obj_set_pos(error, 50, 174); + } else { + lv_obj_t *panel = lv_obj_create(gRoot); + lv_obj_set_size(panel, 300, 220); + lv_obj_set_pos(panel, 90, 88); + lv_obj_set_style_radius(panel, 18, 0); + lv_obj_set_style_bg_color(panel, lv_color_hex(0xFFFFFF), 0); + lv_obj_set_style_bg_opa(panel, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(panel, 0, 0); + lv_obj_set_style_shadow_width(panel, 0, 0); + lv_obj_clear_flag(panel, LV_OBJ_FLAG_SCROLLABLE); - drawQrCanvasOnParent(tapSurface, 120, 76, 240, walletQrUri()); + lv_obj_t *qr = lv_qrcode_create(panel, 180, lv_color_hex(0x111111), lv_color_hex(0xFFFFFF)); + if (qr == nullptr) { + saveUiErrorDiag("Wallet QR create failed", "stage=create\n"); + lv_obj_t *error = lv_label_create(panel); + lv_label_set_text(error, "QR create failed"); + lv_obj_set_width(error, 248); + lv_label_set_long_mode(error, LV_LABEL_LONG_WRAP); + lv_obj_set_style_text_align(error, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_text_font(error, &lv_font_montserrat_18, 0); + lv_obj_set_style_text_color(error, lv_color_hex(0xA23232), 0); + lv_obj_align(error, LV_ALIGN_CENTER, 0, 0); + } else { + lv_obj_center(qr); + if (lv_qrcode_update(qr, qrPayload.c_str(), qrPayload.length()) != LV_RES_OK) { + saveUiErrorDiag("Wallet QR update failed", + String("stage=update\npayload_len=") + String(qrPayload.length()) + "\n"); + lv_obj_del(qr); + lv_obj_t *error = lv_label_create(panel); + lv_label_set_text(error, "QR update failed"); + lv_obj_set_width(error, 248); + lv_label_set_long_mode(error, LV_LABEL_LONG_WRAP); + lv_obj_set_style_text_align(error, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_text_font(error, &lv_font_montserrat_18, 0); + lv_obj_set_style_text_color(error, lv_color_hex(0xA23232), 0); + lv_obj_align(error, LV_ALIGN_CENTER, 0, 0); + } + } + } - lv_obj_t *address = lv_label_create(tapSurface); + lv_obj_t *address = lv_label_create(gRoot); lv_label_set_text(address, walletQrAddressLine().c_str()); lv_obj_set_width(address, 420); lv_label_set_long_mode(address, LV_LABEL_LONG_WRAP); @@ -4651,7 +4562,7 @@ static void drawWalletQrScreen() { lv_obj_set_style_text_color(address, lv_color_hex(0xB8C6D3), 0); lv_obj_set_pos(address, 30, 344); - lv_obj_t *hint = lv_label_create(tapSurface); + lv_obj_t *hint = lv_label_create(gRoot); lv_label_set_text(hint, "Tap anywhere to return"); lv_obj_set_width(hint, 420); lv_obj_set_style_text_align(hint, LV_TEXT_ALIGN_CENTER, 0); @@ -5377,6 +5288,10 @@ void setup() { void loop() { handleUsbSerialCommands(); lv_timer_handler(); + if (gWalletQrTapReturnPending) { + gWalletQrTapReturnPending = false; + showScreen(SCREEN_HOME); + } manageWifiReconnect(); manageAccountPdaRefresh(); manageShineConnection(); diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_spec.md b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_spec.md index d92128f..b3f7f5f 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_spec.md +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_homeserver_ui_spec.md @@ -20,7 +20,7 @@ - локальный UI на тач-экране; - хранение настроек и секретов во внутренней памяти `ESP32` через `NVS`; - русский текст в логике интерфейса; в текущей временной сборке отображение на экране идёт через ASCII-транслитерацию, потому что `U8g2`-шрифты на устройстве временно не рисуются; -- экран пополнения с реальным `solana:` URI и рисованием QR-кода; +- экран пополнения с реальным `solana:` URI и рисованием QR-кода через `LVGL`; - реальное подключение к `Wi-Fi` по сохранённым `SSID/паролю`; - реальная проверка доступности `API`, `RPC` и `WS`-адресов; - реальное чтение баланса кошелька из `Solana RPC`; @@ -164,7 +164,7 @@ В зоне баланса: - основная кнопка показа/обновления баланса занимает примерно 80% строки; -- справа от неё стоит отдельная кнопка с иконкой `QR`; +- справа от неё стоит отдельная кнопка `QR`; - нажатие на кнопку `QR` открывает экран `WALLET_QR`. Нижние кнопки: diff --git a/VERSION.properties b/VERSION.properties index 9d79143..f92f98e 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.185 -server.version=1.2.174 +client.version=1.2.186 +server.version=1.2.175