Revert "Merge pull request #25 from jpirnay/fix-pr1582" and refactor koreader

This commit is contained in:
jpirnay
2026-04-07 20:24:51 +02:00
parent fb331ae122
commit abbf69649e
17 changed files with 516 additions and 268 deletions
+1 -1
View File
@@ -12,7 +12,7 @@
#include "parsers/ChapterHtmlSlimParser.h"
namespace {
constexpr uint8_t SECTION_FILE_VERSION = 22;
constexpr uint8_t SECTION_FILE_VERSION = 20;
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + // SECTION_FILE_VERSION
sizeof(int) + // fontId
sizeof(float) + // lineCompression
+46 -72
View File
@@ -122,28 +122,6 @@ bool isZeroHeightSpacerParagraph(const char* name, const std::string& styleAttr)
return hasZeroHeight && hasZeroMargin && hasZeroBorder;
}
BlockStyle getInheritedBlockStyle(const BlockStyle& parent, const BlockStyle& child) {
BlockStyle inherited = child;
inherited.marginLeft = static_cast<int16_t>(parent.marginLeft + child.marginLeft);
inherited.marginRight = static_cast<int16_t>(parent.marginRight + child.marginRight);
inherited.paddingLeft = static_cast<int16_t>(parent.paddingLeft + child.paddingLeft);
inherited.paddingRight = static_cast<int16_t>(parent.paddingRight + child.paddingRight);
if (!child.textIndentDefined) {
inherited.textIndent = parent.textIndent;
inherited.textIndentDefined = parent.textIndentDefined;
}
if (!child.textAlignDefined) {
inherited.alignment = parent.alignment;
inherited.textAlignDefined = parent.textAlignDefined;
}
inherited.fromBrElement = false;
return inherited;
}
// Update effective bold/italic/underline based on block style and inline style stack
void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
// Start with block-level styles
@@ -198,9 +176,11 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
if (currentTextBlock) {
// already have a text block running and it is empty - just reuse it
if (currentTextBlock->isEmpty()) {
// Merge with existing block style to accumulate CSS styling from parent block elements.
// This handles cases like <div style="margin-bottom:2em"><h1>text</h1></div> where the
// div's margin should be preserved, even though it has no direct text content.
BlockStyle incoming = blockStyle;
const BlockStyle& currentStyle = currentTextBlock->getBlockStyle();
const bool brGapPending = currentStyle.fromBrElement;
const bool brGapPending = currentTextBlock->getBlockStyle().fromBrElement;
if (brGapPending) {
// The empty block was created by a <br> section separator. Inject a full line of
// blank space before the following paragraph so the scene/section break is visible.
@@ -208,8 +188,12 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
const int16_t lineHeight = static_cast<int16_t>(renderer.getLineHeight(fontId) * lineCompression + 0.5f);
incoming.marginTop = static_cast<int16_t>(incoming.marginTop + lineHeight);
}
incoming.fromBrElement = blockStyle.fromBrElement;
currentTextBlock->setBlockStyle(incoming);
BlockStyle merged = currentTextBlock->getBlockStyle().getCombinedBlockStyle(incoming);
// Preserve only whether the current empty block still represents <br> separators.
// This lets consecutive <br> accumulate one line each without leaking the flag to real content blocks.
merged.fromBrElement = blockStyle.fromBrElement;
currentTextBlock->setBlockStyle(merged);
if (!pendingAnchorId.empty()) {
if (std::find(tocAnchors.begin(), tocAnchors.end(), pendingAnchorId) != tocAnchors.end()) {
@@ -491,25 +475,19 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
}
const bool hasCssHeight = imgStyle.hasImageHeight();
const bool hasCssWidth = imgStyle.hasImageWidth();
int containerWidth = self->viewportWidth;
if (self->currentTextBlock) {
const int inset = self->currentTextBlock->getBlockStyle().totalHorizontalInset();
if (inset > 0 && inset < self->viewportWidth) {
containerWidth = self->viewportWidth - inset;
}
}
if (hasCssHeight && hasCssWidth && dims.width > 0 && dims.height > 0) {
// Both CSS height and width set: resolve both, then clamp to the current container preserving ratio.
// Both CSS height and width set: resolve both, then clamp to viewport preserving requested ratio
displayHeight = static_cast<int>(
imgStyle.imageHeight.toPixels(emSize, static_cast<float>(self->viewportHeight)) + 0.5f);
displayWidth =
static_cast<int>(imgStyle.imageWidth.toPixels(emSize, static_cast<float>(containerWidth)) + 0.5f);
displayWidth = static_cast<int>(
imgStyle.imageWidth.toPixels(emSize, static_cast<float>(self->viewportWidth)) + 0.5f);
if (displayHeight < 1) displayHeight = 1;
if (displayWidth < 1) displayWidth = 1;
if (displayWidth > containerWidth || displayHeight > self->viewportHeight) {
float scaleX =
(displayWidth > containerWidth) ? static_cast<float>(containerWidth) / displayWidth : 1.0f;
if (displayWidth > self->viewportWidth || displayHeight > self->viewportHeight) {
float scaleX = (displayWidth > self->viewportWidth)
? static_cast<float>(self->viewportWidth) / displayWidth
: 1.0f;
float scaleY = (displayHeight > self->viewportHeight)
? static_cast<float>(self->viewportHeight) / displayHeight
: 1.0f;
@@ -534,8 +512,8 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
static_cast<int>(displayHeight * (static_cast<float>(dims.width) / dims.height) + 0.5f);
if (displayWidth < 1) displayWidth = 1;
}
if (displayWidth > containerWidth) {
displayWidth = containerWidth;
if (displayWidth > self->viewportWidth) {
displayWidth = self->viewportWidth;
// Rescale height to preserve aspect ratio when width is clamped
displayHeight =
static_cast<int>(displayWidth * (static_cast<float>(dims.height) / dims.width) + 0.5f);
@@ -544,10 +522,10 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
if (displayWidth < 1) displayWidth = 1;
LOG_DBG("EHP", "Display size from CSS height: %dx%d", displayWidth, displayHeight);
} else if (hasCssWidth && !hasCssHeight && dims.width > 0 && dims.height > 0) {
// Use CSS width (resolve % against container width) and derive height from aspect ratio.
displayWidth =
static_cast<int>(imgStyle.imageWidth.toPixels(emSize, static_cast<float>(containerWidth)) + 0.5f);
if (displayWidth > containerWidth) displayWidth = containerWidth;
// Use CSS width (resolve % against viewport width) and derive height from aspect ratio
displayWidth = static_cast<int>(
imgStyle.imageWidth.toPixels(emSize, static_cast<float>(self->viewportWidth)) + 0.5f);
if (displayWidth > self->viewportWidth) displayWidth = self->viewportWidth;
if (displayWidth < 1) displayWidth = 1;
displayHeight =
static_cast<int>(displayWidth * (static_cast<float>(dims.height) / dims.width) + 0.5f);
@@ -561,8 +539,8 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
if (displayHeight < 1) displayHeight = 1;
LOG_DBG("EHP", "Display size from CSS width: %dx%d", displayWidth, displayHeight);
} else {
// Scale to fit the current container while maintaining aspect ratio.
int maxWidth = containerWidth;
// Scale to fit viewport while maintaining aspect ratio
int maxWidth = self->viewportWidth;
int maxHeight = self->viewportHeight;
float scaleX = (dims.width > maxWidth) ? (float)maxWidth / dims.width : 1.0f;
float scaleY = (dims.height > maxHeight) ? (float)maxHeight / dims.height : 1.0f;
@@ -676,10 +654,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
// Fallback to alt text if image processing fails
if (!alt.empty()) {
alt = "[Image: " + alt + "]";
const BlockStyle altBlockStyle = self->blockStyleStack.empty()
? centeredBlockStyle
: getInheritedBlockStyle(self->blockStyleStack.back(), centeredBlockStyle);
self->startNewTextBlock(altBlockStyle);
self->startNewTextBlock(centeredBlockStyle);
self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth);
self->depth += 1;
self->characterData(userData, alt.c_str(), alt.length());
@@ -793,9 +768,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
if (self->embeddedStyle && cssStyle.hasTextAlign()) {
headerBlockStyle.alignment = cssStyle.textAlign;
}
const BlockStyle inheritedHeaderBlockStyle = getInheritedBlockStyle(self->blockStyleStack.back(), headerBlockStyle);
self->blockStyleStack.push_back(inheritedHeaderBlockStyle);
self->startNewTextBlock(inheritedHeaderBlockStyle);
self->startNewTextBlock(headerBlockStyle);
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
self->updateEffectiveInlineStyle();
} else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
@@ -807,9 +780,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
blockStyle.alignment = cssStyle.textAlign;
blockStyle.textAlignDefined = true;
}
const BlockStyle inheritedBlockStyle = getInheritedBlockStyle(self->blockStyleStack.back(), blockStyle);
self->blockStyleStack.push_back(inheritedBlockStyle);
self->startNewTextBlock(inheritedBlockStyle);
self->startNewTextBlock(blockStyle);
self->updateEffectiveInlineStyle();
self->skipTextUntilDepth = self->depth;
@@ -843,9 +814,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
blockStyle.alignment = cssStyle.textAlign;
blockStyle.textAlignDefined = true;
}
const BlockStyle inheritedBlockStyle = getInheritedBlockStyle(self->blockStyleStack.back(), blockStyle);
self->blockStyleStack.push_back(inheritedBlockStyle);
self->startNewTextBlock(inheritedBlockStyle);
self->startNewTextBlock(blockStyle);
self->updateEffectiveInlineStyle();
if (strcmp(name, "li") == 0) {
@@ -1289,24 +1258,29 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
self->currentCssStyle.reset();
self->updateEffectiveInlineStyle();
if (strcmp(name, "br") != 0 && self->blockStyleStack.size() > 1) {
self->blockStyleStack.pop_back();
if (self->currentTextBlock && self->currentTextBlock->isEmpty()) {
self->currentTextBlock->setBlockStyle(self->blockStyleStack.back());
// Reset alignment on empty text blocks to prevent stale alignment from bleeding
// into the next sibling element. This fixes issue #1026 where an empty <h1> (default
// Center) followed by an image-only <p> causes Center to persist through the chain
// of empty block reuse into subsequent text paragraphs.
// Margins/padding are preserved so parent element spacing still accumulates correctly.
if (self->currentTextBlock && self->currentTextBlock->isEmpty()) {
auto style = self->currentTextBlock->getBlockStyle();
// Keep alignment only when closing the <br> separator itself so subsequent text
// within the same block container stays aligned. Reset alignment when closing
// other block tags (e.g. div/p) to avoid leaking centered/right alignment globally.
const bool preserveForBrClose = style.fromBrElement && strcmp(name, "br") == 0;
if (!preserveForBrClose) {
style.textAlignDefined = false;
style.alignment = (self->paragraphAlignment == static_cast<uint8_t>(CssTextAlign::None))
? CssTextAlign::Justify
: static_cast<CssTextAlign>(self->paragraphAlignment);
self->currentTextBlock->setBlockStyle(style);
}
}
}
}
bool ChapterHtmlSlimParser::parseAndBuildPages() {
BlockStyle rootBlockStyle;
rootBlockStyle.alignment = (this->paragraphAlignment == static_cast<uint8_t>(CssTextAlign::None))
? CssTextAlign::Justify
: static_cast<CssTextAlign>(this->paragraphAlignment);
blockStyleStack.clear();
blockStyleStack.reserve(8);
blockStyleStack.push_back(rootBlockStyle);
auto paragraphAlignmentBlockStyle = BlockStyle();
paragraphAlignmentBlockStyle.textAlignDefined = true;
// Resolve None sentinel to Justify for initial block (no CSS context yet)
@@ -65,7 +65,6 @@ class ChapterHtmlSlimParser {
bool hasUnderline = false, underline = false;
};
std::vector<StyleStackEntry> inlineStyleStack;
std::vector<BlockStyle> blockStyleStack;
CssStyle currentCssStyle;
bool effectiveBold = false;
bool effectiveItalic = false;
+5
View File
@@ -1917,7 +1917,12 @@ void GfxRenderer::restoreBwBuffer() {
}
if (missingChunks) {
// Store failed part-way (or was skipped), so we cannot restore BW bytes safely.
// Still cleanup grayscale staging buffers to avoid retaining large temporary
// allocations that can later starve TLS handshakes.
display.cleanupGrayscaleBuffers(frameBuffer);
freeBwBufferChunks();
LOG_ERR("GFX", "BW restore skipped due to missing chunks; cleaned grayscale buffers only");
return;
}
+174 -41
View File
@@ -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.
+13 -1
View File
@@ -70,6 +70,17 @@ class KOReaderSyncClient {
*/
static Error updateProgress(const KOReaderProgress& progress);
/**
* Keep HTTP/TLS session alive across multiple sync requests (GET/PUT).
* Intended for KOReaderSyncActivity to reduce repeated handshake churn.
*/
static void beginPersistentSession();
/**
* Close and release any persistent HTTP/TLS session.
*/
static void endPersistentSession();
/**
* Get human-readable error message (short, for status line).
*/
@@ -100,5 +111,6 @@ class KOReaderSyncClient {
* 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;
static constexpr unsigned MIN_CONTIG_HEAP_FOR_TLS = 36 * 1024;
static constexpr unsigned MIN_CONTIG_HEAP_FOR_TLS_UPLOAD = 34 * 1024;
};