From 5464d9de3aa3e2b2fe34caca91ca93ba31952a82 Mon Sep 17 00:00:00 2001 From: cottongin Date: Fri, 30 Jan 2026 22:00:15 -0500 Subject: [PATCH] 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., 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 --- ef-CHANGELOG.md | 39 ++++++ .../Epub/parsers/ChapterHtmlSlimParser.cpp | 40 ++++++ scripts/pre_flash.py | 33 +++-- .../network/CrossPointWebServerActivity.cpp | 71 +++++++--- .../network/CrossPointWebServerActivity.h | 19 +++ .../network/WifiSelectionActivity.cpp | 55 ++++---- src/main.cpp | 50 +++---- src/network/CrossPointWebServer.cpp | 128 +++++------------- src/network/CrossPointWebServer.h | 3 +- 9 files changed, 260 insertions(+), 178 deletions(-) diff --git a/ef-CHANGELOG.md b/ef-CHANGELOG.md index 18b3400..74aa02d 100644 --- a/ef-CHANGELOG.md +++ b/ef-CHANGELOG.md @@ -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 `` 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** diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 168a847..e1abad2 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -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
  • but no text block was created yet (direct text without inner

    ), + // 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(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; diff --git a/scripts/pre_flash.py b/scripts/pre_flash.py index 910d499..88f1e8f 100644 --- a/scripts/pre_flash.py +++ b/scripts/pre_flash.py @@ -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) diff --git a/src/activities/network/CrossPointWebServerActivity.cpp b/src/activities/network/CrossPointWebServerActivity.cpp index 9488bbc..2701e19 100644 --- a/src/activities/network/CrossPointWebServerActivity.cpp +++ b/src/activities/network/CrossPointWebServerActivity.cpp @@ -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); } diff --git a/src/activities/network/CrossPointWebServerActivity.h b/src/activities/network/CrossPointWebServerActivity.h index fb64ff6..a787158 100644 --- a/src/activities/network/CrossPointWebServerActivity.h +++ b/src/activities/network/CrossPointWebServerActivity.h @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -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, diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index 5d378e1..f011eb4 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -3,7 +3,7 @@ #include #include -#include +#include #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 uniqueNetworks; + // Deduplicate directly into the networks vector (avoids std::map overhead) + networks.clear(); + networks.reserve(std::min(scanResult, static_cast(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; diff --git a/src/main.cpp b/src/main.cpp index 6551e03..f3dcb1c 100644 --- a/src/main.cpp +++ b/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); diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index dd07e77..cf1a02c 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -371,27 +371,10 @@ void CrossPointWebServer::scanFiles(const char* path, const std::function