ESP32: добавить реальный wallet QR через LVGL

This commit is contained in:
AidarKC 2026-06-14 10:27:10 +04:00
parent 4b15cabd4f
commit 0ebb71daf1
5 changed files with 114 additions and 164 deletions

9
.gitignore vendored
View File

@ -81,6 +81,15 @@ ESP32/**/.idea/
ESP32-wallet/.idea/ ESP32-wallet/.idea/
ESP32/**/.arduino-build/ ESP32/**/.arduino-build/
ESP32/**/official-demo/ 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
ESP32/**/original-firmware/*.bin.sha256 ESP32/**/original-firmware/*.bin.sha256
ESP32/**/*.elf ESP32/**/*.elf

View File

@ -0,0 +1,26 @@
# ESP32 wallet QR через LVGL
- Статус: `pending`
## Что сделано
- для `WALLET_QR` включён штатный `LVGL`-виджет `lv_qrcode`;
- кнопка на главном экране оставлена текстовой `QR`;
- экран должен показывать реальный `QR` для `solana:<wallet_address>`;
- тап по экрану должен возвращать на `HOME`, без перезагрузки устройства и без потери `Wi-Fi`/`SHiNE`.
## Что проверять
1. На главном экране нажать кнопку `QR`.
2. Убедиться, что открывается экран `WALLET QR`.
3. Проверить, что виден настоящий QR-код.
4. Проверить, что внизу мелким текстом показан адрес кошелька.
5. Нажать в любое место экрана и убедиться, что устройство возвращается на `HOME`.
6. Убедиться, что после открытия и закрытия QR-экрана не рвутся `Wi-Fi` и подключение к `SHiNE`.
## Ожидаемый результат
- QR-экран открывается стабильно;
- QR-код читается приложением кошелька;
- возврат на главный экран работает обычным тапом;
- устройство не перезагружается, сетевые подключения не теряются.

View File

@ -18,7 +18,7 @@
#include <stdint.h> #include <stdint.h>
extern "C" { 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 #define PIN_LCD_CS 12
@ -254,6 +254,7 @@ static bool gBlockClick = false;
static bool gSuppressTouchUntilRelease = false; static bool gSuppressTouchUntilRelease = false;
static uint32_t gTouchSequence = 0; static uint32_t gTouchSequence = 0;
static uint32_t gLastHandledTouchSequence = 0; static uint32_t gLastHandledTouchSequence = 0;
static bool gWalletQrTapReturnPending = false;
static String gWifiSavedSsid; static String gWifiSavedSsid;
static String gWifiSavedPassword; static String gWifiSavedPassword;
@ -409,7 +410,6 @@ static String shineHomeLine();
static String balanceHomeLine(); static String balanceHomeLine();
static String walletQrUri(); static String walletQrUri();
static String walletQrAddressLine(); static String walletQrAddressLine();
static void releaseTransientUiBuffers();
static uint64_t shineNowMs(); static uint64_t shineNowMs();
static bool loadWalletBalanceLamports(uint64_t &lamportsOut, String &messageOut); static bool loadWalletBalanceLamports(uint64_t &lamportsOut, String &messageOut);
static void shortVecEncode(size_t value, std::vector<uint8_t> &out); static void shortVecEncode(size_t value, std::vector<uint8_t> &out);
@ -588,6 +588,11 @@ static void lvglTouchRead(lv_indev_drv_t *indevDrv, lv_indev_data_t *data) {
if (gTouchDown) { if (gTouchDown) {
gTouchDown = false; gTouchDown = false;
gPendingSwipe = detectSwipe(gTouchStartX, gTouchStartY, gTouchLastX, gTouchLastY); gPendingSwipe = detectSwipe(gTouchStartX, gTouchStartY, gTouchLastX, gTouchLastY);
if (gCurrentScreen == SCREEN_WALLET_QR
&& gPendingSwipe == SWIPE_NONE
&& !movedTooFarForTap(gTouchStartX, gTouchStartY, gTouchLastX, gTouchLastY)) {
gWalletQrTapReturnPending = true;
}
} }
gSuppressTouchUntilRelease = false; gSuppressTouchUntilRelease = false;
@ -1149,6 +1154,17 @@ static String walletQrAddressLine() {
return gDevicePubB58.isEmpty() ? String("Wallet not set") : gDevicePubB58; 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) { static String buildSessionKeyStringFromPublicBase64(const String &pubB64) {
return String("ed25519/") + pubB64; return String("ed25519/") + pubB64;
} }
@ -4401,148 +4417,6 @@ static void makeVersionTag() {
lv_obj_align(tag, LV_ALIGN_BOTTOM_MID, 0, -10); 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<void *>(static_cast<uintptr_t>(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<uint8_t *>(
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() { static void drawHome() {
setRootStyle(); setRootStyle();
@ -4606,7 +4480,7 @@ static void drawHome() {
makeTitle("STATUS", 138, &lv_font_montserrat_28); makeTitle("STATUS", 138, &lv_font_montserrat_28);
showMessageAt(wifiHomeSummary(), 214); showMessageAt(wifiHomeSummary(), 214);
makeButton(balanceHomeLine().c_str(), 22, 254, 340, 56, 0x355C7D, ACTION_REFRESH_BALANCE, &lv_font_montserrat_18); 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); showMessageAt(shineHomeLine(), 322);
if (gShowRegisterAccountButton) { if (gShowRegisterAccountButton) {
makeButton("REGISTER ACCOUNT", 20, 360, 210, 78, 0x2A9D8F, ACTION_REGISTER_ACCOUNT, &lv_font_montserrat_20); 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() { static void drawWalletQrScreen() {
setRootStyle(); setRootStyle();
gWalletQrTapReturnPending = false;
lv_obj_t *tapSurface = lv_obj_create(gRoot); lv_obj_t *title = lv_label_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<void *>(static_cast<uintptr_t>(ACTION_BACK_HOME)));
lv_obj_t *title = lv_label_create(tapSurface);
lv_label_set_text(title, "WALLET QR"); lv_label_set_text(title, "WALLET QR");
lv_obj_set_style_text_font(title, &lv_font_montserrat_24, 0); 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_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 22); 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_label_set_text(address, walletQrAddressLine().c_str());
lv_obj_set_width(address, 420); lv_obj_set_width(address, 420);
lv_label_set_long_mode(address, LV_LABEL_LONG_WRAP); 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_style_text_color(address, lv_color_hex(0xB8C6D3), 0);
lv_obj_set_pos(address, 30, 344); 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_label_set_text(hint, "Tap anywhere to return");
lv_obj_set_width(hint, 420); lv_obj_set_width(hint, 420);
lv_obj_set_style_text_align(hint, LV_TEXT_ALIGN_CENTER, 0); lv_obj_set_style_text_align(hint, LV_TEXT_ALIGN_CENTER, 0);
@ -5377,6 +5288,10 @@ void setup() {
void loop() { void loop() {
handleUsbSerialCommands(); handleUsbSerialCommands();
lv_timer_handler(); lv_timer_handler();
if (gWalletQrTapReturnPending) {
gWalletQrTapReturnPending = false;
showScreen(SCREEN_HOME);
}
manageWifiReconnect(); manageWifiReconnect();
manageAccountPdaRefresh(); manageAccountPdaRefresh();
manageShineConnection(); manageShineConnection();

View File

@ -20,7 +20,7 @@
- локальный UI на тач-экране; - локальный UI на тач-экране;
- хранение настроек и секретов во внутренней памяти `ESP32` через `NVS`; - хранение настроек и секретов во внутренней памяти `ESP32` через `NVS`;
- русский текст в логике интерфейса; в текущей временной сборке отображение на экране идёт через ASCII-транслитерацию, потому что `U8g2`-шрифты на устройстве временно не рисуются; - русский текст в логике интерфейса; в текущей временной сборке отображение на экране идёт через ASCII-транслитерацию, потому что `U8g2`-шрифты на устройстве временно не рисуются;
- экран пополнения с реальным `solana:` URI и рисованием QR-кода; - экран пополнения с реальным `solana:` URI и рисованием QR-кода через `LVGL`;
- реальное подключение к `Wi-Fi` по сохранённым `SSID/паролю`; - реальное подключение к `Wi-Fi` по сохранённым `SSID/паролю`;
- реальная проверка доступности `API`, `RPC` и `WS`-адресов; - реальная проверка доступности `API`, `RPC` и `WS`-адресов;
- реальное чтение баланса кошелька из `Solana RPC`; - реальное чтение баланса кошелька из `Solana RPC`;
@ -164,7 +164,7 @@
В зоне баланса: В зоне баланса:
- основная кнопка показа/обновления баланса занимает примерно 80% строки; - основная кнопка показа/обновления баланса занимает примерно 80% строки;
- справа от неё стоит отдельная кнопка с иконкой `QR`; - справа от неё стоит отдельная кнопка `QR`;
- нажатие на кнопку `QR` открывает экран `WALLET_QR`. - нажатие на кнопку `QR` открывает экран `WALLET_QR`.
Нижние кнопки: Нижние кнопки:

View File

@ -1,2 +1,2 @@
client.version=1.2.185 client.version=1.2.186
server.version=1.2.174 server.version=1.2.175