fix ble error loop crash in serialbleinterface and main; same ble crash fix in webreaderscreen

This commit is contained in:
pelgraine
2026-02-24 02:17:42 +11:00
parent ea04d515ea
commit d5b79cf0b4
3 changed files with 102 additions and 25 deletions

View File

@@ -1400,11 +1400,18 @@ void handleKeyboardInput() {
Serial.printf("WebReader: heap BEFORE BT release: free=%d, largest=%d\n",
ESP.getFreeHeap(), ESP.getMaxAllocHeap());
// 1) Stop BLE controller (disable + deinit)
// 1) Gracefully disable BLE interface (stop advertising, disconnect, clear queues)
// Must happen BEFORE btStop() while BLE objects are still valid
#ifdef BLE_PIN_CODE
serial_interface.disable();
delay(50);
#endif
// 2) Stop BLE controller (disable + deinit)
btStop();
delay(50);
// 2) Release the BT controller's reserved memory region back to heap
// 3) Release the BT controller's reserved memory region back to heap
esp_bt_controller_mem_release(ESP_BT_MODE_BTDM);
delay(50);

View File

@@ -442,6 +442,7 @@ inline ParseResult parseHtml(const char* html, int htmlLen,
char pendingLabel[48] = {0};
bool inLabel = false;
int labelTextStart = 0;
int listDepth = 0; // Nesting depth of <ul>/<ol>/<dl> (for smart <li> formatting)
// Find <body> tag to skip <head> section
for (int i = 0; i < htmlLen - 6; i++) {
@@ -533,26 +534,50 @@ inline ParseResult parseHtml(const char* html, int htmlLen,
}
}
// Track <ul>/<ol>/<dl> nesting depth for smart <li> formatting
if (tagNameLen == 2) {
bool isList = (tagName[0] == 'u' && tagName[1] == 'l') ||
(tagName[0] == 'o' && tagName[1] == 'l') ||
(tagName[0] == 'd' && tagName[1] == 'l');
if (isList) {
if (isClosing) { if (listDepth > 0) listDepth--; }
else listDepth++;
}
}
// Handle <h1>-<h6> opening: ensure line break before heading
if (!isClosing && tagNameLen == 2 && tagName[0] == 'h' &&
tagName[1] >= '1' && tagName[1] <= '6') {
if (ti < textMax - 2) {
if (ti < textMax - 12) {
if (!lastWasBreak && ti > 0) {
textOut[ti++] = '\n';
}
// Separator line before h4 headings (work titles on listing pages)
if (tagName[1] == '4' && ti > 1) {
const char* sep = "--------";
for (int s = 0; sep[s]; s++) textOut[ti++] = sep[s];
textOut[ti++] = '\n';
}
// Double break before h1/h2 for visual separation
if (tagName[1] <= '2' && ti > 0 && ti < textMax - 1) {
textOut[ti++] = '\n';
}
// Heading markers for h1-h4 (renderer draws underline)
if (tagName[1] <= '4') {
textOut[ti++] = '\x01';
}
lastWasBreak = true;
lastWasSpace = false;
}
}
// Handle closing </h1>-</h6>: line break after heading
// Handle closing </h1>-</h6>: heading end marker + line break
if (isClosing && tagNameLen == 2 && tagName[0] == 'h' &&
tagName[1] >= '1' && tagName[1] <= '6') {
if (ti < textMax - 1) {
if (ti < textMax - 2) {
if (tagName[1] <= '4') {
textOut[ti++] = '\x02'; // Heading end marker
}
textOut[ti++] = '\n';
lastWasBreak = true;
lastWasSpace = false;
@@ -607,18 +632,15 @@ inline ParseResult parseHtml(const char* html, int htmlLen,
currentHref[0] = '\0';
}
// Handle <li> - comma-separated inline flow (compact for tag lists, pagination)
// Handle <li> - always comma-separated inline flow
if (!isClosing && tagNameLen == 2 && tagName[0] == 'l' && tagName[1] == 'i') {
if (ti < textMax - 3) {
// Trim trailing whitespace from buffer (between </li> and <li>)
while (ti > 0 && textOut[ti-1] == ' ') ti--;
// Add comma separator if continuing from a previous item
if (ti > 0 && textOut[ti-1] != '\n' && textOut[ti-1] != ',') {
textOut[ti++] = ',';
textOut[ti++] = ' ';
lastWasSpace = true;
} else if (ti > 0 && textOut[ti-1] == ',') {
// Previous item was empty — comma exists, just add space
textOut[ti++] = ' ';
lastWasSpace = true;
}
@@ -1066,7 +1088,7 @@ private:
unsigned long _toastTime; // millis() when toast was set
// Forms
WebForm _forms[WEB_MAX_FORMS];
WebForm* _forms; // PSRAM allocated
int _formCount;
int _activeForm; // Which form is being filled (-1 = none)
int _activeField; // Which field in the active form (index into visible fields)
@@ -1082,7 +1104,7 @@ private:
char name[64];
char value[512]; // AO3 session cookies are 300+ chars of base64
};
Cookie _cookies[WEB_MAX_COOKIES];
Cookie* _cookies; // PSRAM allocated
int _cookieCount;
// Fetch state
@@ -1841,12 +1863,10 @@ private:
// Connection-level failure (timeout, TLS error, etc) — retry once
if (httpCode < 0 && connRetries < 1) {
http.end();
// Destroy and recreate TLS client — old connection state is broken
delete tlsClient;
tlsClient = new WiFiClientSecure();
tlsClient->setInsecure();
tlsClient->setHandshakeTimeout(30);
lastHost = ""; // Force fresh connection
// Reset TLS client — stop connection but keep the object alive
// (HTTPClient destructor will access it when 'http' goes out of scope)
tlsClient->stop();
lastHost = ""; // Force fresh connection on next begin()
connRetries++;
redirectCount++;
Serial.printf("WebReader: Connection error %d, retrying...\n", httpCode);
@@ -2610,6 +2630,13 @@ private:
int pos = pageStart;
int maxY = display.height() - _footerHeight - _lineHeight;
// Check if page starts inside a heading (marker from previous page)
bool inHeading = false;
for (int scan = 0; scan < pageStart && scan < _textLen; scan++) {
if (_textBuffer[scan] == '\x01') inHeading = true;
if (_textBuffer[scan] == '\x02') inHeading = false;
}
while (pos < pageEnd && lineCount < _linesPerPage && y <= maxY) {
int oldPos = pos;
WebWrapResult wrap = webFindLineBreak(_textBuffer, pageEnd, pos, _charsPerLine);
@@ -2618,12 +2645,25 @@ private:
display.setCursor(0, y);
// Check if this line contains heading text (for underline)
bool lineHasHeading = inHeading;
if (!lineHasHeading) {
for (int scan = pos; scan < wrap.lineEnd && scan < pageEnd; scan++) {
if (_textBuffer[scan] == '\x01') { lineHasHeading = true; break; }
}
}
// Render characters with UTF-8/CP437 handling
char charStr[2] = {0, 0};
int j = pos;
while (j < wrap.lineEnd && j < pageEnd) {
uint8_t b = (uint8_t)_textBuffer[j];
// Heading markers: \x01 = start, \x02 = end
if (b == 0x01) { inHeading = true; j++; continue; }
if (b == 0x02) { inHeading = false; j++; continue; }
if (b < 32) { j++; continue; }
// Detect link markers [N] and render in different color
@@ -2688,6 +2728,22 @@ private:
}
}
// Draw underline below the last line of a heading
// (the line where \x02 marker ends the heading, or heading continues to next line)
bool headingEndsHere = false;
if (lineHasHeading) {
// Check if heading ends on this line
for (int scan = pos; scan < wrap.lineEnd && scan < pageEnd; scan++) {
if (_textBuffer[scan] == '\x02') { headingEndsHere = true; break; }
}
// Also underline if this is the last line of the page and still in heading
if (!headingEndsHere && wrap.nextStart >= pageEnd) headingEndsHere = true;
}
if (headingEndsHere) {
display.setColor(DisplayDriver::LIGHT);
display.drawRect(0, y + _lineHeight - 1, display.width(), 1);
}
y += _lineHeight;
lineCount++;
pos = wrap.nextStart;
@@ -2954,7 +3010,7 @@ private:
if (c == 'x' || c == 'X') {
bool hadData = (_cookieCount > 0 || !_history.empty());
_cookieCount = 0;
memset(_cookies, 0, sizeof(_cookies));
memset(_cookies, 0, sizeof(Cookie) * WEB_MAX_COOKIES);
_history.clear();
saveHistory();
Serial.println("WebReader: Cookies and history cleared");
@@ -4238,8 +4294,9 @@ public:
_currentPage(0), _totalPages(0),
_homeSelected(0), _urlEditing(false),
_linkInput(0), _linkInputActive(false),
_formCount(0), _activeForm(-1), _activeField(0),
_formFieldEditing(false), _formEditLen(0), _formLastCharAt(0), _cookieCount(0),
_formCount(0), _forms(nullptr), _activeForm(-1), _activeField(0),
_formFieldEditing(false), _formEditLen(0), _formLastCharAt(0),
_cookies(nullptr), _cookieCount(0),
_fetchStartTime(0), _fetchProgress(0),
_ircClient(nullptr), _ircUseTLS(false), _ircConnected(false), _ircRegistered(false),
_ircJoined(false), _ircPort(6697), _ircMessages(nullptr),
@@ -4259,13 +4316,18 @@ public:
_ircCompose[0] = '\0';
_ircSetupBuf[0] = '\0';
_ircLineBuf[0] = '\0';
memset(_forms, 0, sizeof(_forms));
memset(_cookies, 0, sizeof(_cookies));
_toastMsg[0] = '\0';
_toastTime = 0;
// Allocate forms and cookies in PSRAM to free internal heap for TLS
_forms = (WebForm*)ps_calloc(WEB_MAX_FORMS, sizeof(WebForm));
_cookies = (Cookie*)ps_calloc(WEB_MAX_COOKIES, sizeof(Cookie));
loadIRCConfig();
}
~WebReaderScreen() {
freeBuffers();
if (_forms) { free(_forms); _forms = nullptr; }
if (_cookies) { free(_cookies); _cookies = nullptr; }
if (_ircClient) {
if (_ircClient->connected()) _ircClient->stop();
delete _ircClient;

View File

@@ -147,15 +147,21 @@ void SerialBLEInterface::enable() {
}
void SerialBLEInterface::disable() {
bool wasEnabled = _isEnabled;
_isEnabled = false;
BLE_DEBUG_PRINTLN("SerialBLEInterface::disable");
pServer->getAdvertising()->stop();
pServer->disconnect(last_conn_id);
pService->stop();
// Only try BLE operations if we were previously enabled
// (avoids accessing dead BLE objects after btStop/mem_release)
if (wasEnabled && pServer) {
pServer->getAdvertising()->stop();
pServer->disconnect(last_conn_id);
pService->stop();
}
oldDeviceConnected = deviceConnected = false;
adv_restart_time = 0;
clearBuffers();
}
size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
@@ -186,6 +192,8 @@ bool SerialBLEInterface::isWriteBusy() const {
}
size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
if (!_isEnabled) return 0; // BLE disabled — skip all BLE operations
if (send_queue_len > 0 // first, check send queue
&& millis() >= _last_write + BLE_WRITE_MIN_INTERVAL // space the writes apart
) {