mirror of
https://github.com/pelgraine/Meck.git
synced 2026-03-28 17:42:44 +01:00
fix ble error loop crash in serialbleinterface and main; same ble crash fix in webreaderscreen
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user