diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index e5b25be..08420bf 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -201,9 +201,6 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con if (screenY >= getScreenHeight()) { break; } - if (screenY < 0) { - continue; - } if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) { Serial.printf("[%lu] [GFX] Failed to read row %d from bitmap\n", millis(), bmpY); @@ -212,6 +209,10 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con return; } + if (screenY < 0) { + continue; + } + if (bmpY < cropPixY) { // Skip the row if it's outside the crop area continue; diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index bf2b585..c0d6844 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -167,7 +167,7 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const { Serial.printf("[%lu] [SLP] Cropping bitmap y: %f\n", millis(), cropY); ratio = static_cast(bitmap.getWidth()) / ((1.0f - cropY) * static_cast(bitmap.getHeight())); } - x = std::round((pageWidth - pageHeight * ratio) / 2); + x = std::round((static_cast(pageWidth) - static_cast(pageHeight) * ratio) / 2); y = 0; Serial.printf("[%lu] [SLP] Centering with ratio %f to x=%d\n", millis(), ratio, x); } diff --git a/src/activities/browser/OpdsBookBrowserActivity.cpp b/src/activities/browser/OpdsBookBrowserActivity.cpp index 4e0a08d..677f9ca 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.cpp +++ b/src/activities/browser/OpdsBookBrowserActivity.cpp @@ -1,5 +1,6 @@ #include "OpdsBookBrowserActivity.h" +#include #include #include #include @@ -355,6 +356,12 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) { if (result == HttpDownloader::OK) { Serial.printf("[%lu] [OPDS] Download complete: %s\n", millis(), filename.c_str()); + + // Invalidate any existing cache for this file to prevent stale metadata issues + Epub epub(filename, "/.crosspoint"); + epub.clearCache(); + Serial.printf("[%lu] [OPDS] Cleared cache for: %s\n", millis(), filename.c_str()); + state = BrowserState::BROWSING; updateRequired = true; } else { diff --git a/src/activities/settings/CategorySettingsActivity.cpp b/src/activities/settings/CategorySettingsActivity.cpp index 77ff0b0..a6182b5 100644 --- a/src/activities/settings/CategorySettingsActivity.cpp +++ b/src/activities/settings/CategorySettingsActivity.cpp @@ -6,6 +6,7 @@ #include #include "CalibreSettingsActivity.h" +#include "ClearCacheActivity.h" #include "CrossPointSettings.h" #include "KOReaderSettingsActivity.h" #include "MappedInputManager.h" @@ -110,6 +111,14 @@ void CategorySettingsActivity::toggleCurrentSetting() { updateRequired = true; })); xSemaphoreGive(renderingMutex); + } else if (strcmp(setting.name, "Clear Cache") == 0) { + xSemaphoreTake(renderingMutex, portMAX_DELAY); + exitActivity(); + enterNewActivity(new ClearCacheActivity(renderer, mappedInput, [this] { + exitActivity(); + updateRequired = true; + })); + xSemaphoreGive(renderingMutex); } else if (strcmp(setting.name, "Check for updates") == 0) { xSemaphoreTake(renderingMutex, portMAX_DELAY); exitActivity(); diff --git a/src/activities/settings/ClearCacheActivity.cpp b/src/activities/settings/ClearCacheActivity.cpp new file mode 100644 index 0000000..1e10c14 --- /dev/null +++ b/src/activities/settings/ClearCacheActivity.cpp @@ -0,0 +1,178 @@ +#include "ClearCacheActivity.h" + +#include +#include +#include + +#include "MappedInputManager.h" +#include "fontIds.h" + +void ClearCacheActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); + self->displayTaskLoop(); +} + +void ClearCacheActivity::onEnter() { + ActivityWithSubactivity::onEnter(); + + renderingMutex = xSemaphoreCreateMutex(); + state = WARNING; + updateRequired = true; + + xTaskCreate(&ClearCacheActivity::taskTrampoline, "ClearCacheActivityTask", + 4096, // Stack size + this, // Parameters + 1, // Priority + &displayTaskHandle // Task handle + ); +} + +void ClearCacheActivity::onExit() { + ActivityWithSubactivity::onExit(); + + // Wait until not rendering to delete task to avoid killing mid-instruction to EPD + xSemaphoreTake(renderingMutex, portMAX_DELAY); + if (displayTaskHandle) { + vTaskDelete(displayTaskHandle); + displayTaskHandle = nullptr; + } + vSemaphoreDelete(renderingMutex); + renderingMutex = nullptr; +} + +void ClearCacheActivity::displayTaskLoop() { + while (true) { + if (updateRequired) { + updateRequired = false; + xSemaphoreTake(renderingMutex, portMAX_DELAY); + render(); + xSemaphoreGive(renderingMutex); + } + vTaskDelay(10 / portTICK_PERIOD_MS); + } +} + +void ClearCacheActivity::render() { + const auto pageHeight = renderer.getScreenHeight(); + + renderer.clearScreen(); + renderer.drawCenteredText(UI_12_FONT_ID, 15, "Clear Cache", true, EpdFontFamily::BOLD); + + if (state == WARNING) { + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 60, "This will clear all cached book data.", true); + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 30, "All reading progress will be lost!", true, + EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, "Books will need to be re-indexed", true); + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 30, "when opened again.", true); + + const auto labels = mappedInput.mapLabels("« Cancel", "Clear", "", ""); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + renderer.displayBuffer(); + return; + } + + if (state == CLEARING) { + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "Clearing cache...", true, EpdFontFamily::BOLD); + renderer.displayBuffer(); + return; + } + + if (state == SUCCESS) { + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, "Cache Cleared", true, EpdFontFamily::BOLD); + String resultText = String(clearedCount) + " items removed"; + if (failedCount > 0) { + resultText += ", " + String(failedCount) + " failed"; + } + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, resultText.c_str()); + + const auto labels = mappedInput.mapLabels("« Back", "", "", ""); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + renderer.displayBuffer(); + return; + } + + if (state == FAILED) { + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, "Failed to clear cache", true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, "Check serial output for details"); + + const auto labels = mappedInput.mapLabels("« Back", "", "", ""); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + renderer.displayBuffer(); + return; + } +} + +void ClearCacheActivity::clearCache() { + Serial.printf("[%lu] [CLEAR_CACHE] Clearing cache...\n", millis()); + + // Open .crosspoint directory + auto root = SdMan.open("/.crosspoint"); + if (!root || !root.isDirectory()) { + Serial.printf("[%lu] [CLEAR_CACHE] Failed to open cache directory\n", millis()); + if (root) root.close(); + state = FAILED; + updateRequired = true; + return; + } + + clearedCount = 0; + failedCount = 0; + char name[128]; + + // Iterate through all entries in the directory + for (auto file = root.openNextFile(); file; file = root.openNextFile()) { + file.getName(name, sizeof(name)); + String itemName(name); + + // Only delete directories starting with epub_ or xtc_ + if (file.isDirectory() && (itemName.startsWith("epub_") || itemName.startsWith("xtc_"))) { + String fullPath = "/.crosspoint/" + itemName; + Serial.printf("[%lu] [CLEAR_CACHE] Removing cache: %s\n", millis(), fullPath.c_str()); + + file.close(); // Close before attempting to delete + + if (SdMan.removeDir(fullPath.c_str())) { + clearedCount++; + } else { + Serial.printf("[%lu] [CLEAR_CACHE] Failed to remove: %s\n", millis(), fullPath.c_str()); + failedCount++; + } + } else { + file.close(); + } + } + root.close(); + + Serial.printf("[%lu] [CLEAR_CACHE] Cache cleared: %d removed, %d failed\n", millis(), clearedCount, failedCount); + + state = SUCCESS; + updateRequired = true; +} + +void ClearCacheActivity::loop() { + if (state == WARNING) { + if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { + Serial.printf("[%lu] [CLEAR_CACHE] User confirmed, starting cache clear\n", millis()); + xSemaphoreTake(renderingMutex, portMAX_DELAY); + state = CLEARING; + xSemaphoreGive(renderingMutex); + updateRequired = true; + vTaskDelay(10 / portTICK_PERIOD_MS); + + clearCache(); + } + + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + Serial.printf("[%lu] [CLEAR_CACHE] User cancelled\n", millis()); + goBack(); + } + return; + } + + if (state == SUCCESS || state == FAILED) { + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + goBack(); + } + return; + } +} diff --git a/src/activities/settings/ClearCacheActivity.h b/src/activities/settings/ClearCacheActivity.h new file mode 100644 index 0000000..31795a9 --- /dev/null +++ b/src/activities/settings/ClearCacheActivity.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include + +#include + +#include "activities/ActivityWithSubactivity.h" + +class ClearCacheActivity final : public ActivityWithSubactivity { + public: + explicit ClearCacheActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& goBack) + : ActivityWithSubactivity("ClearCache", renderer, mappedInput), goBack(goBack) {} + + void onEnter() override; + void onExit() override; + void loop() override; + + private: + enum State { WARNING, CLEARING, SUCCESS, FAILED }; + + State state = WARNING; + TaskHandle_t displayTaskHandle = nullptr; + SemaphoreHandle_t renderingMutex = nullptr; + bool updateRequired = false; + const std::function goBack; + + int clearedCount = 0; + int failedCount = 0; + + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); + void render(); + void clearCache(); +}; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 822bacb..c9a20ab 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -45,11 +45,11 @@ const SettingInfo controlsSettings[controlsSettingsCount] = { SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip), SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"})}; -constexpr int systemSettingsCount = 4; +constexpr int systemSettingsCount = 5; const SettingInfo systemSettings[systemSettingsCount] = { SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout, {"1 min", "5 min", "10 min", "15 min", "30 min"}), - SettingInfo::Action("KOReader Sync"), SettingInfo::Action("Calibre Settings"), + SettingInfo::Action("KOReader Sync"), SettingInfo::Action("Calibre Settings"), SettingInfo::Action("Clear Cache"), SettingInfo::Action("Check for updates")}; } // namespace diff --git a/src/main.cpp b/src/main.cpp index 1a3ee60..c0222e0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -276,13 +276,30 @@ void setupDisplayAndFonts() { Serial.printf("[%lu] [ ] Fonts setup\n", millis()); } +bool isUsbConnected() { + // U0RXD/GPIO20 reads HIGH when USB is connected + return digitalRead(UART0_RXD) == HIGH; +} + +bool isWakeupAfterFlashing() { + const auto wakeupCause = esp_sleep_get_wakeup_cause(); + const auto resetReason = esp_reset_reason(); + + return isUsbConnected() && (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED) && (resetReason == ESP_RST_UNKNOWN); +} + void setup() { t1 = millis(); // Only start serial if USB connected pinMode(UART0_RXD, INPUT); - if (digitalRead(UART0_RXD) == HIGH) { + 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); + } } inputManager.begin(); @@ -305,8 +322,10 @@ void setup() { SETTINGS.loadFromFile(); KOREADER_STORE.loadFromFile(); - // verify power button press duration after we've read settings. - verifyWakeupLongPress(); + if (!isWakeupAfterFlashing()) { + // For normal wakeups (not immediately after flashing), verify long press + verifyWakeupLongPress(); + } // First serial output only here to avoid timing inconsistencies for power button press duration verification Serial.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION "\n", millis()); diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index 23ba36b..90dfed7 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -1,6 +1,7 @@ #include "CrossPointWebServer.h" #include +#include #include #include #include @@ -10,6 +11,7 @@ #include "html/FilesPageHtml.generated.h" #include "html/HomePageHtml.generated.h" +#include "util/StringUtils.h" namespace { // Folders/files to hide from the web interface file browser @@ -28,6 +30,15 @@ size_t wsUploadSize = 0; size_t wsUploadReceived = 0; unsigned long wsUploadStartTime = 0; bool wsUploadInProgress = false; + +// Helper function to clear epub cache after upload +void clearEpubCacheIfNeeded(const String& filePath) { + // Only clear cache for .epub files + if (StringUtils::checkFileExtension(filePath, ".epub")) { + Epub(filePath.c_str(), "/.crosspoint").clearCache(); + Serial.printf("[%lu] [WEB] Cleared epub cache for: %s\n", millis(), filePath.c_str()); + } +} } // namespace // File listing page template - now using generated headers: @@ -500,6 +511,12 @@ void CrossPointWebServer::handleUpload() const { uploadFileName.c_str(), uploadSize, elapsed, avgKbps); Serial.printf("[%lu] [WEB] [UPLOAD] Diagnostics: %d writes, total write time: %lu ms (%.1f%%)\n", millis(), writeCount, totalWriteTime, writePercent); + + // Clear epub cache to prevent stale metadata issues when overwriting files + String filePath = uploadPath; + if (!filePath.endsWith("/")) filePath += "/"; + filePath += uploadFileName; + clearEpubCacheIfNeeded(filePath); } } } else if (upload.status == UPLOAD_FILE_ABORTED) { @@ -787,6 +804,12 @@ void CrossPointWebServer::onWebSocketEvent(uint8_t num, WStype_t type, uint8_t* Serial.printf("[%lu] [WS] Upload complete: %s (%d bytes in %lu ms, %.1f KB/s)\n", millis(), wsUploadFileName.c_str(), wsUploadSize, elapsed, kbps); + // Clear epub cache to prevent stale metadata issues when overwriting files + String filePath = wsUploadPath; + if (!filePath.endsWith("/")) filePath += "/"; + filePath += wsUploadFileName; + clearEpubCacheIfNeeded(filePath); + wsServer->sendTXT(num, "DONE"); lastProgressSent = 0; } diff --git a/src/util/StringUtils.cpp b/src/util/StringUtils.cpp index e56bc9d..2426b68 100644 --- a/src/util/StringUtils.cpp +++ b/src/util/StringUtils.cpp @@ -49,6 +49,18 @@ bool checkFileExtension(const std::string& fileName, const char* extension) { return true; } +bool checkFileExtension(const String& fileName, const char* extension) { + if (fileName.length() < strlen(extension)) { + return false; + } + + String localFile(fileName); + String localExtension(extension); + localFile.toLowerCase(); + localExtension.toLowerCase(); + return localFile.endsWith(localExtension); +} + size_t utf8RemoveLastChar(std::string& str) { if (str.empty()) return 0; size_t pos = str.size() - 1; diff --git a/src/util/StringUtils.h b/src/util/StringUtils.h index e001d7b..5c8332f 100644 --- a/src/util/StringUtils.h +++ b/src/util/StringUtils.h @@ -1,5 +1,7 @@ #pragma once +#include + #include namespace StringUtils { @@ -15,6 +17,7 @@ std::string sanitizeFilename(const std::string& name, size_t maxLength = 100); * Check if the given filename ends with the specified extension (case-insensitive). */ bool checkFileExtension(const std::string& fileName, const char* extension); +bool checkFileExtension(const String& fileName, const char* extension); // UTF-8 safe string truncation - removes one character from the end // Returns the new size after removing one UTF-8 character