diff --git a/Dev_Docs/Pending_Features/2026-06-07_1650_esp32_homeserver_ui_прототип.md b/Dev_Docs/Pending_Features/2026-06-07_1650_esp32_homeserver_ui_прототип.md index bef19aa..0d8ef8c 100644 --- a/Dev_Docs/Pending_Features/2026-06-07_1650_esp32_homeserver_ui_прототип.md +++ b/Dev_Docs/Pending_Features/2026-06-07_1650_esp32_homeserver_ui_прототип.md @@ -13,11 +13,12 @@ 7. Открыть `Кошелёк`, нажать `Проверить` и убедиться, что баланс реально читается из `Solana RPC`; затем открыть `QR и URI` и проверить, что QR-код отрисовывается и сканируется как `solana:`-ссылка. 8. При необходимости отдельно проверить тестовые кнопки `+/- SOL`: они меняют локальный баланс для UX-сценариев, но после следующей реальной RPC-проверки баланс должен вернуться к сетевому значению. 9. Вернуться на главный экран и проверить, что до выполнения всех условий кнопка регистрации недоступна, а после выполнения становится доступной; также убедиться, что между двумя нижними кнопками есть небольшой зазор. - 10. Нажать кнопку регистрации и убедиться, что она запускает on-chain flow сразу без отдельного confirm-экрана, а после завершения статус меняется на `Homeserver активен`, онлайн-статус становится активным, и на экране аккаунта появляются краткие отпечатки `PDA/TX`. - 11. После регистрации проверить через `Solana`/UI проекта, что `user_pda` для этого логина реально создана, сохранена в `NVS` и соответствует `device`-адресу устройства. - 12. Открыть `Запросы`, поочерёдно открыть оба демонстрационных запроса и проверить, что кнопки `Разрешить` и `Отклонить` меняют их статус. - 13. При необходимости открыть `Настройки -> Сменить PIN` и убедиться, что новый PIN сохраняется, хотя вход по PIN временно не используется на старте. - 14. Выполнить `Полный сброс` и убедиться, что все поля, секрет, баланс, онлайн и регистрация очищаются. + 10. Нажать кнопку регистрации и убедиться, что открывается отдельный экран проверки, где ещё раз видно `login`, статус свободного `PDA`, баланс, `homeserver1` с пометкой о стандартном значении и сообщение, если `Wi-Fi` не подключён. + 11. На экране проверки нажать `ЗАРЕГИСТРИРОВАТЬ В СИЯНИИ` и убедиться, что после этого появляется отдельный экран результата с успехом либо подробной ошибкой. + 12. После успешной регистрации проверить через `Solana`/UI проекта, что `user_pda` для этого логина реально создана, сохранена в `NVS` и соответствует `device`-адресу устройства, а `tx signature` тоже сохранён. + 13. Открыть `Запросы`, поочерёдно открыть оба демонстрационных запроса и проверить, что кнопки `Разрешить` и `Отклонить` меняют их статус. + 14. При необходимости открыть `Настройки -> Сменить PIN` и убедиться, что новый PIN сохраняется, хотя вход по PIN временно не используется на старте. + 15. Выполнить `Полный сброс` и убедиться, что все поля, секрет, баланс, онлайн и регистрация очищаются. - ожидаемый результат: новый `ESP32`-скетч стабильно запускается, показывает читаемый интерфейс хотя бы в ASCII-транслитерации, сохраняет данные во внутренней памяти устройства, реально подключается к `Wi-Fi`, реально проверяет `API/RPC/WS`, реально читает баланс из `Solana RPC`, рисует рабочий `QR` для `solana:`-URI и позволяет вручную пройти полный сценарий on-chain регистрации homeserver. 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 5919f63..f7575dc 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 @@ -84,7 +84,8 @@ enum Screen { SCREEN_SECRET_GENERATE_RUNNING, SCREEN_SECRET_GENERATE_CANCEL_CONFIRM, SCREEN_TEXT_EDIT, - SCREEN_REGISTER_ACCOUNT_PLACEHOLDER, + SCREEN_REGISTER_ACCOUNT_CONFIRM, + SCREEN_REGISTER_ACCOUNT_RESULT, }; enum SwipeDirection { @@ -123,6 +124,7 @@ enum ActionId { ACTION_BACK_ACCOUNT, ACTION_REFRESH_BALANCE, ACTION_REGISTER_ACCOUNT, + ACTION_REGISTER_ACCOUNT_EXECUTE, ACTION_EDITOR_SAVE, ACTION_EDITOR_CANCEL, }; @@ -246,6 +248,14 @@ static bool gShowRegisterAccountButton = false; static String gUserPdaAddress; static String gRegistrationSignature; static String gShineStatusLine = "SHiNE: account not configured"; +static String gRegisterConfirmMessage; +static String gRegisterConfirmBalanceLine; +static String gRegisterConfirmPdaLine; +static String gRegisterConfirmHomeserverLine; +static bool gRegisterConfirmCanSubmit = false; +static String gRegisterResultMessage; +static String gRegisterResultDetails; +static bool gRegisterResultSuccess = false; static String gShineSessionId; static String gShineSessionKey; static String gShineStoragePwd; @@ -340,6 +350,7 @@ static String shineWsUrl(); static String shineHomeLine(); static String balanceHomeLine(); static uint64_t shineNowMs(); +static bool loadWalletBalanceLamports(uint64_t &lamportsOut, String &messageOut); static void shortVecEncode(size_t value, std::vector &out); static void pushU32LE(std::vector &out, uint32_t value); static void pushU64LE(std::vector &out, uint64_t value); @@ -385,6 +396,7 @@ static bool signMessageEd25519(const std::vector &message, const uint8_ static String encodeTransactionBase64(const uint8_t signature[64], const std::vector &message); static bool awaitTransactionConfirmation(const String &signatureB58, String &messageOut); static bool registerHomeserverOnSolana(String &messageOut); +static void prepareRegisterAccountScreen(); static String buildSessionKeyStringFromPublicBase64(const String &pubB64); static bool deriveKeypairFromSeed32(const uint8_t seed32[32], uint8_t pub32[32], uint8_t sec64[64]); static bool deriveSeedKeypairFromBase58(const String &seedB58, uint8_t seed32[32], uint8_t pub32[32], uint8_t sec64[64]); @@ -846,6 +858,14 @@ static void markAccountStateDirty() { gShowRegisterAccountButton = false; gUserPdaAddress = ""; gRegistrationSignature = ""; + gRegisterConfirmMessage = ""; + gRegisterConfirmBalanceLine = ""; + gRegisterConfirmPdaLine = ""; + gRegisterConfirmHomeserverLine = ""; + gRegisterConfirmCanSubmit = false; + gRegisterResultMessage = ""; + gRegisterResultDetails = ""; + gRegisterResultSuccess = false; clearShineSessionState(true); gShineStatusLine = "SHiNE: account not configured"; } @@ -947,15 +967,26 @@ static String formatSolValue(uint64_t lamports) { } static bool refreshWalletBalance(String &messageOut) { + uint64_t lamports = 0; + if (!loadWalletBalanceLamports(lamports, messageOut)) { + gBalanceStatusMessage = messageOut; + return false; + } + + gBalanceStatusMessage = formatSolValue(lamports); + messageOut = gBalanceStatusMessage; + return true; +} + +static bool loadWalletBalanceLamports(uint64_t &lamportsOut, String &messageOut) { messageOut = ""; + lamportsOut = 0; if (WiFi.status() != WL_CONNECTED) { - gBalanceStatusMessage = "Balance: Wi-Fi disconnected"; - messageOut = gBalanceStatusMessage; + messageOut = "Баланс: Wi-Fi не подключен"; return false; } if (gDevicePubB58.isEmpty()) { - gBalanceStatusMessage = "Balance: secret not set"; - messageOut = gBalanceStatusMessage; + messageOut = "Баланс: секрет не задан"; return false; } @@ -963,20 +994,15 @@ static bool refreshWalletBalance(String &messageOut) { String payload; String req = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getBalance\",\"params\":[\"" + gDevicePubB58 + "\",{\"commitment\":\"confirmed\"}]}"; if (!httpPostJson(gSolanaRpcUrl, req, code, payload)) { - gBalanceStatusMessage = "Balance: failed to load"; - messageOut = gBalanceStatusMessage; + messageOut = "Баланс: RPC не ответил"; return false; } - uint64_t lamports = 0; - if (!jsonInt64Field(payload, "value", lamports)) { - gBalanceStatusMessage = "Balance: failed to load"; - messageOut = gBalanceStatusMessage; + if (!jsonInt64Field(payload, "value", lamportsOut)) { + messageOut = "Баланс: не удалось прочитать"; return false; } - gBalanceStatusMessage = formatSolValue(lamports); - messageOut = gBalanceStatusMessage; return true; } @@ -1525,6 +1551,76 @@ static bool registerHomeserverOnSolana(String &messageOut) { return true; } +static void prepareRegisterAccountScreen() { + gRegisterResultMessage = ""; + gRegisterResultDetails = ""; + gRegisterResultSuccess = false; + gRegisterConfirmMessage = ""; + gRegisterConfirmBalanceLine = ""; + gRegisterConfirmPdaLine = ""; + gRegisterConfirmHomeserverLine = ""; + gRegisterConfirmCanSubmit = false; + + String cleanLogin = normalizeLoginValue(gLoginValue); + if (cleanLogin.isEmpty()) { + gRegisterConfirmMessage = "Логин не задан"; + return; + } + if (!gSecretConfigured) { + gRegisterConfirmMessage = "Секрет не задан"; + return; + } + if (WiFi.status() != WL_CONNECTED) { + gRegisterConfirmMessage = "Wi-Fi не подключен"; + return; + } + + String balanceMessage; + uint64_t lamports = 0; + if (!loadWalletBalanceLamports(lamports, balanceMessage)) { + gRegisterConfirmMessage = balanceMessage; + } else { + gRegisterConfirmBalanceLine = formatSolValue(lamports); + if (lamports < 20000000ULL) { + gRegisterConfirmMessage = "Баланс меньше 0.020 SOL"; + } else { + gRegisterConfirmMessage = "Баланс подходит: 0.020 SOL или выше"; + } + } + + if (gHomeserverValue.isEmpty()) { + gRegisterConfirmHomeserverLine = "Homeserver не задан"; + } else if (gHomeserverValue == "homeserver1") { + gRegisterConfirmHomeserverLine = "Homeserver: homeserver1 (стандартное значение)"; + } else { + gRegisterConfirmHomeserverLine = String("Homeserver: ") + gHomeserverValue; + } + + ShinePdaUserState pdaState; + String pdaError; + if (!readShineUserPda(cleanLogin, pdaState, pdaError)) { + gRegisterConfirmPdaLine = pdaError.isEmpty() ? "PDA: не удалось проверить" : String("PDA: ") + pdaError; + if (gRegisterConfirmMessage.isEmpty()) { + gRegisterConfirmMessage = pdaError.isEmpty() ? "Не удалось проверить PDA" : pdaError; + } + return; + } + + if (pdaState.found) { + gRegisterConfirmPdaLine = "PDA уже занят, этот login не свободен"; + if (gRegisterConfirmMessage.isEmpty()) { + gRegisterConfirmMessage = "Login уже зарегистрирован"; + } + return; + } + + gRegisterConfirmPdaLine = "PDA свободен для регистрации"; + if (gRegisterConfirmMessage.isEmpty()) { + gRegisterConfirmMessage = "Все проверки пройдены"; + } + gRegisterConfirmCanSubmit = true; +} + static bool parseShineUserPdaBytes(const std::vector &bytes, ShinePdaUserState &outState, String &errorOut) { outState = ShinePdaUserState{}; errorOut = ""; @@ -2982,19 +3078,30 @@ static void actionButtonCb(lv_event_t *event) { showScreen(SCREEN_SETTINGS_MENU); break; case ACTION_REGISTER_ACCOUNT: - gAccountStatusMessage = "Регистрация запущена..."; - gShineStatusLine = "SHiNE: регистрация запущена"; - { - String registerMessage; - if (registerHomeserverOnSolana(registerMessage)) { - gAccountStatusMessage = registerMessage; - } else { - gAccountStatusMessage = registerMessage; - gShineStatusLine = String("SHiNE: ") + registerMessage; - } - rebuildScreen(); - } + prepareRegisterAccountScreen(); + showScreen(SCREEN_REGISTER_ACCOUNT_CONFIRM); break; + case ACTION_REGISTER_ACCOUNT_EXECUTE: { + String registerMessage; + if (registerHomeserverOnSolana(registerMessage)) { + gRegisterResultSuccess = true; + gRegisterResultMessage = "Регистрация в Сиянии завершена"; + gRegisterResultDetails = registerMessage; + gAccountStatusMessage = "Регистрация завершена"; + } else { + gRegisterResultSuccess = false; + gRegisterResultMessage = "Регистрация не выполнена"; + gRegisterResultDetails = registerMessage; + gAccountStatusMessage = registerMessage; + } + gRegisterConfirmMessage = ""; + gRegisterConfirmBalanceLine = ""; + gRegisterConfirmPdaLine = ""; + gRegisterConfirmHomeserverLine = ""; + gRegisterConfirmCanSubmit = false; + showScreen(SCREEN_REGISTER_ACCOUNT_RESULT); + break; + } case ACTION_OPEN_WIFI: gWifiViewMode = WIFI_VIEW_OVERVIEW; showScreen(SCREEN_WIFI); @@ -3525,12 +3632,50 @@ static void drawSecretGenerateCancelConfirmScreen() { makeVersionTag(); } -static void drawRegisterAccountPlaceholderScreen() { +static void drawRegisterAccountConfirmScreen() { setRootStyle(); makeTitle("REGISTER ACCOUNT", 22, &lv_font_montserrat_24); - makeBody("Registration now starts directly from the home screen button.", 112, 420); - makeBody("This screen is kept only as a fallback status page.", 156, 420); - makeButton("BACK", 140, 360, 200, 72, 0x5A6570, ACTION_BACK_HOME, &lv_font_montserrat_22); + String topLine = gRegisterConfirmMessage.isEmpty() ? String("Проверка регистрации") : gRegisterConfirmMessage; + makeBody(topLine.c_str(), 96, 420); + if (!gRegisterConfirmPdaLine.isEmpty()) { + makeBody(gRegisterConfirmPdaLine.c_str(), 138, 420); + } + if (!gRegisterConfirmBalanceLine.isEmpty()) { + makeBody(gRegisterConfirmBalanceLine.c_str(), 180, 420); + } + if (!gRegisterConfirmHomeserverLine.isEmpty()) { + makeBody(gRegisterConfirmHomeserverLine.c_str(), 222, 420); + } + if (gRegisterConfirmCanSubmit) { + makeButton("ЗАРЕГИСТРИРОВАТЬ В СИЯНИИ", 22, 296, 436, 74, 0x2A9D8F, ACTION_REGISTER_ACCOUNT_EXECUTE, &lv_font_montserrat_18); + } else { + makeButton("НЕДОСТУПНО", 22, 296, 436, 74, 0x4A5560, ACTION_NONE, &lv_font_montserrat_20); + } + makeButton("BACK", 140, 384, 200, 54, 0x5A6570, ACTION_BACK_HOME, &lv_font_montserrat_20); + makeVersionTag(); +} + +static void drawRegisterAccountResultScreen() { + setRootStyle(); + makeTitle("REGISTER RESULT", 22, &lv_font_montserrat_24); + String resultTopLine = gRegisterResultSuccess ? String("Регистрация завершилась успешно") : String("Регистрация завершилась с ошибкой"); + makeBody(resultTopLine.c_str(), 96, 420); + makeBody(gRegisterResultMessage.c_str(), 140, 420); + if (!gRegisterResultDetails.isEmpty()) { + makeBody(gRegisterResultDetails.c_str(), 184, 420); + } + if (gRegisterResultSuccess) { + if (!gUserPdaAddress.isEmpty()) { + String pdaLine = String("user_pda: ") + abbreviateValue(gUserPdaAddress, 12, 8); + makeBody(pdaLine.c_str(), 228, 420); + } + if (!gRegistrationSignature.isEmpty()) { + String txLine = String("tx: ") + abbreviateValue(gRegistrationSignature, 12, 8); + makeBody(txLine.c_str(), 270, 420); + } + } + makeButton("BACK HOME", 22, 372, 200, 58, 0x5A6570, ACTION_BACK_HOME, &lv_font_montserrat_20); + makeButton("ACCOUNT", 258, 372, 200, 58, 0x2A6F97, ACTION_OPEN_ACCOUNT, &lv_font_montserrat_20); makeVersionTag(); } @@ -3694,8 +3839,11 @@ static void rebuildScreen() { case SCREEN_TEXT_EDIT: drawTextEditScreen(); break; - case SCREEN_REGISTER_ACCOUNT_PLACEHOLDER: - drawRegisterAccountPlaceholderScreen(); + case SCREEN_REGISTER_ACCOUNT_CONFIRM: + drawRegisterAccountConfirmScreen(); + break; + case SCREEN_REGISTER_ACCOUNT_RESULT: + drawRegisterAccountResultScreen(); break; } } @@ -3821,7 +3969,8 @@ static void handleSwipe(SwipeDirection swipe) { case SCREEN_TEXT_EDIT: handleTextEditSwipe(swipe); break; - case SCREEN_REGISTER_ACCOUNT_PLACEHOLDER: + case SCREEN_REGISTER_ACCOUNT_CONFIRM: + case SCREEN_REGISTER_ACCOUNT_RESULT: handleHomeSwipe(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 471ebd8..90e96f9 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 @@ -102,6 +102,8 @@ 13. `PIN_EDIT` 14. `TEXT_INPUT` 15. `CONFIRM` +16. `REGISTER_ACCOUNT_CONFIRM` +17. `REGISTER_ACCOUNT_RESULT` ## Общие правила интерфейса @@ -166,6 +168,52 @@ - вместо призыва к регистрации показывается статус `Homeserver активен`. - две нижние кнопки внизу экрана не прилегают вплотную друг к другу, между ними есть небольшой зазор. +## Экран REGISTER_ACCOUNT_CONFIRM + +Показывает предварительную проверку перед отправкой транзакции регистрации. + +Отображается: + +- повторный `login`; +- сообщение о том, что логин свободен или уже занят; +- баланс кошелька с проверкой порога `0.020 SOL`; +- имя homeserver; +- пометка, если используется стандартное значение `homeserver1`; +- отдельное предупреждение, если `Wi-Fi` не подключён. + +Кнопки: + +- `ЗАРЕГИСТРИРОВАТЬ В СИЯНИИ` +- `BACK` + +Поведение: + +- если `Wi-Fi` не подключён, на экране прямо показывается уведомление об этом и кнопка регистрации недоступна; +- если `login` уже занят в `shine_users`, регистрация недоступна; +- если баланс меньше `0.020 SOL`, на экране показывается соответствующая ошибка; +- если все проверки пройдены, кнопка регистрации активна и запускает on-chain регистрацию. + +## Экран REGISTER_ACCOUNT_RESULT + +Показывает результат отправки регистрации. + +Отображается: + +- текст успеха или ошибки; +- подробное сообщение; +- при успехе краткий `user_pda`; +- при успехе краткий `tx signature`. + +Кнопки: + +- `BACK HOME` +- `ACCOUNT` + +Поведение: + +- после успешной регистрации данные `user_pda` и `tx signature` сохраняются в `NVS`; +- при ошибке на экране показывается причина отказа. + ## Экран STATUS Показывает сводку: @@ -441,7 +489,10 @@ QR должен быть сканируемым, а не декоративны 11. при необходимости пополнить баланс; 12. вернуться на `HOME`; 13. нажать `REGISTER ACCOUNT`; -14. после завершения увидеть статус `Homeserver активен`. +14. на экране проверки ещё раз увидеть `login`, статус свободного `PDA`, баланс, `homeserver1` и при необходимости сообщение о неподключённом `Wi-Fi`; +15. нажать `ЗАРЕГИСТРИРОВАТЬ В СИЯНИИ`; +16. после завершения увидеть либо экран успеха с `user_pda` и `tx signature`, либо подробную ошибку; +17. после успешной регистрации увидеть статус `Homeserver активен`. Примечание: diff --git a/VERSION.properties b/VERSION.properties index 0f2cf43..cca767c 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.164 -server.version=1.2.153 +client.version=1.2.165 +server.version=1.2.154