ESP32: добавить быстрый QR-экран кошелька
This commit is contained in:
parent
be4a2d135a
commit
4b15cabd4f
@ -0,0 +1,20 @@
|
||||
# ESP32 wallet QR button
|
||||
|
||||
- краткое описание:
|
||||
- на `HOME` кнопка баланса стала уже;
|
||||
- справа появилась отдельная кнопка с иконкой `QR`;
|
||||
- по нажатию открывается экран с QR-кодом `solana:<wallet>` и подписью адреса;
|
||||
- тап по любому месту экрана возвращает на главный экран.
|
||||
|
||||
- что именно проверять:
|
||||
- на `HOME` кнопка баланса визуально уже прежней;
|
||||
- справа от неё есть отдельная кнопка с QR-иконкой;
|
||||
- QR-экран открывается по нажатию;
|
||||
- QR сканируется кошельком как `solana:` URI;
|
||||
- тап по экрану QR возвращает на `HOME`.
|
||||
|
||||
- ожидаемый результат:
|
||||
- пользователь может быстро показать адрес кошелька для пополнения без захода в дополнительные экраны.
|
||||
|
||||
- статус:
|
||||
- pending
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
client.version=1.2.184
|
||||
server.version=1.2.173
|
||||
client.version=1.2.185
|
||||
server.version=1.2.174
|
||||
|
||||
Loading…
Reference in New Issue
Block a user