ESP32: добавить баланс и экран ключей

This commit is contained in:
AidarKC 2026-06-10 14:19:00 +04:00
parent f095182673
commit 9e6ff07136
4 changed files with 297 additions and 22 deletions

View File

@ -3,7 +3,7 @@
- Краткое описание: минимальный UI-прототип для сабсервера на базе `LVGL + subserver touch`, с Wi-Fi flow, серверными адресами и общим экраном редактирования текста. - Краткое описание: минимальный UI-прототип для сабсервера на базе `LVGL + subserver touch`, с Wi-Fi flow, серверными адресами и общим экраном редактирования текста.
- Что проверять: - Что проверять:
- стартует экран `HOME`; - стартует экран `HOME`;
- на `HOME` видны реальное значение сабсервера или `subserver not set`, реальное значение логина или `login not set`, при отсутствии секрета строка `secret not set`, а также `STATUS`, верхний правый блок с процентом батареи, иконкой батареи и индикатором Wi-Fi, кнопка `SETTINGS` и нижняя подпись `SHiNE subserver (v.0.18)`; - на `HOME` видны реальное значение сабсервера или `subserver not set`, реальное значение логина или `login not set`, при отсутствии секрета строка `secret not set`, а также `STATUS`, верхний правый блок с процентом батареи, иконкой батареи и индикатором Wi-Fi, кнопка баланса, кнопка `SETTINGS` уменьшенной ширины у правого края и нижняя подпись `SHiNE subserver (v.0.18)`;
- строка Wi-Fi на `HOME` корректно показывает одно из состояний: - строка Wi-Fi на `HOME` корректно показывает одно из состояний:
- `Wi-Fi (not configured) not configured` - `Wi-Fi (not configured) not configured`
- `Wi-Fi (<saved_ssid>) disconnected` - `Wi-Fi (<saved_ssid>) disconnected`
@ -64,9 +64,18 @@
- `Subserver` открывает промежуточный экран с `USE SUBSERVER1` и `EDIT MANUALLY`; - `Subserver` открывает промежуточный экран с `USE SUBSERVER1` и `EDIT MANUALLY`;
- `USE SUBSERVER1` возвращает стандартное значение `subserver1`; - `USE SUBSERVER1` возвращает стандартное значение `subserver1`;
- `EDIT MANUALLY` открывает общий экран редактирования и сохраняет значение в NVS; - `EDIT MANUALLY` открывает общий экран редактирования и сохраняет значение в NVS;
- `Secret` открывает экран-заглушку, где сказано, что настройка ещё не реализована;
- `Secret` теперь открывает меню секрета с показом секрета, ручным вводом и генерацией; - `Secret` теперь открывает меню секрета с показом секрета, ручным вводом и генерацией;
- в `SHOW SECRET` сам секрет показывается увеличенным шрифтом и разбит по `10` символов в строке; - в `SHOW SECRET` показывается прокручиваемый список всех ключей:
- `Secret (base58)`
- `Root key (base58)`
- `Root key priv (base58)`
- `Blockchain key (base58)`
- `Blockchain key priv (base58)`
- `Device key (base58)`
- `Device key priv (base58)`
- `Subserver key (base58)`
- `Subserver key priv (base58)`
- значения ключей показываются полными строками увеличенным шрифтом;
- при смене `login` сохранённый секрет сбрасывается в `not set`; - при смене `login` сохранённый секрет сбрасывается в `not set`;
- во время генерации секрета есть `CANCEL` и подтверждение остановки; - во время генерации секрета есть `CANCEL` и подтверждение остановки;
- при отмене генерации старый секрет, если он был, не должен теряться; - при отмене генерации старый секрет, если он был, не должен теряться;

View File

@ -41,7 +41,8 @@
- индикатор Wi-Fi уровня сигнала; - индикатор Wi-Fi уровня сигнала;
- по центру крупный текст `STATUS`; - по центру крупный текст `STATUS`;
- одна строка Wi-Fi вида `Wi-Fi (<ssid>) connected/disconnected`; - одна строка Wi-Fi вида `Wi-Fi (<ssid>) connected/disconnected`;
- снизу большую кнопку `SETTINGS`. - кнопка баланса вида `Balance: <value SOL>` или `Balance: failed to load`, по нажатию выполняет повторный запрос;
- снизу кнопку `SETTINGS`, уменьшенную примерно до половины ширины экрана и сдвинутую к правому краю.
- внизу на тёмной полосе подпись `SHiNE subserver (v.0.18)`. - внизу на тёмной полосе подпись `SHiNE subserver (v.0.18)`.
Строка Wi-Fi на `HOME`: Строка Wi-Fi на `HOME`:
@ -184,10 +185,18 @@
Показывает: Показывает:
- заголовок `SECRET`; - заголовок `SECRET`;
- подпись `Current secret in base58:`; - вертикально прокручиваемый список ключей;
- полный секрет открытым текстом; - `Secret (base58)`;
- увеличенный шрифт для значения секрета; - `Root key (base58)`;
- разбиение секрета по строкам по `10` символов; - `Root key priv (base58)`;
- `Blockchain key (base58)`;
- `Blockchain key priv (base58)`;
- `Device key (base58)`;
- `Device key priv (base58)`;
- `Subserver key (base58)`;
- `Subserver key priv (base58)`;
- для каждого поля показывается формула derivation;
- значения ключей показываются полными строками увеличенным шрифтом;
- кнопку `BACK`. - кнопку `BACK`.
## TEXT_EDIT_SCREEN ## TEXT_EDIT_SCREEN

View File

@ -1,10 +1,15 @@
#include <Arduino.h> #include <Arduino.h>
#include <Wire.h> #include <Wire.h>
#include <WiFi.h> #include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <Preferences.h> #include <Preferences.h>
#include <lvgl.h> #include <lvgl.h>
#include <Arduino_GFX_Library.h> #include <Arduino_GFX_Library.h>
#include <TouchDrvCSTXXX.hpp> #include <TouchDrvCSTXXX.hpp>
#include <mbedtls/sha256.h>
#include <mbedtls/base64.h>
#include <Ed25519.h>
#define XPOWERS_CHIP_AXP2101 #define XPOWERS_CHIP_AXP2101
#include "XPowersLib.h" #include "XPowersLib.h"
#include "shine_secret_generation.h" #include "shine_secret_generation.h"
@ -87,6 +92,7 @@ enum ActionId {
ACTION_SECRET_GENERATE_CANCEL_NO, ACTION_SECRET_GENERATE_CANCEL_NO,
ACTION_BACK_SECRET_MENU, ACTION_BACK_SECRET_MENU,
ACTION_BACK_ACCOUNT, ACTION_BACK_ACCOUNT,
ACTION_REFRESH_BALANCE,
ACTION_EDITOR_SAVE, ACTION_EDITOR_SAVE,
ACTION_EDITOR_CANCEL, ACTION_EDITOR_CANCEL,
}; };
@ -161,6 +167,7 @@ static bool gSecretConfigured = false;
static String gSecretBase58; static String gSecretBase58;
static uint8_t gSecretBytes[32] = {}; static uint8_t gSecretBytes[32] = {};
static String gAccountStatusMessage = "Edit account fields"; static String gAccountStatusMessage = "Edit account fields";
static String gBalanceStatusMessage = "Balance: tap to load";
static bool gWifiKnownGood = false; static bool gWifiKnownGood = false;
static bool gWifiReconnectEnabled = false; static bool gWifiReconnectEnabled = false;
static bool gWifiOperationBusy = false; static bool gWifiOperationBusy = false;
@ -169,6 +176,21 @@ static unsigned long gWifiLastReconnectAttemptMs = 0;
static wl_status_t gLastWifiStatus = WL_IDLE_STATUS; static wl_status_t gLastWifiStatus = WL_IDLE_STATUS;
static bool gPowerReady = false; static bool gPowerReady = false;
struct DerivedKeyInfo {
String title;
String formula;
String value;
};
static String gRootPubB58;
static String gRootPrivB58;
static String gBlockchainPubB58;
static String gBlockchainPrivB58;
static String gDevicePubB58;
static String gDevicePrivB58;
static String gSubserverPubB58;
static String gSubserverPrivB58;
static EditContext gEditContext = EDIT_CONTEXT_NONE; static EditContext gEditContext = EDIT_CONTEXT_NONE;
static Screen gEditReturnScreen = SCREEN_HOME; static Screen gEditReturnScreen = SCREEN_HOME;
static String gEditTitle; static String gEditTitle;
@ -189,6 +211,7 @@ static void clearSavedWifiList();
static int findKnownWifiIndex(const String &ssid); static int findKnownWifiIndex(const String &ssid);
static String savedPasswordForSsid(const String &ssid); static String savedPasswordForSsid(const String &ssid);
static void upsertKnownWifi(const String &ssid, const String &password); static void upsertKnownWifi(const String &ssid, const String &password);
static bool refreshWalletBalance(String &messageOut);
static void saveServerPrefs(); static void saveServerPrefs();
static void saveAccountPrefs(); static void saveAccountPrefs();
static void beginSavedWifi(); static void beginSavedWifi();
@ -210,6 +233,8 @@ static bool isTextEditKeyboardSwipeArea(int16_t x, int16_t y);
static void syncEditValueFromTextarea(); static void syncEditValueFromTextarea();
static void keepCursorAtEnd(); static void keepCursorAtEnd();
static void restoreTextareaFromEditValue(); static void restoreTextareaFromEditValue();
static void refreshDerivedKeys();
static void clearDerivedKeys();
static String loginDisplayValue(); static String loginDisplayValue();
static String subserverDisplayValue(); static String subserverDisplayValue();
static String homeSecretStatus(); static String homeSecretStatus();
@ -380,6 +405,182 @@ static String splitFixedWidth(const String &value, size_t chunkSize) {
return out; return out;
} }
static void sha256calc(const uint8_t *in, size_t len, uint8_t *out32) {
mbedtls_sha256_context ctx;
mbedtls_sha256_init(&ctx);
mbedtls_sha256_starts(&ctx, 0);
mbedtls_sha256_update(&ctx, in, len);
mbedtls_sha256_finish(&ctx, out32);
mbedtls_sha256_free(&ctx);
}
static String base64Std(const uint8_t *data, size_t len) {
char out[96] = {};
size_t outLen = 0;
if (mbedtls_base64_encode(reinterpret_cast<uint8_t *>(out), sizeof(out), &outLen, data, len) != 0) {
return "";
}
out[min(outLen, sizeof(out) - 1)] = '\0';
return String(out);
}
static String base58From32(const uint8_t *data32) {
char out[64] = {};
shineSecretBase58Encode(data32, 32, out, sizeof(out));
return String(out);
}
static String subserverKeySuffix() {
String name = gSubserverValue;
name.trim();
if (name.isEmpty()) {
name = "subserver1";
}
return String("subserver.key:") + name;
}
static void deriveKeyPairFromSecretSuffix(const uint8_t *secret32, const String &suffix, String &pubB58, String &privB58) {
pubB58 = "";
privB58 = "";
if (!secret32) {
return;
}
String material = base64Std(secret32, 32) + "|" + suffix;
uint8_t seed[32] = {};
uint8_t pub[32] = {};
sha256calc(reinterpret_cast<const uint8_t *>(material.c_str()), material.length(), seed);
Ed25519::derivePublicKey(pub, seed);
privB58 = base58From32(seed);
pubB58 = base58From32(pub);
}
static void clearDerivedKeys() {
gRootPubB58 = "";
gRootPrivB58 = "";
gBlockchainPubB58 = "";
gBlockchainPrivB58 = "";
gDevicePubB58 = "";
gDevicePrivB58 = "";
gSubserverPubB58 = "";
gSubserverPrivB58 = "";
}
static void refreshDerivedKeys() {
clearDerivedKeys();
if (!gSecretConfigured) {
return;
}
deriveKeyPairFromSecretSuffix(gSecretBytes, "root.key", gRootPubB58, gRootPrivB58);
deriveKeyPairFromSecretSuffix(gSecretBytes, "bch.key", gBlockchainPubB58, gBlockchainPrivB58);
deriveKeyPairFromSecretSuffix(gSecretBytes, "dev.key", gDevicePubB58, gDevicePrivB58);
deriveKeyPairFromSecretSuffix(gSecretBytes, subserverKeySuffix(), gSubserverPubB58, gSubserverPrivB58);
}
static bool isHttpUrl(const String &url) {
return url.startsWith("http://") || url.startsWith("https://");
}
static bool httpPostJson(const String &url, const String &body, int &statusCode, String &payload) {
statusCode = -1;
payload = "";
if (!isHttpUrl(url)) {
return false;
}
HTTPClient http;
WiFiClientSecure secureClient;
WiFiClient plainClient;
bool secure = url.startsWith("https://");
if (secure) {
secureClient.setInsecure();
if (!http.begin(secureClient, url)) {
return false;
}
} else {
if (!http.begin(plainClient, url)) {
return false;
}
}
http.setTimeout(7000);
http.addHeader("Content-Type", "application/json");
statusCode = http.POST(body);
if (statusCode > 0) {
payload = http.getString();
}
http.end();
return statusCode > 0;
}
static bool jsonInt64Field(const String &json, const String &field, uint64_t &valueOut) {
String needle = "\"" + field + "\"";
int keyPos = json.indexOf(needle);
if (keyPos < 0) {
return false;
}
int colon = json.indexOf(':', keyPos + needle.length());
if (colon < 0) {
return false;
}
int pos = colon + 1;
while (pos < (int)json.length() && (json[pos] == ' ' || json[pos] == '\n' || json[pos] == '\r' || json[pos] == '\t')) {
pos++;
}
int start = pos;
while (pos < (int)json.length() && isDigit((unsigned char)json[pos])) {
pos++;
}
if (pos == start) {
return false;
}
valueOut = strtoull(json.substring(start, pos).c_str(), nullptr, 10);
return true;
}
static String formatSolValue(uint64_t lamports) {
uint64_t whole = lamports / 1000000000ULL;
uint64_t frac = (lamports % 1000000000ULL) / 1000000ULL;
char out[48];
snprintf(out, sizeof(out), "Balance: %llu.%03llu SOL",
(unsigned long long)whole,
(unsigned long long)frac);
return String(out);
}
static bool refreshWalletBalance(String &messageOut) {
messageOut = "";
if (WiFi.status() != WL_CONNECTED) {
gBalanceStatusMessage = "Balance: Wi-Fi disconnected";
messageOut = gBalanceStatusMessage;
return false;
}
if (gDevicePubB58.isEmpty()) {
gBalanceStatusMessage = "Balance: secret not set";
messageOut = gBalanceStatusMessage;
return false;
}
int code = -1;
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;
return false;
}
uint64_t lamports = 0;
if (!jsonInt64Field(payload, "value", lamports)) {
gBalanceStatusMessage = "Balance: failed to load";
messageOut = gBalanceStatusMessage;
return false;
}
gBalanceStatusMessage = formatSolValue(lamports);
messageOut = gBalanceStatusMessage;
return true;
}
static void upsertKnownWifi(const String &ssid, const String &password) { static void upsertKnownWifi(const String &ssid, const String &password) {
if (ssid.isEmpty()) { if (ssid.isEmpty()) {
return; return;
@ -435,6 +636,8 @@ static void loadPrefs() {
gSecretBase58 = ""; gSecretBase58 = "";
} }
} }
refreshDerivedKeys();
gBalanceStatusMessage = gDevicePubB58.isEmpty() ? "Balance: secret not set" : "Balance: tap to load";
} }
static void saveWifiPrefs() { static void saveWifiPrefs() {
@ -647,6 +850,8 @@ static void clearSecretValue() {
gSecretConfigured = false; gSecretConfigured = false;
gSecretBase58 = ""; gSecretBase58 = "";
memset(gSecretBytes, 0, sizeof(gSecretBytes)); memset(gSecretBytes, 0, sizeof(gSecretBytes));
refreshDerivedKeys();
gBalanceStatusMessage = "Balance: secret not set";
saveAccountPrefs(); saveAccountPrefs();
} }
@ -656,6 +861,8 @@ static void setSecretValue(const uint8_t *bytes32) {
shineSecretBase58Encode(bytes32, 32, b58, sizeof(b58)); shineSecretBase58Encode(bytes32, 32, b58, sizeof(b58));
gSecretBase58 = b58; gSecretBase58 = b58;
gSecretConfigured = true; gSecretConfigured = true;
refreshDerivedKeys();
gBalanceStatusMessage = "Balance: tap to load";
saveAccountPrefs(); saveAccountPrefs();
} }
@ -837,6 +1044,7 @@ static void applyEditorValue() {
if (gEditContext == EDIT_CONTEXT_SUBSERVER) { if (gEditContext == EDIT_CONTEXT_SUBSERVER) {
value.trim(); value.trim();
gSubserverValue = value; gSubserverValue = value;
refreshDerivedKeys();
saveAccountPrefs(); saveAccountPrefs();
gAccountStatusMessage = gSubserverValue.isEmpty() ? "Subserver cleared" : "Subserver saved"; gAccountStatusMessage = gSubserverValue.isEmpty() ? "Subserver cleared" : "Subserver saved";
showScreen(SCREEN_ACCOUNT); showScreen(SCREEN_ACCOUNT);
@ -1103,6 +1311,7 @@ static void actionButtonCb(lv_event_t *event) {
break; break;
case ACTION_ACCOUNT_SUBSERVER_USE_DEFAULT: case ACTION_ACCOUNT_SUBSERVER_USE_DEFAULT:
gSubserverValue = "subserver1"; gSubserverValue = "subserver1";
refreshDerivedKeys();
saveAccountPrefs(); saveAccountPrefs();
gAccountStatusMessage = "Subserver set to subserver1"; gAccountStatusMessage = "Subserver set to subserver1";
showScreen(SCREEN_ACCOUNT); showScreen(SCREEN_ACCOUNT);
@ -1118,6 +1327,17 @@ static void actionButtonCb(lv_event_t *event) {
case ACTION_BACK_SECRET_MENU: case ACTION_BACK_SECRET_MENU:
showScreen(SCREEN_ACCOUNT_SECRET); showScreen(SCREEN_ACCOUNT_SECRET);
break; break;
case ACTION_REFRESH_BALANCE: {
String message;
gBalanceStatusMessage = "Balance: loading...";
rebuildScreen();
lv_timer_handler();
refreshWalletBalance(message);
if (gCurrentScreen == SCREEN_HOME) {
rebuildScreen();
}
break;
}
case ACTION_EDITOR_SAVE: case ACTION_EDITOR_SAVE:
applyEditorValue(); applyEditorValue();
break; break;
@ -1221,8 +1441,9 @@ static void drawHome() {
drawTopStatusIndicators(); drawTopStatusIndicators();
makeTitle("STATUS", 150, &lv_font_montserrat_28); makeTitle("STATUS", 150, &lv_font_montserrat_28);
showMessageAt(wifiHomeSummary(), 214); showMessageAt(wifiHomeSummary(), 214);
makeBody("Swipe left or tap the button below.", 274, 360); makeButton(gBalanceStatusMessage.c_str(), 22, 254, 436, 58, 0x355C7D, ACTION_REFRESH_BALANCE, &lv_font_montserrat_18);
makeButton("SETTINGS", 22, 360, 436, 78, 0x2A6F97, ACTION_OPEN_SETTINGS, &lv_font_montserrat_24); makeBody("Swipe left or tap the button below.", 328, 360);
makeButton("SETTINGS", 238, 360, 220, 78, 0x2A6F97, ACTION_OPEN_SETTINGS, &lv_font_montserrat_24);
makeVersionTag(); makeVersionTag();
} }
@ -1376,19 +1597,55 @@ static void drawSecretShowScreen() {
setRootStyle(); setRootStyle();
makeTitle("SECRET", 18, &lv_font_montserrat_24); makeTitle("SECRET", 18, &lv_font_montserrat_24);
if (gSecretConfigured) { if (gSecretConfigured) {
showMessageAt("Current secret in base58:", 56); lv_obj_t *panel = lv_obj_create(gRoot);
lv_obj_t *label = lv_label_create(gRoot); lv_obj_set_size(panel, 440, 320);
String secretLines = splitFixedWidth(gSecretBase58, 10); lv_obj_set_pos(panel, 20, 60);
lv_label_set_text(label, secretLines.c_str()); lv_obj_set_style_bg_color(panel, lv_color_hex(0x0F1B27), 0);
lv_obj_set_width(label, 420); lv_obj_set_style_bg_opa(panel, LV_OPA_COVER, 0);
lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP); lv_obj_set_style_border_width(panel, 1, 0);
lv_obj_set_style_text_font(label, &lv_font_montserrat_20, 0); lv_obj_set_style_border_color(panel, lv_color_hex(0x425466), 0);
lv_obj_set_style_text_color(label, lv_color_hex(0xD9E1EA), 0); lv_obj_set_style_radius(panel, 14, 0);
lv_obj_align(label, LV_ALIGN_TOP_MID, 0, 102); lv_obj_set_style_pad_all(panel, 14, 0);
lv_obj_set_style_pad_row(panel, 8, 0);
lv_obj_set_scroll_dir(panel, LV_DIR_VER);
lv_obj_set_scrollbar_mode(panel, LV_SCROLLBAR_MODE_ACTIVE);
lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(panel, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
auto addKeyBlock = [&](const String &title, const String &formula, const String &value) {
lv_obj_t *titleLabel = lv_label_create(panel);
lv_label_set_text(titleLabel, title.c_str());
lv_obj_set_width(titleLabel, 400);
lv_obj_set_style_text_font(titleLabel, &lv_font_montserrat_18, 0);
lv_obj_set_style_text_color(titleLabel, lv_color_hex(0xFFFFFF), 0);
lv_obj_t *formulaLabel = lv_label_create(panel);
lv_label_set_text(formulaLabel, formula.c_str());
lv_obj_set_width(formulaLabel, 400);
lv_obj_set_style_text_font(formulaLabel, &lv_font_montserrat_12, 0);
lv_obj_set_style_text_color(formulaLabel, lv_color_hex(0x8FA4B8), 0);
lv_obj_t *valueLabel = lv_label_create(panel);
lv_label_set_text(valueLabel, value.c_str());
lv_obj_set_width(valueLabel, 400);
lv_label_set_long_mode(valueLabel, LV_LABEL_LONG_WRAP);
lv_obj_set_style_text_font(valueLabel, &lv_font_montserrat_20, 0);
lv_obj_set_style_text_color(valueLabel, lv_color_hex(0xD9E1EA), 0);
};
addKeyBlock("Secret (base58)", "master secret", gSecretBase58);
addKeyBlock("Root key (base58)", "pub from sha256(base64(secret)|root.key)", gRootPubB58);
addKeyBlock("Root key priv (base58)", "sha256(base64(secret)|root.key)", gRootPrivB58);
addKeyBlock("Blockchain key (base58)", "pub from sha256(base64(secret)|bch.key)", gBlockchainPubB58);
addKeyBlock("Blockchain key priv (base58)", "sha256(base64(secret)|bch.key)", gBlockchainPrivB58);
addKeyBlock("Device key (base58)", "pub from sha256(base64(secret)|dev.key)", gDevicePubB58);
addKeyBlock("Device key priv (base58)", "sha256(base64(secret)|dev.key)", gDevicePrivB58);
addKeyBlock("Subserver key (base58)", String("pub from sha256(base64(secret)|") + subserverKeySuffix() + ")", gSubserverPubB58);
addKeyBlock("Subserver key priv (base58)", String("sha256(base64(secret)|") + subserverKeySuffix() + ")", gSubserverPrivB58);
} else { } else {
showMessageAt("Secret not set", 96); showMessageAt("Secret not set", 96);
} }
makeButton("BACK", 140, 360, 200, 72, 0x5A6570, ACTION_BACK_SECRET_MENU, &lv_font_montserrat_22); makeButton("BACK", 140, 392, 200, 56, 0x5A6570, ACTION_BACK_SECRET_MENU, &lv_font_montserrat_20);
makeVersionTag(); makeVersionTag();
} }

View File

@ -1,2 +1,2 @@
client.version=1.2.151 client.version=1.2.152
server.version=1.2.143 server.version=1.2.144