Revert "Merge pull request #25 from jpirnay/fix-pr1582" and refactor koreader
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <Logging.h>
|
||||
#include <WiFi.h>
|
||||
#include <esp_crt_bundle.h>
|
||||
#include <esp_err.h>
|
||||
#include <esp_heap_caps.h>
|
||||
@@ -10,6 +11,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
|
||||
@@ -22,6 +24,9 @@ unsigned KOReaderSyncClient::lastContigHeapAtFailure = 0;
|
||||
const char* KOReaderSyncClient::lastOperation = "";
|
||||
|
||||
namespace {
|
||||
bool g_keepSessionOpen = false;
|
||||
esp_http_client_handle_t g_sessionClient = nullptr;
|
||||
|
||||
// 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.
|
||||
@@ -41,10 +46,20 @@ void beginRequest(const char* operation) {
|
||||
constexpr char DEVICE_NAME[] = "CrossPoint";
|
||||
constexpr char DEVICE_ID[] = "crosspoint-reader";
|
||||
|
||||
// Small TLS buffers to fit in ESP32-C3's limited heap (~46KB free after WiFi).
|
||||
// KOSync payloads are tiny JSON (<1KB), so 2KB buffers are sufficient.
|
||||
// Default 16KB buffers cause OOM during TLS handshake.
|
||||
constexpr int HTTP_BUF_SIZE = 2048;
|
||||
// Use small HTTP/TLS buffers to reduce peak handshake memory on ESP32-C3.
|
||||
// Payloads are tiny JSON, so throughput impact is minimal while avoiding
|
||||
// large transient allocations from default client buffer sizes.
|
||||
constexpr int HTTP_BUF_SIZE = 1024;
|
||||
constexpr unsigned TLS_CONTIG_HEAP_TOLERANCE = 512;
|
||||
|
||||
// Captures radio/link state around failed connects.
|
||||
// Why: many field failures look like TLS errors but are actually weak WiFi.
|
||||
void logWifiSnapshot(const char* stage) {
|
||||
const wl_status_t status = WiFi.status();
|
||||
const int32_t rssi = WiFi.RSSI();
|
||||
LOG_DBG("KOSync", "%s: wifi_status=%d rssi=%ld ip=%s", stage, static_cast<int>(status), static_cast<long>(rssi),
|
||||
WiFi.localIP().toString().c_str());
|
||||
}
|
||||
|
||||
// Response buffer for reading HTTP body
|
||||
struct ResponseBuffer {
|
||||
@@ -64,6 +79,20 @@ struct ResponseBuffer {
|
||||
}
|
||||
};
|
||||
|
||||
ResponseBuffer g_sessionResponseBuf;
|
||||
|
||||
void clearResponseBuffer(ResponseBuffer* buf) {
|
||||
if (!buf) return;
|
||||
if (buf->data) {
|
||||
buf->len = 0;
|
||||
buf->data[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
ResponseBuffer* effectiveResponseBuffer(ResponseBuffer* localBuf) {
|
||||
return g_keepSessionOpen ? &g_sessionResponseBuf : localBuf;
|
||||
}
|
||||
|
||||
// HTTP event handler to collect response body
|
||||
esp_err_t httpEventHandler(esp_http_client_event_t* evt) {
|
||||
auto* buf = static_cast<ResponseBuffer*>(evt->user_data);
|
||||
@@ -105,10 +134,23 @@ std::string base64Encode(const std::string& input) {
|
||||
// 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() {
|
||||
const bool hasReusableSession = g_keepSessionOpen && g_sessionClient != nullptr;
|
||||
const bool isUpload = (KOReaderSyncClient::lastOperation &&
|
||||
strcmp(KOReaderSyncClient::lastOperation, "update progress") == 0);
|
||||
const unsigned requiredContig =
|
||||
isUpload ? KOReaderSyncClient::MIN_CONTIG_HEAP_FOR_TLS_UPLOAD : KOReaderSyncClient::MIN_CONTIG_HEAP_FOR_TLS;
|
||||
|
||||
// Upload can often reuse the already-established GET connection. In that case
|
||||
// a full handshake allocation is typically unnecessary, so avoid failing fast
|
||||
// on contiguous-heap threshold and let the HTTP client attempt reuse.
|
||||
if (isUpload && hasReusableSession) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// beginRequest() already populated lastContigHeapAtFailure for the diagnostic path.
|
||||
if (KOReaderSyncClient::lastContigHeapAtFailure < KOReaderSyncClient::MIN_CONTIG_HEAP_FOR_TLS) {
|
||||
if (KOReaderSyncClient::lastContigHeapAtFailure + TLS_CONTIG_HEAP_TOLERANCE < requiredContig) {
|
||||
LOG_ERR("KOSync", "Insufficient contiguous heap for TLS: %u available, %u required",
|
||||
KOReaderSyncClient::lastContigHeapAtFailure, KOReaderSyncClient::MIN_CONTIG_HEAP_FOR_TLS);
|
||||
KOReaderSyncClient::lastContigHeapAtFailure, requiredContig);
|
||||
// Synthesize an esp_err_t-shaped value so the diagnostic detail string is uniform.
|
||||
KOReaderSyncClient::lastEspError = ESP_ERR_NO_MEM;
|
||||
return false;
|
||||
@@ -119,15 +161,33 @@ bool checkHeapForTls() {
|
||||
// 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) {
|
||||
ResponseBuffer* activeBuf = effectiveResponseBuffer(buf);
|
||||
|
||||
if (g_keepSessionOpen && g_sessionClient) {
|
||||
esp_http_client_set_url(g_sessionClient, url);
|
||||
esp_http_client_set_method(g_sessionClient, method);
|
||||
|
||||
// KOSync auth headers
|
||||
esp_http_client_set_header(g_sessionClient, "Accept", "application/vnd.koreader.v1+json");
|
||||
esp_http_client_set_header(g_sessionClient, "x-auth-user", KOREADER_STORE.getUsername().c_str());
|
||||
esp_http_client_set_header(g_sessionClient, "x-auth-key", KOREADER_STORE.getMd5Password().c_str());
|
||||
|
||||
std::string credentials = KOREADER_STORE.getUsername() + ":" + KOREADER_STORE.getPassword();
|
||||
std::string authHeader = "Basic " + base64Encode(credentials);
|
||||
esp_http_client_set_header(g_sessionClient, "Authorization", authHeader.c_str());
|
||||
return g_sessionClient;
|
||||
}
|
||||
|
||||
esp_http_client_config_t config = {};
|
||||
config.url = url;
|
||||
config.event_handler = httpEventHandler;
|
||||
config.user_data = buf;
|
||||
config.user_data = activeBuf;
|
||||
config.method = method;
|
||||
config.timeout_ms = 15000;
|
||||
config.buffer_size = HTTP_BUF_SIZE;
|
||||
config.buffer_size_tx = HTTP_BUF_SIZE;
|
||||
config.crt_bundle_attach = esp_crt_bundle_attach;
|
||||
config.keep_alive_enable = g_keepSessionOpen;
|
||||
|
||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||
if (!client) return nullptr;
|
||||
@@ -142,10 +202,28 @@ esp_http_client_handle_t createClient(const char* url, ResponseBuffer* buf,
|
||||
std::string authHeader = "Basic " + base64Encode(credentials);
|
||||
esp_http_client_set_header(client, "Authorization", authHeader.c_str());
|
||||
|
||||
if (g_keepSessionOpen) {
|
||||
g_sessionClient = client;
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void KOReaderSyncClient::beginPersistentSession() {
|
||||
g_keepSessionOpen = true;
|
||||
clearResponseBuffer(&g_sessionResponseBuf);
|
||||
}
|
||||
|
||||
void KOReaderSyncClient::endPersistentSession() {
|
||||
g_keepSessionOpen = false;
|
||||
if (g_sessionClient) {
|
||||
esp_http_client_cleanup(g_sessionClient);
|
||||
g_sessionClient = nullptr;
|
||||
}
|
||||
clearResponseBuffer(&g_sessionResponseBuf);
|
||||
}
|
||||
|
||||
KOReaderSyncClient::Error KOReaderSyncClient::registerUser() {
|
||||
if (!KOREADER_STORE.hasCredentials()) {
|
||||
LOG_DBG("KOSync", "No credentials configured");
|
||||
@@ -168,6 +246,8 @@ KOReaderSyncClient::Error KOReaderSyncClient::registerUser() {
|
||||
LOG_DBG("KOSync", "Register request body: <redacted credentials>");
|
||||
|
||||
ResponseBuffer buf;
|
||||
ResponseBuffer* activeBuf = effectiveResponseBuffer(&buf);
|
||||
clearResponseBuffer(activeBuf);
|
||||
esp_http_client_handle_t client = createClient(url.c_str(), &buf, HTTP_METHOD_POST);
|
||||
if (!client) {
|
||||
lastEspError = ESP_ERR_NO_MEM;
|
||||
@@ -181,10 +261,12 @@ KOReaderSyncClient::Error KOReaderSyncClient::registerUser() {
|
||||
const int httpCode = esp_http_client_get_status_code(client);
|
||||
lastHttpCode = httpCode;
|
||||
lastEspError = err;
|
||||
esp_http_client_cleanup(client);
|
||||
if (!g_keepSessionOpen) {
|
||||
esp_http_client_cleanup(client);
|
||||
}
|
||||
|
||||
LOG_DBG("KOSync", "Register response: %d (err: %s) | body: %s", httpCode, esp_err_to_name(err),
|
||||
buf.data ? buf.data : "");
|
||||
activeBuf->data ? activeBuf->data : "");
|
||||
|
||||
if (err != ESP_OK) {
|
||||
return NETWORK_ERROR;
|
||||
@@ -198,7 +280,7 @@ KOReaderSyncClient::Error KOReaderSyncClient::registerUser() {
|
||||
} else if (httpCode == 402) {
|
||||
// Both "user already exists" (error 2002) and "registration disabled" (error 2005)
|
||||
// return HTTP 402 on the original kosync server. Distinguish them by body text.
|
||||
std::string lowerBody = buf.data ? buf.data : "";
|
||||
std::string lowerBody = activeBuf->data ? activeBuf->data : "";
|
||||
std::transform(lowerBody.begin(), lowerBody.end(), lowerBody.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
if (lowerBody.find("already") != std::string::npos) {
|
||||
@@ -226,6 +308,8 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
|
||||
lastContigHeapAtFailure);
|
||||
|
||||
ResponseBuffer buf;
|
||||
ResponseBuffer* activeBuf = effectiveResponseBuffer(&buf);
|
||||
clearResponseBuffer(activeBuf);
|
||||
esp_http_client_handle_t client = createClient(url.c_str(), &buf);
|
||||
if (!client) {
|
||||
lastEspError = ESP_ERR_NO_MEM;
|
||||
@@ -236,7 +320,9 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
|
||||
const int httpCode = esp_http_client_get_status_code(client);
|
||||
lastHttpCode = httpCode;
|
||||
lastEspError = err;
|
||||
esp_http_client_cleanup(client);
|
||||
if (!g_keepSessionOpen) {
|
||||
esp_http_client_cleanup(client);
|
||||
}
|
||||
|
||||
LOG_DBG("KOSync", "Auth response: %d (err: %s)", httpCode, esp_err_to_name(err));
|
||||
|
||||
@@ -261,25 +347,47 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc
|
||||
lastContigHeapAtFailure);
|
||||
|
||||
ResponseBuffer buf;
|
||||
esp_http_client_handle_t client = createClient(url.c_str(), &buf);
|
||||
if (!client) {
|
||||
lastEspError = ESP_ERR_NO_MEM;
|
||||
return NETWORK_ERROR;
|
||||
ResponseBuffer* activeBuf = effectiveResponseBuffer(&buf);
|
||||
esp_err_t err = ESP_FAIL;
|
||||
int httpCode = 0;
|
||||
|
||||
for (int attempt = 1; attempt <= 2; attempt++) {
|
||||
clearResponseBuffer(activeBuf);
|
||||
|
||||
esp_http_client_handle_t client = createClient(url.c_str(), &buf);
|
||||
if (!client) {
|
||||
lastEspError = ESP_ERR_NO_MEM;
|
||||
return NETWORK_ERROR;
|
||||
}
|
||||
|
||||
logWifiSnapshot("WiFi before getProgress");
|
||||
err = esp_http_client_perform(client);
|
||||
httpCode = esp_http_client_get_status_code(client);
|
||||
lastHttpCode = httpCode;
|
||||
lastEspError = err;
|
||||
if (!g_keepSessionOpen) {
|
||||
esp_http_client_cleanup(client);
|
||||
}
|
||||
|
||||
LOG_DBG("KOSync", "Get progress response: %d (err: %s) [attempt %d]", httpCode, esp_err_to_name(err), attempt);
|
||||
|
||||
// Retry exactly once for connect-level failures only.
|
||||
// Why: this recovers short AP/roaming hiccups without masking persistent
|
||||
// TLS/auth/server errors that should be surfaced immediately.
|
||||
if (err == ESP_OK || err != ESP_ERR_HTTP_CONNECT || attempt == 2) {
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_ERR("KOSync", "getProgress connect failed on attempt %d, retrying once", attempt);
|
||||
logWifiSnapshot("WiFi before getProgress retry");
|
||||
delay(400);
|
||||
}
|
||||
|
||||
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: %s)", httpCode, esp_err_to_name(err));
|
||||
|
||||
if (err != ESP_OK) return NETWORK_ERROR;
|
||||
|
||||
if (httpCode == 200 && buf.data) {
|
||||
if (httpCode == 200 && activeBuf->data) {
|
||||
JsonDocument doc;
|
||||
const DeserializationError error = deserializeJson(doc, buf.data);
|
||||
const DeserializationError error = deserializeJson(doc, activeBuf->data);
|
||||
|
||||
if (error) {
|
||||
LOG_ERR("KOSync", "JSON parse failed: %s", error.c_str());
|
||||
@@ -329,23 +437,45 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr
|
||||
LOG_DBG("KOSync", "Request body: %s", body.c_str());
|
||||
|
||||
ResponseBuffer buf;
|
||||
esp_http_client_handle_t client = createClient(url.c_str(), &buf, HTTP_METHOD_PUT);
|
||||
if (!client) {
|
||||
lastEspError = ESP_ERR_NO_MEM;
|
||||
return NETWORK_ERROR;
|
||||
ResponseBuffer* activeBuf = effectiveResponseBuffer(&buf);
|
||||
esp_err_t err = ESP_FAIL;
|
||||
int httpCode = 0;
|
||||
|
||||
for (int attempt = 1; attempt <= 2; attempt++) {
|
||||
clearResponseBuffer(activeBuf);
|
||||
|
||||
esp_http_client_handle_t client = createClient(url.c_str(), &buf, HTTP_METHOD_PUT);
|
||||
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());
|
||||
|
||||
logWifiSnapshot("WiFi before updateProgress");
|
||||
err = esp_http_client_perform(client);
|
||||
httpCode = esp_http_client_get_status_code(client);
|
||||
lastHttpCode = httpCode;
|
||||
lastEspError = err;
|
||||
if (!g_keepSessionOpen) {
|
||||
esp_http_client_cleanup(client);
|
||||
}
|
||||
|
||||
LOG_DBG("KOSync", "Update progress response: %d (err: %s) [attempt %d]", httpCode, esp_err_to_name(err),
|
||||
attempt);
|
||||
|
||||
// Retry exactly once for connect-level failures only.
|
||||
// Why: same policy as GET keeps behavior predictable across both endpoints.
|
||||
if (err == ESP_OK || err != ESP_ERR_HTTP_CONNECT || attempt == 2) {
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_ERR("KOSync", "updateProgress connect failed on attempt %d, retrying once", attempt);
|
||||
logWifiSnapshot("WiFi before updateProgress retry");
|
||||
delay(400);
|
||||
}
|
||||
|
||||
esp_http_client_set_header(client, "Content-Type", "application/json");
|
||||
esp_http_client_set_post_field(client, body.c_str(), body.length());
|
||||
|
||||
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: %s)", httpCode, esp_err_to_name(err));
|
||||
|
||||
if (err != ESP_OK) return NETWORK_ERROR;
|
||||
if (httpCode == 200 || httpCode == 202) return OK;
|
||||
if (httpCode == 401) return AUTH_FAILED;
|
||||
@@ -353,11 +483,14 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr
|
||||
}
|
||||
|
||||
const char* KOReaderSyncClient::lastFailureDetail() {
|
||||
const bool isUpload = (lastOperation && strcmp(lastOperation, "update progress") == 0);
|
||||
const unsigned requiredContig = isUpload ? MIN_CONTIG_HEAP_FOR_TLS_UPLOAD : MIN_CONTIG_HEAP_FOR_TLS;
|
||||
|
||||
// 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);
|
||||
lastContigHeapAtFailure, requiredContig);
|
||||
return g_failureDetailBuf;
|
||||
}
|
||||
// Network/TLS case: esp_http_client_perform() failed before getting a status code.
|
||||
|
||||
Reference in New Issue
Block a user