updated bq27220 function for better fcc battery readings; updates to webreader to enable epub downloads to sd

This commit is contained in:
pelgraine
2026-02-25 19:14:56 +11:00
parent 668aff8105
commit ccb4280ae2
5 changed files with 312 additions and 56 deletions

View File

@@ -715,8 +715,10 @@ restart:
// Primary detection is via "VOICE CALL: BEGIN" URC (handled by
// drainURCs/processURCLine above). CLCC polling is a safety net
// in case the URC is missed or delayed.
// Skip when paused to avoid Core 0 contention with WiFi TLS.
// ================================================================
if (_state == ModemState::DIALING &&
if (!_paused &&
_state == ModemState::DIALING &&
millis() - lastCLCCPoll > CLCC_POLL_INTERVAL) {
if (sendAT("AT+CLCC", "OK", 2000)) {
// +CLCC: 1,0,0,0,0,"number",129 — stat field:
@@ -747,8 +749,11 @@ restart:
// ================================================================
// Step 4: SMS and signal polling (only when not in a call)
// Skip when paused to avoid Core 0 contention with WiFi/TLS.
// The modem task's sendAT() calls (AT+CMGL 5s, AT+CSQ 2s) do
// tight UART poll loops that disrupt WiFi packet timing.
// ================================================================
if (!isCallActive()) {
if (!_paused && !isCallActive()) {
// Check for outgoing SMS in queue
SMSOutgoing outMsg;
if (xQueueReceive(_sendQueue, &outMsg, 0) == pdTRUE) {
@@ -766,7 +771,7 @@ restart:
}
// Periodic signal strength update (always, even during calls)
if (millis() - lastCSQPoll > CSQ_POLL_INTERVAL) {
if (!_paused && millis() - lastCSQPoll > CSQ_POLL_INTERVAL) {
// Only poll CSQ if not actively in a call (avoid interrupting audio)
if (!isCallActive()) {
pollCSQ();

View File

@@ -158,6 +158,14 @@ public:
const char* getCallPhone() const { return _callPhone; }
uint32_t getCallStartTime() const { return _callStartTime; }
// Pause/resume polling — used by web reader to avoid Core 0 contention
// during WiFi TLS handshakes. While paused, the task skips AT commands
// (SMS poll, CSQ poll) but still drains URCs and handles call commands
// so incoming calls aren't missed.
void pausePolling() { _paused = true; }
void resumePolling() { _paused = false; }
bool isPaused() const { return _paused; }
static const char* stateToString(ModemState s);
// Persistent enable/disable config (SD file /sms/modem.cfg)
@@ -167,6 +175,7 @@ public:
private:
volatile ModemState _state = ModemState::OFF;
volatile int _csq = 99; // 99 = unknown
volatile bool _paused = false; // Suppresses AT polling when true
char _operator[24] = {0};
// Call state (written by modem task, read by main loop)

View File

@@ -642,8 +642,11 @@ public:
display.drawTextRightAlign(display.width()-1, y, buf);
y += 10;
// Remaining capacity
// Remaining capacity (clamped to design capacity — gauge FCC may be
// stale from factory defaults until a full charge cycle re-learns it)
uint16_t remCap = board.getRemainingCapacity();
uint16_t desCap = board.getDesignCapacity();
if (desCap > 0 && remCap > desCap) remCap = desCap;
display.drawTextLeftAlign(0, y, "remaining cap");
sprintf(buf, "%d mAh", remCap);
display.drawTextRightAlign(display.width()-1, y, buf);

View File

@@ -35,6 +35,9 @@
#include <esp_heap_caps.h>
#include <SD.h>
#include <vector>
#ifdef HAS_4G_MODEM
#include "ModemManager.h"
#endif
#include "Utf8CP437.h"
// Forward declarations
@@ -1104,8 +1107,15 @@ private:
// Fetch state
unsigned long _fetchStartTime;
int _fetchProgress; // Bytes received so far
int _fetchRetryCount; // Current retry attempt (0 = first try)
String _fetchError;
// Persistent TLS client — kept alive between page loads to reuse TCP
// connections to the same host (avoids repeated 5-8s TLS handshakes).
// Destroyed on error, host change, or WiFi disconnect.
WiFiClientSecure* _tlsClient;
String _tlsHost; // Host _tlsClient is connected/configured for
// Download state (for EPUB/file downloads to SD)
char _downloadedFile[64]; // Filename of last downloaded file
bool _downloadOk; // true if download succeeded
@@ -1228,6 +1238,27 @@ private:
return result;
}
// Remove Cloudflare challenge cookies for a domain.
// Stale __cf_bm and _cfuvid tokens from failed handshakes can poison
// subsequent requests, causing Cloudflare to keep returning 525/503.
void clearCloudflareCookies(const char* domain) {
int dst = 0;
for (int i = 0; i < _cookieCount; i++) {
bool isCfDomain = (strstr(domain, _cookies[i].domain) ||
strcmp(domain, _cookies[i].domain) == 0);
bool isCfCookie = (strncmp(_cookies[i].name, "__cf_bm", 7) == 0 ||
strncmp(_cookies[i].name, "_cfuvid", 7) == 0 ||
strncmp(_cookies[i].name, "cf_", 3) == 0);
if (isCfDomain && isCfCookie) {
Serial.printf("WebReader: Cleared CF cookie '%s'\n", _cookies[i].name);
continue; // Skip — don't copy to dst
}
if (dst != i) _cookies[dst] = _cookies[i];
dst++;
}
_cookieCount = dst;
}
// Parse Set-Cookie header(s) from HTTP response
void parseSetCookie(const String& headerVal, const char* domain) {
// Format: name=value; Path=/; ...
@@ -1919,6 +1950,17 @@ private:
const char* referer = nullptr) {
Serial.printf("WebReader: fetchPage('%s', post=%s, ref=%s)\n",
url, postBody ? "yes" : "no", referer ? referer : "(null)");
// Pause modem polling during fetch to avoid Core 0 contention with
// WiFi/TLS. The modem task's 10ms UART poll loop on Core 0 disrupts
// TLS handshakes, causing Cloudflare 525/503 errors.
#ifdef HAS_4G_MODEM
struct ModemPauseGuard {
ModemPauseGuard() { modemManager.pausePolling(); Serial.println("WebReader: modem paused"); }
~ModemPauseGuard() { modemManager.resumePolling(); Serial.println("WebReader: modem resumed"); }
} _modemGuard;
#endif
if (!allocateBuffers()) {
_fetchError = "Out of memory";
_mode = HOME;
@@ -1928,6 +1970,7 @@ private:
_mode = FETCHING;
_fetchStartTime = millis();
_fetchProgress = 0;
_fetchRetryCount = 0;
_fetchError = "";
// Push current URL to back stack (if we have one)
@@ -1970,16 +2013,44 @@ private:
const int maxRedirects = 5;
bool isPost = (postBody != nullptr);
// Pre-create TLS client outside loop for connection reuse
// Use persistent TLS client (member variable) — survives across
// fetchPage() calls for connection reuse to the same host.
ensureTlsUsesPsram();
WiFiClientSecure* tlsClient = new WiFiClientSecure();
tlsClient->setInsecure();
tlsClient->setHandshakeTimeout(30);
String lastHost = ""; // Track host for connection reuse
int connRetries = 0; // Connection failure retries (max 1)
int serverRetries = 0; // 5xx error retries (max 1)
int totalRetries = 0; // Combined conn + server retries (max 4)
bool needsFreshTls = false; // Deferred TLS client recreation
while (redirectCount <= maxRedirects) {
// Determine current host for TLS session management
bool isHttps = currentUrl.startsWith("https://");
String currentHost;
if (isHttps) {
currentHost = currentUrl.substring(8); // skip "https://"
int slashIdx = currentHost.indexOf('/');
if (slashIdx > 0) currentHost = currentHost.substring(0, slashIdx);
}
// Unified TLS client creation/recreation.
// This runs BEFORE 'HTTPClient http' below is constructed, so it's safe
// to delete _tlsClient (no stack-local HTTPClient holds a reference yet).
// Triggers: error recovery, host change, first use, stale connection.
bool tlsStale = (_tlsClient && !_tlsClient->connected());
if (tlsStale) {
Serial.println("WebReader: TLS connection closed by server, reconnecting");
}
if (isHttps && (needsFreshTls || !_tlsClient || _tlsHost != currentHost || tlsStale)) {
if (_tlsClient) {
_tlsClient->stop();
delete _tlsClient;
}
_tlsClient = new WiFiClientSecure();
_tlsClient->setInsecure();
_tlsClient->setHandshakeTimeout(15);
_tlsHost = currentHost;
needsFreshTls = false;
Serial.printf("WebReader: New TLS session for %s (heap: %d, largest: %d)\n",
currentHost.c_str(), ESP.getFreeHeap(), ESP.getMaxAllocHeap());
}
// Update domain and cookies for current URL
extractDomain(currentUrl.c_str(), domain, sizeof(domain));
cookieHeader = buildCookieHeader(domain);
@@ -1989,31 +2060,15 @@ private:
Serial.printf("WebReader: heap: %d, largest: %d\n",
ESP.getFreeHeap(), ESP.getMaxAllocHeap());
bool isHttps = currentUrl.startsWith("https://");
HTTPClient http;
http.setUserAgent(WEB_USER_AGENT);
http.setFollowRedirects(HTTPC_DISABLE_FOLLOW_REDIRECTS);
http.setTimeout(30000);
http.setTimeout(15000); // 15s — fail fast to allow retries
http.setReuse(true); // Keep connection alive for redirects
bool beginOk;
if (isHttps) {
// Check if we need a fresh TLS client (different host)
String currentHost = currentUrl.substring(8); // skip "https://"
int slashIdx = currentHost.indexOf('/');
if (slashIdx > 0) currentHost = currentHost.substring(0, slashIdx);
if (lastHost.length() > 0 && lastHost != currentHost) {
// Different host — need fresh TLS client
delete tlsClient;
tlsClient = new WiFiClientSecure();
tlsClient->setInsecure();
tlsClient->setHandshakeTimeout(30);
}
lastHost = currentHost;
beginOk = http.begin(*tlsClient, currentUrl);
beginOk = http.begin(*_tlsClient, currentUrl);
} else {
beginOk = http.begin(currentUrl);
}
@@ -2086,17 +2141,42 @@ private:
// Capture all Set-Cookie headers from this response
captureResponseCookies(http, domain);
// Connection-level failure (timeout, TLS error, etc) — retry once
if (httpCode < 0 && connRetries < 1) {
// Connection-level failure (timeout, TLS error, etc)
if (httpCode < 0 && totalRetries < 4) {
http.end();
// 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);
delay(2000);
totalRetries++;
_fetchRetryCount++;
needsFreshTls = true; // Recreate at top of next iteration (after http destructor)
// After 2 failures, try WiFi reconnect — the lwIP stack may be wedged
if (totalRetries == 2 && isWiFiConnected()) {
Serial.println("WebReader: WiFi reconnect after persistent failures");
// Destroy TLS before WiFi teardown
if (_tlsClient) { delete _tlsClient; _tlsClient = nullptr; }
_tlsHost = "";
WiFi.disconnect(false);
delay(500);
WiFi.reconnect();
unsigned long wt = millis();
while (WiFi.status() != WL_CONNECTED && millis() - wt < 8000) delay(100);
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WebReader: WiFi reconnect failed");
_fetchError = "WiFi reconnect failed";
break;
}
Serial.printf("WebReader: WiFi reconnected, IP: %s\n",
WiFi.localIP().toString().c_str());
}
int retryDelay = 1000 + totalRetries * 1000; // 2s, 3s, 4s, 5s
Serial.printf("WebReader: Connection error %d, retrying in %dms... (attempt %d/4)\n",
httpCode, retryDelay, totalRetries);
if (_display) {
_display->startFrame();
renderFetching(*_display);
_display->endFrame();
}
delay(retryDelay);
continue;
}
if (httpCode < 0) {
@@ -2108,10 +2188,11 @@ private:
// Handle redirects
if (httpCode == 301 || httpCode == 302 || httpCode == 303 || httpCode == 307) {
String location = http.header("Location");
// Don't call http.end() for same-host redirects — preserve connection
String body = http.getString(); // consume response body to free connection
// End the connection — the next loop iteration creates a fresh
// HTTPClient. Don't use getString() to "consume" the body because
// chunked responses without proper termination can hang forever.
http.end();
if (location.length() == 0) {
http.end();
_fetchError = "Redirect with no Location";
break;
}
@@ -2138,7 +2219,7 @@ private:
Serial.println("WebReader: EPUB detected, downloading to SD");
free(htmlBuffer);
bool dlOk = downloadToSD(http, currentUrl.c_str(), cdisp);
delete tlsClient;
// _tlsClient persists as member — cleaned up by exitReader()
return dlOk;
}
@@ -2146,14 +2227,48 @@ private:
success = (htmlLen > 0);
http.end();
break;
} else if (httpCode >= 500 && httpCode < 600 && serverRetries < 1) {
// Server error (e.g. Cloudflare 525) — retry once after brief delay
String body = http.getString();
} else if (httpCode >= 500 && httpCode < 600 && totalRetries < 4) {
// Server error (e.g. Cloudflare 525/503)
// Don't call getString() — CF error responses can use chunked encoding
// without proper termination, causing getString() to block forever.
// http.end() forcefully closes the socket which is fine since we're
// recreating the TLS client anyway.
http.end();
serverRetries++;
redirectCount++;
Serial.printf("WebReader: Server error %d, retrying...\n", httpCode);
delay(1500);
needsFreshTls = true; // Recreate at top of next iteration
totalRetries++;
_fetchRetryCount++;
// Clear Cloudflare cookies — stale __cf_bm tokens from failed
// handshakes cause CF to keep rejecting us
clearCloudflareCookies(domain);
// WiFi reconnect after 2 total failures (same logic as conn errors)
if (totalRetries == 2 && isWiFiConnected()) {
Serial.println("WebReader: WiFi reconnect after persistent failures");
if (_tlsClient) { delete _tlsClient; _tlsClient = nullptr; }
_tlsHost = "";
WiFi.disconnect(false);
delay(500);
WiFi.reconnect();
unsigned long wt = millis();
while (WiFi.status() != WL_CONNECTED && millis() - wt < 8000) delay(100);
if (WiFi.status() != WL_CONNECTED) {
_fetchError = "WiFi reconnect failed";
break;
}
Serial.printf("WebReader: WiFi reconnected, IP: %s\n",
WiFi.localIP().toString().c_str());
}
int retryDelay = 1000 + totalRetries * 1000; // 2s, 3s, 4s, 5s
Serial.printf("WebReader: Server error %d, retrying in %dms... (attempt %d/4)\n",
httpCode, retryDelay, totalRetries);
if (_display) {
_display->startFrame();
renderFetching(*_display);
_display->endFrame();
}
delay(retryDelay);
continue;
} else {
_fetchError = httpErrorString(httpCode);
@@ -2162,7 +2277,8 @@ private:
}
} // end redirect loop
delete tlsClient;
// Don't delete _tlsClient — keep for connection reuse on next fetchPage().
// Cleaned up by exitReader() or on next fetch to a different host.
if (redirectCount > maxRedirects && !success) {
_fetchError = "Too many redirects";
@@ -2968,9 +3084,12 @@ private:
display.print(urlDisp);
display.setCursor(10, 60);
char progBuf[40];
char progBuf[48];
int elapsed = (int)((millis() - _fetchStartTime) / 1000);
if (_fetchProgress > 0) {
if (_fetchRetryCount > 0) {
snprintf(progBuf, sizeof(progBuf), "Retry %d/4... %ds", _fetchRetryCount, elapsed);
display.setColor(DisplayDriver::YELLOW);
} else if (_fetchProgress > 0) {
snprintf(progBuf, sizeof(progBuf), "%d bytes (%ds)", _fetchProgress, elapsed);
} else if (elapsed >= 2) {
snprintf(progBuf, sizeof(progBuf), "Connecting... %ds", elapsed);
@@ -4694,7 +4813,8 @@ public:
_formCount(0), _forms(nullptr), _activeForm(-1), _activeField(0),
_formFieldEditing(false), _formEditLen(0), _formLastCharAt(0),
_cookies(nullptr), _cookieCount(0),
_fetchStartTime(0), _fetchProgress(0),
_fetchStartTime(0), _fetchProgress(0), _fetchRetryCount(0),
_tlsClient(nullptr),
_downloadOk(false),
_requestTextReader(false),
_ircClient(nullptr), _ircUseTLS(false), _ircConnected(false), _ircRegistered(false),
@@ -4734,6 +4854,7 @@ public:
_ircClient = nullptr;
}
if (_ircMessages) { free(_ircMessages); _ircMessages = nullptr; }
if (_tlsClient) { delete _tlsClient; _tlsClient = nullptr; }
}
// Called when entering the web reader screen
@@ -4828,6 +4949,10 @@ public:
_fetchError = String();
_connectedSSID = String();
// Destroy persistent TLS client before WiFi shutdown
if (_tlsClient) { delete _tlsClient; _tlsClient = nullptr; }
_tlsHost = String();
// Shut down WiFi to reclaim ~50-70KB internal RAM
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);

View File

@@ -177,7 +177,85 @@ bool TDeckBoard::configureFuelGauge(uint16_t designCapacity_mAh) {
Serial.printf("BQ27220: Design Capacity = %d mAh (target %d)\n", currentDC, designCapacity_mAh);
if (currentDC == designCapacity_mAh) {
Serial.println("BQ27220: Design Capacity already correct, skipping");
// Design Capacity correct, but check if Full Charge Capacity is sane.
// After a Design Capacity change, FCC may still hold the old factory
// value (e.g. 3000 mAh) until a RESET forces reinitialization.
uint16_t fcc = bq27220_read16(BQ27220_REG_FULL_CAP);
Serial.printf("BQ27220: Design Capacity already correct, FCC=%d mAh\n", fcc);
if (fcc >= designCapacity_mAh * 3 / 2) {
// FCC is >=150% of design — stale from factory defaults.
// The gauge derives FCC from Design Energy (not just Design Capacity).
// Design Energy = capacity × nominal voltage (3.7V for LiPo).
// If Design Energy still reflects 3000 mAh, FCC stays at 3000.
// Fix: enter CFG_UPDATE and write correct Design Energy.
Serial.printf("BQ27220: FCC %d >> DC %d, updating Design Energy\n",
fcc, designCapacity_mAh);
uint16_t designEnergy = (uint16_t)((uint32_t)designCapacity_mAh * 37 / 10);
Serial.printf("BQ27220: Target Design Energy = %d mWh\n", designEnergy);
// Unseal
bq27220_writeControl(0x0414); delay(2);
bq27220_writeControl(0x3672); delay(2);
// Full Access
bq27220_writeControl(0xFFFF); delay(2);
bq27220_writeControl(0xFFFF); delay(2);
// Enter CFG_UPDATE
bq27220_writeControl(0x0090);
bool ready = false;
for (int i = 0; i < 50; i++) {
delay(20);
uint16_t opSt = bq27220_read16(BQ27220_REG_OP_STATUS);
if (opSt & 0x0400) { ready = true; break; }
}
if (ready) {
// Design Energy is at data memory address 0x92A1 (2 bytes after DC at 0x929F)
// Read old values for checksum calculation
Wire.beginTransmission(BQ27220_I2C_ADDR);
Wire.write(0x3E); Wire.write(0xA1); Wire.write(0x92);
Wire.endTransmission();
delay(10);
uint8_t oldMSB = bq27220_read8(0x40);
uint8_t oldLSB = bq27220_read8(0x41);
uint8_t oldChk = bq27220_read8(0x60);
uint8_t dLen = bq27220_read8(0x61);
uint8_t newMSB = (designEnergy >> 8) & 0xFF;
uint8_t newLSB = designEnergy & 0xFF;
uint8_t temp = (255 - oldChk - oldMSB - oldLSB);
uint8_t newChk = 255 - ((temp + newMSB + newLSB) & 0xFF);
Serial.printf("BQ27220: DE old=0x%02X%02X new=0x%02X%02X chk=0x%02X\n",
oldMSB, oldLSB, newMSB, newLSB, newChk);
// Write new Design Energy
Wire.beginTransmission(BQ27220_I2C_ADDR);
Wire.write(0x3E); Wire.write(0xA1); Wire.write(0x92);
Wire.write(newMSB); Wire.write(newLSB);
Wire.endTransmission();
delay(5);
// Write checksum
Wire.beginTransmission(BQ27220_I2C_ADDR);
Wire.write(0x60); Wire.write(newChk); Wire.write(dLen);
Wire.endTransmission();
delay(10);
// Exit CFG_UPDATE with reinit
bq27220_writeControl(0x0091);
delay(200);
Serial.println("BQ27220: Design Energy updated, exited CFG_UPDATE");
} else {
Serial.println("BQ27220: Failed to enter CFG_UPDATE for DE fix");
bq27220_writeControl(0x0092); // Exit cleanly
}
// Seal
bq27220_writeControl(0x0030);
delay(5);
fcc = bq27220_read16(BQ27220_REG_FULL_CAP);
Serial.printf("BQ27220: FCC after Design Energy update: %d mAh\n", fcc);
}
return true;
}
@@ -281,6 +359,39 @@ bool TDeckBoard::configureFuelGauge(uint16_t designCapacity_mAh) {
Serial.printf("BQ27220: Verify in CFGUPDATE: DC bytes=0x%02X 0x%02X (%d mAh)\n",
verMSB, verLSB, (verMSB << 8) | verLSB);
// Step 4g: Also update Design Energy (address 0x92A1) while in CFG_UPDATE.
// Design Energy = capacity × 3.7V (nominal LiPo voltage).
// The gauge uses both DC and DE to compute Full Charge Capacity.
{
uint16_t designEnergy = (uint16_t)((uint32_t)designCapacity_mAh * 37 / 10);
Wire.beginTransmission(BQ27220_I2C_ADDR);
Wire.write(0x3E); Wire.write(0xA1); Wire.write(0x92);
Wire.endTransmission();
delay(10);
uint8_t deOldMSB = bq27220_read8(0x40);
uint8_t deOldLSB = bq27220_read8(0x41);
uint8_t deOldChk = bq27220_read8(0x60);
uint8_t deLen = bq27220_read8(0x61);
uint8_t deNewMSB = (designEnergy >> 8) & 0xFF;
uint8_t deNewLSB = designEnergy & 0xFF;
uint8_t deTemp = (255 - deOldChk - deOldMSB - deOldLSB);
uint8_t deNewChk = 255 - ((deTemp + deNewMSB + deNewLSB) & 0xFF);
Serial.printf("BQ27220: Design Energy: old=%d new=%d mWh\n",
(deOldMSB << 8) | deOldLSB, designEnergy);
Wire.beginTransmission(BQ27220_I2C_ADDR);
Wire.write(0x3E); Wire.write(0xA1); Wire.write(0x92);
Wire.write(deNewMSB); Wire.write(deNewLSB);
Wire.endTransmission();
delay(5);
Wire.beginTransmission(BQ27220_I2C_ADDR);
Wire.write(0x60); Wire.write(deNewChk); Wire.write(deLen);
Wire.endTransmission();
delay(10);
}
// Step 5: Exit CFG_UPDATE (with reinit to apply changes immediately)
bq27220_writeControl(0x0091); // EXIT_CFG_UPDATE_REINIT
Serial.println("BQ27220: Sent EXIT_CFG_UPDATE_REINIT, waiting...");
@@ -291,13 +402,16 @@ bool TDeckBoard::configureFuelGauge(uint16_t designCapacity_mAh) {
Serial.printf("BQ27220: Design Capacity now reads %d mAh (expected %d)\n",
verifyDC, designCapacity_mAh);
uint16_t newFCC = bq27220_read16(BQ27220_REG_FULL_CAP);
Serial.printf("BQ27220: Full Charge Capacity: %d mAh\n", newFCC);
if (verifyDC == designCapacity_mAh) {
Serial.println("BQ27220: Configuration SUCCESS");
} else {
Serial.println("BQ27220: Configuration FAILED");
}
// Step 7: Seal the device
// Step 6: Seal the device
bq27220_writeControl(0x0030);
delay(5);