fix: webserver stability, memory leaks, epub underlining, flash screen
Webserver: - Remove MD5 hash computation from file listings (caused EAGAIN errors) - Implement JSON batching (2KB) with pacing for file listings - Simplify sendContentSafe() flow control Memory: - Cache QR codes on server start instead of regenerating per render - Optimize WiFi scan: vector deduplication, 20 network limit, early scanDelete() - Fix 48KB cover buffer leak when navigating Home -> File Transfer EPUB Reader: - Fix errant underlining by flushing partWordBuffer before style changes - Text before styled inline elements (e.g., <a> with CSS underline) no longer incorrectly receives the element's styling Flash Screen: - Fix version string buffer overflow (30 -> 50 char limit) - Use half refresh for cleaner display - Adjust pre_flash.py timing for half refresh completion
This commit is contained in:
parent
48267ad848
commit
5464d9de3a
@ -6,6 +6,45 @@ Base: CrossPoint Reader 0.15.0
|
||||
|
||||
---
|
||||
|
||||
## ef-1.0.5
|
||||
|
||||
**Stability & Memory Improvements**
|
||||
|
||||
### Bug Fixes - Webserver
|
||||
|
||||
- **File Transfer Stability**: Removed blocking MD5 hash computation from file listings that caused EAGAIN errors and connection stalls
|
||||
- **JSON Batching**: Implemented 2KB batch streaming for file listings with pacing to prevent TCP buffer overflow
|
||||
- **Simplified Flow Control**: Removed unnecessary yield/delay logic from content streaming
|
||||
|
||||
### Bug Fixes - Memory
|
||||
|
||||
- **QR Code Caching**: Generate QR codes once on server start instead of regenerating on each screen render
|
||||
- **WiFi Scan Optimization**: Replaced memory-heavy `std::map` deduplication with in-place vector search, limited results to 20 networks, earlier `WiFi.scanDelete()` for faster memory recovery
|
||||
- **Cover Buffer Leak**: Fixed 48KB memory leak when navigating from Home to File Transfer (cover buffer now explicitly freed)
|
||||
|
||||
### Bug Fixes - EPUB Reader
|
||||
|
||||
- **Errant Underlining**: Fixed words before styled inline elements (like `<a>` tags with CSS underline) incorrectly receiving the element's style by flushing the text buffer before style changes
|
||||
|
||||
### Bug Fixes - Flashing Screen
|
||||
|
||||
- **Version String Overflow**: Fixed flash notification parsing failing on longer version strings (buffer limit increased from 30 to 50 characters)
|
||||
- **Display Quality**: Changed flashing screen to half refresh for cleaner appearance
|
||||
- **Timing**: Adjusted pre-flash script timing for half refresh completion
|
||||
|
||||
### Files Changed
|
||||
|
||||
- `src/main.cpp` - flash screen fixes, cover buffer free on File Transfer entry
|
||||
- `scripts/pre_flash.py` - timing adjustments for full refresh
|
||||
- `src/network/CrossPointWebServer.cpp` - JSON batching, removed MD5 from listings
|
||||
- `src/network/CrossPointWebServer.h` - removed md5 from FileInfo, simplified sendContentSafe
|
||||
- `src/activities/network/CrossPointWebServerActivity.cpp` - QR code caching
|
||||
- `src/activities/network/CrossPointWebServerActivity.h` - QR code cache members
|
||||
- `src/activities/network/WifiSelectionActivity.cpp` - WiFi scan memory optimization
|
||||
- `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` - flush buffer before style changes
|
||||
|
||||
---
|
||||
|
||||
## ef-1.0.4
|
||||
|
||||
**EPUB Rendering & Stability**
|
||||
|
||||
@ -485,6 +485,9 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
}
|
||||
}
|
||||
} else if (matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS)) {
|
||||
// Flush buffer with CURRENT style before changing effective style
|
||||
self->flushPartWordBuffer();
|
||||
|
||||
self->underlineUntilDepth = std::min(self->underlineUntilDepth, self->depth);
|
||||
// Push inline style entry for underline tag
|
||||
StyleStackEntry entry;
|
||||
@ -502,6 +505,9 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
self->inlineStyleStack.push_back(entry);
|
||||
self->updateEffectiveInlineStyle();
|
||||
} else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
|
||||
// Flush buffer with CURRENT style before changing effective style
|
||||
self->flushPartWordBuffer();
|
||||
|
||||
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
||||
// Push inline style entry for bold tag
|
||||
StyleStackEntry entry;
|
||||
@ -519,6 +525,9 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
self->inlineStyleStack.push_back(entry);
|
||||
self->updateEffectiveInlineStyle();
|
||||
} else if (matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS)) {
|
||||
// Flush buffer with CURRENT style before changing effective style
|
||||
self->flushPartWordBuffer();
|
||||
|
||||
self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth);
|
||||
// Push inline style entry for italic tag
|
||||
StyleStackEntry entry;
|
||||
@ -538,6 +547,10 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
} else if (strcmp(name, "span") == 0 || !isBlockElement) {
|
||||
// Handle span and other inline elements for CSS styling
|
||||
if (cssStyle.hasFontWeight() || cssStyle.hasFontStyle() || cssStyle.hasTextDecoration()) {
|
||||
// Flush buffer with CURRENT style before changing effective style
|
||||
// This prevents text accumulated before this element from getting the new style
|
||||
self->flushPartWordBuffer();
|
||||
|
||||
StyleStackEntry entry;
|
||||
entry.depth = self->depth; // Track depth for matching pop
|
||||
if (cssStyle.hasFontWeight()) {
|
||||
@ -573,6 +586,33 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
||||
self->lastCharDataOffset = XML_GetCurrentByteIndex(self->xmlParser);
|
||||
}
|
||||
|
||||
// If we're inside an <li> but no text block was created yet (direct text without inner <p>),
|
||||
// create a text block and add the list marker now
|
||||
if (self->insideListItem && !self->listItemHasContent) {
|
||||
// Apply left margin for list items
|
||||
CssStyle cssStyle;
|
||||
cssStyle.marginLeft = 24.0f; // Default indent (~1.5em at 16px base)
|
||||
cssStyle.defined.marginLeft = 1;
|
||||
|
||||
BlockStyle blockStyle = createBlockStyleFromCss(cssStyle);
|
||||
self->startNewTextBlock(static_cast<TextBlock::Style>(self->paragraphAlignment), blockStyle);
|
||||
|
||||
// Add the list marker
|
||||
if (!self->listStack.empty()) {
|
||||
const ListContext& ctx = self->listStack.back();
|
||||
if (ctx.isOrdered) {
|
||||
std::string marker = std::to_string(ctx.counter) + ". ";
|
||||
self->currentTextBlock->addWord(marker, EpdFontFamily::REGULAR);
|
||||
} else {
|
||||
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
||||
}
|
||||
} else {
|
||||
// No list context (orphan li), use bullet as fallback
|
||||
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
||||
}
|
||||
self->listItemHasContent = true;
|
||||
}
|
||||
|
||||
// Determine font style from depth-based tracking and CSS effective style
|
||||
const bool isBold = self->boldUntilDepth < self->depth || self->effectiveBold;
|
||||
const bool isItalic = self->italicUntilDepth < self->depth || self->effectiveItalic;
|
||||
|
||||
@ -5,7 +5,11 @@ This allows the firmware to display "Flashing firmware..." on the e-ink display
|
||||
before the actual flash begins. The e-ink retains this message throughout the
|
||||
flash process since it doesn't require power to maintain the display.
|
||||
|
||||
Protocol: Sends "FLASH:version\n" where version is read from platformio.ini
|
||||
Protocol (Plan A - Simple timing):
|
||||
1. Host opens serial port and sends "FLASH:version"
|
||||
2. Host keeps port open briefly for device to receive and process
|
||||
3. Device displays flash screen when it receives the command
|
||||
4. Host proceeds with flash
|
||||
"""
|
||||
|
||||
Import("env")
|
||||
@ -15,7 +19,7 @@ from version_utils import get_version
|
||||
|
||||
|
||||
def before_upload(source, target, env):
|
||||
"""Send FLASH command with version to device before upload begins."""
|
||||
"""Send FLASH command to device before uploading firmware."""
|
||||
port = env.GetProjectOption("upload_port", None)
|
||||
|
||||
if not port:
|
||||
@ -29,19 +33,20 @@ def before_upload(source, target, env):
|
||||
]
|
||||
port = ports[0] if ports else None
|
||||
|
||||
if port:
|
||||
try:
|
||||
version = get_version(env)
|
||||
ser = serial.Serial(port, 115200, timeout=1)
|
||||
ser.write(f"FLASH:{version}\n".encode())
|
||||
ser.flush()
|
||||
ser.close()
|
||||
time.sleep(0.8) # Wait for e-ink fast refresh (~500ms) plus margin
|
||||
print(f"[pre_flash] Flash notification sent to {port} (version {version})")
|
||||
except Exception as e:
|
||||
print(f"[pre_flash] Notification skipped: {e}")
|
||||
else:
|
||||
if not port:
|
||||
print("[pre_flash] No serial port found, skipping notification")
|
||||
return
|
||||
|
||||
try:
|
||||
version = get_version(env)
|
||||
ser = serial.Serial(port, 115200, timeout=1)
|
||||
ser.write(f"FLASH:{version}\n".encode())
|
||||
ser.flush()
|
||||
time.sleep(4.0) # Keep port open for device to receive and complete full refresh (~2-3s)
|
||||
ser.close()
|
||||
print(f"[pre_flash] Flash notification sent to {port} (version {version})")
|
||||
except Exception as e:
|
||||
print(f"[pre_flash] Notification skipped: {e}")
|
||||
|
||||
|
||||
env.AddPreAction("upload", before_upload)
|
||||
|
||||
@ -300,6 +300,36 @@ void CrossPointWebServerActivity::startAccessPoint() {
|
||||
startWebServer();
|
||||
}
|
||||
|
||||
void CrossPointWebServerActivity::generateQRCodes() {
|
||||
Serial.printf("[%lu] [WEBACT] Generating QR codes (cached)...\n", millis());
|
||||
const unsigned long startTime = millis();
|
||||
|
||||
// Web browser URL QR code
|
||||
std::string webUrl = "http://" + connectedIP + "/";
|
||||
qrcode_initText(&qrWebBrowser, qrWebBrowserBuffer, 4, ECC_LOW, webUrl.c_str());
|
||||
Serial.printf("[%lu] [WEBACT] QR cached: %s\n", millis(), webUrl.c_str());
|
||||
|
||||
// Companion App (Files) deep link QR code
|
||||
std::string filesUrl = getCompanionAppUrl();
|
||||
qrcode_initText(&qrCompanionApp, qrCompanionAppBuffer, 4, ECC_LOW, filesUrl.c_str());
|
||||
Serial.printf("[%lu] [WEBACT] QR cached: %s\n", millis(), filesUrl.c_str());
|
||||
|
||||
// Companion App (Library) deep link QR code
|
||||
std::string libraryUrl = getCompanionAppLibraryUrl();
|
||||
qrcode_initText(&qrCompanionAppLibrary, qrCompanionAppLibraryBuffer, 4, ECC_LOW, libraryUrl.c_str());
|
||||
Serial.printf("[%lu] [WEBACT] QR cached: %s\n", millis(), libraryUrl.c_str());
|
||||
|
||||
// WiFi config QR code (for AP mode)
|
||||
if (isApMode) {
|
||||
std::string wifiConfig = std::string("WIFI:S:") + connectedSSID + ";;";
|
||||
qrcode_initText(&qrWifiConfig, qrWifiConfigBuffer, 4, ECC_LOW, wifiConfig.c_str());
|
||||
Serial.printf("[%lu] [WEBACT] QR cached: %s\n", millis(), wifiConfig.c_str());
|
||||
}
|
||||
|
||||
qrCacheValid = true;
|
||||
Serial.printf("[%lu] [WEBACT] QR codes cached in %lu ms\n", millis(), millis() - startTime);
|
||||
}
|
||||
|
||||
void CrossPointWebServerActivity::startWebServer() {
|
||||
Serial.printf("[%lu] [WEBACT] Starting web server...\n", millis());
|
||||
|
||||
@ -311,6 +341,9 @@ void CrossPointWebServerActivity::startWebServer() {
|
||||
state = WebServerActivityState::SERVER_RUNNING;
|
||||
Serial.printf("[%lu] [WEBACT] Web server started successfully\n", millis());
|
||||
|
||||
// Generate and cache QR codes now that we have IP and server ports
|
||||
generateQRCodes();
|
||||
|
||||
// Force an immediate render since we're transitioning from a subactivity
|
||||
// that had its own rendering task. We need to make sure our display is shown.
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
@ -468,23 +501,18 @@ void CrossPointWebServerActivity::render() const {
|
||||
}
|
||||
}
|
||||
|
||||
// Draw QR code at specified position with configurable pixel size per module
|
||||
// Draw QR code from pre-computed QRCode data at specified position
|
||||
// Returns the size of the QR code in pixels (width = height = size * pixelsPerModule)
|
||||
int drawQRCode(const GfxRenderer& renderer, const int x, const int y, const std::string& data,
|
||||
const uint8_t pixelsPerModule = 7) {
|
||||
QRCode qrcode;
|
||||
uint8_t qrcodeBytes[qrcode_getBufferSize(4)];
|
||||
Serial.printf("[%lu] [WEBACT] QR Code (%lu): %s\n", millis(), data.length(), data.c_str());
|
||||
|
||||
qrcode_initText(&qrcode, qrcodeBytes, 4, ECC_LOW, data.c_str());
|
||||
for (uint8_t cy = 0; cy < qrcode.size; cy++) {
|
||||
for (uint8_t cx = 0; cx < qrcode.size; cx++) {
|
||||
if (qrcode_getModule(&qrcode, cx, cy)) {
|
||||
int drawQRCodeCached(const GfxRenderer& renderer, const int x, const int y, QRCode* qrcode,
|
||||
const uint8_t pixelsPerModule = 7) {
|
||||
for (uint8_t cy = 0; cy < qrcode->size; cy++) {
|
||||
for (uint8_t cx = 0; cx < qrcode->size; cx++) {
|
||||
if (qrcode_getModule(qrcode, cx, cy)) {
|
||||
renderer.fillRect(x + pixelsPerModule * cx, y + pixelsPerModule * cy, pixelsPerModule, pixelsPerModule, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
return qrcode.size * pixelsPerModule;
|
||||
return qrcode->size * pixelsPerModule;
|
||||
}
|
||||
|
||||
// Helper to format bytes into human-readable sizes
|
||||
@ -612,8 +640,7 @@ void CrossPointWebServerActivity::renderWebBrowserScreen() const {
|
||||
|
||||
if (isApMode) {
|
||||
// AP mode: Show WiFi QR code on left, connection info on right
|
||||
const std::string wifiConfig = std::string("WIFI:S:") + connectedSSID + ";;";
|
||||
drawQRCode(renderer, QR_X, QR_Y, wifiConfig, QR_PX);
|
||||
drawQRCodeCached(renderer, QR_X, QR_Y, &qrWifiConfig, QR_PX);
|
||||
|
||||
std::string ssidInfo = "Network: " + connectedSSID;
|
||||
renderer.drawText(NOTOSANS_12_FONT_ID, TEXT_X, textY, ssidInfo.c_str());
|
||||
@ -635,8 +662,7 @@ void CrossPointWebServerActivity::renderWebBrowserScreen() const {
|
||||
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, hostnameUrl.c_str());
|
||||
} else {
|
||||
// STA mode: Show URL QR code on left, connection info on right
|
||||
std::string webUrl = "http://" + connectedIP + "/";
|
||||
drawQRCode(renderer, QR_X, QR_Y, webUrl, QR_PX);
|
||||
drawQRCodeCached(renderer, QR_X, QR_Y, &qrWebBrowser, QR_PX);
|
||||
|
||||
std::string ssidInfo = "Network: " + connectedSSID;
|
||||
if (ssidInfo.length() > 35) {
|
||||
@ -650,6 +676,7 @@ void CrossPointWebServerActivity::renderWebBrowserScreen() const {
|
||||
renderer.drawText(NOTOSANS_12_FONT_ID, TEXT_X, textY, ipInfo.c_str());
|
||||
textY += LINE_SPACING + 8;
|
||||
|
||||
std::string webUrl = "http://" + connectedIP + "/";
|
||||
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, webUrl.c_str(), true, EpdFontFamily::BOLD);
|
||||
textY += LINE_SPACING - 4;
|
||||
|
||||
@ -704,12 +731,12 @@ void CrossPointWebServerActivity::renderCompanionAppScreen() const {
|
||||
std::string webUrl = "http://" + connectedIP + "/files";
|
||||
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, webUrl.c_str());
|
||||
|
||||
// Draw QR code on left
|
||||
const std::string appUrl = getCompanionAppUrl();
|
||||
drawQRCode(renderer, QR_X, QR_Y, appUrl, QR_PX);
|
||||
// Draw cached QR code on left
|
||||
drawQRCodeCached(renderer, QR_X, QR_Y, &qrCompanionApp, QR_PX);
|
||||
|
||||
// Show deep link URL below QR code
|
||||
const int urlY = QR_Y + QR_SIZE + 10;
|
||||
const std::string appUrl = getCompanionAppUrl();
|
||||
renderer.drawText(UI_12_FONT_ID, QR_X, urlY, appUrl.c_str(), true, EpdFontFamily::BOLD);
|
||||
}
|
||||
|
||||
@ -754,11 +781,11 @@ void CrossPointWebServerActivity::renderCompanionAppLibraryScreen() const {
|
||||
std::string webUrl = "http://" + connectedIP + "/";
|
||||
renderer.drawText(UI_12_FONT_ID, TEXT_X, textY, webUrl.c_str());
|
||||
|
||||
// Draw QR code on left
|
||||
const std::string appUrl = getCompanionAppLibraryUrl();
|
||||
drawQRCode(renderer, QR_X, QR_Y, appUrl, QR_PX);
|
||||
// Draw cached QR code on left
|
||||
drawQRCodeCached(renderer, QR_X, QR_Y, &qrCompanionAppLibrary, QR_PX);
|
||||
|
||||
// Show deep link URL below QR code
|
||||
const int urlY = QR_Y + QR_SIZE + 10;
|
||||
const std::string appUrl = getCompanionAppLibraryUrl();
|
||||
renderer.drawText(UI_12_FONT_ID, QR_X, urlY, appUrl.c_str(), true, EpdFontFamily::BOLD);
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
#include <qrcode.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
@ -11,6 +12,10 @@
|
||||
#include "activities/ActivityWithSubactivity.h"
|
||||
#include "network/CrossPointWebServer.h"
|
||||
|
||||
// QR code cache - version 4 QR codes (33x33 modules)
|
||||
// Buffer size for version 4: qrcode_getBufferSize(4) ≈ 185 bytes
|
||||
constexpr size_t QR_BUFFER_SIZE = 185;
|
||||
|
||||
// Web server activity states
|
||||
enum class WebServerActivityState {
|
||||
MODE_SELECTION, // Choosing between Join Network and Create Hotspot
|
||||
@ -62,6 +67,19 @@ class CrossPointWebServerActivity final : public ActivityWithSubactivity {
|
||||
FileTransferScreen currentScreen = FileTransferScreen::COMPANION_APP_LIBRARY;
|
||||
unsigned long lastStatsRefresh = 0;
|
||||
|
||||
// Cached QR codes - generated once when server starts
|
||||
// Avoids recomputing QR data on every render (every 30s stats refresh)
|
||||
// Marked mutable since QR drawing doesn't modify logical state but qrcode_getModule takes non-const
|
||||
bool qrCacheValid = false;
|
||||
mutable QRCode qrWebBrowser;
|
||||
mutable QRCode qrCompanionApp;
|
||||
mutable QRCode qrCompanionAppLibrary;
|
||||
mutable QRCode qrWifiConfig; // For AP mode WiFi connection QR
|
||||
uint8_t qrWebBrowserBuffer[QR_BUFFER_SIZE];
|
||||
uint8_t qrCompanionAppBuffer[QR_BUFFER_SIZE];
|
||||
uint8_t qrCompanionAppLibraryBuffer[QR_BUFFER_SIZE];
|
||||
uint8_t qrWifiConfigBuffer[QR_BUFFER_SIZE];
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void render() const;
|
||||
@ -78,6 +96,7 @@ class CrossPointWebServerActivity final : public ActivityWithSubactivity {
|
||||
void startAccessPoint();
|
||||
void startWebServer();
|
||||
void stopWebServer();
|
||||
void generateQRCodes();
|
||||
|
||||
public:
|
||||
explicit CrossPointWebServerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
#include <GfxRenderer.h>
|
||||
#include <WiFi.h>
|
||||
|
||||
#include <map>
|
||||
#include <algorithm>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "WifiCredentialStore.h"
|
||||
@ -124,48 +124,55 @@ void WifiSelectionActivity::processWifiScanResults() {
|
||||
}
|
||||
|
||||
// Scan complete, process results
|
||||
// Use a map to deduplicate networks by SSID, keeping the strongest signal
|
||||
std::map<std::string, WifiNetworkInfo> uniqueNetworks;
|
||||
// Deduplicate directly into the networks vector (avoids std::map overhead)
|
||||
networks.clear();
|
||||
networks.reserve(std::min(scanResult, static_cast<int16_t>(20))); // Limit to 20 networks max
|
||||
|
||||
for (int i = 0; i < scanResult; i++) {
|
||||
std::string ssid = WiFi.SSID(i).c_str();
|
||||
String ssidStr = WiFi.SSID(i);
|
||||
const int32_t rssi = WiFi.RSSI(i);
|
||||
|
||||
// Skip hidden networks (empty SSID)
|
||||
if (ssid.empty()) {
|
||||
if (ssidStr.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if we've already seen this SSID
|
||||
auto it = uniqueNetworks.find(ssid);
|
||||
if (it == uniqueNetworks.end() || rssi > it->second.rssi) {
|
||||
// New network or stronger signal than existing entry
|
||||
std::string ssid = ssidStr.c_str();
|
||||
|
||||
// Check if we've already seen this SSID (linear search is fine for small lists)
|
||||
auto existing = std::find_if(networks.begin(), networks.end(),
|
||||
[&ssid](const WifiNetworkInfo& net) { return net.ssid == ssid; });
|
||||
|
||||
if (existing != networks.end()) {
|
||||
// Update if stronger signal
|
||||
if (rssi > existing->rssi) {
|
||||
existing->rssi = rssi;
|
||||
existing->isEncrypted = (WiFi.encryptionType(i) != WIFI_AUTH_OPEN);
|
||||
}
|
||||
} else if (networks.size() < 20) {
|
||||
// New network - only add if under limit
|
||||
WifiNetworkInfo network;
|
||||
network.ssid = ssid;
|
||||
network.ssid = std::move(ssid);
|
||||
network.rssi = rssi;
|
||||
network.isEncrypted = (WiFi.encryptionType(i) != WIFI_AUTH_OPEN);
|
||||
network.hasSavedPassword = WIFI_STORE.hasSavedCredential(network.ssid);
|
||||
uniqueNetworks[ssid] = network;
|
||||
networks.push_back(std::move(network));
|
||||
}
|
||||
}
|
||||
|
||||
// Convert map to vector
|
||||
networks.clear();
|
||||
for (const auto& pair : uniqueNetworks) {
|
||||
// cppcheck-suppress useStlAlgorithm
|
||||
networks.push_back(pair.second);
|
||||
}
|
||||
// Free WiFi scan memory immediately (before sorting)
|
||||
WiFi.scanDelete();
|
||||
|
||||
// Sort by signal strength (strongest first)
|
||||
std::sort(networks.begin(), networks.end(),
|
||||
[](const WifiNetworkInfo& a, const WifiNetworkInfo& b) { return a.rssi > b.rssi; });
|
||||
|
||||
// Show networks with PW first
|
||||
// Sort by signal strength (strongest first), then by saved password
|
||||
std::sort(networks.begin(), networks.end(), [](const WifiNetworkInfo& a, const WifiNetworkInfo& b) {
|
||||
return a.hasSavedPassword && !b.hasSavedPassword;
|
||||
// Primary: saved passwords first
|
||||
if (a.hasSavedPassword != b.hasSavedPassword) {
|
||||
return a.hasSavedPassword;
|
||||
}
|
||||
// Secondary: strongest signal first
|
||||
return a.rssi > b.rssi;
|
||||
});
|
||||
|
||||
WiFi.scanDelete();
|
||||
state = WifiSelectionState::NETWORK_LIST;
|
||||
selectedNetworkIndex = 0;
|
||||
updateRequired = true;
|
||||
|
||||
50
src/main.cpp
50
src/main.cpp
@ -130,11 +130,14 @@ void logMemoryState(const char* tag, const char* context) {
|
||||
#define logMemoryState(tag, context) ((void)0)
|
||||
#endif
|
||||
|
||||
// Flash command detection - receives "FLASH\n" from pre_flash.py script
|
||||
// Flash command detection - receives "FLASH:version\n" from pre_flash.py script
|
||||
// Plan A: Simple polling - host sends command, device checks when Serial is connected
|
||||
static String flashCmdBuffer;
|
||||
|
||||
void checkForFlashCommand() {
|
||||
if (!Serial) return; // Early exit if Serial not initialized
|
||||
// Only check when Serial is connected (host has port open)
|
||||
if (!Serial) return;
|
||||
|
||||
while (Serial.available()) {
|
||||
char c = Serial.read();
|
||||
if (c == '\n') {
|
||||
@ -165,56 +168,51 @@ void checkForFlashCommand() {
|
||||
const int screenH = renderer.getScreenHeight();
|
||||
|
||||
// Show current version in bottom-left corner (orientation-aware)
|
||||
// "Bottom-left" is relative to the current orientation
|
||||
constexpr int versionMargin = 10;
|
||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION);
|
||||
int versionX, versionY;
|
||||
switch (renderer.getOrientation()) {
|
||||
case GfxRenderer::Portrait: // Bottom-left is actual bottom-left
|
||||
case GfxRenderer::Portrait:
|
||||
versionX = versionMargin;
|
||||
versionY = screenH - 30;
|
||||
break;
|
||||
case GfxRenderer::PortraitInverted: // Bottom-left is actual top-right
|
||||
case GfxRenderer::PortraitInverted:
|
||||
versionX = screenW - textWidth - versionMargin;
|
||||
versionY = 20;
|
||||
break;
|
||||
case GfxRenderer::LandscapeClockwise: // Bottom-left is actual bottom-right
|
||||
case GfxRenderer::LandscapeClockwise:
|
||||
versionX = screenW - textWidth - versionMargin;
|
||||
versionY = screenH - 30;
|
||||
break;
|
||||
case GfxRenderer::LandscapeCounterClockwise: // Bottom-left is actual bottom-left
|
||||
case GfxRenderer::LandscapeCounterClockwise:
|
||||
versionX = versionMargin;
|
||||
versionY = screenH - 30;
|
||||
break;
|
||||
}
|
||||
renderer.drawText(SMALL_FONT_ID, versionX, versionY, CROSSPOINT_VERSION, false);
|
||||
|
||||
// Position and rotate lock icon based on current orientation (USB port location)
|
||||
// USB port locations: Portrait=bottom-left, PortraitInverted=top-right,
|
||||
// LandscapeCW=top-left, LandscapeCCW=bottom-right
|
||||
// Position offsets: edge margin + half-width offset to center on USB port
|
||||
constexpr int edgeMargin = 28; // Distance from screen edge
|
||||
constexpr int halfWidth = LOCK_ICON_WIDTH / 2; // 16px offset for centering
|
||||
// Position and rotate lock icon based on current orientation
|
||||
constexpr int edgeMargin = 28;
|
||||
constexpr int halfWidth = LOCK_ICON_WIDTH / 2;
|
||||
int iconX, iconY;
|
||||
GfxRenderer::ImageRotation rotation;
|
||||
// Note: 90/270 rotation swaps output dimensions (W<->H)
|
||||
switch (renderer.getOrientation()) {
|
||||
case GfxRenderer::Portrait: // USB at bottom-left, shackle points right
|
||||
case GfxRenderer::Portrait:
|
||||
rotation = GfxRenderer::ROTATE_90;
|
||||
iconX = edgeMargin;
|
||||
iconY = screenH - LOCK_ICON_WIDTH - edgeMargin - halfWidth;
|
||||
break;
|
||||
case GfxRenderer::PortraitInverted: // USB at top-right, shackle points left
|
||||
case GfxRenderer::PortraitInverted:
|
||||
rotation = GfxRenderer::ROTATE_270;
|
||||
iconX = screenW - LOCK_ICON_HEIGHT - edgeMargin;
|
||||
iconY = edgeMargin + halfWidth;
|
||||
break;
|
||||
case GfxRenderer::LandscapeClockwise: // USB at top-left, shackle points down
|
||||
case GfxRenderer::LandscapeClockwise:
|
||||
rotation = GfxRenderer::ROTATE_180;
|
||||
iconX = edgeMargin + halfWidth;
|
||||
iconY = edgeMargin;
|
||||
break;
|
||||
case GfxRenderer::LandscapeCounterClockwise: // USB at bottom-right, shackle points up
|
||||
case GfxRenderer::LandscapeCounterClockwise:
|
||||
rotation = GfxRenderer::ROTATE_0;
|
||||
iconX = screenW - LOCK_ICON_WIDTH - edgeMargin - halfWidth;
|
||||
iconY = screenH - LOCK_ICON_HEIGHT - edgeMargin;
|
||||
@ -222,13 +220,13 @@ void checkForFlashCommand() {
|
||||
}
|
||||
renderer.drawImageRotated(LockIcon, iconX, iconY, LOCK_ICON_WIDTH, LOCK_ICON_HEIGHT, rotation);
|
||||
|
||||
renderer.displayBuffer(EInkDisplay::FAST_REFRESH);
|
||||
// Use full refresh for clean display before flash overwrites firmware
|
||||
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
||||
}
|
||||
flashCmdBuffer = "";
|
||||
} else if (c != '\r') {
|
||||
flashCmdBuffer += c;
|
||||
// Prevent buffer overflow from random serial data (increased for version info)
|
||||
if (flashCmdBuffer.length() > 30) {
|
||||
if (flashCmdBuffer.length() > 50) {
|
||||
flashCmdBuffer = "";
|
||||
}
|
||||
}
|
||||
@ -388,6 +386,7 @@ void onGoToFileTransfer() {
|
||||
APP_STATE.openBookTitle.shrink_to_fit();
|
||||
APP_STATE.openBookAuthor.clear();
|
||||
APP_STATE.openBookAuthor.shrink_to_fit();
|
||||
HomeActivity::freeCoverBufferIfAllocated(); // Free 48KB cover buffer
|
||||
Serial.printf("[%lu] [FT] Cleared non-essential memory before File Transfer\n", millis());
|
||||
|
||||
enterNewActivity(new CrossPointWebServerActivity(renderer, mappedInputManager, onGoHome));
|
||||
@ -485,11 +484,14 @@ bool isWakeupByPowerButton() {
|
||||
void setup() {
|
||||
t1 = millis();
|
||||
|
||||
// Only start serial if USB connected
|
||||
// Always initialize Serial - safe on ESP32-C3 USB CDC even without USB connected
|
||||
// (the peripheral just remains idle).
|
||||
pinMode(UART0_RXD, INPUT);
|
||||
Serial.begin(115200);
|
||||
|
||||
// Only wait for terminal connection if USB is physically connected
|
||||
// This allows catching early debug logs when a serial monitor is attached
|
||||
if (isUsbConnected()) {
|
||||
Serial.begin(115200);
|
||||
// Wait up to 3 seconds for Serial to be ready to catch early logs
|
||||
unsigned long start = millis();
|
||||
while (!Serial && (millis() - start) < 3000) {
|
||||
delay(10);
|
||||
|
||||
@ -371,27 +371,10 @@ void CrossPointWebServer::scanFiles(const char* path, const std::function<void(F
|
||||
if (info.isDirectory) {
|
||||
info.size = 0;
|
||||
info.isEpub = false;
|
||||
// md5 remains empty for directories
|
||||
} else {
|
||||
info.size = file.size();
|
||||
info.isEpub = isEpubFile(info.name);
|
||||
|
||||
// For EPUBs, try to get cached MD5 hash
|
||||
if (info.isEpub) {
|
||||
// Build full file path
|
||||
String fullPath = String(path);
|
||||
if (!fullPath.endsWith("/")) {
|
||||
fullPath += "/";
|
||||
}
|
||||
fullPath += fileName;
|
||||
|
||||
const std::string cachedMd5 =
|
||||
Md5Utils::getCachedMd5(fullPath.c_str(), BookManager::CROSSPOINT_DIR, info.size);
|
||||
if (!cachedMd5.empty()) {
|
||||
info.md5 = String(cachedMd5.c_str());
|
||||
}
|
||||
// If not cached, md5 remains empty (companion app can request via /api/hash)
|
||||
}
|
||||
// MD5 not included in listing - clients can request via /api/hash endpoint
|
||||
}
|
||||
|
||||
callback(info);
|
||||
@ -435,89 +418,72 @@ void CrossPointWebServer::handleFileListData() const {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we should show hidden files
|
||||
bool showHidden = false;
|
||||
if (server->hasArg("showHidden")) {
|
||||
showHidden = server->arg("showHidden") == "true";
|
||||
}
|
||||
|
||||
// Check client connection before starting
|
||||
if (!server->client().connected()) {
|
||||
Serial.printf("[%lu] [WEB] Client disconnected before file list could start\n", millis());
|
||||
return;
|
||||
}
|
||||
// Check if we should show hidden files (fork addition)
|
||||
bool showHidden = server->hasArg("showHidden") && server->arg("showHidden") == "true";
|
||||
|
||||
server->setContentLength(CONTENT_LENGTH_UNKNOWN);
|
||||
server->send(200, "application/json", "");
|
||||
if (!sendContentSafe("[")) {
|
||||
Serial.printf("[%lu] [WEB] Client disconnected at start of file list\n", millis());
|
||||
return;
|
||||
}
|
||||
|
||||
char output[512];
|
||||
// Batch JSON entries to reduce number of sendContent calls
|
||||
// This helps prevent TCP buffer overflow on memory-constrained systems
|
||||
constexpr size_t BATCH_SIZE = 2048;
|
||||
char batch[BATCH_SIZE];
|
||||
size_t batchPos = 0;
|
||||
batch[batchPos++] = '[';
|
||||
|
||||
char output[256]; // Single entry buffer (reduced from 512)
|
||||
constexpr size_t outputSize = sizeof(output);
|
||||
bool seenFirst = false;
|
||||
bool clientDisconnected = false;
|
||||
JsonDocument doc;
|
||||
|
||||
scanFiles(
|
||||
currentPath.c_str(),
|
||||
[this, &output, &doc, &seenFirst, &clientDisconnected](const FileInfo& info) mutable {
|
||||
// Skip remaining files if client already disconnected
|
||||
if (clientDisconnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
[this, &batch, &batchPos, &output, &doc, &seenFirst](const FileInfo& info) mutable {
|
||||
doc.clear();
|
||||
doc["name"] = info.name;
|
||||
doc["size"] = info.size;
|
||||
doc["isDirectory"] = info.isDirectory;
|
||||
doc["isEpub"] = info.isEpub;
|
||||
|
||||
// Include md5 field for EPUBs (null if not cached, hash string if available)
|
||||
if (info.isEpub) {
|
||||
if (info.md5.isEmpty()) {
|
||||
doc["md5"] = nullptr; // JSON null
|
||||
} else {
|
||||
doc["md5"] = info.md5;
|
||||
}
|
||||
}
|
||||
|
||||
const size_t written = serializeJson(doc, output, outputSize);
|
||||
if (written >= outputSize) {
|
||||
// JSON output truncated; skip this entry to avoid sending malformed JSON
|
||||
Serial.printf("[%lu] [WEB] Skipping file entry with oversized JSON for name: %s\n", millis(),
|
||||
info.name.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// Send comma separator before all entries except the first
|
||||
// Calculate space needed: comma (if not first) + entry
|
||||
const size_t needed = (seenFirst ? 1 : 0) + written;
|
||||
|
||||
// If batch would overflow, send it first
|
||||
if (batchPos + needed >= BATCH_SIZE - 1) {
|
||||
batch[batchPos] = '\0';
|
||||
server->sendContent(batch);
|
||||
delay(5); // Brief delay between batch sends
|
||||
batchPos = 0;
|
||||
}
|
||||
|
||||
// Add comma separator if not first entry
|
||||
if (seenFirst) {
|
||||
if (!sendContentSafe(",")) {
|
||||
clientDisconnected = true;
|
||||
Serial.printf("[%lu] [WEB] Client disconnected during file list\n", millis());
|
||||
return;
|
||||
}
|
||||
batch[batchPos++] = ',';
|
||||
} else {
|
||||
seenFirst = true;
|
||||
}
|
||||
|
||||
// Send the JSON entry with flow control
|
||||
if (!sendContentSafe(output)) {
|
||||
clientDisconnected = true;
|
||||
Serial.printf("[%lu] [WEB] Client disconnected during file list\n", millis());
|
||||
return;
|
||||
}
|
||||
// Copy entry to batch
|
||||
memcpy(batch + batchPos, output, written);
|
||||
batchPos += written;
|
||||
},
|
||||
showHidden);
|
||||
|
||||
// Only send closing bracket if client is still connected
|
||||
if (!clientDisconnected) {
|
||||
sendContentSafe("]");
|
||||
// End of streamed response, empty chunk to signal client
|
||||
server->sendContent("");
|
||||
Serial.printf("[%lu] [WEB] Served file listing page for path: %s\n", millis(), currentPath.c_str());
|
||||
}
|
||||
// Send remaining batch with closing bracket
|
||||
batch[batchPos++] = ']';
|
||||
batch[batchPos] = '\0';
|
||||
server->sendContent(batch);
|
||||
|
||||
// End of streamed response, empty chunk to signal client
|
||||
server->sendContent("");
|
||||
Serial.printf("[%lu] [WEB] Served file listing page for path: %s\n", millis(), currentPath.c_str());
|
||||
}
|
||||
|
||||
// Static variables for upload handling
|
||||
@ -1295,33 +1261,11 @@ void CrossPointWebServer::handleRename() const {
|
||||
}
|
||||
}
|
||||
|
||||
// Counter for flow control pacing
|
||||
static uint8_t sendContentCounter = 0;
|
||||
|
||||
bool CrossPointWebServer::sendContentSafe(const char* content) const {
|
||||
if (!server || !server->client().connected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Send the content
|
||||
server->sendContent(content);
|
||||
|
||||
// Flow control: give TCP stack time to transmit data and drain the send buffer
|
||||
// The ESP32 TCP buffer is limited and fills quickly when streaming many small chunks.
|
||||
// We use progressive delays:
|
||||
// - yield() after every send to allow WiFi processing
|
||||
// - delay(5ms) every send to allow buffer draining
|
||||
// - delay(50ms) every 10 sends to allow larger buffer flush
|
||||
yield();
|
||||
sendContentCounter++;
|
||||
|
||||
if (sendContentCounter >= 10) {
|
||||
sendContentCounter = 0;
|
||||
delay(50); // Longer pause every 10 sends for buffer catchup
|
||||
} else {
|
||||
delay(5); // Short pause each send
|
||||
}
|
||||
|
||||
return server->client().connected();
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,6 @@ struct FileInfo {
|
||||
size_t size;
|
||||
bool isEpub;
|
||||
bool isDirectory;
|
||||
String md5; // MD5 hash for EPUBs (empty if not cached/available)
|
||||
};
|
||||
|
||||
class CrossPointWebServer {
|
||||
@ -108,7 +107,7 @@ class CrossPointWebServer {
|
||||
bool copyFile(const String& srcPath, const String& destPath) const;
|
||||
bool copyFolder(const String& srcPath, const String& destPath) const;
|
||||
|
||||
// Helper for safe content sending with flow control
|
||||
// Helper for safe content sending with connection check
|
||||
// Returns false if client disconnected, true otherwise
|
||||
bool sendContentSafe(const char* content) const;
|
||||
bool sendContentSafe(const String& content) const;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user