ESP32: экран подтверждения регистрации

This commit is contained in:
AidarKC 2026-06-12 23:02:07 +04:00
parent b83543d018
commit 0c9afea67a
4 changed files with 241 additions and 40 deletions

View File

@ -13,11 +13,12 @@
7. Открыть `Кошелёк`, нажать `Проверить` и убедиться, что баланс реально читается из `Solana RPC`; затем открыть `QR и URI` и проверить, что QR-код отрисовывается и сканируется как `solana:`-ссылка. 7. Открыть `Кошелёк`, нажать `Проверить` и убедиться, что баланс реально читается из `Solana RPC`; затем открыть `QR и URI` и проверить, что QR-код отрисовывается и сканируется как `solana:`-ссылка.
8. При необходимости отдельно проверить тестовые кнопки `+/- SOL`: они меняют локальный баланс для UX-сценариев, но после следующей реальной RPC-проверки баланс должен вернуться к сетевому значению. 8. При необходимости отдельно проверить тестовые кнопки `+/- SOL`: они меняют локальный баланс для UX-сценариев, но после следующей реальной RPC-проверки баланс должен вернуться к сетевому значению.
9. Вернуться на главный экран и проверить, что до выполнения всех условий кнопка регистрации недоступна, а после выполнения становится доступной; также убедиться, что между двумя нижними кнопками есть небольшой зазор. 9. Вернуться на главный экран и проверить, что до выполнения всех условий кнопка регистрации недоступна, а после выполнения становится доступной; также убедиться, что между двумя нижними кнопками есть небольшой зазор.
10. Нажать кнопку регистрации и убедиться, что она запускает on-chain flow сразу без отдельного confirm-экрана, а после завершения статус меняется на `Homeserver активен`, онлайн-статус становится активным, и на экране аккаунта появляются краткие отпечатки `PDA/TX`. 10. Нажать кнопку регистрации и убедиться, что открывается отдельный экран проверки, где ещё раз видно `login`, статус свободного `PDA`, баланс, `homeserver1` с пометкой о стандартном значении и сообщение, если `Wi-Fi` не подключён.
11. После регистрации проверить через `Solana`/UI проекта, что `user_pda` для этого логина реально создана, сохранена в `NVS` и соответствует `device`-адресу устройства. 11. На экране проверки нажать `ЗАРЕГИСТРИРОВАТЬ В СИЯНИИ` и убедиться, что после этого появляется отдельный экран результата с успехом либо подробной ошибкой.
12. Открыть `Запросы`, поочерёдно открыть оба демонстрационных запроса и проверить, что кнопки `Разрешить` и `Отклонить` меняют их статус. 12. После успешной регистрации проверить через `Solana`/UI проекта, что `user_pda` для этого логина реально создана, сохранена в `NVS` и соответствует `device`-адресу устройства, а `tx signature` тоже сохранён.
13. При необходимости открыть `Настройки -> Сменить PIN` и убедиться, что новый PIN сохраняется, хотя вход по PIN временно не используется на старте. 13. Открыть `Запросы`, поочерёдно открыть оба демонстрационных запроса и проверить, что кнопки `Разрешить` и `Отклонить` меняют их статус.
14. Выполнить `Полный сброс` и убедиться, что все поля, секрет, баланс, онлайн и регистрация очищаются. 14. При необходимости открыть `Настройки -> Сменить PIN` и убедиться, что новый PIN сохраняется, хотя вход по PIN временно не используется на старте.
15. Выполнить `Полный сброс` и убедиться, что все поля, секрет, баланс, онлайн и регистрация очищаются.
- ожидаемый результат: - ожидаемый результат:
новый `ESP32`-скетч стабильно запускается, показывает читаемый интерфейс хотя бы в ASCII-транслитерации, сохраняет данные во внутренней памяти устройства, реально подключается к `Wi-Fi`, реально проверяет `API/RPC/WS`, реально читает баланс из `Solana RPC`, рисует рабочий `QR` для `solana:`-URI и позволяет вручную пройти полный сценарий on-chain регистрации homeserver. новый `ESP32`-скетч стабильно запускается, показывает читаемый интерфейс хотя бы в ASCII-транслитерации, сохраняет данные во внутренней памяти устройства, реально подключается к `Wi-Fi`, реально проверяет `API/RPC/WS`, реально читает баланс из `Solana RPC`, рисует рабочий `QR` для `solana:`-URI и позволяет вручную пройти полный сценарий on-chain регистрации homeserver.

View File

@ -84,7 +84,8 @@ enum Screen {
SCREEN_SECRET_GENERATE_RUNNING, SCREEN_SECRET_GENERATE_RUNNING,
SCREEN_SECRET_GENERATE_CANCEL_CONFIRM, SCREEN_SECRET_GENERATE_CANCEL_CONFIRM,
SCREEN_TEXT_EDIT, SCREEN_TEXT_EDIT,
SCREEN_REGISTER_ACCOUNT_PLACEHOLDER, SCREEN_REGISTER_ACCOUNT_CONFIRM,
SCREEN_REGISTER_ACCOUNT_RESULT,
}; };
enum SwipeDirection { enum SwipeDirection {
@ -123,6 +124,7 @@ enum ActionId {
ACTION_BACK_ACCOUNT, ACTION_BACK_ACCOUNT,
ACTION_REFRESH_BALANCE, ACTION_REFRESH_BALANCE,
ACTION_REGISTER_ACCOUNT, ACTION_REGISTER_ACCOUNT,
ACTION_REGISTER_ACCOUNT_EXECUTE,
ACTION_EDITOR_SAVE, ACTION_EDITOR_SAVE,
ACTION_EDITOR_CANCEL, ACTION_EDITOR_CANCEL,
}; };
@ -246,6 +248,14 @@ static bool gShowRegisterAccountButton = false;
static String gUserPdaAddress; static String gUserPdaAddress;
static String gRegistrationSignature; static String gRegistrationSignature;
static String gShineStatusLine = "SHiNE: account not configured"; 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 gShineSessionId;
static String gShineSessionKey; static String gShineSessionKey;
static String gShineStoragePwd; static String gShineStoragePwd;
@ -340,6 +350,7 @@ static String shineWsUrl();
static String shineHomeLine(); static String shineHomeLine();
static String balanceHomeLine(); static String balanceHomeLine();
static uint64_t shineNowMs(); static uint64_t shineNowMs();
static bool loadWalletBalanceLamports(uint64_t &lamportsOut, String &messageOut);
static void shortVecEncode(size_t value, std::vector<uint8_t> &out); static void shortVecEncode(size_t value, std::vector<uint8_t> &out);
static void pushU32LE(std::vector<uint8_t> &out, uint32_t value); static void pushU32LE(std::vector<uint8_t> &out, uint32_t value);
static void pushU64LE(std::vector<uint8_t> &out, uint64_t value); static void pushU64LE(std::vector<uint8_t> &out, uint64_t value);
@ -385,6 +396,7 @@ static bool signMessageEd25519(const std::vector<uint8_t> &message, const uint8_
static String encodeTransactionBase64(const uint8_t signature[64], const std::vector<uint8_t> &message); static String encodeTransactionBase64(const uint8_t signature[64], const std::vector<uint8_t> &message);
static bool awaitTransactionConfirmation(const String &signatureB58, String &messageOut); static bool awaitTransactionConfirmation(const String &signatureB58, String &messageOut);
static bool registerHomeserverOnSolana(String &messageOut); static bool registerHomeserverOnSolana(String &messageOut);
static void prepareRegisterAccountScreen();
static String buildSessionKeyStringFromPublicBase64(const String &pubB64); static String buildSessionKeyStringFromPublicBase64(const String &pubB64);
static bool deriveKeypairFromSeed32(const uint8_t seed32[32], uint8_t pub32[32], uint8_t sec64[64]); 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]); 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; gShowRegisterAccountButton = false;
gUserPdaAddress = ""; gUserPdaAddress = "";
gRegistrationSignature = ""; gRegistrationSignature = "";
gRegisterConfirmMessage = "";
gRegisterConfirmBalanceLine = "";
gRegisterConfirmPdaLine = "";
gRegisterConfirmHomeserverLine = "";
gRegisterConfirmCanSubmit = false;
gRegisterResultMessage = "";
gRegisterResultDetails = "";
gRegisterResultSuccess = false;
clearShineSessionState(true); clearShineSessionState(true);
gShineStatusLine = "SHiNE: account not configured"; gShineStatusLine = "SHiNE: account not configured";
} }
@ -947,15 +967,26 @@ static String formatSolValue(uint64_t lamports) {
} }
static bool refreshWalletBalance(String &messageOut) { 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 = ""; messageOut = "";
lamportsOut = 0;
if (WiFi.status() != WL_CONNECTED) { if (WiFi.status() != WL_CONNECTED) {
gBalanceStatusMessage = "Balance: Wi-Fi disconnected"; messageOut = "Баланс: Wi-Fi не подключен";
messageOut = gBalanceStatusMessage;
return false; return false;
} }
if (gDevicePubB58.isEmpty()) { if (gDevicePubB58.isEmpty()) {
gBalanceStatusMessage = "Balance: secret not set"; messageOut = "Баланс: секрет не задан";
messageOut = gBalanceStatusMessage;
return false; return false;
} }
@ -963,20 +994,15 @@ static bool refreshWalletBalance(String &messageOut) {
String payload; String payload;
String req = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getBalance\",\"params\":[\"" + gDevicePubB58 + "\",{\"commitment\":\"confirmed\"}]}"; String req = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getBalance\",\"params\":[\"" + gDevicePubB58 + "\",{\"commitment\":\"confirmed\"}]}";
if (!httpPostJson(gSolanaRpcUrl, req, code, payload)) { if (!httpPostJson(gSolanaRpcUrl, req, code, payload)) {
gBalanceStatusMessage = "Balance: failed to load"; messageOut = "Баланс: RPC не ответил";
messageOut = gBalanceStatusMessage;
return false; return false;
} }
uint64_t lamports = 0; if (!jsonInt64Field(payload, "value", lamportsOut)) {
if (!jsonInt64Field(payload, "value", lamports)) { messageOut = "Баланс: не удалось прочитать";
gBalanceStatusMessage = "Balance: failed to load";
messageOut = gBalanceStatusMessage;
return false; return false;
} }
gBalanceStatusMessage = formatSolValue(lamports);
messageOut = gBalanceStatusMessage;
return true; return true;
} }
@ -1525,6 +1551,76 @@ static bool registerHomeserverOnSolana(String &messageOut) {
return true; 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<uint8_t> &bytes, ShinePdaUserState &outState, String &errorOut) { static bool parseShineUserPdaBytes(const std::vector<uint8_t> &bytes, ShinePdaUserState &outState, String &errorOut) {
outState = ShinePdaUserState{}; outState = ShinePdaUserState{};
errorOut = ""; errorOut = "";
@ -2982,19 +3078,30 @@ static void actionButtonCb(lv_event_t *event) {
showScreen(SCREEN_SETTINGS_MENU); showScreen(SCREEN_SETTINGS_MENU);
break; break;
case ACTION_REGISTER_ACCOUNT: case ACTION_REGISTER_ACCOUNT:
gAccountStatusMessage = "Регистрация запущена..."; prepareRegisterAccountScreen();
gShineStatusLine = "SHiNE: регистрация запущена"; showScreen(SCREEN_REGISTER_ACCOUNT_CONFIRM);
{
String registerMessage;
if (registerHomeserverOnSolana(registerMessage)) {
gAccountStatusMessage = registerMessage;
} else {
gAccountStatusMessage = registerMessage;
gShineStatusLine = String("SHiNE: ") + registerMessage;
}
rebuildScreen();
}
break; 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: case ACTION_OPEN_WIFI:
gWifiViewMode = WIFI_VIEW_OVERVIEW; gWifiViewMode = WIFI_VIEW_OVERVIEW;
showScreen(SCREEN_WIFI); showScreen(SCREEN_WIFI);
@ -3525,12 +3632,50 @@ static void drawSecretGenerateCancelConfirmScreen() {
makeVersionTag(); makeVersionTag();
} }
static void drawRegisterAccountPlaceholderScreen() { static void drawRegisterAccountConfirmScreen() {
setRootStyle(); setRootStyle();
makeTitle("REGISTER ACCOUNT", 22, &lv_font_montserrat_24); makeTitle("REGISTER ACCOUNT", 22, &lv_font_montserrat_24);
makeBody("Registration now starts directly from the home screen button.", 112, 420); String topLine = gRegisterConfirmMessage.isEmpty() ? String("Проверка регистрации") : gRegisterConfirmMessage;
makeBody("This screen is kept only as a fallback status page.", 156, 420); makeBody(topLine.c_str(), 96, 420);
makeButton("BACK", 140, 360, 200, 72, 0x5A6570, ACTION_BACK_HOME, &lv_font_montserrat_22); 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(); makeVersionTag();
} }
@ -3694,8 +3839,11 @@ static void rebuildScreen() {
case SCREEN_TEXT_EDIT: case SCREEN_TEXT_EDIT:
drawTextEditScreen(); drawTextEditScreen();
break; break;
case SCREEN_REGISTER_ACCOUNT_PLACEHOLDER: case SCREEN_REGISTER_ACCOUNT_CONFIRM:
drawRegisterAccountPlaceholderScreen(); drawRegisterAccountConfirmScreen();
break;
case SCREEN_REGISTER_ACCOUNT_RESULT:
drawRegisterAccountResultScreen();
break; break;
} }
} }
@ -3821,7 +3969,8 @@ static void handleSwipe(SwipeDirection swipe) {
case SCREEN_TEXT_EDIT: case SCREEN_TEXT_EDIT:
handleTextEditSwipe(swipe); handleTextEditSwipe(swipe);
break; break;
case SCREEN_REGISTER_ACCOUNT_PLACEHOLDER: case SCREEN_REGISTER_ACCOUNT_CONFIRM:
case SCREEN_REGISTER_ACCOUNT_RESULT:
handleHomeSwipe(swipe); handleHomeSwipe(swipe);
break; break;
} }

View File

@ -102,6 +102,8 @@
13. `PIN_EDIT` 13. `PIN_EDIT`
14. `TEXT_INPUT` 14. `TEXT_INPUT`
15. `CONFIRM` 15. `CONFIRM`
16. `REGISTER_ACCOUNT_CONFIRM`
17. `REGISTER_ACCOUNT_RESULT`
## Общие правила интерфейса ## Общие правила интерфейса
@ -166,6 +168,52 @@
- вместо призыва к регистрации показывается статус `Homeserver активен`. - вместо призыва к регистрации показывается статус `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 ## Экран STATUS
Показывает сводку: Показывает сводку:
@ -441,7 +489,10 @@ QR должен быть сканируемым, а не декоративны
11. при необходимости пополнить баланс; 11. при необходимости пополнить баланс;
12. вернуться на `HOME`; 12. вернуться на `HOME`;
13. нажать `REGISTER ACCOUNT`; 13. нажать `REGISTER ACCOUNT`;
14. после завершения увидеть статус `Homeserver активен`. 14. на экране проверки ещё раз увидеть `login`, статус свободного `PDA`, баланс, `homeserver1` и при необходимости сообщение о неподключённом `Wi-Fi`;
15. нажать `ЗАРЕГИСТРИРОВАТЬ В СИЯНИИ`;
16. после завершения увидеть либо экран успеха с `user_pda` и `tx signature`, либо подробную ошибку;
17. после успешной регистрации увидеть статус `Homeserver активен`.
Примечание: Примечание:

View File

@ -1,2 +1,2 @@
client.version=1.2.164 client.version=1.2.165
server.version=1.2.153 server.version=1.2.154