Revert "Merge pull request #25 from jpirnay/fix-pr1582" and refactor koreader
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user