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:`-ссылка.
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.

View File

@ -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<uint8_t> &out);
static void pushU32LE(std::vector<uint8_t> &out, uint32_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 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<uint8_t> &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;
}

View File

@ -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 активен`.
Примечание:

View File

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