ESP32: добавить диагностику подключения SHiNE и починить WS handshake

This commit is contained in:
AidarKC 2026-06-13 13:09:32 +04:00
parent 477ab3b580
commit 3b8ea70d3c
3 changed files with 76 additions and 53 deletions

View File

@ -16,7 +16,6 @@
6. Если действие завершается ошибкой, проверить, что: 6. Если действие завершается ошибкой, проверить, что:
- текст ошибки показан на экране результата; - текст ошибки показан на экране результата;
- команда `last_error` по USB возвращает полный сохранённый payload. - команда `last_error` по USB возвращает полный сохранённый payload.
7. Во временной диагностической сборке после старта устройства подождать 15 секунд и проверить, что при наличии жёлтой homeserver-кнопки устройство само делает попытку `ADD/FIX HOMESERVER`, а результат пишет в USB-диагностику.
- ожидаемый результат: - ожидаемый результат:
устройство после обычной регистрации пользователя способно отдельной транзакцией добавить или исправить homeserver-сессию в `shine_users` PDA, а ошибки этого шага сохраняются в ту же USB/NVS-диагностику. устройство после обычной регистрации пользователя способно отдельной транзакцией добавить или исправить homeserver-сессию в `shine_users` PDA, а ошибки этого шага сохраняются в ту же USB/NVS-диагностику.

View File

@ -304,10 +304,6 @@ static String gLastRegisterDiagSummary;
static String gLastRegisterDiagDetails; static String gLastRegisterDiagDetails;
static String gLastRegisterDiagTime; static String gLastRegisterDiagTime;
static String gRegisterTriggerSource = "manual"; static String gRegisterTriggerSource = "manual";
static unsigned long gBootMillis = 0;
static bool gAutoHomeserverTestArmed = true;
static bool gAutoHomeserverTestStarted = false;
static unsigned long gLastAutoHomeserverAttemptMs = 0;
static String gSerialCommandBuffer; static String gSerialCommandBuffer;
static String gShineSessionId; static String gShineSessionId;
static String gShineSessionKey; static String gShineSessionKey;
@ -498,6 +494,7 @@ static bool readShineUserPda(const String &login, ShinePdaUserState &outState, S
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);
static void refreshAccountPdaStatus(); static void refreshAccountPdaStatus();
static void manageAccountPdaRefresh(); static void manageAccountPdaRefresh();
static void saveShineConnectDiag(const String &status, const String &summary, const String &details);
static bool ensureWebSocketConnected(SimpleWebSocketClient &ws, const String &url, String &errorOut); static bool ensureWebSocketConnected(SimpleWebSocketClient &ws, const String &url, String &errorOut);
static void closeWebSocket(SimpleWebSocketClient &ws); static void closeWebSocket(SimpleWebSocketClient &ws);
static bool wsSendText(SimpleWebSocketClient &ws, const String &payload); static bool wsSendText(SimpleWebSocketClient &ws, const String &payload);
@ -2921,7 +2918,7 @@ static bool ensureWebSocketConnected(SimpleWebSocketClient &ws, const String &ur
} }
ws.client.setInsecure(); ws.client.setInsecure();
ws.client.setTimeout(5); ws.client.setTimeout(5000);
if (!ws.client.connect(ws.host.c_str(), ws.port)) { if (!ws.client.connect(ws.host.c_str(), ws.port)) {
errorOut = "WS connect failed"; errorOut = "WS connect failed";
return false; return false;
@ -2944,7 +2941,7 @@ static bool ensureWebSocketConnected(SimpleWebSocketClient &ws, const String &ur
String statusLine = ws.client.readStringUntil('\n'); String statusLine = ws.client.readStringUntil('\n');
if (statusLine.indexOf("101") < 0) { if (statusLine.indexOf("101") < 0) {
errorOut = "WS handshake failed"; errorOut = String("WS handshake failed: ") + statusLine;
closeWebSocket(ws); closeWebSocket(ws);
return false; return false;
} }
@ -3100,41 +3097,58 @@ static bool shineWsRequest(SimpleWebSocketClient &ws, const String &op, const St
static bool ensureShineSessionAuthenticated(String &errorOut) { static bool ensureShineSessionAuthenticated(String &errorOut) {
errorOut = ""; errorOut = "";
String diagDetails;
auto failWithDiag = [&](const String &summary) -> bool {
errorOut = summary;
saveShineConnectDiag("error", summary, diagDetails);
printRegisterDiagToSerial();
return false;
};
diagDetails += "kind=shine_connect\n";
diagDetails += String("uptime_ms=") + String(millis()) + "\n";
diagDetails += String("login=") + gLoginValue + "\n";
diagDetails += String("homeserver=") + gHomeserverValue + "\n";
diagDetails += String("server_url=") + gShineServerUrl + "\n";
diagDetails += String("ws_url=") + shineWsUrl() + "\n";
diagDetails += String("pda_status=") + gAccountPdaStatusMessage + "\n";
if (WiFi.status() != WL_CONNECTED) { if (WiFi.status() != WL_CONNECTED) {
errorOut = "Wi-Fi disconnected"; diagDetails += "wifi=disconnected\n";
return false; return failWithDiag("Wi-Fi disconnected");
} }
diagDetails += String("wifi=connected:") + WiFi.localIP().toString() + "\n";
if (gLoginValue.isEmpty() || !gSecretConfigured || gHomeserverValue.isEmpty()) { if (gLoginValue.isEmpty() || !gSecretConfigured || gHomeserverValue.isEmpty()) {
errorOut = "account not configured"; return failWithDiag("account not configured");
return false;
} }
if (gAccountPdaStatus != ACCOUNT_PDA_OK) { if (gAccountPdaStatus != ACCOUNT_PDA_OK) {
errorOut = "account not ready"; return failWithDiag("account not ready");
return false;
} }
String wsUrl = shineWsUrl(); String wsUrl = shineWsUrl();
if (wsUrl.isEmpty()) { if (wsUrl.isEmpty()) {
errorOut = "shine server not set"; return failWithDiag("shine server not set");
return false;
} }
if (!ensureWebSocketConnected(gShineWs, wsUrl, errorOut)) { if (!ensureWebSocketConnected(gShineWs, wsUrl, errorOut)) {
return false; diagDetails += String("ws_connect_error=") + errorOut + "\n";
return failWithDiag(errorOut.isEmpty() ? "WS connect failed" : errorOut);
} }
diagDetails += "ws_connected=true\n";
{ {
String pingResp; String pingResp;
if (!shineWsRequest(gShineWs, "Ping", "{\"ts\":0}", pingResp, SHINE_RPC_TIMEOUT_MS)) { if (!shineWsRequest(gShineWs, "Ping", "{\"ts\":0}", pingResp, SHINE_RPC_TIMEOUT_MS)) {
errorOut = "Ping failed"; diagDetails += "ping_error=request_failed\n";
return false; return failWithDiag("Ping failed");
} }
diagDetails += "ping_response<<\n";
diagDetails += pingResp;
diagDetails += "\n>>ping_response\n";
uint64_t pingStatus = 0; uint64_t pingStatus = 0;
uint64_t serverTs = 0; uint64_t serverTs = 0;
if (!jsonInt64Field(pingResp, "status", pingStatus) || pingStatus != 200 || !jsonInt64Field(pingResp, "ts", serverTs)) { if (!jsonInt64Field(pingResp, "status", pingStatus) || pingStatus != 200 || !jsonInt64Field(pingResp, "ts", serverTs)) {
errorOut = "Ping rejected"; return failWithDiag("Ping rejected");
return false;
} }
gShineServerTimeOffsetMs = (int64_t)serverTs - (int64_t)millis(); gShineServerTimeOffsetMs = (int64_t)serverTs - (int64_t)millis();
diagDetails += String("server_time_offset_ms=") + String((long long)gShineServerTimeOffsetMs) + "\n";
} }
uint8_t deviceSeed[32] = {}; uint8_t deviceSeed[32] = {};
@ -3145,21 +3159,26 @@ static bool ensureShineSessionAuthenticated(String &errorOut) {
uint8_t subSec[64] = {}; uint8_t subSec[64] = {};
if (!deriveSeedKeypairFromBase58(gDevicePrivB58, deviceSeed, devicePub, deviceSec) if (!deriveSeedKeypairFromBase58(gDevicePrivB58, deviceSeed, devicePub, deviceSec)
|| !deriveSeedKeypairFromBase58(gHomeserverPrivB58, subSeed, subPub, subSec)) { || !deriveSeedKeypairFromBase58(gHomeserverPrivB58, subSeed, subPub, subSec)) {
errorOut = "local key derive failed"; return failWithDiag("local key derive failed");
return false;
} }
String sessionKey = buildSessionKeyStringFromPublicBase64(bytesToBase64String(subPub, 32)); String sessionKey = buildSessionKeyStringFromPublicBase64(bytesToBase64String(subPub, 32));
diagDetails += String("session_key=") + sessionKey + "\n";
if (!gShineSessionKey.isEmpty() && gShineSessionKey != sessionKey) { if (!gShineSessionKey.isEmpty() && gShineSessionKey != sessionKey) {
clearShineSessionState(true); clearShineSessionState(true);
diagDetails += "stored_session_key_reset=true\n";
} }
if (!gShineSessionId.isEmpty()) { if (!gShineSessionId.isEmpty()) {
diagDetails += String("stored_session_id=") + gShineSessionId + "\n";
String response; String response;
if (shineWsRequest(gShineWs, if (shineWsRequest(gShineWs,
"SessionChallenge", "SessionChallenge",
String("{\"sessionId\":\"") + jsonEscape(gShineSessionId) + "\"}", String("{\"sessionId\":\"") + jsonEscape(gShineSessionId) + "\"}",
response)) { response)) {
diagDetails += "session_challenge_response<<\n";
diagDetails += response;
diagDetails += "\n>>session_challenge_response\n";
uint64_t statusCode = 0; uint64_t statusCode = 0;
String nonce; String nonce;
if (jsonInt64Field(response, "status", statusCode) && statusCode == 200 && jsonStringField(response, "nonce", nonce)) { if (jsonInt64Field(response, "status", statusCode) && statusCode == 200 && jsonStringField(response, "nonce", nonce)) {
@ -3176,6 +3195,9 @@ static bool ensureShineSessionAuthenticated(String &errorOut) {
+ "\",\"clientInfo\":\"ESP32 homeserver\"}"; + "\",\"clientInfo\":\"ESP32 homeserver\"}";
String loginResp; String loginResp;
if (shineWsRequest(gShineWs, "SessionLogin", loginReq, loginResp)) { if (shineWsRequest(gShineWs, "SessionLogin", loginReq, loginResp)) {
diagDetails += "session_login_response<<\n";
diagDetails += loginResp;
diagDetails += "\n>>session_login_response\n";
if (jsonInt64Field(loginResp, "status", statusCode) && statusCode == 200) { if (jsonInt64Field(loginResp, "status", statusCode) && statusCode == 200) {
String storagePwd; String storagePwd;
if (jsonStringField(loginResp, "storagePwd", storagePwd)) { if (jsonStringField(loginResp, "storagePwd", storagePwd)) {
@ -3184,6 +3206,7 @@ static bool ensureShineSessionAuthenticated(String &errorOut) {
gShineSessionKey = sessionKey; gShineSessionKey = sessionKey;
gShineAuthenticated = true; gShineAuthenticated = true;
saveShineSessionPrefs(); saveShineSessionPrefs();
saveShineConnectDiag("ok", "SessionLogin success", diagDetails);
return true; return true;
} }
} }
@ -3191,8 +3214,10 @@ static bool ensureShineSessionAuthenticated(String &errorOut) {
} }
clearShineSessionState(true); clearShineSessionState(true);
if (!ensureWebSocketConnected(gShineWs, wsUrl, errorOut)) { if (!ensureWebSocketConnected(gShineWs, wsUrl, errorOut)) {
return false; diagDetails += String("ws_reconnect_error=") + errorOut + "\n";
return failWithDiag(errorOut.isEmpty() ? "WS reconnect failed" : errorOut);
} }
diagDetails += "stored_session_login_failed=true\n";
} }
if (gShineStoragePwd.isEmpty()) { if (gShineStoragePwd.isEmpty()) {
@ -3207,14 +3232,16 @@ static bool ensureShineSessionAuthenticated(String &errorOut) {
if (!shineWsRequest(gShineWs, "AuthChallenge", if (!shineWsRequest(gShineWs, "AuthChallenge",
String("{\"login\":\"") + jsonEscape(gLoginValue) + "\"}", String("{\"login\":\"") + jsonEscape(gLoginValue) + "\"}",
authResp)) { authResp)) {
errorOut = "AuthChallenge failed"; diagDetails += "auth_challenge_error=request_failed\n";
return false; return failWithDiag("AuthChallenge failed");
} }
diagDetails += "auth_challenge_response<<\n";
diagDetails += authResp;
diagDetails += "\n>>auth_challenge_response\n";
uint64_t statusCode = 0; uint64_t statusCode = 0;
String authNonce; String authNonce;
if (!jsonInt64Field(authResp, "status", statusCode) || statusCode != 200 || !jsonStringField(authResp, "authNonce", authNonce)) { if (!jsonInt64Field(authResp, "status", statusCode) || statusCode != 200 || !jsonStringField(authResp, "authNonce", authNonce)) {
errorOut = "AuthChallenge rejected"; return failWithDiag("AuthChallenge rejected");
return false;
} }
uint64_t timeMs = shineNowMs(); uint64_t timeMs = shineNowMs();
@ -3233,17 +3260,21 @@ static bool ensureShineSessionAuthenticated(String &errorOut) {
+ "\",\"clientInfo\":\"ESP32 homeserver\"}"; + "\",\"clientInfo\":\"ESP32 homeserver\"}";
String createResp; String createResp;
if (!shineWsRequest(gShineWs, "CreateAuthSession", createReq, createResp)) { if (!shineWsRequest(gShineWs, "CreateAuthSession", createReq, createResp)) {
errorOut = "CreateAuthSession failed"; diagDetails += "create_auth_session_error=request_failed\n";
return false; return failWithDiag("CreateAuthSession failed");
} }
diagDetails += "create_auth_session_response<<\n";
diagDetails += createResp;
diagDetails += "\n>>create_auth_session_response\n";
if (!jsonInt64Field(createResp, "status", statusCode) || statusCode != 200 || !jsonStringField(createResp, "sessionId", gShineSessionId)) { if (!jsonInt64Field(createResp, "status", statusCode) || statusCode != 200 || !jsonStringField(createResp, "sessionId", gShineSessionId)) {
errorOut = "CreateAuthSession rejected"; return failWithDiag("CreateAuthSession rejected");
return false;
} }
gShineSessionKey = sessionKey; gShineSessionKey = sessionKey;
gShineAuthenticated = true; gShineAuthenticated = true;
saveShineSessionPrefs(); saveShineSessionPrefs();
diagDetails += String("new_session_id=") + gShineSessionId + "\n";
saveShineConnectDiag("ok", "CreateAuthSession success", diagDetails);
return true; return true;
} }
@ -3292,6 +3323,15 @@ static void manageShineConnection() {
return; return;
} }
} }
String details;
details += "kind=shine_ping\n";
details += String("uptime_ms=") + String(millis()) + "\n";
details += String("server_url=") + gShineServerUrl + "\n";
details += "ping_response<<\n";
details += pingResp;
details += "\n>>ping_response\n";
saveShineConnectDiag("error", "Ping keepalive failed", details);
printRegisterDiagToSerial();
gShineStatusLine = String("SHiNE: ") + serverLabel + " unavailable"; gShineStatusLine = String("SHiNE: ") + serverLabel + " unavailable";
clearShineSessionState(false); clearShineSessionState(false);
} }
@ -3562,6 +3602,10 @@ static void clearRegisterDiag() {
clearRegisterDiagDetailsFromPrefs(); clearRegisterDiagDetailsFromPrefs();
} }
static void saveShineConnectDiag(const String &status, const String &summary, const String &details) {
saveRegisterDiag(status, summary, details);
}
static void printRegisterDiagToSerial() { static void printRegisterDiagToSerial() {
Serial.println("LAST_REGISTER_DIAG_BEGIN"); Serial.println("LAST_REGISTER_DIAG_BEGIN");
Serial.println(String("status=") + gLastRegisterDiagStatus); Serial.println(String("status=") + gLastRegisterDiagStatus);
@ -5053,7 +5097,6 @@ static void handleSwipe(SwipeDirection swipe) {
void setup() { void setup() {
Serial.begin(115200); Serial.begin(115200);
sodium_init(); sodium_init();
gBootMillis = millis();
Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL); Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL);
initPowerManagement(); initPowerManagement();
@ -5115,25 +5158,6 @@ void loop() {
manageAccountPdaRefresh(); manageAccountPdaRefresh();
manageShineConnection(); manageShineConnection();
if (gAutoHomeserverTestArmed
&& !gTouchDown
&& millis() - gBootMillis >= 15000UL
&& millis() - gLastAutoHomeserverAttemptMs >= 45000UL
&& gShowHomeserverPdaActionButton
&& (gHomeserverPdaCanAdd || gHomeserverPdaCanFix)) {
gAutoHomeserverTestStarted = true;
gLastAutoHomeserverAttemptMs = millis();
gRegisterTriggerSource = gHomeserverPdaCanFix ? "auto-boot-fix-homeserver" : "auto-boot-add-homeserver";
String updateMessage;
Serial.println("AUTO_HOMESERVER_PDA_BEGIN");
if (!updateHomeserverSessionOnSolana(gHomeserverPdaCanFix, updateMessage)) {
gHomeserverPdaResultSuccess = false;
gHomeserverPdaResultMessage = updateMessage;
gHomeserverPdaResultDetails = "";
}
Serial.println("AUTO_HOMESERVER_PDA_END");
}
static unsigned long lastHomeRefreshMs = 0; static unsigned long lastHomeRefreshMs = 0;
if (gCurrentScreen == SCREEN_HOME && !gTouchDown && millis() - lastHomeRefreshMs >= HOME_REFRESH_MS) { if (gCurrentScreen == SCREEN_HOME && !gTouchDown && millis() - lastHomeRefreshMs >= HOME_REFRESH_MS) {
lastHomeRefreshMs = millis(); lastHomeRefreshMs = millis();

View File

@ -1,2 +1,2 @@
client.version=1.2.177 client.version=1.2.179
server.version=1.2.166 server.version=1.2.168