ESP32: добавить реальный wallet QR через LVGL
This commit is contained in:
parent
4b15cabd4f
commit
0ebb71daf1
9
.gitignore
vendored
9
.gitignore
vendored
@ -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
|
||||
|
||||
@ -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-код читается приложением кошелька;
|
||||
- возврат на главный экран работает обычным тапом;
|
||||
- устройство не перезагружается, сетевые подключения не теряются.
|
||||
@ -18,7 +18,7 @@
|
||||
#include <stdint.h>
|
||||
|
||||
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<uint8_t> &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<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() {
|
||||
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<void *>(static_cast<uintptr_t>(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();
|
||||
|
||||
@ -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`.
|
||||
|
||||
Нижние кнопки:
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
client.version=1.2.185
|
||||
server.version=1.2.174
|
||||
client.version=1.2.186
|
||||
server.version=1.2.175
|
||||
|
||||
Loading…
Reference in New Issue
Block a user