ESP32: добавить быстрый QR-экран кошелька

This commit is contained in:
AidarKC 2026-06-13 23:20:35 +04:00
parent be4a2d135a
commit 4b15cabd4f
4 changed files with 253 additions and 11 deletions

View File

@ -0,0 +1,20 @@
# ESP32 wallet QR button
- краткое описание:
- на `HOME` кнопка баланса стала уже;
- справа появилась отдельная кнопка с иконкой `QR`;
- по нажатию открывается экран с QR-кодом `solana:<wallet>` и подписью адреса;
- тап по любому месту экрана возвращает на главный экран.
- что именно проверять:
- на `HOME` кнопка баланса визуально уже прежней;
- справа от неё есть отдельная кнопка с QR-иконкой;
- QR-экран открывается по нажатию;
- QR сканируется кошельком как `solana:` URI;
- тап по экрану QR возвращает на `HOME`.
- ожидаемый результат:
- пользователь может быстро показать адрес кошелька для пополнения без захода в дополнительные экраны.
- статус:
- pending

View File

@ -17,6 +17,10 @@
#include "shine_secret_generation.h"
#include <stdint.h>
extern "C" {
#include "../../official-demo/examples/Arduino-v3.3.5/libraries/lvgl/src/extra/libs/qrcode/qrcodegen.h"
}
#define PIN_LCD_CS 12
#define PIN_LCD_SCLK 38
#define PIN_LCD_D0 4
@ -85,6 +89,7 @@ static const char *kSessionClientPlatformEsp32 = "ESP32";
enum Screen {
SCREEN_HOME,
SCREEN_WALLET_QR,
SCREEN_SETTINGS_MENU,
SCREEN_WIFI,
SCREEN_SERVER,
@ -113,6 +118,7 @@ enum SwipeDirection {
enum ActionId {
ACTION_NONE,
ACTION_OPEN_SETTINGS,
ACTION_OPEN_WALLET_QR,
ACTION_OPEN_WIFI,
ACTION_OPEN_SERVER,
ACTION_OPEN_ACCOUNT,
@ -226,6 +232,7 @@ static lv_color_t *gBuf1 = nullptr;
static lv_color_t *gBuf2 = nullptr;
static lv_obj_t *gRoot = nullptr;
static lv_obj_t *gInputTextArea = nullptr;
static uint8_t *gWalletQrCanvasBuffer = nullptr;
Arduino_DataBus *gBus = new Arduino_ESP32QSPI(
PIN_LCD_CS, PIN_LCD_SCLK, PIN_LCD_D0, PIN_LCD_D1, PIN_LCD_D2, PIN_LCD_D3);
@ -385,6 +392,7 @@ static void initPowerManagement();
static int batteryPercentValue();
static int wifiSignalLevel();
static void drawTopStatusIndicators();
static void actionButtonCb(lv_event_t *event);
static void markAccountStateDirty();
static void clearShineSessionState(bool clearStoredSession);
static void saveShineSessionPrefs();
@ -399,6 +407,9 @@ static bool parseUrlHostPortPath(const String &url, String &hostOut, uint16_t &p
static String shineWsUrl();
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);
@ -1127,6 +1138,17 @@ static String balanceHomeLine() {
return gBalanceStatusMessage;
}
static String walletQrUri() {
if (gDevicePubB58.isEmpty()) {
return "";
}
return String("solana:") + gDevicePubB58;
}
static String walletQrAddressLine() {
return gDevicePubB58.isEmpty() ? String("Wallet not set") : gDevicePubB58;
}
static String buildSessionKeyStringFromPublicBase64(const String &pubB64) {
return String("ed25519/") + pubB64;
}
@ -4142,6 +4164,9 @@ static void actionButtonCb(lv_event_t *event) {
case ACTION_OPEN_SETTINGS:
showScreen(SCREEN_SETTINGS_MENU);
break;
case ACTION_OPEN_WALLET_QR:
showScreen(SCREEN_WALLET_QR);
break;
case ACTION_REGISTER_ACCOUNT:
prepareRegisterAccountScreen();
showScreen(SCREEN_REGISTER_ACCOUNT_CONFIRM);
@ -4376,6 +4401,149 @@ 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();
@ -4437,7 +4605,8 @@ static void drawHome() {
drawTopStatusIndicators();
makeTitle("STATUS", 138, &lv_font_montserrat_28);
showMessageAt(wifiHomeSummary(), 214);
makeButton(balanceHomeLine().c_str(), 22, 254, 436, 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);
showMessageAt(shineHomeLine(), 322);
if (gShowRegisterAccountButton) {
makeButton("REGISTER ACCOUNT", 20, 360, 210, 78, 0x2A9D8F, ACTION_REGISTER_ACCOUNT, &lv_font_montserrat_20);
@ -4451,6 +4620,48 @@ static void drawHome() {
makeVersionTag();
}
static void drawWalletQrScreen() {
setRootStyle();
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_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);
drawQrCanvasOnParent(tapSurface, 120, 76, 240, walletQrUri());
lv_obj_t *address = lv_label_create(tapSurface);
lv_label_set_text(address, walletQrAddressLine().c_str());
lv_obj_set_width(address, 420);
lv_label_set_long_mode(address, LV_LABEL_LONG_WRAP);
lv_obj_set_style_text_align(address, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_font(address, &lv_font_montserrat_12, 0);
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_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);
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(hint, lv_color_hex(0x8398AD), 0);
lv_obj_set_pos(hint, 30, 410);
makeVersionTag();
}
static void drawSettingsMenu() {
setRootStyle();
@ -4915,12 +5126,16 @@ static void rebuildScreen() {
gRoot = lv_scr_act();
}
releaseTransientUiBuffers();
lv_obj_clean(gRoot);
switch (gCurrentScreen) {
case SCREEN_HOME:
drawHome();
break;
case SCREEN_WALLET_QR:
drawWalletQrScreen();
break;
case SCREEN_SETTINGS_MENU:
drawSettingsMenu();
break;
@ -5065,6 +5280,9 @@ static void handleSwipe(SwipeDirection swipe) {
case SCREEN_HOME:
handleHomeSwipe(swipe);
break;
case SCREEN_WALLET_QR:
handleHomeSwipe(swipe);
break;
case SCREEN_SETTINGS_MENU:
handleSettingsSwipe(swipe);
break;

View File

@ -161,6 +161,12 @@
- короткий статус сервера;
- короткий статус баланса.
В зоне баланса:
- основная кнопка показа/обновления баланса занимает примерно 80% строки;
- справа от неё стоит отдельная кнопка с иконкой `QR`;
- нажатие на кнопку `QR` открывает экран `WALLET_QR`.
Нижние кнопки:
- `Статус`
@ -417,16 +423,14 @@
Показывает:
- QR-код для строки вида:
`solana:<wallet>?amount=0.20&label=SHiNE%20Register`;
- адрес кошелька;
- сумму;
- текст URI.
`solana:<wallet>`;
- мелкую подпись с полным адресом кошелька под QR.
Кнопки:
Поведение:
- `Назад`
QR должен быть сканируемым, а не декоративным.
- QR должен быть сканируемым, а не декоративным;
- адрес кошелька берётся из `device key`, вычисленного из сохранённого `master secret`;
- нажатие в любую точку экрана возвращает пользователя на `HOME`.
## Экран REQUESTS

View File

@ -1,2 +1,2 @@
client.version=1.2.184
server.version=1.2.173
client.version=1.2.185
server.version=1.2.174