From 01a6fc1a4a346f0483a26704afde50285f4046cd Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Tue, 16 Dec 2025 17:57:19 +1100 Subject: [PATCH 1/3] Add UploadFileScreen and UploadServer --- .gitignore | 1 + platformio.ini | 5 ++ script/build_html.py | 51 +++++++++++ src/screens/UploadFileScreen.cpp | 133 +++++++++++++++++++++++++++++ src/screens/UploadFileScreen.h | 33 +++++++ src/server/UploadServer.cpp | 62 ++++++++++++++ src/server/UploadServer.h | 24 ++++++ src/server/html/Upload.html | 86 +++++++++++++++++++ src/server/html/UploadSuccess.html | 76 +++++++++++++++++ 9 files changed, 471 insertions(+) create mode 100644 script/build_html.py create mode 100644 src/screens/UploadFileScreen.cpp create mode 100644 src/screens/UploadFileScreen.h create mode 100644 src/server/UploadServer.cpp create mode 100644 src/server/UploadServer.h create mode 100644 src/server/html/Upload.html create mode 100644 src/server/html/UploadSuccess.html diff --git a/.gitignore b/.gitignore index 29bccdd..5e39157 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .pio .idea .DS_Store +*.generated.h diff --git a/platformio.ini b/platformio.ini index 7998dcb..cf4aea5 100644 --- a/platformio.ini +++ b/platformio.ini @@ -30,8 +30,13 @@ board_build.flash_mode = dio board_build.flash_size = 16MB board_build.partitions = partitions.csv +extra_scripts = + pre:script/build_html.py + ; Libraries lib_deps = + ESP32Async/AsyncTCP + ESP32Async/ESPAsyncWebServer BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor InputManager=symlink://open-x4-sdk/libs/hardware/InputManager EInkDisplay=symlink://open-x4-sdk/libs/display/EInkDisplay diff --git a/script/build_html.py b/script/build_html.py new file mode 100644 index 0000000..3b3ac57 --- /dev/null +++ b/script/build_html.py @@ -0,0 +1,51 @@ +import os +import re + +SRC_DIR = "src" + +def minify_html(html: str) -> str: + # Tags where whitespace should be preserved + preserve_tags = ['pre', 'code', 'textarea'] + preserve_regex = '|'.join(preserve_tags) + + # Protect preserve blocks with placeholders + preserve_blocks = [] + def preserve(match): + preserve_blocks.append(match.group(0)) + return f"__PRESERVE_BLOCK_{len(preserve_blocks)-1}__" + + html = re.sub(rf'<({preserve_regex})[\s\S]*?', preserve, html, flags=re.IGNORECASE) + + # Remove HTML comments + html = re.sub(r'', '', html, flags=re.DOTALL) + + # Collapse all whitespace between tags + html = re.sub(r'>\s+<', '><', html) + + # Collapse multiple spaces inside tags + html = re.sub(r'\s+', ' ', html) + + # Restore preserved blocks + for i, block in enumerate(preserve_blocks): + html = html.replace(f"__PRESERVE_BLOCK_{i}__", block) + + return html.strip() + +for root, _, files in os.walk(SRC_DIR): + for file in files: + if file.endswith(".html"): + html_path = os.path.join(root, file) + with open(html_path, "r", encoding="utf-8") as f: + html_content = f.read() + + # minified = regex.sub("\g<1>", html_content) + minified = minify_html(html_content) + base_name = f"{os.path.splitext(file)[0]}Html" + header_path = os.path.join(root, f"{base_name}.generated.h") + + with open(header_path, "w", encoding="utf-8") as h: + h.write(f"// THIS FILE IS AUTOGENERATED, DO NOT EDIT MANUALLY\n\n") + h.write(f"#pragma once\n") + h.write(f'constexpr char {base_name}[] PROGMEM = R"rawliteral({minified})rawliteral";\n') + + print(f"Generated: {header_path}") diff --git a/src/screens/UploadFileScreen.cpp b/src/screens/UploadFileScreen.cpp new file mode 100644 index 0000000..090da24 --- /dev/null +++ b/src/screens/UploadFileScreen.cpp @@ -0,0 +1,133 @@ +#include "UploadFileScreen.h" + +#include +#include + +#include "config.h" +#include "images/CrossLarge.h" +#include "server/UploadServer.h" + +void UploadFileScreen::taskTrampoline(void* param) { + auto* self = static_cast(param); + self->displayTaskLoop(); +} + +void UploadFileScreen::displayTaskLoop() { + while (true) { + if (updateRequired) { + updateRequired = false; + xSemaphoreTake(renderingMutex, portMAX_DELAY); + render(); + xSemaphoreGive(renderingMutex); + } + vTaskDelay(10 / portTICK_PERIOD_MS); + } +} + +void UploadFileScreen::render() const { + const auto pageWidth = GfxRenderer::getScreenWidth(); + const auto pageHeight = GfxRenderer::getScreenHeight(); + + renderer.clearScreen(); + renderer.drawImage(CrossLarge, (pageWidth - 128) / 2, (pageHeight - 128) / 2, 128, 128); + renderer.drawCenteredText(UI_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, BOLD); + renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "UPLOADING"); + + if (currentUploadStatus == InProgress) { + renderer.drawRect(20, pageHeight / 2 + 110, pageWidth - 40, 50); + renderer.fillRect(22, pageHeight / 2 + 112, + static_cast(pageWidth - 44) * currentUploadCompleteSize / currentUploadTotalSize, 46); + } + + renderer.displayBuffer(); +} + +void UploadFileScreen::onFileUploadStart(AsyncWebServerRequest* request, const String& filename) { + if (request->hasHeader("Content-Length")) { + const String contentLengthStr = request->header("Content-Length"); + currentUploadTotalSize = contentLengthStr.toInt(); + } else { + currentUploadTotalSize = 0; + } + currentUploadCompleteSize = 0; + currentUploadFilename = filename; + currentUploadStatus = InProgress; + updateRequired = true; + + // First chunk of data: open the file in write mode + // Use request->_tempFile to manage the file object across chunks + // Writing to SD uses SPI, so lock the screen + xSemaphoreTake(renderingMutex, portMAX_DELAY); + request->_tempFile = SD.open("/" + filename, FILE_WRITE, true); + xSemaphoreGive(renderingMutex); +} + +void UploadFileScreen::onFileUploadPart(AsyncWebServerRequest* request, const uint8_t* data, const size_t len) { + // Write the received chunk of data to the file + // Writing to SD uses SPI, so lock the screen + xSemaphoreTake(renderingMutex, portMAX_DELAY); + request->_tempFile.write(data, len); + xSemaphoreGive(renderingMutex); + + currentUploadCompleteSize += len; + // Only update the screen at most every 5% to avoid blocking the SPI channel + if (currentUploadTotalSize > 0 && (currentUploadCompleteSize - len) * 100 / currentUploadTotalSize / 5 < + currentUploadCompleteSize * 100 / currentUploadTotalSize / 5) { + updateRequired = true; + } +} + +void UploadFileScreen::onFileUploadEnd(AsyncWebServerRequest* request) { + currentUploadStatus = Complete; + + // Final chunk of data: close the file and send a response + // Writing to SD uses SPI, so lock the screen + xSemaphoreTake(renderingMutex, portMAX_DELAY); + request->_tempFile.close(); + xSemaphoreGive(renderingMutex); + + updateRequired = true; +} + +void UploadFileScreen::onEnter() { + renderingMutex = xSemaphoreCreateMutex(); + + uploadServer.reset(new UploadServer( + [this](AsyncWebServerRequest* request, const String& filename) { onFileUploadStart(request, filename); }, + [this](AsyncWebServerRequest* request, const uint8_t* data, const size_t len) { + onFileUploadPart(request, data, len); + }, + [this](AsyncWebServerRequest* request) { onFileUploadEnd(request); })); + uploadServer->begin(); + + // Trigger first update + updateRequired = true; + + xTaskCreate(&UploadFileScreen::taskTrampoline, "UploadFileScreenTask", + 2048, // Stack size + this, // Parameters + 1, // Priority + &displayTaskHandle // Task handle + ); +} + +void UploadFileScreen::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; + } + uploadServer->end(); + uploadServer.reset(); + vSemaphoreDelete(renderingMutex); + renderingMutex = nullptr; +} + +void UploadFileScreen::handleInput() { + uploadServer->loop(); + + if (inputManager.wasPressed(InputManager::BTN_BACK)) { + onGoHome(); + } +} diff --git a/src/screens/UploadFileScreen.h b/src/screens/UploadFileScreen.h new file mode 100644 index 0000000..9470150 --- /dev/null +++ b/src/screens/UploadFileScreen.h @@ -0,0 +1,33 @@ +#pragma once +#include + +#include "Screen.h" +#include "server/UploadServer.h" + +class UploadFileScreen final : public Screen { + enum UploadStatus { Idle, InProgress, Complete }; + + TaskHandle_t displayTaskHandle = nullptr; + SemaphoreHandle_t renderingMutex = nullptr; + std::unique_ptr uploadServer = nullptr; + size_t currentUploadTotalSize = 0; + size_t currentUploadCompleteSize = 0; + String currentUploadFilename = ""; + UploadStatus currentUploadStatus = Idle; + bool updateRequired = false; + const std::function onGoHome; + + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); + void render() const; + void onFileUploadStart(AsyncWebServerRequest* request, const String& filename); + void onFileUploadPart(AsyncWebServerRequest* request, const uint8_t* data, size_t len); + void onFileUploadEnd(AsyncWebServerRequest* request); + + public: + explicit UploadFileScreen(GfxRenderer& renderer, InputManager& inputManager, const std::function& onGoHome) + : Screen(renderer, inputManager), onGoHome(onGoHome) {} + void onEnter() override; + void onExit() override; + void handleInput() override; +}; diff --git a/src/server/UploadServer.cpp b/src/server/UploadServer.cpp new file mode 100644 index 0000000..4e18ec6 --- /dev/null +++ b/src/server/UploadServer.cpp @@ -0,0 +1,62 @@ +#include "UploadServer.h" + +#include + +#include "html/UploadHtml.generated.h" +#include "html/UploadSuccessHtml.generated.h" + +void UploadServer::begin() { + dnsServer.reset(new DNSServer()); + server.reset(new AsyncWebServer(80)); + + WiFi.mode(WIFI_AP); + WiFi.softAP("CrossPoint"); + + server->on("/upload", HTTP_GET, [](AsyncWebServerRequest* request) { request->send(200, "text/html", UploadHtml); }); + + server->on( + "/upload", HTTP_POST, + [](AsyncWebServerRequest* request) { + request->send(200, "text/html", UploadSuccessHtml); + }, + [this](AsyncWebServerRequest* request, const String& filename, const size_t index, const uint8_t* data, + const size_t len, const bool final) { + // This function is called multiple times as data chunks are received + if (!index) { + onStart(request, filename); + } + + if (len) { + onPart(request, data, len); + } + + if (final) { + onEnd(request); + } + }); + + server->onNotFound([](AsyncWebServerRequest* request) { request->redirect("/upload"); }); + + dnsServer->start(53, "*", WiFi.softAPIP()); + server->begin(); + running = true; +} + +void UploadServer::loop() { + if (running) { + dnsServer->processNextRequest(); + } +} + +void UploadServer::end() { + if (running) { + server->reset(); + server->end(); + dnsServer->stop(); + WiFi.softAPdisconnect(true); + WiFi.mode(WIFI_OFF); + server.reset(); + dnsServer.reset(); + running = false; + } +} diff --git a/src/server/UploadServer.h b/src/server/UploadServer.h new file mode 100644 index 0000000..f38b917 --- /dev/null +++ b/src/server/UploadServer.h @@ -0,0 +1,24 @@ +#pragma once +#include +#include + +#include + +class UploadServer { + bool running = false; + std::unique_ptr dnsServer = nullptr; + std::unique_ptr server = nullptr; + std::function onStart; + std::function onPart; + std::function onEnd; + + public: + UploadServer(const std::function& onStart, + const std::function& onPart, + const std::function& onEnd) + : onStart(onStart), onPart(onPart), onEnd(onEnd) {} + ~UploadServer() = default; + void begin(); + void loop(); + void end(); +}; diff --git a/src/server/html/Upload.html b/src/server/html/Upload.html new file mode 100644 index 0000000..860a83c --- /dev/null +++ b/src/server/html/Upload.html @@ -0,0 +1,86 @@ + + + + + + CrossPoint File Upload + + + + +
+

CrossPoint Upload

+ +
+ + + + + +
+ +

Check your device for upload progress.

+
+ + diff --git a/src/server/html/UploadSuccess.html b/src/server/html/UploadSuccess.html new file mode 100644 index 0000000..0511d76 --- /dev/null +++ b/src/server/html/UploadSuccess.html @@ -0,0 +1,76 @@ + + + + + + Upload Successful + + + + +
+

🎉 Success!

+

Your file has been uploaded to your device.

+ + Upload Another File +
+ + + From d552ace462a8665439cfb3ab90d5dfc4fef86382 Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Tue, 16 Dec 2025 18:19:06 +1100 Subject: [PATCH 2/3] Fix progress bar overflow --- src/screens/UploadFileScreen.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/screens/UploadFileScreen.cpp b/src/screens/UploadFileScreen.cpp index 090da24..df41289 100644 --- a/src/screens/UploadFileScreen.cpp +++ b/src/screens/UploadFileScreen.cpp @@ -34,9 +34,9 @@ void UploadFileScreen::render() const { renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "UPLOADING"); if (currentUploadStatus == InProgress) { + const double complete = static_cast(currentUploadCompleteSize) / static_cast(currentUploadTotalSize); renderer.drawRect(20, pageHeight / 2 + 110, pageWidth - 40, 50); - renderer.fillRect(22, pageHeight / 2 + 112, - static_cast(pageWidth - 44) * currentUploadCompleteSize / currentUploadTotalSize, 46); + renderer.fillRect(22, pageHeight / 2 + 112, (pageWidth - 44) * complete, 46); } renderer.displayBuffer(); @@ -69,10 +69,12 @@ void UploadFileScreen::onFileUploadPart(AsyncWebServerRequest* request, const ui request->_tempFile.write(data, len); xSemaphoreGive(renderingMutex); + const int oldPercent = static_cast(currentUploadCompleteSize) / static_cast(currentUploadTotalSize) * 100; currentUploadCompleteSize += len; + const int newPercent = static_cast(currentUploadCompleteSize) / static_cast(currentUploadTotalSize) * 100; + // Only update the screen at most every 5% to avoid blocking the SPI channel - if (currentUploadTotalSize > 0 && (currentUploadCompleteSize - len) * 100 / currentUploadTotalSize / 5 < - currentUploadCompleteSize * 100 / currentUploadTotalSize / 5) { + if (oldPercent / 5 < newPercent / 5) { updateRequired = true; } } From c8cdf5cd5a9dd8317848aee53584d3474ef27572 Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Wed, 17 Dec 2025 01:22:14 +1100 Subject: [PATCH 3/3] Add new home screen and link to file selection and upload screens --- src/main.cpp | 24 ++++++-- src/screens/FileSelectionScreen.cpp | 6 +- src/screens/FileSelectionScreen.h | 6 +- src/screens/HomeScreen.cpp | 87 +++++++++++++++++++++++++++++ src/screens/HomeScreen.h | 38 +++++++++++++ src/screens/UploadFileScreen.cpp | 9 ++- src/server/UploadServer.cpp | 5 +- 7 files changed, 156 insertions(+), 19 deletions(-) create mode 100644 src/screens/HomeScreen.cpp create mode 100644 src/screens/HomeScreen.h diff --git a/src/main.cpp b/src/main.cpp index eb3bc0b..39a14b3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -21,8 +21,10 @@ #include "screens/EpubReaderScreen.h" #include "screens/FileSelectionScreen.h" #include "screens/FullScreenMessageScreen.h" +#include "screens/HomeScreen.h" #include "screens/SettingsScreen.h" #include "screens/SleepScreen.h" +#include "screens/UploadFileScreen.h" #define SPI_FQ 40000000 // Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults) @@ -150,6 +152,7 @@ void enterDeepSleep() { } void onGoHome(); +void onGoToFileSelection(); void onSelectEpubFile(const std::string& path) { exitScreen(); enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Loading...")); @@ -165,18 +168,28 @@ void onSelectEpubFile(const std::string& path) { enterNewScreen( new FullScreenMessageScreen(renderer, inputManager, "Failed to load epub", REGULAR, EInkDisplay::HALF_REFRESH)); delay(2000); - onGoHome(); + onGoToFileSelection(); } } +void onGoToFileSelection() { + exitScreen(); + enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile, onGoHome)); +} + void onGoToSettings() { exitScreen(); enterNewScreen(new SettingsScreen(renderer, inputManager, onGoHome)); } +void onGoToUploadFile() { + exitScreen(); + enterNewScreen(new UploadFileScreen(renderer, inputManager, onGoHome)); +} + void onGoHome() { exitScreen(); - enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile, onGoToSettings)); + enterNewScreen(new HomeScreen(renderer, inputManager, onGoToFileSelection, onGoToSettings, onGoToUploadFile)); } void setup() { @@ -221,8 +234,7 @@ void setup() { } } - exitScreen(); - enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile, onGoToSettings)); + onGoHome(); // Ensure we're not still holding the power button before leaving setup waitForPowerRelease(); @@ -233,8 +245,8 @@ void loop() { static unsigned long lastMemPrint = 0; if (Serial && millis() - lastMemPrint >= 10000) { - Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(), - ESP.getHeapSize(), ESP.getMinFreeHeap()); + Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes, Max alloc: %d\n", millis(), + ESP.getFreeHeap(), ESP.getHeapSize(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap()); lastMemPrint = millis(); } diff --git a/src/screens/FileSelectionScreen.cpp b/src/screens/FileSelectionScreen.cpp index 09f2efc..ce4d000 100644 --- a/src/screens/FileSelectionScreen.cpp +++ b/src/screens/FileSelectionScreen.cpp @@ -98,8 +98,8 @@ void FileSelectionScreen::handleInput() { loadFiles(); updateRequired = true; } else { - // At root level, go to settings - onSettingsOpen(); + // At root level, go back home + onGoHome(); } } else if (prevPressed) { selectorIndex = (selectorIndex + files.size() - 1) % files.size(); @@ -129,7 +129,7 @@ void FileSelectionScreen::render() const { renderer.drawCenteredText(READER_FONT_ID, 10, "CrossPoint Reader", true, BOLD); // Help text - renderer.drawText(SMALL_FONT_ID, 20, GfxRenderer::getScreenHeight() - 30, "Press BACK for Settings"); + renderer.drawText(SMALL_FONT_ID, 20, GfxRenderer::getScreenHeight() - 30, "Press BACK for Home"); if (files.empty()) { renderer.drawText(UI_FONT_ID, 20, 60, "No EPUBs found"); diff --git a/src/screens/FileSelectionScreen.h b/src/screens/FileSelectionScreen.h index 00947a5..aaa076d 100644 --- a/src/screens/FileSelectionScreen.h +++ b/src/screens/FileSelectionScreen.h @@ -17,7 +17,7 @@ class FileSelectionScreen final : public Screen { int selectorIndex = 0; bool updateRequired = false; const std::function onSelect; - const std::function onSettingsOpen; + const std::function onGoHome; static void taskTrampoline(void* param); [[noreturn]] void displayTaskLoop(); @@ -27,8 +27,8 @@ class FileSelectionScreen final : public Screen { public: explicit FileSelectionScreen(GfxRenderer& renderer, InputManager& inputManager, const std::function& onSelect, - const std::function& onSettingsOpen) - : Screen(renderer, inputManager), onSelect(onSelect), onSettingsOpen(onSettingsOpen) {} + const std::function& onGoHome) + : Screen(renderer, inputManager), onSelect(onSelect), onGoHome(onGoHome) {} void onEnter() override; void onExit() override; void handleInput() override; diff --git a/src/screens/HomeScreen.cpp b/src/screens/HomeScreen.cpp new file mode 100644 index 0000000..052bcd0 --- /dev/null +++ b/src/screens/HomeScreen.cpp @@ -0,0 +1,87 @@ +#include "HomeScreen.h" + +#include +#include + +#include "config.h" + +void HomeScreen::taskTrampoline(void* param) { + auto* self = static_cast(param); + self->displayTaskLoop(); +} + +void HomeScreen::onEnter() { + renderingMutex = xSemaphoreCreateMutex(); + + selectorIndex = 0; + + // Trigger first update + updateRequired = true; + + xTaskCreate(&HomeScreen::taskTrampoline, "HomeScreenTask", + 2048, // Stack size + this, // Parameters + 1, // Priority + &displayTaskHandle // Task handle + ); +} + +void HomeScreen::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 HomeScreen::handleInput() { + const bool prevPressed = + inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT); + const bool nextPressed = + inputManager.wasPressed(InputManager::BTN_DOWN) || inputManager.wasPressed(InputManager::BTN_RIGHT); + + if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) { + if (selectorIndex == 0) { + onFileSelectionOpen(); + } else if (selectorIndex == 1) { + onUploadFileOpen(); + } else if (selectorIndex == 2) { + onSettingsOpen(); + } + } else if (prevPressed) { + selectorIndex = (selectorIndex + menuItemCount - 1) % menuItemCount; + updateRequired = true; + } else if (nextPressed) { + selectorIndex = (selectorIndex + 1) % menuItemCount; + updateRequired = true; + } +} + +void HomeScreen::displayTaskLoop() { + while (true) { + if (updateRequired) { + updateRequired = false; + xSemaphoreTake(renderingMutex, portMAX_DELAY); + render(); + xSemaphoreGive(renderingMutex); + } + vTaskDelay(10 / portTICK_PERIOD_MS); + } +} + +void HomeScreen::render() const { + renderer.clearScreen(); + + const auto pageWidth = GfxRenderer::getScreenWidth(); + renderer.drawCenteredText(READER_FONT_ID, 10, "CrossPoint Reader", true, BOLD); + + // Draw selection + renderer.fillRect(0, 60 + selectorIndex * 30 + 2, pageWidth - 1, 30); + renderer.drawText(UI_FONT_ID, 20, 60, "Read", selectorIndex != 0); + renderer.drawText(UI_FONT_ID, 20, 90, "Upload", selectorIndex != 1); + renderer.drawText(UI_FONT_ID, 20, 120, "Settings", selectorIndex != 2); + renderer.displayBuffer(); +} diff --git a/src/screens/HomeScreen.h b/src/screens/HomeScreen.h new file mode 100644 index 0000000..755cb37 --- /dev/null +++ b/src/screens/HomeScreen.h @@ -0,0 +1,38 @@ +#pragma once +#include +#include +#include + +#include +#include +#include + +#include "Screen.h" + +class HomeScreen final : public Screen { + TaskHandle_t displayTaskHandle = nullptr; + SemaphoreHandle_t renderingMutex = nullptr; + int selectorIndex = 0; + bool updateRequired = false; + const std::function onFileSelectionOpen; + const std::function onSettingsOpen; + const std::function onUploadFileOpen; + + static constexpr int menuItemCount = 3; + + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); + void render() const; + + public: + explicit HomeScreen(GfxRenderer& renderer, InputManager& inputManager, + const std::function& onFileSelectionOpen, const std::function& onSettingsOpen, + const std::function& onUploadFileOpen) + : Screen(renderer, inputManager), + onFileSelectionOpen(onFileSelectionOpen), + onSettingsOpen(onSettingsOpen), + onUploadFileOpen(onUploadFileOpen) {} + void onEnter() override; + void onExit() override; + void handleInput() override; +}; diff --git a/src/screens/UploadFileScreen.cpp b/src/screens/UploadFileScreen.cpp index df41289..899a47d 100644 --- a/src/screens/UploadFileScreen.cpp +++ b/src/screens/UploadFileScreen.cpp @@ -34,7 +34,8 @@ void UploadFileScreen::render() const { renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "UPLOADING"); if (currentUploadStatus == InProgress) { - const double complete = static_cast(currentUploadCompleteSize) / static_cast(currentUploadTotalSize); + const double complete = + static_cast(currentUploadCompleteSize) / static_cast(currentUploadTotalSize); renderer.drawRect(20, pageHeight / 2 + 110, pageWidth - 40, 50); renderer.fillRect(22, pageHeight / 2 + 112, (pageWidth - 44) * complete, 46); } @@ -69,9 +70,11 @@ void UploadFileScreen::onFileUploadPart(AsyncWebServerRequest* request, const ui request->_tempFile.write(data, len); xSemaphoreGive(renderingMutex); - const int oldPercent = static_cast(currentUploadCompleteSize) / static_cast(currentUploadTotalSize) * 100; + const int oldPercent = + static_cast(currentUploadCompleteSize) / static_cast(currentUploadTotalSize) * 100; currentUploadCompleteSize += len; - const int newPercent = static_cast(currentUploadCompleteSize) / static_cast(currentUploadTotalSize) * 100; + const int newPercent = + static_cast(currentUploadCompleteSize) / static_cast(currentUploadTotalSize) * 100; // Only update the screen at most every 5% to avoid blocking the SPI channel if (oldPercent / 5 < newPercent / 5) { diff --git a/src/server/UploadServer.cpp b/src/server/UploadServer.cpp index 4e18ec6..c5c38ff 100644 --- a/src/server/UploadServer.cpp +++ b/src/server/UploadServer.cpp @@ -15,10 +15,7 @@ void UploadServer::begin() { server->on("/upload", HTTP_GET, [](AsyncWebServerRequest* request) { request->send(200, "text/html", UploadHtml); }); server->on( - "/upload", HTTP_POST, - [](AsyncWebServerRequest* request) { - request->send(200, "text/html", UploadSuccessHtml); - }, + "/upload", HTTP_POST, [](AsyncWebServerRequest* request) { request->send(200, "text/html", UploadSuccessHtml); }, [this](AsyncWebServerRequest* request, const String& filename, const size_t index, const uint8_t* data, const size_t len, const bool final) { // This function is called multiple times as data chunks are received