From a67799b04c1d302c42ea6c1fc74c1f3c239c37a5 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Tue, 7 Apr 2026 10:56:56 +0200 Subject: [PATCH] Extend error messages --- lib/KOReaderSync/KOReaderSyncClient.cpp | 122 ++++++++++++++++-- lib/KOReaderSync/KOReaderSyncClient.h | 26 +++- .../reader/KOReaderSyncActivity.cpp | 14 ++ 3 files changed, 149 insertions(+), 13 deletions(-) diff --git a/lib/KOReaderSync/KOReaderSyncClient.cpp b/lib/KOReaderSync/KOReaderSyncClient.cpp index 83b1e478..ac5136cf 100644 --- a/lib/KOReaderSync/KOReaderSyncClient.cpp +++ b/lib/KOReaderSync/KOReaderSyncClient.cpp @@ -1,19 +1,42 @@ #include "KOReaderSyncClient.h" +#include #include #include #include +#include +#include #include #include #include +#include #include #include "KOReaderCredentialStore.h" int KOReaderSyncClient::lastHttpCode = 0; +int KOReaderSyncClient::lastEspError = 0; +unsigned KOReaderSyncClient::lastHeapAtFailure = 0; +unsigned KOReaderSyncClient::lastContigHeapAtFailure = 0; +const char* KOReaderSyncClient::lastOperation = ""; namespace { +// Static buffer for the detail string returned by lastFailureDetail() — sized to fit +// the longest expected message including esp_err name (~32 chars), opcode (~10), heap +// numbers, and HTTP status. Single-threaded sync flow makes static safe. +char g_failureDetailBuf[160] = {0}; + +// Reset the static diagnostic state at the start of each request and capture pre-flight +// heap so failure reporting always reflects what was available when the request started. +void beginRequest(const char* operation) { + KOReaderSyncClient::lastOperation = operation; + KOReaderSyncClient::lastEspError = 0; + KOReaderSyncClient::lastHttpCode = 0; + KOReaderSyncClient::lastHeapAtFailure = ESP.getFreeHeap(); + KOReaderSyncClient::lastContigHeapAtFailure = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT); +} + // Device identifier for CrossPoint reader constexpr char DEVICE_NAME[] = "CrossPoint"; constexpr char DEVICE_ID[] = "crosspoint-reader"; @@ -75,6 +98,24 @@ std::string base64Encode(const std::string& input) { return out; } +// Verify there is enough contiguous heap to attempt a TLS handshake. mbedTLS needs a +// large contiguous block during the handshake (~24-32 KB depending on cert chain depth). +// Total free heap can mislead because fragmentation leaves no single block big enough, +// which is precisely the scenario after recent PNG/JPG decode activity. Returns true if +// we should proceed; false means caller must abort with NETWORK_ERROR — in which case +// lastFailureDetail() will report the heap shortage instead of attempting a doomed handshake. +bool checkHeapForTls() { + // beginRequest() already populated lastContigHeapAtFailure for the diagnostic path. + if (KOReaderSyncClient::lastContigHeapAtFailure < KOReaderSyncClient::MIN_CONTIG_HEAP_FOR_TLS) { + LOG_ERR("KOSync", "Insufficient contiguous heap for TLS: %u available, %u required", + KOReaderSyncClient::lastContigHeapAtFailure, KOReaderSyncClient::MIN_CONTIG_HEAP_FOR_TLS); + // Synthesize an esp_err_t-shaped value so the diagnostic detail string is uniform. + KOReaderSyncClient::lastEspError = ESP_ERR_NO_MEM; + return false; + } + return true; +} + // Create configured esp_http_client with small TLS buffers esp_http_client_handle_t createClient(const char* url, ResponseBuffer* buf, esp_http_client_method_t method = HTTP_METHOD_GET) { @@ -111,8 +152,12 @@ KOReaderSyncClient::Error KOReaderSyncClient::registerUser() { return NO_CREDENTIALS; } + beginRequest("register"); + if (!checkHeapForTls()) return NETWORK_ERROR; + std::string url = KOREADER_STORE.getBaseUrl() + "/users/create"; - LOG_DBG("KOSync", "Registering user: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap()); + LOG_DBG("KOSync", "Registering user: %s (heap: %u, contig: %u)", url.c_str(), lastHeapAtFailure, + lastContigHeapAtFailure); JsonDocument doc; doc["username"] = KOREADER_STORE.getUsername(); @@ -124,7 +169,10 @@ KOReaderSyncClient::Error KOReaderSyncClient::registerUser() { ResponseBuffer buf; esp_http_client_handle_t client = createClient(url.c_str(), &buf, HTTP_METHOD_POST); - if (!client) return NETWORK_ERROR; + if (!client) { + lastEspError = ESP_ERR_NO_MEM; + return NETWORK_ERROR; + } esp_http_client_set_header(client, "Content-Type", "application/json"); esp_http_client_set_post_field(client, body.c_str(), body.length()); @@ -132,9 +180,11 @@ KOReaderSyncClient::Error KOReaderSyncClient::registerUser() { esp_err_t err = esp_http_client_perform(client); const int httpCode = esp_http_client_get_status_code(client); lastHttpCode = httpCode; + lastEspError = err; esp_http_client_cleanup(client); - LOG_DBG("KOSync", "Register response: %d (err: %d) | body: %s", httpCode, err, buf.data ? buf.data : ""); + LOG_DBG("KOSync", "Register response: %d (err: %s) | body: %s", httpCode, esp_err_to_name(err), + buf.data ? buf.data : ""); if (err != ESP_OK) { return NETWORK_ERROR; @@ -168,19 +218,27 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() { return NO_CREDENTIALS; } + beginRequest("auth"); + if (!checkHeapForTls()) return NETWORK_ERROR; + std::string url = KOREADER_STORE.getBaseUrl() + "/users/auth"; - LOG_DBG("KOSync", "Authenticating: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap()); + LOG_DBG("KOSync", "Authenticating: %s (heap: %u, contig: %u)", url.c_str(), lastHeapAtFailure, + lastContigHeapAtFailure); ResponseBuffer buf; esp_http_client_handle_t client = createClient(url.c_str(), &buf); - if (!client) return NETWORK_ERROR; + if (!client) { + lastEspError = ESP_ERR_NO_MEM; + return NETWORK_ERROR; + } esp_err_t err = esp_http_client_perform(client); const int httpCode = esp_http_client_get_status_code(client); lastHttpCode = httpCode; + lastEspError = err; esp_http_client_cleanup(client); - LOG_DBG("KOSync", "Auth response: %d (err: %d)", httpCode, err); + LOG_DBG("KOSync", "Auth response: %d (err: %s)", httpCode, esp_err_to_name(err)); if (err != ESP_OK) return NETWORK_ERROR; if (httpCode == 200) return OK; @@ -195,19 +253,27 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc return NO_CREDENTIALS; } + beginRequest("get progress"); + if (!checkHeapForTls()) return NETWORK_ERROR; + std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress/" + documentHash; - LOG_DBG("KOSync", "Getting progress: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap()); + LOG_DBG("KOSync", "Getting progress: %s (heap: %u, contig: %u)", url.c_str(), lastHeapAtFailure, + lastContigHeapAtFailure); ResponseBuffer buf; esp_http_client_handle_t client = createClient(url.c_str(), &buf); - if (!client) return NETWORK_ERROR; + if (!client) { + lastEspError = ESP_ERR_NO_MEM; + return NETWORK_ERROR; + } esp_err_t err = esp_http_client_perform(client); const int httpCode = esp_http_client_get_status_code(client); lastHttpCode = httpCode; + lastEspError = err; esp_http_client_cleanup(client); - LOG_DBG("KOSync", "Get progress response: %d (err: %d)", httpCode, err); + LOG_DBG("KOSync", "Get progress response: %d (err: %s)", httpCode, esp_err_to_name(err)); if (err != ESP_OK) return NETWORK_ERROR; @@ -242,8 +308,12 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr return NO_CREDENTIALS; } + beginRequest("update progress"); + if (!checkHeapForTls()) return NETWORK_ERROR; + std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress"; - LOG_DBG("KOSync", "Updating progress: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap()); + LOG_DBG("KOSync", "Updating progress: %s (heap: %u, contig: %u)", url.c_str(), lastHeapAtFailure, + lastContigHeapAtFailure); // Build JSON body JsonDocument doc; @@ -260,7 +330,10 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr ResponseBuffer buf; esp_http_client_handle_t client = createClient(url.c_str(), &buf, HTTP_METHOD_PUT); - if (!client) return NETWORK_ERROR; + if (!client) { + lastEspError = ESP_ERR_NO_MEM; + return NETWORK_ERROR; + } esp_http_client_set_header(client, "Content-Type", "application/json"); esp_http_client_set_post_field(client, body.c_str(), body.length()); @@ -268,9 +341,10 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr esp_err_t err = esp_http_client_perform(client); const int httpCode = esp_http_client_get_status_code(client); lastHttpCode = httpCode; + lastEspError = err; esp_http_client_cleanup(client); - LOG_DBG("KOSync", "Update progress response: %d (err: %d)", httpCode, err); + LOG_DBG("KOSync", "Update progress response: %d (err: %s)", httpCode, esp_err_to_name(err)); if (err != ESP_OK) return NETWORK_ERROR; if (httpCode == 200 || httpCode == 202) return OK; @@ -278,6 +352,30 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr return SERVER_ERROR; } +const char* KOReaderSyncClient::lastFailureDetail() { + // Heap-pressure case: surfaced when checkHeapForTls() refused before any TCP/TLS work happened. + if (lastEspError == ESP_ERR_NO_MEM && lastHttpCode == 0) { + snprintf(g_failureDetailBuf, sizeof(g_failureDetailBuf), + "%s: low memory (%u free, %u contig, need %u). Reboot device.", lastOperation, lastHeapAtFailure, + lastContigHeapAtFailure, MIN_CONTIG_HEAP_FOR_TLS); + return g_failureDetailBuf; + } + // Network/TLS case: esp_http_client_perform() failed before getting a status code. + if (lastHttpCode == 0 && lastEspError != 0) { + snprintf(g_failureDetailBuf, sizeof(g_failureDetailBuf), "%s: %s (heap %u/%u contig)", lastOperation, + esp_err_to_name(lastEspError), lastHeapAtFailure, lastContigHeapAtFailure); + return g_failureDetailBuf; + } + // Server case: got an HTTP status the client didn't recognize as success. + if (lastHttpCode != 0) { + snprintf(g_failureDetailBuf, sizeof(g_failureDetailBuf), "%s: HTTP %d", lastOperation, lastHttpCode); + return g_failureDetailBuf; + } + // No prior request, or success. + g_failureDetailBuf[0] = '\0'; + return g_failureDetailBuf; +} + const char* KOReaderSyncClient::errorString(Error error) { switch (error) { case OK: diff --git a/lib/KOReaderSync/KOReaderSyncClient.h b/lib/KOReaderSync/KOReaderSyncClient.h index 5fe4844d..3697c34a 100644 --- a/lib/KOReaderSync/KOReaderSyncClient.h +++ b/lib/KOReaderSync/KOReaderSyncClient.h @@ -71,10 +71,34 @@ class KOReaderSyncClient { static Error updateProgress(const KOReaderProgress& progress); /** - * Get human-readable error message. + * Get human-readable error message (short, for status line). */ static const char* errorString(Error error); + /** + * Get a detailed diagnostic string for the last failure, combining the error + * category, the underlying esp_err_t (when applicable), the HTTP status code, + * and the free heap at the time of failure. Intended for the SYNC_FAILED screen + * so users and bug-reporters can tell network/TLS/server failures apart. + * Returns a stable c-string valid until the next request. + */ + static const char* lastFailureDetail(); + /** HTTP status code from the last request (for diagnostics). */ static int lastHttpCode; + /** Last esp_err_t from esp_http_client_perform (ESP_OK if request reached the server). */ + static int lastEspError; + /** Free heap (bytes) captured at the start of the last failing request. */ + static unsigned lastHeapAtFailure; + /** Largest contiguous free block (bytes) at the start of the last failing request. */ + static unsigned lastContigHeapAtFailure; + /** Operation tag set at the start of each request, surfaced in lastFailureDetail. */ + static const char* lastOperation; + + /** + * Minimum largest-contiguous-free heap block (bytes) required before attempting + * a request. Below this, the client refuses with NETWORK_ERROR and lastFailureDetail + * reports a heap-pressure message instead of attempting (and crashing) the TLS handshake. + */ + static constexpr unsigned MIN_CONTIG_HEAP_FOR_TLS = 32 * 1024; }; diff --git a/src/activities/reader/KOReaderSyncActivity.cpp b/src/activities/reader/KOReaderSyncActivity.cpp index b06dffd8..46333b14 100644 --- a/src/activities/reader/KOReaderSyncActivity.cpp +++ b/src/activities/reader/KOReaderSyncActivity.cpp @@ -87,7 +87,14 @@ void KOReaderSyncActivity::performSync() { { RenderLock lock(*this); state = SYNC_FAILED; + // Combine the short category label with the rich diagnostic so users (and bug + // reports) can tell network/TLS/server/heap failures apart at a glance. statusMessage = KOReaderSyncClient::errorString(result); + const char* detail = KOReaderSyncClient::lastFailureDetail(); + if (detail && detail[0]) { + statusMessage += " — "; + statusMessage += detail; + } } requestUpdate(true); return; @@ -154,7 +161,14 @@ void KOReaderSyncActivity::performUpload() { { RenderLock lock(*this); state = SYNC_FAILED; + // Combine the short category label with the rich diagnostic so users (and bug + // reports) can tell network/TLS/server/heap failures apart at a glance. statusMessage = KOReaderSyncClient::errorString(result); + const char* detail = KOReaderSyncClient::lastFailureDetail(); + if (detail && detail[0]) { + statusMessage += " — "; + statusMessage += detail; + } } requestUpdate(); return;