diff --git a/Dev_Docs/Pending_Features/2026-06-13_2258_esp32_wallet_qr_кнопка.md b/Dev_Docs/Pending_Features/2026-06-13_2258_esp32_wallet_qr_кнопка.md new file mode 100644 index 0000000..20ee2d4 --- /dev/null +++ b/Dev_Docs/Pending_Features/2026-06-13_2258_esp32_wallet_qr_кнопка.md @@ -0,0 +1,20 @@ +# ESP32 wallet QR button + +- краткое описание: + - на `HOME` кнопка баланса стала уже; + - справа появилась отдельная кнопка с иконкой `QR`; + - по нажатию открывается экран с QR-кодом `solana:` и подписью адреса; + - тап по любому месту экрана возвращает на главный экран. + +- что именно проверять: + - на `HOME` кнопка баланса визуально уже прежней; + - справа от неё есть отдельная кнопка с QR-иконкой; + - QR-экран открывается по нажатию; + - QR сканируется кошельком как `solana:` URI; + - тап по экрану QR возвращает на `HOME`. + +- ожидаемый результат: + - пользователь может быстро показать адрес кошелька для пополнения без захода в дополнительные экраны. + +- статус: + - pending 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 bb1b38c..de0841c 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 @@ -17,6 +17,10 @@ #include "shine_secret_generation.h" #include +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 &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(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(); @@ -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(static_cast(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; 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 7a900c9..d92128f 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 @@ -161,6 +161,12 @@ - короткий статус сервера; - короткий статус баланса. +В зоне баланса: + +- основная кнопка показа/обновления баланса занимает примерно 80% строки; +- справа от неё стоит отдельная кнопка с иконкой `QR`; +- нажатие на кнопку `QR` открывает экран `WALLET_QR`. + Нижние кнопки: - `Статус` @@ -417,16 +423,14 @@ Показывает: - QR-код для строки вида: - `solana:?amount=0.20&label=SHiNE%20Register`; -- адрес кошелька; -- сумму; -- текст URI. + `solana:`; +- мелкую подпись с полным адресом кошелька под QR. -Кнопки: +Поведение: -- `Назад` - -QR должен быть сканируемым, а не декоративным. +- QR должен быть сканируемым, а не декоративным; +- адрес кошелька берётся из `device key`, вычисленного из сохранённого `master secret`; +- нажатие в любую точку экрана возвращает пользователя на `HOME`. ## Экран REQUESTS diff --git a/VERSION.properties b/VERSION.properties index f62ce72..9d79143 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.184 -server.version=1.2.173 +client.version=1.2.185 +server.version=1.2.174