diff --git a/Dev_Docs/Pending_Features/2026-06-08_1940_esp32_nav_minimal_test.md b/Dev_Docs/Pending_Features/2026-06-08_1940_esp32_nav_minimal_test.md index 7535120..cde144d 100644 --- a/Dev_Docs/Pending_Features/2026-06-08_1940_esp32_nav_minimal_test.md +++ b/Dev_Docs/Pending_Features/2026-06-08_1940_esp32_nav_minimal_test.md @@ -3,7 +3,7 @@ - Краткое описание: минимальный UI-прототип для сабсервера на базе `LVGL + subserver touch`, с Wi-Fi flow, серверными адресами и общим экраном редактирования текста. - Что проверять: - стартует экран `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 (not configured) not configured` - `Wi-Fi () disconnected` @@ -64,9 +64,18 @@ - `Subserver` открывает промежуточный экран с `USE SUBSERVER1` и `EDIT MANUALLY`; - `USE SUBSERVER1` возвращает стандартное значение `subserver1`; - `EDIT MANUALLY` открывает общий экран редактирования и сохраняет значение в NVS; - - `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`; - во время генерации секрета есть `CANCEL` и подтверждение остановки; - при отмене генерации старый секрет, если он был, не должен теряться; diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_subserver_ui_nav_minimal_spec.md b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_subserver_ui_nav_minimal_spec.md index 896c39e..82a42fc 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_subserver_ui_nav_minimal_spec.md +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/shine_subserver_ui_nav_minimal_spec.md @@ -41,7 +41,8 @@ - индикатор Wi-Fi уровня сигнала; - по центру крупный текст `STATUS`; - одна строка Wi-Fi вида `Wi-Fi () connected/disconnected`; -- снизу большую кнопку `SETTINGS`. +- кнопка баланса вида `Balance: ` или `Balance: failed to load`, по нажатию выполняет повторный запрос; +- снизу кнопку `SETTINGS`, уменьшенную примерно до половины ширины экрана и сдвинутую к правому краю. - внизу на тёмной полосе подпись `SHiNE subserver (v.0.18)`. Строка Wi-Fi на `HOME`: @@ -184,10 +185,18 @@ Показывает: - заголовок `SECRET`; -- подпись `Current secret in base58:`; -- полный секрет открытым текстом; -- увеличенный шрифт для значения секрета; -- разбиение секрета по строкам по `10` символов; +- вертикально прокручиваемый список ключей; +- `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)`; +- для каждого поля показывается формула derivation; +- значения ключей показываются полными строками увеличенным шрифтом; - кнопку `BACK`. ## TEXT_EDIT_SCREEN diff --git a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/lvgl_nav_minimal_test/lvgl_nav_minimal_test.ino b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/lvgl_nav_minimal_test/lvgl_nav_minimal_test.ino index 141c332..7858ea3 100644 --- a/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/lvgl_nav_minimal_test/lvgl_nav_minimal_test.ino +++ b/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/test_sketches/lvgl_nav_minimal_test/lvgl_nav_minimal_test.ino @@ -1,10 +1,15 @@ #include #include #include +#include +#include #include #include #include #include +#include +#include +#include #define XPOWERS_CHIP_AXP2101 #include "XPowersLib.h" #include "shine_secret_generation.h" @@ -87,6 +92,7 @@ enum ActionId { ACTION_SECRET_GENERATE_CANCEL_NO, ACTION_BACK_SECRET_MENU, ACTION_BACK_ACCOUNT, + ACTION_REFRESH_BALANCE, ACTION_EDITOR_SAVE, ACTION_EDITOR_CANCEL, }; @@ -161,6 +167,7 @@ static bool gSecretConfigured = false; static String gSecretBase58; static uint8_t gSecretBytes[32] = {}; static String gAccountStatusMessage = "Edit account fields"; +static String gBalanceStatusMessage = "Balance: tap to load"; static bool gWifiKnownGood = false; static bool gWifiReconnectEnabled = false; static bool gWifiOperationBusy = false; @@ -169,6 +176,21 @@ static unsigned long gWifiLastReconnectAttemptMs = 0; static wl_status_t gLastWifiStatus = WL_IDLE_STATUS; 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 Screen gEditReturnScreen = SCREEN_HOME; static String gEditTitle; @@ -189,6 +211,7 @@ static void clearSavedWifiList(); static int findKnownWifiIndex(const String &ssid); static String savedPasswordForSsid(const String &ssid); static void upsertKnownWifi(const String &ssid, const String &password); +static bool refreshWalletBalance(String &messageOut); static void saveServerPrefs(); static void saveAccountPrefs(); static void beginSavedWifi(); @@ -210,6 +233,8 @@ static bool isTextEditKeyboardSwipeArea(int16_t x, int16_t y); static void syncEditValueFromTextarea(); static void keepCursorAtEnd(); static void restoreTextareaFromEditValue(); +static void refreshDerivedKeys(); +static void clearDerivedKeys(); static String loginDisplayValue(); static String subserverDisplayValue(); static String homeSecretStatus(); @@ -380,6 +405,182 @@ static String splitFixedWidth(const String &value, size_t chunkSize) { 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(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(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) { if (ssid.isEmpty()) { return; @@ -435,6 +636,8 @@ static void loadPrefs() { gSecretBase58 = ""; } } + refreshDerivedKeys(); + gBalanceStatusMessage = gDevicePubB58.isEmpty() ? "Balance: secret not set" : "Balance: tap to load"; } static void saveWifiPrefs() { @@ -647,6 +850,8 @@ static void clearSecretValue() { gSecretConfigured = false; gSecretBase58 = ""; memset(gSecretBytes, 0, sizeof(gSecretBytes)); + refreshDerivedKeys(); + gBalanceStatusMessage = "Balance: secret not set"; saveAccountPrefs(); } @@ -656,6 +861,8 @@ static void setSecretValue(const uint8_t *bytes32) { shineSecretBase58Encode(bytes32, 32, b58, sizeof(b58)); gSecretBase58 = b58; gSecretConfigured = true; + refreshDerivedKeys(); + gBalanceStatusMessage = "Balance: tap to load"; saveAccountPrefs(); } @@ -837,6 +1044,7 @@ static void applyEditorValue() { if (gEditContext == EDIT_CONTEXT_SUBSERVER) { value.trim(); gSubserverValue = value; + refreshDerivedKeys(); saveAccountPrefs(); gAccountStatusMessage = gSubserverValue.isEmpty() ? "Subserver cleared" : "Subserver saved"; showScreen(SCREEN_ACCOUNT); @@ -1103,6 +1311,7 @@ static void actionButtonCb(lv_event_t *event) { break; case ACTION_ACCOUNT_SUBSERVER_USE_DEFAULT: gSubserverValue = "subserver1"; + refreshDerivedKeys(); saveAccountPrefs(); gAccountStatusMessage = "Subserver set to subserver1"; showScreen(SCREEN_ACCOUNT); @@ -1118,6 +1327,17 @@ static void actionButtonCb(lv_event_t *event) { case ACTION_BACK_SECRET_MENU: showScreen(SCREEN_ACCOUNT_SECRET); 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: applyEditorValue(); break; @@ -1221,8 +1441,9 @@ static void drawHome() { drawTopStatusIndicators(); makeTitle("STATUS", 150, &lv_font_montserrat_28); showMessageAt(wifiHomeSummary(), 214); - makeBody("Swipe left or tap the button below.", 274, 360); - makeButton("SETTINGS", 22, 360, 436, 78, 0x2A6F97, ACTION_OPEN_SETTINGS, &lv_font_montserrat_24); + makeButton(gBalanceStatusMessage.c_str(), 22, 254, 436, 58, 0x355C7D, ACTION_REFRESH_BALANCE, &lv_font_montserrat_18); + 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(); } @@ -1376,19 +1597,55 @@ static void drawSecretShowScreen() { setRootStyle(); makeTitle("SECRET", 18, &lv_font_montserrat_24); if (gSecretConfigured) { - showMessageAt("Current secret in base58:", 56); - lv_obj_t *label = lv_label_create(gRoot); - String secretLines = splitFixedWidth(gSecretBase58, 10); - lv_label_set_text(label, secretLines.c_str()); - lv_obj_set_width(label, 420); - lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP); - lv_obj_set_style_text_font(label, &lv_font_montserrat_20, 0); - lv_obj_set_style_text_color(label, lv_color_hex(0xD9E1EA), 0); - lv_obj_align(label, LV_ALIGN_TOP_MID, 0, 102); + lv_obj_t *panel = lv_obj_create(gRoot); + lv_obj_set_size(panel, 440, 320); + lv_obj_set_pos(panel, 20, 60); + lv_obj_set_style_bg_color(panel, lv_color_hex(0x0F1B27), 0); + lv_obj_set_style_bg_opa(panel, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(panel, 1, 0); + lv_obj_set_style_border_color(panel, lv_color_hex(0x425466), 0); + lv_obj_set_style_radius(panel, 14, 0); + 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 { 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(); } diff --git a/VERSION.properties b/VERSION.properties index 08a46c7..a7e35e2 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.151 -server.version=1.2.143 +client.version=1.2.152 +server.version=1.2.144