From 080a9bb1bb7038e62e690bf817f03684a42bb99b Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Thu, 22 Jan 2026 19:07:18 +0100 Subject: [PATCH 01/10] wip: qemu for development --- docs/emulation.md | 12 ++++++++++ platformio.ini | 13 ++++++++++- scripts/emulation/Dockerfile | 25 +++++++++++++++++++++ scripts/emulation/docker-compose.yml | 33 ++++++++++++++++++++++++++++ src/main.cpp | 2 +- 5 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 docs/emulation.md create mode 100644 scripts/emulation/Dockerfile create mode 100644 scripts/emulation/docker-compose.yml diff --git a/docs/emulation.md b/docs/emulation.md new file mode 100644 index 00000000..0a920bae --- /dev/null +++ b/docs/emulation.md @@ -0,0 +1,12 @@ +# Emulation (development) + +TODO: write this + +Build the program using `env:emulation`, this is required to re-route `Serial.print` to qemu's stdout. + +To run it: + +```sh +cd scripts/emulation +docker compose up +``` diff --git a/platformio.ini b/platformio.ini index 7f42637d..b99d8690 100644 --- a/platformio.ini +++ b/platformio.ini @@ -20,7 +20,6 @@ board_upload.offset_address = 0x10000 build_flags = -DARDUINO_USB_MODE=1 - -DARDUINO_USB_CDC_ON_BOOT=1 -DMINIZ_NO_ZLIB_COMPATIBLE_NAMES=1 -DEINK_DISPLAY_SINGLE_BUFFER_MODE=1 -DDISABLE_FS_H_WARNING=1 @@ -54,9 +53,21 @@ extends = base build_flags = ${base.build_flags} -DCROSSPOINT_VERSION=\"${crosspoint.version}-dev\" + -DCROSSPOINT_EMULATED=0 + -DARDUINO_USB_CDC_ON_BOOT=1 + +[env:emulation] +extends = base +build_flags = + ${base.build_flags} + -DCROSSPOINT_VERSION=\"${crosspoint.version}-emu\" + -DCROSSPOINT_EMULATED=1 + -DARDUINO_USB_CDC_ON_BOOT=0 # Disable USB CDC on boot for Serial output via UART [env:gh_release] extends = base build_flags = ${base.build_flags} -DCROSSPOINT_VERSION=\"${crosspoint.version}\" + -DCROSSPOINT_EMULATED=0 + -DARDUINO_USB_CDC_ON_BOOT=1 diff --git a/scripts/emulation/Dockerfile b/scripts/emulation/Dockerfile new file mode 100644 index 00000000..9439c9f2 --- /dev/null +++ b/scripts/emulation/Dockerfile @@ -0,0 +1,25 @@ +FROM debian:12 + +WORKDIR /root + +ARG DEBIAN_FRONTEND=noninteractive + +# Update container +RUN apt-get update && apt-get upgrade -y + +# Install dependencies +RUN apt-get install git wget flex bison gperf python3 python3-pip python3-setuptools python3-venv cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0 \ + libgcrypt20 libglib2.0-0 libpixman-1-0 libsdl2-2.0-0 libslirp0 -y + +# Install ESP-IDF +ENV PATH="~/.local/bin:${PATH}" +RUN mkdir -p ~/esp && cd ~/esp && git clone --quiet --recursive https://github.com/espressif/esp-idf.git && ln -s /usr/bin/python3 /usr/bin/python +RUN cd ~/esp/esp-idf && export IDF_GITHUB_ASSETS="dl.espressif.com/github_assets" && ./install.sh && /bin/bash -c "source ./export.sh" + +# Install qemu +RUN cd ~/esp/esp-idf && python tools/idf_tools.py install qemu-riscv32 + +RUN echo "alias idf='source /root/esp/esp-idf/export.sh'" >> .bashrc + +# Start from a Bash prompt +CMD [ "/bin/bash" ] diff --git a/scripts/emulation/docker-compose.yml b/scripts/emulation/docker-compose.yml new file mode 100644 index 00000000..ffc7489d --- /dev/null +++ b/scripts/emulation/docker-compose.yml @@ -0,0 +1,33 @@ +services: + crosspoint-emulator: + build: . + container_name: crosspoint-emulator + volumes: + - ../..:/app:ro + tmpfs: + - /tmp + ports: + - "8080:80" + stdin_open: true + tty: true + stop_signal: SIGKILL + entrypoint: + - /bin/bash + - -c + - | + set -x; + source /root/esp/esp-idf/export.sh + + cd /tmp + + cp /app/.pio/build/emulation/*.bin . + dd if=/dev/zero bs=1M count=16 of=./flash.bin + dd if="bootloader.bin" bs=1 count=$(stat -c%s "bootloader.bin") seek=0 conv=notrunc of=./flash.bin + dd if="partitions.bin" bs=1 count=$(stat -c%s "partitions.bin") seek=$((16#8000)) conv=notrunc of=./flash.bin + dd if="firmware.bin" bs=1 count=$(stat -c%s "firmware.bin") seek=$((16#10000)) conv=notrunc of=./flash.bin + + qemu-system-riscv32 \ + -nographic \ + -M esp32c3 \ + -drive \ + file=flash.bin,if=mtd,format=raw diff --git a/src/main.cpp b/src/main.cpp index c0222e0d..f848c8a2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -278,7 +278,7 @@ void setupDisplayAndFonts() { bool isUsbConnected() { // U0RXD/GPIO20 reads HIGH when USB is connected - return digitalRead(UART0_RXD) == HIGH; + return CROSSPOINT_EMULATED || digitalRead(UART0_RXD) == HIGH; } bool isWakeupAfterFlashing() { From 58232d2483d703c3a01720e6bc5a8639a795eb9b Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Thu, 22 Jan 2026 20:16:35 +0100 Subject: [PATCH 02/10] add HalDisplay --- lib/GfxRenderer/GfxRenderer.cpp | 34 ++- lib/GfxRenderer/GfxRenderer.h | 14 +- lib/hal/HalDisplay.cpp | 212 ++++++++++++++++++ lib/hal/HalDisplay.h | 57 +++++ src/activities/boot_sleep/SleepActivity.cpp | 6 +- src/activities/reader/EpubReaderActivity.cpp | 4 +- src/activities/reader/TxtReaderActivity.cpp | 4 +- src/activities/reader/XtcReaderActivity.cpp | 4 +- .../util/FullScreenMessageActivity.h | 6 +- src/main.cpp | 4 +- 10 files changed, 306 insertions(+), 39 deletions(-) create mode 100644 lib/hal/HalDisplay.cpp create mode 100644 lib/hal/HalDisplay.h diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 08420bf9..fb54d803 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -10,19 +10,19 @@ void GfxRenderer::rotateCoordinates(const int x, const int y, int* rotatedX, int // Logical portrait (480x800) → panel (800x480) // Rotation: 90 degrees clockwise *rotatedX = y; - *rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x; + *rotatedY = HalDisplay::DISPLAY_HEIGHT - 1 - x; break; } case LandscapeClockwise: { // Logical landscape (800x480) rotated 180 degrees (swap top/bottom and left/right) - *rotatedX = EInkDisplay::DISPLAY_WIDTH - 1 - x; - *rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - y; + *rotatedX = HalDisplay::DISPLAY_WIDTH - 1 - x; + *rotatedY = HalDisplay::DISPLAY_HEIGHT - 1 - y; break; } case PortraitInverted: { // Logical portrait (480x800) → panel (800x480) // Rotation: 90 degrees counter-clockwise - *rotatedX = EInkDisplay::DISPLAY_WIDTH - 1 - y; + *rotatedX = HalDisplay::DISPLAY_WIDTH - 1 - y; *rotatedY = x; break; } @@ -49,14 +49,14 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const { rotateCoordinates(x, y, &rotatedX, &rotatedY); // Bounds checking against physical panel dimensions - if (rotatedX < 0 || rotatedX >= EInkDisplay::DISPLAY_WIDTH || rotatedY < 0 || - rotatedY >= EInkDisplay::DISPLAY_HEIGHT) { + if (rotatedX < 0 || rotatedX >= HalDisplay::DISPLAY_WIDTH || rotatedY < 0 || + rotatedY >= HalDisplay::DISPLAY_HEIGHT) { Serial.printf("[%lu] [GFX] !! Outside range (%d, %d) -> (%d, %d)\n", millis(), x, y, rotatedX, rotatedY); return; } // Calculate byte position and bit position - const uint16_t byteIndex = rotatedY * EInkDisplay::DISPLAY_WIDTH_BYTES + (rotatedX / 8); + const uint16_t byteIndex = rotatedY * HalDisplay::DISPLAY_WIDTH_BYTES + (rotatedX / 8); const uint8_t bitPosition = 7 - (rotatedX % 8); // MSB first if (state) { @@ -392,12 +392,12 @@ void GfxRenderer::invertScreen() const { Serial.printf("[%lu] [GFX] !! No framebuffer in invertScreen\n", millis()); return; } - for (int i = 0; i < EInkDisplay::BUFFER_SIZE; i++) { + for (int i = 0; i < HalDisplay::BUFFER_SIZE; i++) { buffer[i] = ~buffer[i]; } } -void GfxRenderer::displayBuffer(const EInkDisplay::RefreshMode refreshMode) const { +void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const { einkDisplay.displayBuffer(refreshMode); } @@ -418,13 +418,13 @@ int GfxRenderer::getScreenWidth() const { case Portrait: case PortraitInverted: // 480px wide in portrait logical coordinates - return EInkDisplay::DISPLAY_HEIGHT; + return HalDisplay::DISPLAY_HEIGHT; case LandscapeClockwise: case LandscapeCounterClockwise: // 800px wide in landscape logical coordinates - return EInkDisplay::DISPLAY_WIDTH; + return HalDisplay::DISPLAY_WIDTH; } - return EInkDisplay::DISPLAY_HEIGHT; + return HalDisplay::DISPLAY_HEIGHT; } int GfxRenderer::getScreenHeight() const { @@ -432,13 +432,13 @@ int GfxRenderer::getScreenHeight() const { case Portrait: case PortraitInverted: // 800px tall in portrait logical coordinates - return EInkDisplay::DISPLAY_WIDTH; + return HalDisplay::DISPLAY_WIDTH; case LandscapeClockwise: case LandscapeCounterClockwise: // 480px tall in landscape logical coordinates - return EInkDisplay::DISPLAY_HEIGHT; + return HalDisplay::DISPLAY_HEIGHT; } - return EInkDisplay::DISPLAY_WIDTH; + return HalDisplay::DISPLAY_WIDTH; } int GfxRenderer::getSpaceWidth(const int fontId) const { @@ -640,9 +640,7 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y uint8_t* GfxRenderer::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); } -size_t GfxRenderer::getBufferSize() { return EInkDisplay::BUFFER_SIZE; } - -void GfxRenderer::grayscaleRevert() const { einkDisplay.grayscaleRevert(); } +size_t GfxRenderer::getBufferSize() { return HalDisplay::BUFFER_SIZE; } void GfxRenderer::copyGrayscaleLsbBuffers() const { einkDisplay.copyGrayscaleLsbBuffers(einkDisplay.getFrameBuffer()); } diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index b1fea69b..6e836354 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include @@ -21,11 +21,11 @@ class GfxRenderer { private: static constexpr size_t BW_BUFFER_CHUNK_SIZE = 8000; // 8KB chunks to allow for non-contiguous memory - static constexpr size_t BW_BUFFER_NUM_CHUNKS = EInkDisplay::BUFFER_SIZE / BW_BUFFER_CHUNK_SIZE; - static_assert(BW_BUFFER_CHUNK_SIZE * BW_BUFFER_NUM_CHUNKS == EInkDisplay::BUFFER_SIZE, + static constexpr size_t BW_BUFFER_NUM_CHUNKS = HalDisplay::BUFFER_SIZE / BW_BUFFER_CHUNK_SIZE; + static_assert(BW_BUFFER_CHUNK_SIZE * BW_BUFFER_NUM_CHUNKS == HalDisplay::BUFFER_SIZE, "BW buffer chunking does not line up with display buffer size"); - EInkDisplay& einkDisplay; + HalDisplay& einkDisplay; RenderMode renderMode; Orientation orientation; uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr}; @@ -36,7 +36,7 @@ class GfxRenderer { void rotateCoordinates(int x, int y, int* rotatedX, int* rotatedY) const; public: - explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), renderMode(BW), orientation(Portrait) {} + explicit GfxRenderer(HalDisplay& einkDisplay) : einkDisplay(einkDisplay), renderMode(BW), orientation(Portrait) {} ~GfxRenderer() { freeBwBufferChunks(); } static constexpr int VIEWABLE_MARGIN_TOP = 9; @@ -54,7 +54,7 @@ class GfxRenderer { // Screen ops int getScreenWidth() const; int getScreenHeight() const; - void displayBuffer(EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const; + void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const; // EXPERIMENTAL: Windowed update - display only a rectangular region void displayWindow(int x, int y, int width, int height) const; void invertScreen() const; @@ -106,6 +106,6 @@ class GfxRenderer { // Low level functions uint8_t* getFrameBuffer() const; static size_t getBufferSize(); - void grayscaleRevert() const; + // void grayscaleRevert() const; // unused void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const; }; diff --git a/lib/hal/HalDisplay.cpp b/lib/hal/HalDisplay.cpp new file mode 100644 index 00000000..28bcfa64 --- /dev/null +++ b/lib/hal/HalDisplay.cpp @@ -0,0 +1,212 @@ +#include +#include + +std::string base64_encode(char* buf, unsigned int bufLen); + +HalDisplay::HalDisplay(int8_t sclk, int8_t mosi, int8_t cs, int8_t dc, int8_t rst, int8_t busy) : einkDisplay(sclk, mosi, cs, dc, rst, busy) { + if (is_emulated) { + emuFramebuffer0 = new uint8_t[BUFFER_SIZE]; + } +} + +HalDisplay::~HalDisplay() { + if (emuFramebuffer0) { + delete[] emuFramebuffer0; + emuFramebuffer0 = nullptr; + } +} + +void HalDisplay::begin() { + if (!is_emulated) { + einkDisplay.begin(); + } else { + Serial.printf("[%lu] [ ] Emulated display initialized\n", millis()); + // no-op + } +} + +void HalDisplay::clearScreen(uint8_t color) const { + if (!is_emulated) { + einkDisplay.clearScreen(color); + } else { + Serial.printf("[%lu] [ ] Emulated clear screen with color 0x%02X\n", millis(), color); + memset(emuFramebuffer0, color, BUFFER_SIZE); + } +} + +void HalDisplay::drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h, bool fromProgmem) const { + if (!is_emulated) { + einkDisplay.drawImage(imageData, x, y, w, h, fromProgmem); + } else { + Serial.printf("[%lu] [ ] Emulated draw image at (%u, %u) with size %ux%u\n", millis(), x, y, w, h); + + // Calculate bytes per line for the image + const uint16_t imageWidthBytes = w / 8; + + // Copy image data to frame buffer + for (uint16_t row = 0; row < h; row++) { + const uint16_t destY = y + row; + if (destY >= DISPLAY_HEIGHT) + break; + + const uint16_t destOffset = destY * DISPLAY_WIDTH_BYTES + (x / 8); + const uint16_t srcOffset = row * imageWidthBytes; + + for (uint16_t col = 0; col < imageWidthBytes; col++) { + if ((x / 8 + col) >= DISPLAY_WIDTH_BYTES) + break; + + if (fromProgmem) { + emuFramebuffer0[destOffset + col] = pgm_read_byte(&imageData[srcOffset + col]); + } else { + emuFramebuffer0[destOffset + col] = imageData[srcOffset + col]; + } + } + } + } +} + +void HalDisplay::displayBuffer(RefreshMode mode) { + if (!is_emulated) { + einkDisplay.displayBuffer(mode); + } else { + Serial.printf("[%lu] [ ] Emulated display buffer with mode %d\n", millis(), static_cast(mode)); + std::string b64 = base64_encode(reinterpret_cast(emuFramebuffer0), BUFFER_SIZE); + Serial.printf("$$DATA:DISPLAY:{\"mode\":%d,\"buffer\":\"", static_cast(mode)); + Serial.print(b64.c_str()); + Serial.print("\"}$$\n"); + } +} + +void HalDisplay::refreshDisplay(RefreshMode mode, bool turnOffScreen) { + if (!is_emulated) { + einkDisplay.refreshDisplay(mode, turnOffScreen); + } else { + Serial.printf("[%lu] [ ] Emulated refresh display with mode %d, turnOffScreen %d\n", millis(), static_cast(mode), turnOffScreen); + // emulated delay + if (mode == RefreshMode::FAST_REFRESH) { + delay(50); + } else if (mode == RefreshMode::HALF_REFRESH) { + delay(800); + } else if (mode == RefreshMode::FULL_REFRESH) { + delay(1500); + } + } +} + +void HalDisplay::deepSleep() { + if (!is_emulated) { + einkDisplay.deepSleep(); + } else { + Serial.printf("[%lu] [ ] Emulated deep sleep\n", millis()); + // no-op + } +} + +uint8_t* HalDisplay::getFrameBuffer() const { + if (!is_emulated) { + return einkDisplay.getFrameBuffer(); + } else { + return emuFramebuffer0; + } +} + +void HalDisplay::copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer) { + if (!is_emulated) { + einkDisplay.copyGrayscaleBuffers(lsbBuffer, msbBuffer); + } else { + Serial.printf("[%lu] [ ] Emulated copy grayscale buffers\n", millis()); + // TODO: not sure what this does + } +} + +void HalDisplay::copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer) { + if (!is_emulated) { + einkDisplay.copyGrayscaleLsbBuffers(lsbBuffer); + } else { + Serial.printf("[%lu] [ ] Emulated copy grayscale LSB buffers\n", millis()); + // TODO: not sure what this does + } +} + +void HalDisplay::copyGrayscaleMsbBuffers(const uint8_t* msbBuffer) { + if (!is_emulated) { + einkDisplay.copyGrayscaleMsbBuffers(msbBuffer); + } else { + Serial.printf("[%lu] [ ] Emulated copy grayscale MSB buffers\n", millis()); + // TODO: not sure what this does + } +} + +void HalDisplay::cleanupGrayscaleBuffers(const uint8_t* bwBuffer) { + if (!is_emulated) { + einkDisplay.cleanupGrayscaleBuffers(bwBuffer); + } else { + Serial.printf("[%lu] [ ] Emulated cleanup grayscale buffers\n", millis()); + // TODO: not sure what this does + } +} + +void HalDisplay::displayGrayBuffer() { + if (!is_emulated) { + einkDisplay.displayGrayBuffer(); + } else { + Serial.printf("[%lu] [ ] Emulated display gray buffer\n", millis()); + // TODO: not sure what this does + } +} + +// +// Base64 utilities +// + +static const std::string base64_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + +static inline bool is_base64(char c) { + return (isalnum(c) || (c == '+') || (c == '/')); +} + +std::string base64_encode(char* buf, unsigned int bufLen) { + std::string ret; + ret.reserve(bufLen * 4 / 3 + 4); // reserve enough space + int i = 0; + int j = 0; + char char_array_3[3]; + char char_array_4[4]; + + while (bufLen--) { + char_array_3[i++] = *(buf++); + if (i == 3) { + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for(i = 0; (i < 4) ; i++) + ret += base64_chars[char_array_4[i]]; + i = 0; + } + } + + if (i) { + for(j = i; j < 3; j++) { + char_array_3[j] = '\0'; + } + + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for (j = 0; (j < i + 1); j++) + ret += base64_chars[char_array_4[j]]; + + while((i++ < 3)) + ret += '='; + } + + return ret; +} diff --git a/lib/hal/HalDisplay.h b/lib/hal/HalDisplay.h new file mode 100644 index 00000000..3c2bcc1a --- /dev/null +++ b/lib/hal/HalDisplay.h @@ -0,0 +1,57 @@ +#pragma once +#include +#include +#include + +class HalDisplay { + public: + // Constructor with pin configuration + HalDisplay(int8_t sclk, int8_t mosi, int8_t cs, int8_t dc, int8_t rst, int8_t busy); + + // Destructor + ~HalDisplay(); + + // Refresh modes (guarded to avoid redefinition in test builds) + using RefreshMode = EInkDisplay::RefreshMode; + static constexpr EInkDisplay::RefreshMode FULL_REFRESH = EInkDisplay::FULL_REFRESH; + static constexpr EInkDisplay::RefreshMode HALF_REFRESH = EInkDisplay::HALF_REFRESH; + static constexpr EInkDisplay::RefreshMode FAST_REFRESH = EInkDisplay::FAST_REFRESH; + + // Initialize the display hardware and driver + void begin(); + + // Display dimensions + static constexpr uint16_t DISPLAY_WIDTH = 800; + static constexpr uint16_t DISPLAY_HEIGHT = 480; + static constexpr uint16_t DISPLAY_WIDTH_BYTES = DISPLAY_WIDTH / 8; + static constexpr uint32_t BUFFER_SIZE = DISPLAY_WIDTH_BYTES * DISPLAY_HEIGHT; + + // Frame buffer operations + void clearScreen(uint8_t color = 0xFF) const; + void drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h, bool fromProgmem = false) const; + + void displayBuffer(RefreshMode mode = RefreshMode::FAST_REFRESH); + void refreshDisplay(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false); + + // Power management + void deepSleep(); + + // Access to frame buffer + uint8_t* getFrameBuffer() const; + + void copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer); + void copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer); + void copyGrayscaleMsbBuffers(const uint8_t* msbBuffer); + void cleanupGrayscaleBuffers(const uint8_t* bwBuffer); + + void displayGrayBuffer(); + + private: + bool is_emulated = CROSSPOINT_EMULATED; + + // real display implementation + EInkDisplay einkDisplay; + + // emulated display implementation + uint8_t* emuFramebuffer0 = nullptr; +}; diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index c0d6844f..7e291782 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -133,7 +133,7 @@ void SleepActivity::renderDefaultSleepScreen() const { renderer.invertScreen(); } - renderer.displayBuffer(EInkDisplay::HALF_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); } void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const { @@ -180,7 +180,7 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const { Serial.printf("[%lu] [SLP] drawing to %d x %d\n", millis(), x, y); renderer.clearScreen(); renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY); - renderer.displayBuffer(EInkDisplay::HALF_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); if (bitmap.hasGreyscale()) { bitmap.rewindToData(); @@ -270,5 +270,5 @@ void SleepActivity::renderCoverSleepScreen() const { void SleepActivity::renderBlankSleepScreen() const { renderer.clearScreen(); - renderer.displayBuffer(EInkDisplay::HALF_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); } diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 6ff39c5e..0743df71 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -322,7 +322,7 @@ void EpubReaderActivity::renderScreen() { auto progressCallback = [this, barX, barY, barWidth, barHeight](int progress) { const int fillWidth = (barWidth - 2) * progress / 100; renderer.fillRect(barX + 1, barY + 1, fillWidth, barHeight - 2, true); - renderer.displayBuffer(EInkDisplay::FAST_REFRESH); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); }; if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), @@ -392,7 +392,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); if (pagesUntilFullRefresh <= 1) { - renderer.displayBuffer(EInkDisplay::HALF_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); } else { renderer.displayBuffer(); diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index db725320..7c3ea29f 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -237,7 +237,7 @@ void TxtReaderActivity::buildPageIndex() { // Fill progress bar const int fillWidth = (barWidth - 2) * progressPercent / 100; renderer.fillRect(barX + 1, barY + 1, fillWidth, barHeight - 2, true); - renderer.displayBuffer(EInkDisplay::FAST_REFRESH); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); } // Yield to other tasks periodically @@ -465,7 +465,7 @@ void TxtReaderActivity::renderPage() { renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); if (pagesUntilFullRefresh <= 1) { - renderer.displayBuffer(EInkDisplay::HALF_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); } else { renderer.displayBuffer(); diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index 0a58d7b3..4aba7e66 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -269,7 +269,7 @@ void XtcReaderActivity::renderPage() { // Display BW with conditional refresh based on pagesUntilFullRefresh if (pagesUntilFullRefresh <= 1) { - renderer.displayBuffer(EInkDisplay::HALF_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); } else { renderer.displayBuffer(); @@ -349,7 +349,7 @@ void XtcReaderActivity::renderPage() { // Display with appropriate refresh if (pagesUntilFullRefresh <= 1) { - renderer.displayBuffer(EInkDisplay::HALF_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); } else { renderer.displayBuffer(); diff --git a/src/activities/util/FullScreenMessageActivity.h b/src/activities/util/FullScreenMessageActivity.h index 3e975c91..b6cf8bed 100644 --- a/src/activities/util/FullScreenMessageActivity.h +++ b/src/activities/util/FullScreenMessageActivity.h @@ -1,5 +1,5 @@ #pragma once -#include +#include #include #include @@ -10,12 +10,12 @@ class FullScreenMessageActivity final : public Activity { std::string text; EpdFontFamily::Style style; - EInkDisplay::RefreshMode refreshMode; + HalDisplay::RefreshMode refreshMode; public: explicit FullScreenMessageActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string text, const EpdFontFamily::Style style = EpdFontFamily::REGULAR, - const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) + const HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) : Activity("FullScreenMessage", renderer, mappedInput), text(std::move(text)), style(style), diff --git a/src/main.cpp b/src/main.cpp index f848c8a2..63ada0b2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include #include @@ -39,7 +39,7 @@ #define SD_SPI_MISO 7 -EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY); +HalDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY); InputManager inputManager; MappedInputManager mappedInputManager(inputManager); GfxRenderer renderer(einkDisplay); From c869ca46ed5a2f31532da09f9f664937492a4640 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Thu, 22 Jan 2026 21:02:03 +0100 Subject: [PATCH 03/10] got the display --- scripts/emulation/docker-compose.yml | 13 +- scripts/emulation/web_server.py | 242 +++++++++++++++++++++++++++ scripts/emulation/web_ui.html | 120 +++++++++++++ 3 files changed, 369 insertions(+), 6 deletions(-) create mode 100644 scripts/emulation/web_server.py create mode 100644 scripts/emulation/web_ui.html diff --git a/scripts/emulation/docker-compose.yml b/scripts/emulation/docker-compose.yml index ffc7489d..22d85dad 100644 --- a/scripts/emulation/docker-compose.yml +++ b/scripts/emulation/docker-compose.yml @@ -7,7 +7,7 @@ services: tmpfs: - /tmp ports: - - "8080:80" + - "8090:8090" stdin_open: true tty: true stop_signal: SIGKILL @@ -16,6 +16,7 @@ services: - -c - | set -x; + set -e; source /root/esp/esp-idf/export.sh cd /tmp @@ -26,8 +27,8 @@ services: dd if="partitions.bin" bs=1 count=$(stat -c%s "partitions.bin") seek=$((16#8000)) conv=notrunc of=./flash.bin dd if="firmware.bin" bs=1 count=$(stat -c%s "firmware.bin") seek=$((16#10000)) conv=notrunc of=./flash.bin - qemu-system-riscv32 \ - -nographic \ - -M esp32c3 \ - -drive \ - file=flash.bin,if=mtd,format=raw + # TODO: why pip install doesn't work in Dockerfile? + python3 -m pip install websockets --break-system-packages + + cp /app/scripts/emulation/web_ui.html . + python3 /app/scripts/emulation/web_server.py diff --git a/scripts/emulation/web_server.py b/scripts/emulation/web_server.py new file mode 100644 index 00000000..dea2f9b5 --- /dev/null +++ b/scripts/emulation/web_server.py @@ -0,0 +1,242 @@ +import asyncio +import json +import os +import signal +import sys +from http import HTTPStatus +from pathlib import Path +from typing import Set + +import websockets +from websockets.http11 import Response +from websockets.server import WebSocketServerProtocol + + +""" +This script spawns a QEMU process, then forward its stdout and stderr to WebSocket clients. +""" + + +# WebSocket clients +connected_clients: Set[WebSocketServerProtocol] = set() + +# QEMU process +qemu_process: asyncio.subprocess.Process | None = None + +def process_message(message: str, for_ui: bool) -> str: + if message.startswith("$$DATA:DISPLAY:"): + if for_ui: + return message + else: + return f"[DISPLAY DATA]" + return message + + +async def broadcast_message(msg_type: str, data: str): + """Broadcast a message to all connected WebSocket clients.""" + if not connected_clients: + return + + message = json.dumps({"type": msg_type, "data": data}) + + # Send to all clients, remove disconnected ones + disconnected = set() + for client in connected_clients: + try: + await client.send(message) + except websockets.exceptions.ConnectionClosed: + disconnected.add(client) + + connected_clients.difference_update(disconnected) + + +async def read_stream(stream: asyncio.StreamReader, stream_type: str): + """Read from a stream line by line and broadcast to clients.""" + buffer = b"" + + while True: + try: + chunk = await stream.read(1024) + if not chunk: + break + + buffer += chunk + + # Process complete lines + while b"\n" in buffer: + line, buffer = buffer.split(b"\n", 1) + try: + decoded_line = line.decode("utf-8", errors="replace") + except Exception: + decoded_line = line.decode("latin-1", errors="replace") + + # Forward to parent process + if stream_type == "stdout": + print(process_message(decoded_line, for_ui=False), flush=True) + else: + print(process_message(decoded_line, for_ui=False), file=sys.stderr, flush=True) + + # Broadcast to WebSocket clients + await broadcast_message(stream_type, process_message(decoded_line, for_ui=True)) + + except Exception as e: + print(f"Error reading {stream_type}: {e}", file=sys.stderr) + break + + # Process remaining buffer + if buffer: + try: + decoded_line = buffer.decode("utf-8", errors="replace") + except Exception: + decoded_line = buffer.decode("latin-1", errors="replace") + + if stream_type == "stdout": + print(decoded_line, flush=True) + else: + print(decoded_line, file=sys.stderr, flush=True) + + await broadcast_message(stream_type, decoded_line) + + +async def spawn_qemu(): + """Spawn the QEMU process and capture its output.""" + global qemu_process + + # Build the command + cmd = [ + "qemu-system-riscv32", + "-nographic", + "-M", "esp32c3", + "-drive", "file=flash.bin,if=mtd,format=raw" + ] + + # Get working directory from environment or use /tmp + work_dir = os.getcwd() + + print(f"Starting QEMU in {work_dir}...", flush=True) + print(f"Command: {' '.join(cmd)}", flush=True) + + try: + qemu_process = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + stdin=asyncio.subprocess.PIPE, + cwd=work_dir, + env=os.environ.copy() + ) + + # Read stdout and stderr concurrently + await asyncio.gather( + read_stream(qemu_process.stdout, "stdout"), + read_stream(qemu_process.stderr, "stderr") + ) + + # Wait for process to complete + await qemu_process.wait() + print(f"QEMU process exited with code {qemu_process.returncode}", flush=True) + + except FileNotFoundError: + print("Error: qemu-system-riscv32 not found. Make sure it's in PATH.", file=sys.stderr) + await broadcast_message("stderr", "Error: qemu-system-riscv32 not found") + except Exception as e: + print(f"Error spawning QEMU: {e}", file=sys.stderr) + await broadcast_message("stderr", f"Error spawning QEMU: {e}") + + +async def websocket_handler(websocket: WebSocketServerProtocol): + """Handle a WebSocket connection.""" + connected_clients.add(websocket) + print(f"Client connected. Total clients: {len(connected_clients)}", flush=True) + + try: + # Send a welcome message + await websocket.send(json.dumps({ + "type": "info", + "data": "Connected to CrossPoint emulator" + })) + + # Handle incoming messages (for stdin forwarding) + async for message in websocket: + try: + data = json.loads(message) + if data.get("type") == "stdin" and qemu_process and qemu_process.stdin: + input_data = data.get("data", "") + qemu_process.stdin.write((input_data + "\n").encode()) + await qemu_process.stdin.drain() + except json.JSONDecodeError: + pass + except Exception as e: + print(f"Error handling client message: {e}", file=sys.stderr) + + except websockets.exceptions.ConnectionClosed: + pass + finally: + connected_clients.discard(websocket) + print(f"Client disconnected. Total clients: {len(connected_clients)}", flush=True) + + +async def main(): + """Main entry point.""" + host = os.environ.get("HOST", "0.0.0.0") + port = int(os.environ.get("PORT", "8090")) + + print(f"Starting WebSocket server on {host}:{port}", flush=True) + + # Start WebSocket server + async with websockets.serve( + websocket_handler, host, port, + process_request=process_request, + origins=None, + ): + print(f"WebSocket server running on ws://{host}:{port}/ws", flush=True) + print(f"Web UI available at http://{host}:{port}/", flush=True) + + # Spawn QEMU process + await spawn_qemu() + + +def signal_handler(signum, frame): + """Handle shutdown signals.""" + print("\nShutting down...", flush=True) + if qemu_process: + qemu_process.terminate() + sys.exit(0) + + +def process_request(connection, request): + """Handle HTTP requests for serving static files.""" + path = request.path + + if path == "/" or path == "/web_ui.html": + # Serve the web_ui.html file + html_path = Path(__file__).parent / "web_ui.html" + try: + content = html_path.read_bytes() + return Response( + HTTPStatus.OK, + "OK", + websockets.Headers([ + ("Content-Type", "text/html; charset=utf-8"), + ("Content-Length", str(len(content))), + ]), + content + ) + except FileNotFoundError: + return Response(HTTPStatus.NOT_FOUND, "Not Found", websockets.Headers(), b"web_ui.html not found") + + if path == "/ws": + # Return None to continue with WebSocket handshake + return None + + # Return 404 for other paths + return Response(HTTPStatus.NOT_FOUND, "Not Found", websockets.Headers(), b"Not found") + + +if __name__ == "__main__": + # Set up signal handlers + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + # Run the main loop + asyncio.run(main()) diff --git a/scripts/emulation/web_ui.html b/scripts/emulation/web_ui.html new file mode 100644 index 00000000..7e69532f --- /dev/null +++ b/scripts/emulation/web_ui.html @@ -0,0 +1,120 @@ + + + + + + CrossPoint Emulator + + + + + +
+ + + + From d65e0fe5f715366ab6aa06538b9c720696000e78 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Thu, 22 Jan 2026 22:38:25 +0100 Subject: [PATCH 04/10] add HAL storage --- lib/Epub/Epub.cpp | 46 ++++++++--------- lib/Epub/Epub/BookMetadataCache.cpp | 22 ++++----- lib/Epub/Epub/BookMetadataCache.h | 2 +- lib/Epub/Epub/Section.cpp | 30 ++++++------ .../Epub/parsers/ChapterHtmlSlimParser.cpp | 4 +- lib/Epub/Epub/parsers/ContentOpfParser.cpp | 10 ++-- lib/KOReaderSync/KOReaderCredentialStore.cpp | 8 +-- lib/KOReaderSync/KOReaderDocumentId.cpp | 4 +- lib/Txt/Txt.cpp | 30 ++++++------ lib/Txt/Txt.h | 2 +- lib/Xtc/Xtc.cpp | 26 +++++----- lib/Xtc/Xtc/XtcParser.cpp | 6 +-- lib/ZipFile/ZipFile.cpp | 4 +- lib/hal/HalStorage.cpp | 0 lib/hal/HalStorage.h | 49 +++++++++++++++++++ src/CrossPointSettings.cpp | 8 +-- src/CrossPointState.cpp | 6 +-- src/RecentBooksStore.cpp | 8 +-- src/WifiCredentialStore.cpp | 8 +-- src/activities/boot_sleep/SleepActivity.cpp | 10 ++-- src/activities/home/HomeActivity.cpp | 6 +-- src/activities/home/MyLibraryActivity.cpp | 6 +-- .../network/CalibreWirelessActivity.cpp | 4 +- .../network/CalibreWirelessActivity.h | 2 +- src/activities/reader/EpubReaderActivity.cpp | 6 +-- src/activities/reader/ReaderActivity.cpp | 6 +-- src/activities/reader/TxtReaderActivity.cpp | 10 ++-- src/activities/reader/XtcReaderActivity.cpp | 6 +-- .../settings/ClearCacheActivity.cpp | 6 +-- src/main.cpp | 4 +- src/network/CrossPointWebServer.cpp | 32 ++++++------ src/network/HttpDownloader.cpp | 12 ++--- src/network/HttpDownloader.h | 2 +- 33 files changed, 217 insertions(+), 168 deletions(-) create mode 100644 lib/hal/HalStorage.cpp create mode 100644 lib/hal/HalStorage.h diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index 78607573..c8fe9d5a 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -3,7 +3,7 @@ #include #include #include -#include +#include #include #include "Epub/parsers/ContainerParser.h" @@ -101,12 +101,12 @@ bool Epub::parseTocNcxFile() const { const auto tmpNcxPath = getCachePath() + "/toc.ncx"; FsFile tempNcxFile; - if (!SdMan.openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) { + if (!HAL_STORAGE.openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) { return false; } readItemContentsToStream(tocNcxItem, tempNcxFile, 1024); tempNcxFile.close(); - if (!SdMan.openFileForRead("EBP", tmpNcxPath, tempNcxFile)) { + if (!HAL_STORAGE.openFileForRead("EBP", tmpNcxPath, tempNcxFile)) { return false; } const auto ncxSize = tempNcxFile.size(); @@ -141,7 +141,7 @@ bool Epub::parseTocNcxFile() const { free(ncxBuffer); tempNcxFile.close(); - SdMan.remove(tmpNcxPath.c_str()); + HAL_STORAGE.remove(tmpNcxPath.c_str()); Serial.printf("[%lu] [EBP] Parsed TOC items\n", millis()); return true; @@ -158,12 +158,12 @@ bool Epub::parseTocNavFile() const { const auto tmpNavPath = getCachePath() + "/toc.nav"; FsFile tempNavFile; - if (!SdMan.openFileForWrite("EBP", tmpNavPath, tempNavFile)) { + if (!HAL_STORAGE.openFileForWrite("EBP", tmpNavPath, tempNavFile)) { return false; } readItemContentsToStream(tocNavItem, tempNavFile, 1024); tempNavFile.close(); - if (!SdMan.openFileForRead("EBP", tmpNavPath, tempNavFile)) { + if (!HAL_STORAGE.openFileForRead("EBP", tmpNavPath, tempNavFile)) { return false; } const auto navSize = tempNavFile.size(); @@ -198,7 +198,7 @@ bool Epub::parseTocNavFile() const { free(navBuffer); tempNavFile.close(); - SdMan.remove(tmpNavPath.c_str()); + HAL_STORAGE.remove(tmpNavPath.c_str()); Serial.printf("[%lu] [EBP] Parsed TOC nav items\n", millis()); return true; @@ -305,12 +305,12 @@ bool Epub::load(const bool buildIfMissing) { } bool Epub::clearCache() const { - if (!SdMan.exists(cachePath.c_str())) { + if (!HAL_STORAGE.exists(cachePath.c_str())) { Serial.printf("[%lu] [EPB] Cache does not exist, no action needed\n", millis()); return true; } - if (!SdMan.removeDir(cachePath.c_str())) { + if (!HAL_STORAGE.removeDir(cachePath.c_str())) { Serial.printf("[%lu] [EPB] Failed to clear cache\n", millis()); return false; } @@ -320,11 +320,11 @@ bool Epub::clearCache() const { } void Epub::setupCacheDir() const { - if (SdMan.exists(cachePath.c_str())) { + if (HAL_STORAGE.exists(cachePath.c_str())) { return; } - SdMan.mkdir(cachePath.c_str()); + HAL_STORAGE.mkdir(cachePath.c_str()); } const std::string& Epub::getCachePath() const { return cachePath; } @@ -365,7 +365,7 @@ std::string Epub::getCoverBmpPath(bool cropped) const { bool Epub::generateCoverBmp(bool cropped) const { // Already generated, return true - if (SdMan.exists(getCoverBmpPath(cropped).c_str())) { + if (HAL_STORAGE.exists(getCoverBmpPath(cropped).c_str())) { return true; } @@ -386,29 +386,29 @@ bool Epub::generateCoverBmp(bool cropped) const { const auto coverJpgTempPath = getCachePath() + "/.cover.jpg"; FsFile coverJpg; - if (!SdMan.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) { + if (!HAL_STORAGE.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) { return false; } readItemContentsToStream(coverImageHref, coverJpg, 1024); coverJpg.close(); - if (!SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg)) { + if (!HAL_STORAGE.openFileForRead("EBP", coverJpgTempPath, coverJpg)) { return false; } FsFile coverBmp; - if (!SdMan.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) { + if (!HAL_STORAGE.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) { coverJpg.close(); return false; } const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp); coverJpg.close(); coverBmp.close(); - SdMan.remove(coverJpgTempPath.c_str()); + HAL_STORAGE.remove(coverJpgTempPath.c_str()); if (!success) { Serial.printf("[%lu] [EBP] Failed to generate BMP from JPG cover image\n", millis()); - SdMan.remove(getCoverBmpPath(cropped).c_str()); + HAL_STORAGE.remove(getCoverBmpPath(cropped).c_str()); } Serial.printf("[%lu] [EBP] Generated BMP from JPG cover image, success: %s\n", millis(), success ? "yes" : "no"); return success; @@ -423,7 +423,7 @@ std::string Epub::getThumbBmpPath() const { return cachePath + "/thumb.bmp"; } bool Epub::generateThumbBmp() const { // Already generated, return true - if (SdMan.exists(getThumbBmpPath().c_str())) { + if (HAL_STORAGE.exists(getThumbBmpPath().c_str())) { return true; } @@ -444,18 +444,18 @@ bool Epub::generateThumbBmp() const { const auto coverJpgTempPath = getCachePath() + "/.cover.jpg"; FsFile coverJpg; - if (!SdMan.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) { + if (!HAL_STORAGE.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) { return false; } readItemContentsToStream(coverImageHref, coverJpg, 1024); coverJpg.close(); - if (!SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg)) { + if (!HAL_STORAGE.openFileForRead("EBP", coverJpgTempPath, coverJpg)) { return false; } FsFile thumbBmp; - if (!SdMan.openFileForWrite("EBP", getThumbBmpPath(), thumbBmp)) { + if (!HAL_STORAGE.openFileForWrite("EBP", getThumbBmpPath(), thumbBmp)) { coverJpg.close(); return false; } @@ -467,11 +467,11 @@ bool Epub::generateThumbBmp() const { THUMB_TARGET_HEIGHT); coverJpg.close(); thumbBmp.close(); - SdMan.remove(coverJpgTempPath.c_str()); + HAL_STORAGE.remove(coverJpgTempPath.c_str()); if (!success) { Serial.printf("[%lu] [EBP] Failed to generate thumb BMP from JPG cover image\n", millis()); - SdMan.remove(getThumbBmpPath().c_str()); + HAL_STORAGE.remove(getThumbBmpPath().c_str()); } Serial.printf("[%lu] [EBP] Generated thumb BMP from JPG cover image, success: %s\n", millis(), success ? "yes" : "no"); diff --git a/lib/Epub/Epub/BookMetadataCache.cpp b/lib/Epub/Epub/BookMetadataCache.cpp index 374cad2f..2d9cf30f 100644 --- a/lib/Epub/Epub/BookMetadataCache.cpp +++ b/lib/Epub/Epub/BookMetadataCache.cpp @@ -29,7 +29,7 @@ bool BookMetadataCache::beginContentOpfPass() { Serial.printf("[%lu] [BMC] Beginning content opf pass\n", millis()); // Open spine file for writing - return SdMan.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile); + return HAL_STORAGE.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile); } bool BookMetadataCache::endContentOpfPass() { @@ -41,10 +41,10 @@ bool BookMetadataCache::beginTocPass() { Serial.printf("[%lu] [BMC] Beginning toc pass\n", millis()); // Open spine file for reading - if (!SdMan.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) { + if (!HAL_STORAGE.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) { return false; } - if (!SdMan.openFileForWrite("BMC", cachePath + tmpTocBinFile, tocFile)) { + if (!HAL_STORAGE.openFileForWrite("BMC", cachePath + tmpTocBinFile, tocFile)) { spineFile.close(); return false; } @@ -70,16 +70,16 @@ bool BookMetadataCache::endWrite() { bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMetadata& metadata) { // Open all three files, writing to meta, reading from spine and toc - if (!SdMan.openFileForWrite("BMC", cachePath + bookBinFile, bookFile)) { + if (!HAL_STORAGE.openFileForWrite("BMC", cachePath + bookBinFile, bookFile)) { return false; } - if (!SdMan.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) { + if (!HAL_STORAGE.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) { bookFile.close(); return false; } - if (!SdMan.openFileForRead("BMC", cachePath + tmpTocBinFile, tocFile)) { + if (!HAL_STORAGE.openFileForRead("BMC", cachePath + tmpTocBinFile, tocFile)) { bookFile.close(); spineFile.close(); return false; @@ -201,11 +201,11 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta } bool BookMetadataCache::cleanupTmpFiles() const { - if (SdMan.exists((cachePath + tmpSpineBinFile).c_str())) { - SdMan.remove((cachePath + tmpSpineBinFile).c_str()); + if (HAL_STORAGE.exists((cachePath + tmpSpineBinFile).c_str())) { + HAL_STORAGE.remove((cachePath + tmpSpineBinFile).c_str()); } - if (SdMan.exists((cachePath + tmpTocBinFile).c_str())) { - SdMan.remove((cachePath + tmpTocBinFile).c_str()); + if (HAL_STORAGE.exists((cachePath + tmpTocBinFile).c_str())) { + HAL_STORAGE.remove((cachePath + tmpTocBinFile).c_str()); } return true; } @@ -273,7 +273,7 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri /* ============= READING / LOADING FUNCTIONS ================ */ bool BookMetadataCache::load() { - if (!SdMan.openFileForRead("BMC", cachePath + bookBinFile, bookFile)) { + if (!HAL_STORAGE.openFileForRead("BMC", cachePath + bookBinFile, bookFile)) { return false; } diff --git a/lib/Epub/Epub/BookMetadataCache.h b/lib/Epub/Epub/BookMetadataCache.h index 29b2ae4a..96ac3207 100644 --- a/lib/Epub/Epub/BookMetadataCache.h +++ b/lib/Epub/Epub/BookMetadataCache.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 581a364f..552c755d 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -1,6 +1,6 @@ #include "Section.h" -#include +#include #include #include "Page.h" @@ -58,7 +58,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint16_t viewportHeight, const bool hyphenationEnabled) { - if (!SdMan.openFileForRead("SCT", filePath, file)) { + if (!HAL_STORAGE.openFileForRead("SCT", filePath, file)) { return false; } @@ -106,12 +106,12 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con // Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem) bool Section::clearCache() const { - if (!SdMan.exists(filePath.c_str())) { + if (!HAL_STORAGE.exists(filePath.c_str())) { Serial.printf("[%lu] [SCT] Cache does not exist, no action needed\n", millis()); return true; } - if (!SdMan.remove(filePath.c_str())) { + if (!HAL_STORAGE.remove(filePath.c_str())) { Serial.printf("[%lu] [SCT] Failed to clear cache\n", millis()); return false; } @@ -132,7 +132,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c // Create cache directory if it doesn't exist { const auto sectionsDir = epub->getCachePath() + "/sections"; - SdMan.mkdir(sectionsDir.c_str()); + HAL_STORAGE.mkdir(sectionsDir.c_str()); } // Retry logic for SD card timing issues @@ -145,12 +145,12 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c } // Remove any incomplete file from previous attempt before retrying - if (SdMan.exists(tmpHtmlPath.c_str())) { - SdMan.remove(tmpHtmlPath.c_str()); + if (HAL_STORAGE.exists(tmpHtmlPath.c_str())) { + HAL_STORAGE.remove(tmpHtmlPath.c_str()); } FsFile tmpHtml; - if (!SdMan.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) { + if (!HAL_STORAGE.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) { continue; } success = epub->readItemContentsToStream(localPath, tmpHtml, 1024); @@ -158,8 +158,8 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c tmpHtml.close(); // If streaming failed, remove the incomplete file immediately - if (!success && SdMan.exists(tmpHtmlPath.c_str())) { - SdMan.remove(tmpHtmlPath.c_str()); + if (!success && HAL_STORAGE.exists(tmpHtmlPath.c_str())) { + HAL_STORAGE.remove(tmpHtmlPath.c_str()); Serial.printf("[%lu] [SCT] Removed incomplete temp file after failed attempt\n", millis()); } } @@ -176,7 +176,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c progressSetupFn(); } - if (!SdMan.openFileForWrite("SCT", filePath, file)) { + if (!HAL_STORAGE.openFileForWrite("SCT", filePath, file)) { return false; } writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, @@ -191,11 +191,11 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c Hyphenator::setPreferredLanguage(epub->getLanguage()); success = visitor.parseAndBuildPages(); - SdMan.remove(tmpHtmlPath.c_str()); + HAL_STORAGE.remove(tmpHtmlPath.c_str()); if (!success) { Serial.printf("[%lu] [SCT] Failed to parse XML and build pages\n", millis()); file.close(); - SdMan.remove(filePath.c_str()); + HAL_STORAGE.remove(filePath.c_str()); return false; } @@ -213,7 +213,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c if (hasFailedLutRecords) { Serial.printf("[%lu] [SCT] Failed to write LUT due to invalid page positions\n", millis()); file.close(); - SdMan.remove(filePath.c_str()); + HAL_STORAGE.remove(filePath.c_str()); return false; } @@ -226,7 +226,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c } std::unique_ptr Section::loadPageFromSectionFile() { - if (!SdMan.openFileForRead("SCT", filePath, file)) { + if (!HAL_STORAGE.openFileForRead("SCT", filePath, file)) { return nullptr; } diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 1d7e2ab3..1cbbb165 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -2,7 +2,7 @@ #include #include -#include +#include #include #include "../Page.h" @@ -264,7 +264,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() { } FsFile file; - if (!SdMan.openFileForRead("EHP", filepath, file)) { + if (!HAL_STORAGE.openFileForRead("EHP", filepath, file)) { XML_ParserFree(parser); return false; } diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.cpp b/lib/Epub/Epub/parsers/ContentOpfParser.cpp index 9fbeb386..d25e614b 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.cpp +++ b/lib/Epub/Epub/parsers/ContentOpfParser.cpp @@ -35,8 +35,8 @@ ContentOpfParser::~ContentOpfParser() { if (tempItemStore) { tempItemStore.close(); } - if (SdMan.exists((cachePath + itemCacheFile).c_str())) { - SdMan.remove((cachePath + itemCacheFile).c_str()); + if (HAL_STORAGE.exists((cachePath + itemCacheFile).c_str())) { + HAL_STORAGE.remove((cachePath + itemCacheFile).c_str()); } } @@ -114,7 +114,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name if (self->state == IN_PACKAGE && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) { self->state = IN_MANIFEST; - if (!SdMan.openFileForWrite("COF", self->cachePath + itemCacheFile, self->tempItemStore)) { + if (!HAL_STORAGE.openFileForWrite("COF", self->cachePath + itemCacheFile, self->tempItemStore)) { Serial.printf( "[%lu] [COF] Couldn't open temp items file for writing. This is probably going to be a fatal error.\n", millis()); @@ -124,7 +124,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name if (self->state == IN_PACKAGE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) { self->state = IN_SPINE; - if (!SdMan.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) { + if (!HAL_STORAGE.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) { Serial.printf( "[%lu] [COF] Couldn't open temp items file for reading. This is probably going to be a fatal error.\n", millis()); @@ -136,7 +136,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name self->state = IN_GUIDE; // TODO Remove print Serial.printf("[%lu] [COF] Entering guide state.\n", millis()); - if (!SdMan.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) { + if (!HAL_STORAGE.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) { Serial.printf( "[%lu] [COF] Couldn't open temp items file for reading. This is probably going to be a fatal error.\n", millis()); diff --git a/lib/KOReaderSync/KOReaderCredentialStore.cpp b/lib/KOReaderSync/KOReaderCredentialStore.cpp index c5737809..4436e2a7 100644 --- a/lib/KOReaderSync/KOReaderCredentialStore.cpp +++ b/lib/KOReaderSync/KOReaderCredentialStore.cpp @@ -2,7 +2,7 @@ #include #include -#include +#include #include // Initialize the static instance @@ -32,10 +32,10 @@ void KOReaderCredentialStore::obfuscate(std::string& data) const { bool KOReaderCredentialStore::saveToFile() const { // Make sure the directory exists - SdMan.mkdir("/.crosspoint"); + HAL_STORAGE.mkdir("/.crosspoint"); FsFile file; - if (!SdMan.openFileForWrite("KRS", KOREADER_FILE, file)) { + if (!HAL_STORAGE.openFileForWrite("KRS", KOREADER_FILE, file)) { return false; } @@ -64,7 +64,7 @@ bool KOReaderCredentialStore::saveToFile() const { bool KOReaderCredentialStore::loadFromFile() { FsFile file; - if (!SdMan.openFileForRead("KRS", KOREADER_FILE, file)) { + if (!HAL_STORAGE.openFileForRead("KRS", KOREADER_FILE, file)) { Serial.printf("[%lu] [KRS] No credentials file found\n", millis()); return false; } diff --git a/lib/KOReaderSync/KOReaderDocumentId.cpp b/lib/KOReaderSync/KOReaderDocumentId.cpp index 2c52464c..db48f5b5 100644 --- a/lib/KOReaderSync/KOReaderDocumentId.cpp +++ b/lib/KOReaderSync/KOReaderDocumentId.cpp @@ -2,7 +2,7 @@ #include #include -#include +#include namespace { // Extract filename from path (everything after last '/') @@ -43,7 +43,7 @@ size_t KOReaderDocumentId::getOffset(int i) { std::string KOReaderDocumentId::calculate(const std::string& filePath) { FsFile file; - if (!SdMan.openFileForRead("KODoc", filePath, file)) { + if (!HAL_STORAGE.openFileForRead("KODoc", filePath, file)) { Serial.printf("[%lu] [KODoc] Failed to open file: %s\n", millis(), filePath.c_str()); return ""; } diff --git a/lib/Txt/Txt.cpp b/lib/Txt/Txt.cpp index 52c75ed7..7dad632e 100644 --- a/lib/Txt/Txt.cpp +++ b/lib/Txt/Txt.cpp @@ -15,13 +15,13 @@ bool Txt::load() { return true; } - if (!SdMan.exists(filepath.c_str())) { + if (!HAL_STORAGE.exists(filepath.c_str())) { Serial.printf("[%lu] [TXT] File does not exist: %s\n", millis(), filepath.c_str()); return false; } FsFile file; - if (!SdMan.openFileForRead("TXT", filepath, file)) { + if (!HAL_STORAGE.openFileForRead("TXT", filepath, file)) { Serial.printf("[%lu] [TXT] Failed to open file: %s\n", millis(), filepath.c_str()); return false; } @@ -48,11 +48,11 @@ std::string Txt::getTitle() const { } void Txt::setupCacheDir() const { - if (!SdMan.exists(cacheBasePath.c_str())) { - SdMan.mkdir(cacheBasePath.c_str()); + if (!HAL_STORAGE.exists(cacheBasePath.c_str())) { + HAL_STORAGE.mkdir(cacheBasePath.c_str()); } - if (!SdMan.exists(cachePath.c_str())) { - SdMan.mkdir(cachePath.c_str()); + if (!HAL_STORAGE.exists(cachePath.c_str())) { + HAL_STORAGE.mkdir(cachePath.c_str()); } } @@ -73,7 +73,7 @@ std::string Txt::findCoverImage() const { // First priority: look for image with same name as txt file (e.g., mybook.jpg) for (const auto& ext : extensions) { std::string coverPath = folder + "/" + baseName + ext; - if (SdMan.exists(coverPath.c_str())) { + if (HAL_STORAGE.exists(coverPath.c_str())) { Serial.printf("[%lu] [TXT] Found matching cover image: %s\n", millis(), coverPath.c_str()); return coverPath; } @@ -84,7 +84,7 @@ std::string Txt::findCoverImage() const { for (const auto& name : coverNames) { for (const auto& ext : extensions) { std::string coverPath = folder + "/" + std::string(name) + ext; - if (SdMan.exists(coverPath.c_str())) { + if (HAL_STORAGE.exists(coverPath.c_str())) { Serial.printf("[%lu] [TXT] Found fallback cover image: %s\n", millis(), coverPath.c_str()); return coverPath; } @@ -98,7 +98,7 @@ std::string Txt::getCoverBmpPath() const { return cachePath + "/cover.bmp"; } bool Txt::generateCoverBmp() const { // Already generated, return true - if (SdMan.exists(getCoverBmpPath().c_str())) { + if (HAL_STORAGE.exists(getCoverBmpPath().c_str())) { return true; } @@ -122,10 +122,10 @@ bool Txt::generateCoverBmp() const { // Copy BMP file to cache Serial.printf("[%lu] [TXT] Copying BMP cover image to cache\n", millis()); FsFile src, dst; - if (!SdMan.openFileForRead("TXT", coverImagePath, src)) { + if (!HAL_STORAGE.openFileForRead("TXT", coverImagePath, src)) { return false; } - if (!SdMan.openFileForWrite("TXT", getCoverBmpPath(), dst)) { + if (!HAL_STORAGE.openFileForWrite("TXT", getCoverBmpPath(), dst)) { src.close(); return false; } @@ -144,10 +144,10 @@ bool Txt::generateCoverBmp() const { // Convert JPG/JPEG to BMP (same approach as Epub) Serial.printf("[%lu] [TXT] Generating BMP from JPG cover image\n", millis()); FsFile coverJpg, coverBmp; - if (!SdMan.openFileForRead("TXT", coverImagePath, coverJpg)) { + if (!HAL_STORAGE.openFileForRead("TXT", coverImagePath, coverJpg)) { return false; } - if (!SdMan.openFileForWrite("TXT", getCoverBmpPath(), coverBmp)) { + if (!HAL_STORAGE.openFileForWrite("TXT", getCoverBmpPath(), coverBmp)) { coverJpg.close(); return false; } @@ -157,7 +157,7 @@ bool Txt::generateCoverBmp() const { if (!success) { Serial.printf("[%lu] [TXT] Failed to generate BMP from JPG cover image\n", millis()); - SdMan.remove(getCoverBmpPath().c_str()); + HAL_STORAGE.remove(getCoverBmpPath().c_str()); } else { Serial.printf("[%lu] [TXT] Generated BMP from JPG cover image\n", millis()); } @@ -175,7 +175,7 @@ bool Txt::readContent(uint8_t* buffer, size_t offset, size_t length) const { } FsFile file; - if (!SdMan.openFileForRead("TXT", filepath, file)) { + if (!HAL_STORAGE.openFileForRead("TXT", filepath, file)) { return false; } diff --git a/lib/Txt/Txt.h b/lib/Txt/Txt.h index b75c7738..b342ca88 100644 --- a/lib/Txt/Txt.h +++ b/lib/Txt/Txt.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include diff --git a/lib/Xtc/Xtc.cpp b/lib/Xtc/Xtc.cpp index c79421d7..11b1eb03 100644 --- a/lib/Xtc/Xtc.cpp +++ b/lib/Xtc/Xtc.cpp @@ -9,7 +9,7 @@ #include #include -#include +#include bool Xtc::load() { Serial.printf("[%lu] [XTC] Loading XTC: %s\n", millis(), filepath.c_str()); @@ -31,12 +31,12 @@ bool Xtc::load() { } bool Xtc::clearCache() const { - if (!SdMan.exists(cachePath.c_str())) { + if (!HAL_STORAGE.exists(cachePath.c_str())) { Serial.printf("[%lu] [XTC] Cache does not exist, no action needed\n", millis()); return true; } - if (!SdMan.removeDir(cachePath.c_str())) { + if (!HAL_STORAGE.removeDir(cachePath.c_str())) { Serial.printf("[%lu] [XTC] Failed to clear cache\n", millis()); return false; } @@ -46,17 +46,17 @@ bool Xtc::clearCache() const { } void Xtc::setupCacheDir() const { - if (SdMan.exists(cachePath.c_str())) { + if (HAL_STORAGE.exists(cachePath.c_str())) { return; } // Create directories recursively for (size_t i = 1; i < cachePath.length(); i++) { if (cachePath[i] == '/') { - SdMan.mkdir(cachePath.substr(0, i).c_str()); + HAL_STORAGE.mkdir(cachePath.substr(0, i).c_str()); } } - SdMan.mkdir(cachePath.c_str()); + HAL_STORAGE.mkdir(cachePath.c_str()); } std::string Xtc::getTitle() const { @@ -106,7 +106,7 @@ std::string Xtc::getCoverBmpPath() const { return cachePath + "/cover.bmp"; } bool Xtc::generateCoverBmp() const { // Already generated - if (SdMan.exists(getCoverBmpPath().c_str())) { + if (HAL_STORAGE.exists(getCoverBmpPath().c_str())) { return true; } @@ -158,7 +158,7 @@ bool Xtc::generateCoverBmp() const { // Create BMP file FsFile coverBmp; - if (!SdMan.openFileForWrite("XTC", getCoverBmpPath(), coverBmp)) { + if (!HAL_STORAGE.openFileForWrite("XTC", getCoverBmpPath(), coverBmp)) { Serial.printf("[%lu] [XTC] Failed to create cover BMP file\n", millis()); free(pageBuffer); return false; @@ -297,7 +297,7 @@ std::string Xtc::getThumbBmpPath() const { return cachePath + "/thumb.bmp"; } bool Xtc::generateThumbBmp() const { // Already generated - if (SdMan.exists(getThumbBmpPath().c_str())) { + if (HAL_STORAGE.exists(getThumbBmpPath().c_str())) { return true; } @@ -339,8 +339,8 @@ bool Xtc::generateThumbBmp() const { // Copy cover.bmp to thumb.bmp if (generateCoverBmp()) { FsFile src, dst; - if (SdMan.openFileForRead("XTC", getCoverBmpPath(), src)) { - if (SdMan.openFileForWrite("XTC", getThumbBmpPath(), dst)) { + if (HAL_STORAGE.openFileForRead("XTC", getCoverBmpPath(), src)) { + if (HAL_STORAGE.openFileForWrite("XTC", getThumbBmpPath(), dst)) { uint8_t buffer[512]; while (src.available()) { size_t bytesRead = src.read(buffer, sizeof(buffer)); @@ -351,7 +351,7 @@ bool Xtc::generateThumbBmp() const { src.close(); } Serial.printf("[%lu] [XTC] Copied cover to thumb (no scaling needed)\n", millis()); - return SdMan.exists(getThumbBmpPath().c_str()); + return HAL_STORAGE.exists(getThumbBmpPath().c_str()); } return false; } @@ -385,7 +385,7 @@ bool Xtc::generateThumbBmp() const { // Create thumbnail BMP file - use 1-bit format for fast home screen rendering (no gray passes) FsFile thumbBmp; - if (!SdMan.openFileForWrite("XTC", getThumbBmpPath(), thumbBmp)) { + if (!HAL_STORAGE.openFileForWrite("XTC", getThumbBmpPath(), thumbBmp)) { Serial.printf("[%lu] [XTC] Failed to create thumb BMP file\n", millis()); free(pageBuffer); return false; diff --git a/lib/Xtc/Xtc/XtcParser.cpp b/lib/Xtc/Xtc/XtcParser.cpp index c33e7193..759d06f1 100644 --- a/lib/Xtc/Xtc/XtcParser.cpp +++ b/lib/Xtc/Xtc/XtcParser.cpp @@ -9,7 +9,7 @@ #include #include -#include +#include #include @@ -34,7 +34,7 @@ XtcError XtcParser::open(const char* filepath) { } // Open file - if (!SdMan.openFileForRead("XTC", filepath, m_file)) { + if (!HAL_STORAGE.openFileForRead("XTC", filepath, m_file)) { m_lastError = XtcError::FILE_NOT_FOUND; return m_lastError; } @@ -421,7 +421,7 @@ XtcError XtcParser::loadPageStreaming(uint32_t pageIndex, bool XtcParser::isValidXtcFile(const char* filepath) { FsFile file; - if (!SdMan.openFileForRead("XTC", filepath, file)) { + if (!HAL_STORAGE.openFileForRead("XTC", filepath, file)) { return false; } diff --git a/lib/ZipFile/ZipFile.cpp b/lib/ZipFile/ZipFile.cpp index 2a97858a..663362a3 100644 --- a/lib/ZipFile/ZipFile.cpp +++ b/lib/ZipFile/ZipFile.cpp @@ -1,7 +1,7 @@ #include "ZipFile.h" #include -#include +#include #include bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t* outputBuf, const size_t inflatedSize) { @@ -243,7 +243,7 @@ bool ZipFile::loadZipDetails() { } bool ZipFile::open() { - if (!SdMan.openFileForRead("ZIP", filePath, file)) { + if (!HAL_STORAGE.openFileForRead("ZIP", filePath, file)) { return false; } return true; diff --git a/lib/hal/HalStorage.cpp b/lib/hal/HalStorage.cpp new file mode 100644 index 00000000..e69de29b diff --git a/lib/hal/HalStorage.h b/lib/hal/HalStorage.h new file mode 100644 index 00000000..1ff1b763 --- /dev/null +++ b/lib/hal/HalStorage.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include + +class HalStorage { + public: + HalStorage(); + bool begin(); + bool ready() const; + std::vector listFiles(const char* path = "/", int maxFiles = 200); + // Read the entire file at `path` into a String. Returns empty string on failure. + String readFile(const char* path); + // Low-memory helpers: + // Stream the file contents to a `Print` (e.g. `Serial`, or any `Print`-derived object). + // Returns true on success, false on failure. + bool readFileToStream(const char* path, Print& out, size_t chunkSize = 256); + // Read up to `bufferSize-1` bytes into `buffer`, null-terminating it. Returns bytes read. + size_t readFileToBuffer(const char* path, char* buffer, size_t bufferSize, size_t maxBytes = 0); + // Write a string to `path` on the SD card. Overwrites existing file. + // Returns true on success. + bool writeFile(const char* path, const String& content); + // Ensure a directory exists, creating it if necessary. Returns true on success. + bool ensureDirectoryExists(const char* path); + + FsFile open(const char* path, const oflag_t oflag = O_RDONLY); + bool mkdir(const char* path, const bool pFlag = true); + bool exists(const char* path); + bool remove(const char* path); + bool rmdir(const char* path); + + bool openFileForRead(const char* moduleName, const char* path, FsFile& file); + bool openFileForRead(const char* moduleName, const std::string& path, FsFile& file); + bool openFileForRead(const char* moduleName, const String& path, FsFile& file); + bool openFileForWrite(const char* moduleName, const char* path, FsFile& file); + bool openFileForWrite(const char* moduleName, const std::string& path, FsFile& file); + bool openFileForWrite(const char* moduleName, const String& path, FsFile& file); + bool removeDir(const char* path); + + static HalStorage& getInstance() { return instance; } + + private: + static HalStorage instance; + + bool initialized = false; + SdFat sd; +}; + +#define HAL_STORAGE HalStorage::getInstance() \ No newline at end of file diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index f5e8ded5..60ea67e8 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -1,7 +1,7 @@ #include "CrossPointSettings.h" #include -#include +#include #include #include @@ -20,10 +20,10 @@ constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; bool CrossPointSettings::saveToFile() const { // Make sure the directory exists - SdMan.mkdir("/.crosspoint"); + HAL_STORAGE.mkdir("/.crosspoint"); FsFile outputFile; - if (!SdMan.openFileForWrite("CPS", SETTINGS_FILE, outputFile)) { + if (!HAL_STORAGE.openFileForWrite("CPS", SETTINGS_FILE, outputFile)) { return false; } @@ -57,7 +57,7 @@ bool CrossPointSettings::saveToFile() const { bool CrossPointSettings::loadFromFile() { FsFile inputFile; - if (!SdMan.openFileForRead("CPS", SETTINGS_FILE, inputFile)) { + if (!HAL_STORAGE.openFileForRead("CPS", SETTINGS_FILE, inputFile)) { return false; } diff --git a/src/CrossPointState.cpp b/src/CrossPointState.cpp index 91aa2536..b317e2eb 100644 --- a/src/CrossPointState.cpp +++ b/src/CrossPointState.cpp @@ -1,7 +1,7 @@ #include "CrossPointState.h" #include -#include +#include #include namespace { @@ -13,7 +13,7 @@ CrossPointState CrossPointState::instance; bool CrossPointState::saveToFile() const { FsFile outputFile; - if (!SdMan.openFileForWrite("CPS", STATE_FILE, outputFile)) { + if (!HAL_STORAGE.openFileForWrite("CPS", STATE_FILE, outputFile)) { return false; } @@ -26,7 +26,7 @@ bool CrossPointState::saveToFile() const { bool CrossPointState::loadFromFile() { FsFile inputFile; - if (!SdMan.openFileForRead("CPS", STATE_FILE, inputFile)) { + if (!HAL_STORAGE.openFileForRead("CPS", STATE_FILE, inputFile)) { return false; } diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index 03cfbbd7..071bb527 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -1,7 +1,7 @@ #include "RecentBooksStore.h" #include -#include +#include #include #include @@ -34,10 +34,10 @@ void RecentBooksStore::addBook(const std::string& path) { bool RecentBooksStore::saveToFile() const { // Make sure the directory exists - SdMan.mkdir("/.crosspoint"); + HAL_STORAGE.mkdir("/.crosspoint"); FsFile outputFile; - if (!SdMan.openFileForWrite("RBS", RECENT_BOOKS_FILE, outputFile)) { + if (!HAL_STORAGE.openFileForWrite("RBS", RECENT_BOOKS_FILE, outputFile)) { return false; } @@ -56,7 +56,7 @@ bool RecentBooksStore::saveToFile() const { bool RecentBooksStore::loadFromFile() { FsFile inputFile; - if (!SdMan.openFileForRead("RBS", RECENT_BOOKS_FILE, inputFile)) { + if (!HAL_STORAGE.openFileForRead("RBS", RECENT_BOOKS_FILE, inputFile)) { return false; } diff --git a/src/WifiCredentialStore.cpp b/src/WifiCredentialStore.cpp index be865b86..ddea3840 100644 --- a/src/WifiCredentialStore.cpp +++ b/src/WifiCredentialStore.cpp @@ -1,7 +1,7 @@ #include "WifiCredentialStore.h" #include -#include +#include #include // Initialize the static instance @@ -29,10 +29,10 @@ void WifiCredentialStore::obfuscate(std::string& data) const { bool WifiCredentialStore::saveToFile() const { // Make sure the directory exists - SdMan.mkdir("/.crosspoint"); + HAL_STORAGE.mkdir("/.crosspoint"); FsFile file; - if (!SdMan.openFileForWrite("WCS", WIFI_FILE, file)) { + if (!HAL_STORAGE.openFileForWrite("WCS", WIFI_FILE, file)) { return false; } @@ -60,7 +60,7 @@ bool WifiCredentialStore::saveToFile() const { bool WifiCredentialStore::loadFromFile() { FsFile file; - if (!SdMan.openFileForRead("WCS", WIFI_FILE, file)) { + if (!HAL_STORAGE.openFileForRead("WCS", WIFI_FILE, file)) { return false; } diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 7e291782..0b0ad6ee 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -2,7 +2,7 @@ #include #include -#include +#include #include #include @@ -47,7 +47,7 @@ void SleepActivity::renderPopup(const char* message) const { void SleepActivity::renderCustomSleepScreen() const { // Check if we have a /sleep directory - auto dir = SdMan.open("/sleep"); + auto dir = HAL_STORAGE.open("/sleep"); if (dir && dir.isDirectory()) { std::vector files; char name[500]; @@ -90,7 +90,7 @@ void SleepActivity::renderCustomSleepScreen() const { APP_STATE.saveToFile(); const auto filename = "/sleep/" + files[randomFileIndex]; FsFile file; - if (SdMan.openFileForRead("SLP", filename, file)) { + if (HAL_STORAGE.openFileForRead("SLP", filename, file)) { Serial.printf("[%lu] [SLP] Randomly loading: /sleep/%s\n", millis(), files[randomFileIndex].c_str()); delay(100); Bitmap bitmap(file, true); @@ -107,7 +107,7 @@ void SleepActivity::renderCustomSleepScreen() const { // Look for sleep.bmp on the root of the sd card to determine if we should // render a custom sleep screen instead of the default. FsFile file; - if (SdMan.openFileForRead("SLP", "/sleep.bmp", file)) { + if (HAL_STORAGE.openFileForRead("SLP", "/sleep.bmp", file)) { Bitmap bitmap(file, true); if (bitmap.parseHeaders() == BmpReaderError::Ok) { Serial.printf("[%lu] [SLP] Loading: /sleep.bmp\n", millis()); @@ -257,7 +257,7 @@ void SleepActivity::renderCoverSleepScreen() const { } FsFile file; - if (SdMan.openFileForRead("SLP", coverBmpPath, file)) { + if (HAL_STORAGE.openFileForRead("SLP", coverBmpPath, file)) { Bitmap bitmap(file); if (bitmap.parseHeaders() == BmpReaderError::Ok) { renderBitmapSleepScreen(bitmap); diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index eb11ba95..2dd0e108 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -3,7 +3,7 @@ #include #include #include -#include +#include #include #include @@ -35,7 +35,7 @@ void HomeActivity::onEnter() { renderingMutex = xSemaphoreCreateMutex(); // Check if we have a book to continue reading - hasContinueReading = !APP_STATE.openEpubPath.empty() && SdMan.exists(APP_STATE.openEpubPath.c_str()); + hasContinueReading = !APP_STATE.openEpubPath.empty() && HAL_STORAGE.exists(APP_STATE.openEpubPath.c_str()); // Check if OPDS browser URL is configured hasOpdsUrl = strlen(SETTINGS.opdsServerUrl) > 0; @@ -239,7 +239,7 @@ void HomeActivity::render() { if (hasContinueReading && hasCoverImage && !coverBmpPath.empty() && !coverRendered) { // First time: load cover from SD and render FsFile file; - if (SdMan.openFileForRead("HOME", coverBmpPath, file)) { + if (HAL_STORAGE.openFileForRead("HOME", coverBmpPath, file)) { Bitmap bitmap(file); if (bitmap.parseHeaders() == BmpReaderError::Ok) { // Calculate position to center image within the book card diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 9e6f3734..fd7f6cbb 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -1,7 +1,7 @@ #include "MyLibraryActivity.h" #include -#include +#include #include @@ -80,7 +80,7 @@ void MyLibraryActivity::loadRecentBooks() { } // Skip if file no longer exists - if (!SdMan.exists(path.c_str())) { + if (!HAL_STORAGE.exists(path.c_str())) { continue; } @@ -99,7 +99,7 @@ void MyLibraryActivity::loadRecentBooks() { void MyLibraryActivity::loadFiles() { files.clear(); - auto root = SdMan.open(basepath.c_str()); + auto root = HAL_STORAGE.open(basepath.c_str()); if (!root || !root.isDirectory()) { if (root) root.close(); return; diff --git a/src/activities/network/CalibreWirelessActivity.cpp b/src/activities/network/CalibreWirelessActivity.cpp index 0ad9094a..c1118128 100644 --- a/src/activities/network/CalibreWirelessActivity.cpp +++ b/src/activities/network/CalibreWirelessActivity.cpp @@ -2,7 +2,7 @@ #include #include -#include +#include #include #include @@ -592,7 +592,7 @@ void CalibreWirelessActivity::handleSendBook(const std::string& data) { setStatus("Receiving: " + filename); // Open file for writing - if (!SdMan.openFileForWrite("CAL", currentFilename.c_str(), currentFile)) { + if (!HAL_STORAGE.openFileForWrite("CAL", currentFilename.c_str(), currentFile)) { setError("Failed to create file"); sendJsonResponse(OpCode::ERROR, "{\"message\":\"Failed to create file\"}"); return; diff --git a/src/activities/network/CalibreWirelessActivity.h b/src/activities/network/CalibreWirelessActivity.h index ae2b1767..f1989f6f 100644 --- a/src/activities/network/CalibreWirelessActivity.h +++ b/src/activities/network/CalibreWirelessActivity.h @@ -1,5 +1,5 @@ #pragma once -#include +#include #include #include #include diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 0743df71..a0626859 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -3,7 +3,7 @@ #include #include #include -#include +#include #include "CrossPointSettings.h" #include "CrossPointState.h" @@ -55,7 +55,7 @@ void EpubReaderActivity::onEnter() { epub->setupCacheDir(); FsFile f; - if (SdMan.openFileForRead("ERS", epub->getCachePath() + "/progress.bin", f)) { + if (HAL_STORAGE.openFileForRead("ERS", epub->getCachePath() + "/progress.bin", f)) { uint8_t data[4]; if (f.read(data, 4) == 4) { currentSpineIndex = data[0] + (data[1] << 8); @@ -375,7 +375,7 @@ void EpubReaderActivity::renderScreen() { } FsFile f; - if (SdMan.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) { + if (HAL_STORAGE.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) { uint8_t data[4]; data[0] = currentSpineIndex & 0xFF; data[1] = (currentSpineIndex >> 8) & 0xFF; diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index 14d6623c..9342f5f6 100644 --- a/src/activities/reader/ReaderActivity.cpp +++ b/src/activities/reader/ReaderActivity.cpp @@ -28,7 +28,7 @@ bool ReaderActivity::isTxtFile(const std::string& path) { } std::unique_ptr ReaderActivity::loadEpub(const std::string& path) { - if (!SdMan.exists(path.c_str())) { + if (!HAL_STORAGE.exists(path.c_str())) { Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str()); return nullptr; } @@ -43,7 +43,7 @@ std::unique_ptr ReaderActivity::loadEpub(const std::string& path) { } std::unique_ptr ReaderActivity::loadXtc(const std::string& path) { - if (!SdMan.exists(path.c_str())) { + if (!HAL_STORAGE.exists(path.c_str())) { Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str()); return nullptr; } @@ -58,7 +58,7 @@ std::unique_ptr ReaderActivity::loadXtc(const std::string& path) { } std::unique_ptr ReaderActivity::loadTxt(const std::string& path) { - if (!SdMan.exists(path.c_str())) { + if (!HAL_STORAGE.exists(path.c_str())) { Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str()); return nullptr; } diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 7c3ea29f..8bdcded5 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -1,7 +1,7 @@ #include "TxtReaderActivity.h" #include -#include +#include #include #include @@ -538,7 +538,7 @@ void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int void TxtReaderActivity::saveProgress() const { FsFile f; - if (SdMan.openFileForWrite("TRS", txt->getCachePath() + "/progress.bin", f)) { + if (HAL_STORAGE.openFileForWrite("TRS", txt->getCachePath() + "/progress.bin", f)) { uint8_t data[4]; data[0] = currentPage & 0xFF; data[1] = (currentPage >> 8) & 0xFF; @@ -551,7 +551,7 @@ void TxtReaderActivity::saveProgress() const { void TxtReaderActivity::loadProgress() { FsFile f; - if (SdMan.openFileForRead("TRS", txt->getCachePath() + "/progress.bin", f)) { + if (HAL_STORAGE.openFileForRead("TRS", txt->getCachePath() + "/progress.bin", f)) { uint8_t data[4]; if (f.read(data, 4) == 4) { currentPage = data[0] + (data[1] << 8); @@ -582,7 +582,7 @@ bool TxtReaderActivity::loadPageIndexCache() { std::string cachePath = txt->getCachePath() + "/index.bin"; FsFile f; - if (!SdMan.openFileForRead("TRS", cachePath, f)) { + if (!HAL_STORAGE.openFileForRead("TRS", cachePath, f)) { Serial.printf("[%lu] [TRS] No page index cache found\n", millis()); return false; } @@ -674,7 +674,7 @@ bool TxtReaderActivity::loadPageIndexCache() { void TxtReaderActivity::savePageIndexCache() const { std::string cachePath = txt->getCachePath() + "/index.bin"; FsFile f; - if (!SdMan.openFileForWrite("TRS", cachePath, f)) { + if (!HAL_STORAGE.openFileForWrite("TRS", cachePath, f)) { Serial.printf("[%lu] [TRS] Failed to save page index cache\n", millis()); return; } diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index 4aba7e66..046de5cc 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -9,7 +9,7 @@ #include #include -#include +#include #include "CrossPointSettings.h" #include "CrossPointState.h" @@ -362,7 +362,7 @@ void XtcReaderActivity::renderPage() { void XtcReaderActivity::saveProgress() const { FsFile f; - if (SdMan.openFileForWrite("XTR", xtc->getCachePath() + "/progress.bin", f)) { + if (HAL_STORAGE.openFileForWrite("XTR", xtc->getCachePath() + "/progress.bin", f)) { uint8_t data[4]; data[0] = currentPage & 0xFF; data[1] = (currentPage >> 8) & 0xFF; @@ -375,7 +375,7 @@ void XtcReaderActivity::saveProgress() const { void XtcReaderActivity::loadProgress() { FsFile f; - if (SdMan.openFileForRead("XTR", xtc->getCachePath() + "/progress.bin", f)) { + if (HAL_STORAGE.openFileForRead("XTR", xtc->getCachePath() + "/progress.bin", f)) { uint8_t data[4]; if (f.read(data, 4) == 4) { currentPage = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24); diff --git a/src/activities/settings/ClearCacheActivity.cpp b/src/activities/settings/ClearCacheActivity.cpp index 1e10c14b..cd0ec846 100644 --- a/src/activities/settings/ClearCacheActivity.cpp +++ b/src/activities/settings/ClearCacheActivity.cpp @@ -2,7 +2,7 @@ #include #include -#include +#include #include "MappedInputManager.h" #include "fontIds.h" @@ -106,7 +106,7 @@ void ClearCacheActivity::clearCache() { Serial.printf("[%lu] [CLEAR_CACHE] Clearing cache...\n", millis()); // Open .crosspoint directory - auto root = SdMan.open("/.crosspoint"); + auto root = HAL_STORAGE.open("/.crosspoint"); if (!root || !root.isDirectory()) { Serial.printf("[%lu] [CLEAR_CACHE] Failed to open cache directory\n", millis()); if (root) root.close(); @@ -131,7 +131,7 @@ void ClearCacheActivity::clearCache() { file.close(); // Close before attempting to delete - if (SdMan.removeDir(fullPath.c_str())) { + if (HAL_STORAGE.removeDir(fullPath.c_str())) { clearedCount++; } else { Serial.printf("[%lu] [CLEAR_CACHE] Failed to remove: %s\n", millis(), fullPath.c_str()); diff --git a/src/main.cpp b/src/main.cpp index 63ada0b2..32cafb72 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,7 +3,7 @@ #include #include #include -#include +#include #include #include @@ -311,7 +311,7 @@ void setup() { // SD Card Initialization // We need 6 open files concurrently when parsing a new chapter - if (!SdMan.begin()) { + if (!HAL_STORAGE.begin()) { Serial.printf("[%lu] [ ] SD card initialization failed\n", millis()); setupDisplayAndFonts(); exitActivity(); diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index 90dfed7b..e98997bd 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -3,7 +3,7 @@ #include #include #include -#include +#include #include #include @@ -231,7 +231,7 @@ void CrossPointWebServer::handleStatus() const { } void CrossPointWebServer::scanFiles(const char* path, const std::function& callback) const { - FsFile root = SdMan.open(path); + FsFile root = HAL_STORAGE.open(path); if (!root) { Serial.printf("[%lu] [WEB] Failed to open directory: %s\n", millis(), path); return; @@ -441,15 +441,15 @@ void CrossPointWebServer::handleUpload() const { // Check if file already exists - SD operations can be slow esp_task_wdt_reset(); - if (SdMan.exists(filePath.c_str())) { + if (HAL_STORAGE.exists(filePath.c_str())) { Serial.printf("[%lu] [WEB] [UPLOAD] Overwriting existing file: %s\n", millis(), filePath.c_str()); esp_task_wdt_reset(); - SdMan.remove(filePath.c_str()); + HAL_STORAGE.remove(filePath.c_str()); } // Open file for writing - this can be slow due to FAT cluster allocation esp_task_wdt_reset(); - if (!SdMan.openFileForWrite("WEB", filePath, uploadFile)) { + if (!HAL_STORAGE.openFileForWrite("WEB", filePath, uploadFile)) { uploadError = "Failed to create file on SD card"; Serial.printf("[%lu] [WEB] [UPLOAD] FAILED to create file: %s\n", millis(), filePath.c_str()); return; @@ -527,7 +527,7 @@ void CrossPointWebServer::handleUpload() const { String filePath = uploadPath; if (!filePath.endsWith("/")) filePath += "/"; filePath += uploadFileName; - SdMan.remove(filePath.c_str()); + HAL_STORAGE.remove(filePath.c_str()); } uploadError = "Upload aborted"; Serial.printf("[%lu] [WEB] Upload aborted\n", millis()); @@ -578,13 +578,13 @@ void CrossPointWebServer::handleCreateFolder() const { Serial.printf("[%lu] [WEB] Creating folder: %s\n", millis(), folderPath.c_str()); // Check if already exists - if (SdMan.exists(folderPath.c_str())) { + if (HAL_STORAGE.exists(folderPath.c_str())) { server->send(400, "text/plain", "Folder already exists"); return; } // Create the folder - if (SdMan.mkdir(folderPath.c_str())) { + if (HAL_STORAGE.mkdir(folderPath.c_str())) { Serial.printf("[%lu] [WEB] Folder created successfully: %s\n", millis(), folderPath.c_str()); server->send(200, "text/plain", "Folder created: " + folderName); } else { @@ -634,7 +634,7 @@ void CrossPointWebServer::handleDelete() const { } // Check if item exists - if (!SdMan.exists(itemPath.c_str())) { + if (!HAL_STORAGE.exists(itemPath.c_str())) { Serial.printf("[%lu] [WEB] Delete failed - item not found: %s\n", millis(), itemPath.c_str()); server->send(404, "text/plain", "Item not found"); return; @@ -646,7 +646,7 @@ void CrossPointWebServer::handleDelete() const { if (itemType == "folder") { // For folders, try to remove (will fail if not empty) - FsFile dir = SdMan.open(itemPath.c_str()); + FsFile dir = HAL_STORAGE.open(itemPath.c_str()); if (dir && dir.isDirectory()) { // Check if folder is empty FsFile entry = dir.openNextFile(); @@ -660,10 +660,10 @@ void CrossPointWebServer::handleDelete() const { } dir.close(); } - success = SdMan.rmdir(itemPath.c_str()); + success = HAL_STORAGE.rmdir(itemPath.c_str()); } else { // For files, use remove - success = SdMan.remove(itemPath.c_str()); + success = HAL_STORAGE.remove(itemPath.c_str()); } if (success) { @@ -699,7 +699,7 @@ void CrossPointWebServer::onWebSocketEvent(uint8_t num, WStype_t type, uint8_t* String filePath = wsUploadPath; if (!filePath.endsWith("/")) filePath += "/"; filePath += wsUploadFileName; - SdMan.remove(filePath.c_str()); + HAL_STORAGE.remove(filePath.c_str()); Serial.printf("[%lu] [WS] Deleted incomplete upload: %s\n", millis(), filePath.c_str()); } wsUploadInProgress = false; @@ -743,13 +743,13 @@ void CrossPointWebServer::onWebSocketEvent(uint8_t num, WStype_t type, uint8_t* // Check if file exists and remove it esp_task_wdt_reset(); - if (SdMan.exists(filePath.c_str())) { - SdMan.remove(filePath.c_str()); + if (HAL_STORAGE.exists(filePath.c_str())) { + HAL_STORAGE.remove(filePath.c_str()); } // Open file for writing esp_task_wdt_reset(); - if (!SdMan.openFileForWrite("WS", filePath, wsUploadFile)) { + if (!HAL_STORAGE.openFileForWrite("WS", filePath, wsUploadFile)) { wsServer->sendTXT(num, "ERROR:Failed to create file"); wsUploadInProgress = false; return; diff --git a/src/network/HttpDownloader.cpp b/src/network/HttpDownloader.cpp index fe65ea6b..c88f4264 100644 --- a/src/network/HttpDownloader.cpp +++ b/src/network/HttpDownloader.cpp @@ -83,13 +83,13 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& Serial.printf("[%lu] [HTTP] Content-Length: %zu\n", millis(), contentLength); // Remove existing file if present - if (SdMan.exists(destPath.c_str())) { - SdMan.remove(destPath.c_str()); + if (HAL_STORAGE.exists(destPath.c_str())) { + HAL_STORAGE.remove(destPath.c_str()); } // Open file for writing FsFile file; - if (!SdMan.openFileForWrite("HTTP", destPath.c_str(), file)) { + if (!HAL_STORAGE.openFileForWrite("HTTP", destPath.c_str(), file)) { Serial.printf("[%lu] [HTTP] Failed to open file for writing\n", millis()); http.end(); return FILE_ERROR; @@ -100,7 +100,7 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& if (!stream) { Serial.printf("[%lu] [HTTP] Failed to get stream\n", millis()); file.close(); - SdMan.remove(destPath.c_str()); + HAL_STORAGE.remove(destPath.c_str()); http.end(); return HTTP_ERROR; } @@ -128,7 +128,7 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& if (written != bytesRead) { Serial.printf("[%lu] [HTTP] Write failed: wrote %zu of %zu bytes\n", millis(), written, bytesRead); file.close(); - SdMan.remove(destPath.c_str()); + HAL_STORAGE.remove(destPath.c_str()); http.end(); return FILE_ERROR; } @@ -148,7 +148,7 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& // Verify download size if known if (contentLength > 0 && downloaded != contentLength) { Serial.printf("[%lu] [HTTP] Size mismatch: got %zu, expected %zu\n", millis(), downloaded, contentLength); - SdMan.remove(destPath.c_str()); + HAL_STORAGE.remove(destPath.c_str()); return HTTP_ERROR; } diff --git a/src/network/HttpDownloader.h b/src/network/HttpDownloader.h index ac520a42..fd18dd4c 100644 --- a/src/network/HttpDownloader.h +++ b/src/network/HttpDownloader.h @@ -1,5 +1,5 @@ #pragma once -#include +#include #include #include From dc2649775aa32c256a5f8951079c97672c70361a Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Fri, 23 Jan 2026 01:01:22 +0100 Subject: [PATCH 05/10] revert rename HAL_STORAGE --- lib/Epub/Epub.cpp | 44 +++++++++---------- lib/Epub/Epub/BookMetadataCache.cpp | 22 +++++----- lib/Epub/Epub/Page.h | 2 +- lib/Epub/Epub/Section.cpp | 28 ++++++------ lib/Epub/Epub/blocks/TextBlock.h | 2 +- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 2 +- lib/Epub/Epub/parsers/ContentOpfParser.cpp | 10 ++--- lib/GfxRenderer/Bitmap.h | 2 +- lib/JpegToBmpConverter/JpegToBmpConverter.cpp | 2 +- lib/KOReaderSync/KOReaderCredentialStore.cpp | 6 +-- lib/KOReaderSync/KOReaderDocumentId.cpp | 2 +- lib/Serialization/Serialization.h | 2 +- lib/Txt/Txt.cpp | 30 ++++++------- lib/Xtc/Xtc.cpp | 24 +++++----- lib/Xtc/Xtc/XtcParser.cpp | 4 +- lib/Xtc/Xtc/XtcParser.h | 2 +- lib/ZipFile/ZipFile.cpp | 2 +- lib/ZipFile/ZipFile.h | 2 +- src/CrossPointSettings.cpp | 6 +-- src/CrossPointState.cpp | 4 +- src/RecentBooksStore.cpp | 6 +-- src/WifiCredentialStore.cpp | 6 +-- src/activities/boot_sleep/SleepActivity.cpp | 8 ++-- src/activities/home/HomeActivity.cpp | 4 +- src/activities/home/MyLibraryActivity.cpp | 4 +- .../network/CalibreWirelessActivity.cpp | 2 +- src/activities/reader/EpubReaderActivity.cpp | 4 +- src/activities/reader/ReaderActivity.cpp | 6 +-- src/activities/reader/TxtReaderActivity.cpp | 8 ++-- src/activities/reader/XtcReaderActivity.cpp | 4 +- .../settings/ClearCacheActivity.cpp | 4 +- src/main.cpp | 2 +- src/network/CrossPointWebServer.cpp | 30 ++++++------- src/network/HttpDownloader.cpp | 12 ++--- 34 files changed, 149 insertions(+), 149 deletions(-) diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index c8fe9d5a..e5eaf296 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -101,12 +101,12 @@ bool Epub::parseTocNcxFile() const { const auto tmpNcxPath = getCachePath() + "/toc.ncx"; FsFile tempNcxFile; - if (!HAL_STORAGE.openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) { + if (!SdMan.openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) { return false; } readItemContentsToStream(tocNcxItem, tempNcxFile, 1024); tempNcxFile.close(); - if (!HAL_STORAGE.openFileForRead("EBP", tmpNcxPath, tempNcxFile)) { + if (!SdMan.openFileForRead("EBP", tmpNcxPath, tempNcxFile)) { return false; } const auto ncxSize = tempNcxFile.size(); @@ -141,7 +141,7 @@ bool Epub::parseTocNcxFile() const { free(ncxBuffer); tempNcxFile.close(); - HAL_STORAGE.remove(tmpNcxPath.c_str()); + SdMan.remove(tmpNcxPath.c_str()); Serial.printf("[%lu] [EBP] Parsed TOC items\n", millis()); return true; @@ -158,12 +158,12 @@ bool Epub::parseTocNavFile() const { const auto tmpNavPath = getCachePath() + "/toc.nav"; FsFile tempNavFile; - if (!HAL_STORAGE.openFileForWrite("EBP", tmpNavPath, tempNavFile)) { + if (!SdMan.openFileForWrite("EBP", tmpNavPath, tempNavFile)) { return false; } readItemContentsToStream(tocNavItem, tempNavFile, 1024); tempNavFile.close(); - if (!HAL_STORAGE.openFileForRead("EBP", tmpNavPath, tempNavFile)) { + if (!SdMan.openFileForRead("EBP", tmpNavPath, tempNavFile)) { return false; } const auto navSize = tempNavFile.size(); @@ -198,7 +198,7 @@ bool Epub::parseTocNavFile() const { free(navBuffer); tempNavFile.close(); - HAL_STORAGE.remove(tmpNavPath.c_str()); + SdMan.remove(tmpNavPath.c_str()); Serial.printf("[%lu] [EBP] Parsed TOC nav items\n", millis()); return true; @@ -305,12 +305,12 @@ bool Epub::load(const bool buildIfMissing) { } bool Epub::clearCache() const { - if (!HAL_STORAGE.exists(cachePath.c_str())) { + if (!SdMan.exists(cachePath.c_str())) { Serial.printf("[%lu] [EPB] Cache does not exist, no action needed\n", millis()); return true; } - if (!HAL_STORAGE.removeDir(cachePath.c_str())) { + if (!SdMan.removeDir(cachePath.c_str())) { Serial.printf("[%lu] [EPB] Failed to clear cache\n", millis()); return false; } @@ -320,11 +320,11 @@ bool Epub::clearCache() const { } void Epub::setupCacheDir() const { - if (HAL_STORAGE.exists(cachePath.c_str())) { + if (SdMan.exists(cachePath.c_str())) { return; } - HAL_STORAGE.mkdir(cachePath.c_str()); + SdMan.mkdir(cachePath.c_str()); } const std::string& Epub::getCachePath() const { return cachePath; } @@ -365,7 +365,7 @@ std::string Epub::getCoverBmpPath(bool cropped) const { bool Epub::generateCoverBmp(bool cropped) const { // Already generated, return true - if (HAL_STORAGE.exists(getCoverBmpPath(cropped).c_str())) { + if (SdMan.exists(getCoverBmpPath(cropped).c_str())) { return true; } @@ -386,29 +386,29 @@ bool Epub::generateCoverBmp(bool cropped) const { const auto coverJpgTempPath = getCachePath() + "/.cover.jpg"; FsFile coverJpg; - if (!HAL_STORAGE.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) { + if (!SdMan.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) { return false; } readItemContentsToStream(coverImageHref, coverJpg, 1024); coverJpg.close(); - if (!HAL_STORAGE.openFileForRead("EBP", coverJpgTempPath, coverJpg)) { + if (!SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg)) { return false; } FsFile coverBmp; - if (!HAL_STORAGE.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) { + if (!SdMan.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) { coverJpg.close(); return false; } const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp); coverJpg.close(); coverBmp.close(); - HAL_STORAGE.remove(coverJpgTempPath.c_str()); + SdMan.remove(coverJpgTempPath.c_str()); if (!success) { Serial.printf("[%lu] [EBP] Failed to generate BMP from JPG cover image\n", millis()); - HAL_STORAGE.remove(getCoverBmpPath(cropped).c_str()); + SdMan.remove(getCoverBmpPath(cropped).c_str()); } Serial.printf("[%lu] [EBP] Generated BMP from JPG cover image, success: %s\n", millis(), success ? "yes" : "no"); return success; @@ -423,7 +423,7 @@ std::string Epub::getThumbBmpPath() const { return cachePath + "/thumb.bmp"; } bool Epub::generateThumbBmp() const { // Already generated, return true - if (HAL_STORAGE.exists(getThumbBmpPath().c_str())) { + if (SdMan.exists(getThumbBmpPath().c_str())) { return true; } @@ -444,18 +444,18 @@ bool Epub::generateThumbBmp() const { const auto coverJpgTempPath = getCachePath() + "/.cover.jpg"; FsFile coverJpg; - if (!HAL_STORAGE.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) { + if (!SdMan.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) { return false; } readItemContentsToStream(coverImageHref, coverJpg, 1024); coverJpg.close(); - if (!HAL_STORAGE.openFileForRead("EBP", coverJpgTempPath, coverJpg)) { + if (!SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg)) { return false; } FsFile thumbBmp; - if (!HAL_STORAGE.openFileForWrite("EBP", getThumbBmpPath(), thumbBmp)) { + if (!SdMan.openFileForWrite("EBP", getThumbBmpPath(), thumbBmp)) { coverJpg.close(); return false; } @@ -467,11 +467,11 @@ bool Epub::generateThumbBmp() const { THUMB_TARGET_HEIGHT); coverJpg.close(); thumbBmp.close(); - HAL_STORAGE.remove(coverJpgTempPath.c_str()); + SdMan.remove(coverJpgTempPath.c_str()); if (!success) { Serial.printf("[%lu] [EBP] Failed to generate thumb BMP from JPG cover image\n", millis()); - HAL_STORAGE.remove(getThumbBmpPath().c_str()); + SdMan.remove(getThumbBmpPath().c_str()); } Serial.printf("[%lu] [EBP] Generated thumb BMP from JPG cover image, success: %s\n", millis(), success ? "yes" : "no"); diff --git a/lib/Epub/Epub/BookMetadataCache.cpp b/lib/Epub/Epub/BookMetadataCache.cpp index 2d9cf30f..374cad2f 100644 --- a/lib/Epub/Epub/BookMetadataCache.cpp +++ b/lib/Epub/Epub/BookMetadataCache.cpp @@ -29,7 +29,7 @@ bool BookMetadataCache::beginContentOpfPass() { Serial.printf("[%lu] [BMC] Beginning content opf pass\n", millis()); // Open spine file for writing - return HAL_STORAGE.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile); + return SdMan.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile); } bool BookMetadataCache::endContentOpfPass() { @@ -41,10 +41,10 @@ bool BookMetadataCache::beginTocPass() { Serial.printf("[%lu] [BMC] Beginning toc pass\n", millis()); // Open spine file for reading - if (!HAL_STORAGE.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) { + if (!SdMan.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) { return false; } - if (!HAL_STORAGE.openFileForWrite("BMC", cachePath + tmpTocBinFile, tocFile)) { + if (!SdMan.openFileForWrite("BMC", cachePath + tmpTocBinFile, tocFile)) { spineFile.close(); return false; } @@ -70,16 +70,16 @@ bool BookMetadataCache::endWrite() { bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMetadata& metadata) { // Open all three files, writing to meta, reading from spine and toc - if (!HAL_STORAGE.openFileForWrite("BMC", cachePath + bookBinFile, bookFile)) { + if (!SdMan.openFileForWrite("BMC", cachePath + bookBinFile, bookFile)) { return false; } - if (!HAL_STORAGE.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) { + if (!SdMan.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) { bookFile.close(); return false; } - if (!HAL_STORAGE.openFileForRead("BMC", cachePath + tmpTocBinFile, tocFile)) { + if (!SdMan.openFileForRead("BMC", cachePath + tmpTocBinFile, tocFile)) { bookFile.close(); spineFile.close(); return false; @@ -201,11 +201,11 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta } bool BookMetadataCache::cleanupTmpFiles() const { - if (HAL_STORAGE.exists((cachePath + tmpSpineBinFile).c_str())) { - HAL_STORAGE.remove((cachePath + tmpSpineBinFile).c_str()); + if (SdMan.exists((cachePath + tmpSpineBinFile).c_str())) { + SdMan.remove((cachePath + tmpSpineBinFile).c_str()); } - if (HAL_STORAGE.exists((cachePath + tmpTocBinFile).c_str())) { - HAL_STORAGE.remove((cachePath + tmpTocBinFile).c_str()); + if (SdMan.exists((cachePath + tmpTocBinFile).c_str())) { + SdMan.remove((cachePath + tmpTocBinFile).c_str()); } return true; } @@ -273,7 +273,7 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri /* ============= READING / LOADING FUNCTIONS ================ */ bool BookMetadataCache::load() { - if (!HAL_STORAGE.openFileForRead("BMC", cachePath + bookBinFile, bookFile)) { + if (!SdMan.openFileForRead("BMC", cachePath + bookBinFile, bookFile)) { return false; } diff --git a/lib/Epub/Epub/Page.h b/lib/Epub/Epub/Page.h index 20061941..590e288d 100644 --- a/lib/Epub/Epub/Page.h +++ b/lib/Epub/Epub/Page.h @@ -1,5 +1,5 @@ #pragma once -#include +#include #include #include diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 552c755d..89dba55b 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -58,7 +58,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint16_t viewportHeight, const bool hyphenationEnabled) { - if (!HAL_STORAGE.openFileForRead("SCT", filePath, file)) { + if (!SdMan.openFileForRead("SCT", filePath, file)) { return false; } @@ -106,12 +106,12 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con // Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem) bool Section::clearCache() const { - if (!HAL_STORAGE.exists(filePath.c_str())) { + if (!SdMan.exists(filePath.c_str())) { Serial.printf("[%lu] [SCT] Cache does not exist, no action needed\n", millis()); return true; } - if (!HAL_STORAGE.remove(filePath.c_str())) { + if (!SdMan.remove(filePath.c_str())) { Serial.printf("[%lu] [SCT] Failed to clear cache\n", millis()); return false; } @@ -132,7 +132,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c // Create cache directory if it doesn't exist { const auto sectionsDir = epub->getCachePath() + "/sections"; - HAL_STORAGE.mkdir(sectionsDir.c_str()); + SdMan.mkdir(sectionsDir.c_str()); } // Retry logic for SD card timing issues @@ -145,12 +145,12 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c } // Remove any incomplete file from previous attempt before retrying - if (HAL_STORAGE.exists(tmpHtmlPath.c_str())) { - HAL_STORAGE.remove(tmpHtmlPath.c_str()); + if (SdMan.exists(tmpHtmlPath.c_str())) { + SdMan.remove(tmpHtmlPath.c_str()); } FsFile tmpHtml; - if (!HAL_STORAGE.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) { + if (!SdMan.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) { continue; } success = epub->readItemContentsToStream(localPath, tmpHtml, 1024); @@ -158,8 +158,8 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c tmpHtml.close(); // If streaming failed, remove the incomplete file immediately - if (!success && HAL_STORAGE.exists(tmpHtmlPath.c_str())) { - HAL_STORAGE.remove(tmpHtmlPath.c_str()); + if (!success && SdMan.exists(tmpHtmlPath.c_str())) { + SdMan.remove(tmpHtmlPath.c_str()); Serial.printf("[%lu] [SCT] Removed incomplete temp file after failed attempt\n", millis()); } } @@ -176,7 +176,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c progressSetupFn(); } - if (!HAL_STORAGE.openFileForWrite("SCT", filePath, file)) { + if (!SdMan.openFileForWrite("SCT", filePath, file)) { return false; } writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, @@ -191,11 +191,11 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c Hyphenator::setPreferredLanguage(epub->getLanguage()); success = visitor.parseAndBuildPages(); - HAL_STORAGE.remove(tmpHtmlPath.c_str()); + SdMan.remove(tmpHtmlPath.c_str()); if (!success) { Serial.printf("[%lu] [SCT] Failed to parse XML and build pages\n", millis()); file.close(); - HAL_STORAGE.remove(filePath.c_str()); + SdMan.remove(filePath.c_str()); return false; } @@ -213,7 +213,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c if (hasFailedLutRecords) { Serial.printf("[%lu] [SCT] Failed to write LUT due to invalid page positions\n", millis()); file.close(); - HAL_STORAGE.remove(filePath.c_str()); + SdMan.remove(filePath.c_str()); return false; } @@ -226,7 +226,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c } std::unique_ptr Section::loadPageFromSectionFile() { - if (!HAL_STORAGE.openFileForRead("SCT", filePath, file)) { + if (!SdMan.openFileForRead("SCT", filePath, file)) { return nullptr; } diff --git a/lib/Epub/Epub/blocks/TextBlock.h b/lib/Epub/Epub/blocks/TextBlock.h index 415a18f3..053f8925 100644 --- a/lib/Epub/Epub/blocks/TextBlock.h +++ b/lib/Epub/Epub/blocks/TextBlock.h @@ -1,6 +1,6 @@ #pragma once #include -#include +#include #include #include diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 1cbbb165..9da99a49 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -264,7 +264,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() { } FsFile file; - if (!HAL_STORAGE.openFileForRead("EHP", filepath, file)) { + if (!SdMan.openFileForRead("EHP", filepath, file)) { XML_ParserFree(parser); return false; } diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.cpp b/lib/Epub/Epub/parsers/ContentOpfParser.cpp index d25e614b..9fbeb386 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.cpp +++ b/lib/Epub/Epub/parsers/ContentOpfParser.cpp @@ -35,8 +35,8 @@ ContentOpfParser::~ContentOpfParser() { if (tempItemStore) { tempItemStore.close(); } - if (HAL_STORAGE.exists((cachePath + itemCacheFile).c_str())) { - HAL_STORAGE.remove((cachePath + itemCacheFile).c_str()); + if (SdMan.exists((cachePath + itemCacheFile).c_str())) { + SdMan.remove((cachePath + itemCacheFile).c_str()); } } @@ -114,7 +114,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name if (self->state == IN_PACKAGE && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) { self->state = IN_MANIFEST; - if (!HAL_STORAGE.openFileForWrite("COF", self->cachePath + itemCacheFile, self->tempItemStore)) { + if (!SdMan.openFileForWrite("COF", self->cachePath + itemCacheFile, self->tempItemStore)) { Serial.printf( "[%lu] [COF] Couldn't open temp items file for writing. This is probably going to be a fatal error.\n", millis()); @@ -124,7 +124,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name if (self->state == IN_PACKAGE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) { self->state = IN_SPINE; - if (!HAL_STORAGE.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) { + if (!SdMan.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) { Serial.printf( "[%lu] [COF] Couldn't open temp items file for reading. This is probably going to be a fatal error.\n", millis()); @@ -136,7 +136,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name self->state = IN_GUIDE; // TODO Remove print Serial.printf("[%lu] [COF] Entering guide state.\n", millis()); - if (!HAL_STORAGE.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) { + if (!SdMan.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) { Serial.printf( "[%lu] [COF] Couldn't open temp items file for reading. This is probably going to be a fatal error.\n", millis()); diff --git a/lib/GfxRenderer/Bitmap.h b/lib/GfxRenderer/Bitmap.h index 544869c1..102283de 100644 --- a/lib/GfxRenderer/Bitmap.h +++ b/lib/GfxRenderer/Bitmap.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include diff --git a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp index 01451a05..33727dd7 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp @@ -1,7 +1,7 @@ #include "JpegToBmpConverter.h" #include -#include +#include #include #include diff --git a/lib/KOReaderSync/KOReaderCredentialStore.cpp b/lib/KOReaderSync/KOReaderCredentialStore.cpp index 4436e2a7..3041e237 100644 --- a/lib/KOReaderSync/KOReaderCredentialStore.cpp +++ b/lib/KOReaderSync/KOReaderCredentialStore.cpp @@ -32,10 +32,10 @@ void KOReaderCredentialStore::obfuscate(std::string& data) const { bool KOReaderCredentialStore::saveToFile() const { // Make sure the directory exists - HAL_STORAGE.mkdir("/.crosspoint"); + SdMan.mkdir("/.crosspoint"); FsFile file; - if (!HAL_STORAGE.openFileForWrite("KRS", KOREADER_FILE, file)) { + if (!SdMan.openFileForWrite("KRS", KOREADER_FILE, file)) { return false; } @@ -64,7 +64,7 @@ bool KOReaderCredentialStore::saveToFile() const { bool KOReaderCredentialStore::loadFromFile() { FsFile file; - if (!HAL_STORAGE.openFileForRead("KRS", KOREADER_FILE, file)) { + if (!SdMan.openFileForRead("KRS", KOREADER_FILE, file)) { Serial.printf("[%lu] [KRS] No credentials file found\n", millis()); return false; } diff --git a/lib/KOReaderSync/KOReaderDocumentId.cpp b/lib/KOReaderSync/KOReaderDocumentId.cpp index db48f5b5..6bdef11c 100644 --- a/lib/KOReaderSync/KOReaderDocumentId.cpp +++ b/lib/KOReaderSync/KOReaderDocumentId.cpp @@ -43,7 +43,7 @@ size_t KOReaderDocumentId::getOffset(int i) { std::string KOReaderDocumentId::calculate(const std::string& filePath) { FsFile file; - if (!HAL_STORAGE.openFileForRead("KODoc", filePath, file)) { + if (!SdMan.openFileForRead("KODoc", filePath, file)) { Serial.printf("[%lu] [KODoc] Failed to open file: %s\n", millis(), filePath.c_str()); return ""; } diff --git a/lib/Serialization/Serialization.h b/lib/Serialization/Serialization.h index afea5646..1308822f 100644 --- a/lib/Serialization/Serialization.h +++ b/lib/Serialization/Serialization.h @@ -1,5 +1,5 @@ #pragma once -#include +#include #include diff --git a/lib/Txt/Txt.cpp b/lib/Txt/Txt.cpp index 7dad632e..52c75ed7 100644 --- a/lib/Txt/Txt.cpp +++ b/lib/Txt/Txt.cpp @@ -15,13 +15,13 @@ bool Txt::load() { return true; } - if (!HAL_STORAGE.exists(filepath.c_str())) { + if (!SdMan.exists(filepath.c_str())) { Serial.printf("[%lu] [TXT] File does not exist: %s\n", millis(), filepath.c_str()); return false; } FsFile file; - if (!HAL_STORAGE.openFileForRead("TXT", filepath, file)) { + if (!SdMan.openFileForRead("TXT", filepath, file)) { Serial.printf("[%lu] [TXT] Failed to open file: %s\n", millis(), filepath.c_str()); return false; } @@ -48,11 +48,11 @@ std::string Txt::getTitle() const { } void Txt::setupCacheDir() const { - if (!HAL_STORAGE.exists(cacheBasePath.c_str())) { - HAL_STORAGE.mkdir(cacheBasePath.c_str()); + if (!SdMan.exists(cacheBasePath.c_str())) { + SdMan.mkdir(cacheBasePath.c_str()); } - if (!HAL_STORAGE.exists(cachePath.c_str())) { - HAL_STORAGE.mkdir(cachePath.c_str()); + if (!SdMan.exists(cachePath.c_str())) { + SdMan.mkdir(cachePath.c_str()); } } @@ -73,7 +73,7 @@ std::string Txt::findCoverImage() const { // First priority: look for image with same name as txt file (e.g., mybook.jpg) for (const auto& ext : extensions) { std::string coverPath = folder + "/" + baseName + ext; - if (HAL_STORAGE.exists(coverPath.c_str())) { + if (SdMan.exists(coverPath.c_str())) { Serial.printf("[%lu] [TXT] Found matching cover image: %s\n", millis(), coverPath.c_str()); return coverPath; } @@ -84,7 +84,7 @@ std::string Txt::findCoverImage() const { for (const auto& name : coverNames) { for (const auto& ext : extensions) { std::string coverPath = folder + "/" + std::string(name) + ext; - if (HAL_STORAGE.exists(coverPath.c_str())) { + if (SdMan.exists(coverPath.c_str())) { Serial.printf("[%lu] [TXT] Found fallback cover image: %s\n", millis(), coverPath.c_str()); return coverPath; } @@ -98,7 +98,7 @@ std::string Txt::getCoverBmpPath() const { return cachePath + "/cover.bmp"; } bool Txt::generateCoverBmp() const { // Already generated, return true - if (HAL_STORAGE.exists(getCoverBmpPath().c_str())) { + if (SdMan.exists(getCoverBmpPath().c_str())) { return true; } @@ -122,10 +122,10 @@ bool Txt::generateCoverBmp() const { // Copy BMP file to cache Serial.printf("[%lu] [TXT] Copying BMP cover image to cache\n", millis()); FsFile src, dst; - if (!HAL_STORAGE.openFileForRead("TXT", coverImagePath, src)) { + if (!SdMan.openFileForRead("TXT", coverImagePath, src)) { return false; } - if (!HAL_STORAGE.openFileForWrite("TXT", getCoverBmpPath(), dst)) { + if (!SdMan.openFileForWrite("TXT", getCoverBmpPath(), dst)) { src.close(); return false; } @@ -144,10 +144,10 @@ bool Txt::generateCoverBmp() const { // Convert JPG/JPEG to BMP (same approach as Epub) Serial.printf("[%lu] [TXT] Generating BMP from JPG cover image\n", millis()); FsFile coverJpg, coverBmp; - if (!HAL_STORAGE.openFileForRead("TXT", coverImagePath, coverJpg)) { + if (!SdMan.openFileForRead("TXT", coverImagePath, coverJpg)) { return false; } - if (!HAL_STORAGE.openFileForWrite("TXT", getCoverBmpPath(), coverBmp)) { + if (!SdMan.openFileForWrite("TXT", getCoverBmpPath(), coverBmp)) { coverJpg.close(); return false; } @@ -157,7 +157,7 @@ bool Txt::generateCoverBmp() const { if (!success) { Serial.printf("[%lu] [TXT] Failed to generate BMP from JPG cover image\n", millis()); - HAL_STORAGE.remove(getCoverBmpPath().c_str()); + SdMan.remove(getCoverBmpPath().c_str()); } else { Serial.printf("[%lu] [TXT] Generated BMP from JPG cover image\n", millis()); } @@ -175,7 +175,7 @@ bool Txt::readContent(uint8_t* buffer, size_t offset, size_t length) const { } FsFile file; - if (!HAL_STORAGE.openFileForRead("TXT", filepath, file)) { + if (!SdMan.openFileForRead("TXT", filepath, file)) { return false; } diff --git a/lib/Xtc/Xtc.cpp b/lib/Xtc/Xtc.cpp index 11b1eb03..5d60a4fe 100644 --- a/lib/Xtc/Xtc.cpp +++ b/lib/Xtc/Xtc.cpp @@ -31,12 +31,12 @@ bool Xtc::load() { } bool Xtc::clearCache() const { - if (!HAL_STORAGE.exists(cachePath.c_str())) { + if (!SdMan.exists(cachePath.c_str())) { Serial.printf("[%lu] [XTC] Cache does not exist, no action needed\n", millis()); return true; } - if (!HAL_STORAGE.removeDir(cachePath.c_str())) { + if (!SdMan.removeDir(cachePath.c_str())) { Serial.printf("[%lu] [XTC] Failed to clear cache\n", millis()); return false; } @@ -46,17 +46,17 @@ bool Xtc::clearCache() const { } void Xtc::setupCacheDir() const { - if (HAL_STORAGE.exists(cachePath.c_str())) { + if (SdMan.exists(cachePath.c_str())) { return; } // Create directories recursively for (size_t i = 1; i < cachePath.length(); i++) { if (cachePath[i] == '/') { - HAL_STORAGE.mkdir(cachePath.substr(0, i).c_str()); + SdMan.mkdir(cachePath.substr(0, i).c_str()); } } - HAL_STORAGE.mkdir(cachePath.c_str()); + SdMan.mkdir(cachePath.c_str()); } std::string Xtc::getTitle() const { @@ -106,7 +106,7 @@ std::string Xtc::getCoverBmpPath() const { return cachePath + "/cover.bmp"; } bool Xtc::generateCoverBmp() const { // Already generated - if (HAL_STORAGE.exists(getCoverBmpPath().c_str())) { + if (SdMan.exists(getCoverBmpPath().c_str())) { return true; } @@ -158,7 +158,7 @@ bool Xtc::generateCoverBmp() const { // Create BMP file FsFile coverBmp; - if (!HAL_STORAGE.openFileForWrite("XTC", getCoverBmpPath(), coverBmp)) { + if (!SdMan.openFileForWrite("XTC", getCoverBmpPath(), coverBmp)) { Serial.printf("[%lu] [XTC] Failed to create cover BMP file\n", millis()); free(pageBuffer); return false; @@ -297,7 +297,7 @@ std::string Xtc::getThumbBmpPath() const { return cachePath + "/thumb.bmp"; } bool Xtc::generateThumbBmp() const { // Already generated - if (HAL_STORAGE.exists(getThumbBmpPath().c_str())) { + if (SdMan.exists(getThumbBmpPath().c_str())) { return true; } @@ -339,8 +339,8 @@ bool Xtc::generateThumbBmp() const { // Copy cover.bmp to thumb.bmp if (generateCoverBmp()) { FsFile src, dst; - if (HAL_STORAGE.openFileForRead("XTC", getCoverBmpPath(), src)) { - if (HAL_STORAGE.openFileForWrite("XTC", getThumbBmpPath(), dst)) { + if (SdMan.openFileForRead("XTC", getCoverBmpPath(), src)) { + if (SdMan.openFileForWrite("XTC", getThumbBmpPath(), dst)) { uint8_t buffer[512]; while (src.available()) { size_t bytesRead = src.read(buffer, sizeof(buffer)); @@ -351,7 +351,7 @@ bool Xtc::generateThumbBmp() const { src.close(); } Serial.printf("[%lu] [XTC] Copied cover to thumb (no scaling needed)\n", millis()); - return HAL_STORAGE.exists(getThumbBmpPath().c_str()); + return SdMan.exists(getThumbBmpPath().c_str()); } return false; } @@ -385,7 +385,7 @@ bool Xtc::generateThumbBmp() const { // Create thumbnail BMP file - use 1-bit format for fast home screen rendering (no gray passes) FsFile thumbBmp; - if (!HAL_STORAGE.openFileForWrite("XTC", getThumbBmpPath(), thumbBmp)) { + if (!SdMan.openFileForWrite("XTC", getThumbBmpPath(), thumbBmp)) { Serial.printf("[%lu] [XTC] Failed to create thumb BMP file\n", millis()); free(pageBuffer); return false; diff --git a/lib/Xtc/Xtc/XtcParser.cpp b/lib/Xtc/Xtc/XtcParser.cpp index 759d06f1..834063a1 100644 --- a/lib/Xtc/Xtc/XtcParser.cpp +++ b/lib/Xtc/Xtc/XtcParser.cpp @@ -34,7 +34,7 @@ XtcError XtcParser::open(const char* filepath) { } // Open file - if (!HAL_STORAGE.openFileForRead("XTC", filepath, m_file)) { + if (!SdMan.openFileForRead("XTC", filepath, m_file)) { m_lastError = XtcError::FILE_NOT_FOUND; return m_lastError; } @@ -421,7 +421,7 @@ XtcError XtcParser::loadPageStreaming(uint32_t pageIndex, bool XtcParser::isValidXtcFile(const char* filepath) { FsFile file; - if (!HAL_STORAGE.openFileForRead("XTC", filepath, file)) { + if (!SdMan.openFileForRead("XTC", filepath, file)) { return false; } diff --git a/lib/Xtc/Xtc/XtcParser.h b/lib/Xtc/Xtc/XtcParser.h index 2d2b780e..b4105b2d 100644 --- a/lib/Xtc/Xtc/XtcParser.h +++ b/lib/Xtc/Xtc/XtcParser.h @@ -7,7 +7,7 @@ #pragma once -#include +#include #include #include diff --git a/lib/ZipFile/ZipFile.cpp b/lib/ZipFile/ZipFile.cpp index 663362a3..e5c3f642 100644 --- a/lib/ZipFile/ZipFile.cpp +++ b/lib/ZipFile/ZipFile.cpp @@ -243,7 +243,7 @@ bool ZipFile::loadZipDetails() { } bool ZipFile::open() { - if (!HAL_STORAGE.openFileForRead("ZIP", filePath, file)) { + if (!SdMan.openFileForRead("ZIP", filePath, file)) { return false; } return true; diff --git a/lib/ZipFile/ZipFile.h b/lib/ZipFile/ZipFile.h index 0144ed42..5e5d3f0c 100644 --- a/lib/ZipFile/ZipFile.h +++ b/lib/ZipFile/ZipFile.h @@ -1,5 +1,5 @@ #pragma once -#include +#include #include #include diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index 60ea67e8..b7e3dab9 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -20,10 +20,10 @@ constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; bool CrossPointSettings::saveToFile() const { // Make sure the directory exists - HAL_STORAGE.mkdir("/.crosspoint"); + SdMan.mkdir("/.crosspoint"); FsFile outputFile; - if (!HAL_STORAGE.openFileForWrite("CPS", SETTINGS_FILE, outputFile)) { + if (!SdMan.openFileForWrite("CPS", SETTINGS_FILE, outputFile)) { return false; } @@ -57,7 +57,7 @@ bool CrossPointSettings::saveToFile() const { bool CrossPointSettings::loadFromFile() { FsFile inputFile; - if (!HAL_STORAGE.openFileForRead("CPS", SETTINGS_FILE, inputFile)) { + if (!SdMan.openFileForRead("CPS", SETTINGS_FILE, inputFile)) { return false; } diff --git a/src/CrossPointState.cpp b/src/CrossPointState.cpp index b317e2eb..58a1a4c1 100644 --- a/src/CrossPointState.cpp +++ b/src/CrossPointState.cpp @@ -13,7 +13,7 @@ CrossPointState CrossPointState::instance; bool CrossPointState::saveToFile() const { FsFile outputFile; - if (!HAL_STORAGE.openFileForWrite("CPS", STATE_FILE, outputFile)) { + if (!SdMan.openFileForWrite("CPS", STATE_FILE, outputFile)) { return false; } @@ -26,7 +26,7 @@ bool CrossPointState::saveToFile() const { bool CrossPointState::loadFromFile() { FsFile inputFile; - if (!HAL_STORAGE.openFileForRead("CPS", STATE_FILE, inputFile)) { + if (!SdMan.openFileForRead("CPS", STATE_FILE, inputFile)) { return false; } diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index 071bb527..0da8d2c5 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -34,10 +34,10 @@ void RecentBooksStore::addBook(const std::string& path) { bool RecentBooksStore::saveToFile() const { // Make sure the directory exists - HAL_STORAGE.mkdir("/.crosspoint"); + SdMan.mkdir("/.crosspoint"); FsFile outputFile; - if (!HAL_STORAGE.openFileForWrite("RBS", RECENT_BOOKS_FILE, outputFile)) { + if (!SdMan.openFileForWrite("RBS", RECENT_BOOKS_FILE, outputFile)) { return false; } @@ -56,7 +56,7 @@ bool RecentBooksStore::saveToFile() const { bool RecentBooksStore::loadFromFile() { FsFile inputFile; - if (!HAL_STORAGE.openFileForRead("RBS", RECENT_BOOKS_FILE, inputFile)) { + if (!SdMan.openFileForRead("RBS", RECENT_BOOKS_FILE, inputFile)) { return false; } diff --git a/src/WifiCredentialStore.cpp b/src/WifiCredentialStore.cpp index ddea3840..476dae6f 100644 --- a/src/WifiCredentialStore.cpp +++ b/src/WifiCredentialStore.cpp @@ -29,10 +29,10 @@ void WifiCredentialStore::obfuscate(std::string& data) const { bool WifiCredentialStore::saveToFile() const { // Make sure the directory exists - HAL_STORAGE.mkdir("/.crosspoint"); + SdMan.mkdir("/.crosspoint"); FsFile file; - if (!HAL_STORAGE.openFileForWrite("WCS", WIFI_FILE, file)) { + if (!SdMan.openFileForWrite("WCS", WIFI_FILE, file)) { return false; } @@ -60,7 +60,7 @@ bool WifiCredentialStore::saveToFile() const { bool WifiCredentialStore::loadFromFile() { FsFile file; - if (!HAL_STORAGE.openFileForRead("WCS", WIFI_FILE, file)) { + if (!SdMan.openFileForRead("WCS", WIFI_FILE, file)) { return false; } diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 0b0ad6ee..82a7ba3c 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -47,7 +47,7 @@ void SleepActivity::renderPopup(const char* message) const { void SleepActivity::renderCustomSleepScreen() const { // Check if we have a /sleep directory - auto dir = HAL_STORAGE.open("/sleep"); + auto dir = SdMan.open("/sleep"); if (dir && dir.isDirectory()) { std::vector files; char name[500]; @@ -90,7 +90,7 @@ void SleepActivity::renderCustomSleepScreen() const { APP_STATE.saveToFile(); const auto filename = "/sleep/" + files[randomFileIndex]; FsFile file; - if (HAL_STORAGE.openFileForRead("SLP", filename, file)) { + if (SdMan.openFileForRead("SLP", filename, file)) { Serial.printf("[%lu] [SLP] Randomly loading: /sleep/%s\n", millis(), files[randomFileIndex].c_str()); delay(100); Bitmap bitmap(file, true); @@ -107,7 +107,7 @@ void SleepActivity::renderCustomSleepScreen() const { // Look for sleep.bmp on the root of the sd card to determine if we should // render a custom sleep screen instead of the default. FsFile file; - if (HAL_STORAGE.openFileForRead("SLP", "/sleep.bmp", file)) { + if (SdMan.openFileForRead("SLP", "/sleep.bmp", file)) { Bitmap bitmap(file, true); if (bitmap.parseHeaders() == BmpReaderError::Ok) { Serial.printf("[%lu] [SLP] Loading: /sleep.bmp\n", millis()); @@ -257,7 +257,7 @@ void SleepActivity::renderCoverSleepScreen() const { } FsFile file; - if (HAL_STORAGE.openFileForRead("SLP", coverBmpPath, file)) { + if (SdMan.openFileForRead("SLP", coverBmpPath, file)) { Bitmap bitmap(file); if (bitmap.parseHeaders() == BmpReaderError::Ok) { renderBitmapSleepScreen(bitmap); diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 2dd0e108..11e57b84 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -35,7 +35,7 @@ void HomeActivity::onEnter() { renderingMutex = xSemaphoreCreateMutex(); // Check if we have a book to continue reading - hasContinueReading = !APP_STATE.openEpubPath.empty() && HAL_STORAGE.exists(APP_STATE.openEpubPath.c_str()); + hasContinueReading = !APP_STATE.openEpubPath.empty() && SdMan.exists(APP_STATE.openEpubPath.c_str()); // Check if OPDS browser URL is configured hasOpdsUrl = strlen(SETTINGS.opdsServerUrl) > 0; @@ -239,7 +239,7 @@ void HomeActivity::render() { if (hasContinueReading && hasCoverImage && !coverBmpPath.empty() && !coverRendered) { // First time: load cover from SD and render FsFile file; - if (HAL_STORAGE.openFileForRead("HOME", coverBmpPath, file)) { + if (SdMan.openFileForRead("HOME", coverBmpPath, file)) { Bitmap bitmap(file); if (bitmap.parseHeaders() == BmpReaderError::Ok) { // Calculate position to center image within the book card diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index fd7f6cbb..453360e6 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -80,7 +80,7 @@ void MyLibraryActivity::loadRecentBooks() { } // Skip if file no longer exists - if (!HAL_STORAGE.exists(path.c_str())) { + if (!SdMan.exists(path.c_str())) { continue; } @@ -99,7 +99,7 @@ void MyLibraryActivity::loadRecentBooks() { void MyLibraryActivity::loadFiles() { files.clear(); - auto root = HAL_STORAGE.open(basepath.c_str()); + auto root = SdMan.open(basepath.c_str()); if (!root || !root.isDirectory()) { if (root) root.close(); return; diff --git a/src/activities/network/CalibreWirelessActivity.cpp b/src/activities/network/CalibreWirelessActivity.cpp index c1118128..8dfba0b7 100644 --- a/src/activities/network/CalibreWirelessActivity.cpp +++ b/src/activities/network/CalibreWirelessActivity.cpp @@ -592,7 +592,7 @@ void CalibreWirelessActivity::handleSendBook(const std::string& data) { setStatus("Receiving: " + filename); // Open file for writing - if (!HAL_STORAGE.openFileForWrite("CAL", currentFilename.c_str(), currentFile)) { + if (!SdMan.openFileForWrite("CAL", currentFilename.c_str(), currentFile)) { setError("Failed to create file"); sendJsonResponse(OpCode::ERROR, "{\"message\":\"Failed to create file\"}"); return; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index a0626859..67009314 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -55,7 +55,7 @@ void EpubReaderActivity::onEnter() { epub->setupCacheDir(); FsFile f; - if (HAL_STORAGE.openFileForRead("ERS", epub->getCachePath() + "/progress.bin", f)) { + if (SdMan.openFileForRead("ERS", epub->getCachePath() + "/progress.bin", f)) { uint8_t data[4]; if (f.read(data, 4) == 4) { currentSpineIndex = data[0] + (data[1] << 8); @@ -375,7 +375,7 @@ void EpubReaderActivity::renderScreen() { } FsFile f; - if (HAL_STORAGE.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) { + if (SdMan.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) { uint8_t data[4]; data[0] = currentSpineIndex & 0xFF; data[1] = (currentSpineIndex >> 8) & 0xFF; diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index 9342f5f6..14d6623c 100644 --- a/src/activities/reader/ReaderActivity.cpp +++ b/src/activities/reader/ReaderActivity.cpp @@ -28,7 +28,7 @@ bool ReaderActivity::isTxtFile(const std::string& path) { } std::unique_ptr ReaderActivity::loadEpub(const std::string& path) { - if (!HAL_STORAGE.exists(path.c_str())) { + if (!SdMan.exists(path.c_str())) { Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str()); return nullptr; } @@ -43,7 +43,7 @@ std::unique_ptr ReaderActivity::loadEpub(const std::string& path) { } std::unique_ptr ReaderActivity::loadXtc(const std::string& path) { - if (!HAL_STORAGE.exists(path.c_str())) { + if (!SdMan.exists(path.c_str())) { Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str()); return nullptr; } @@ -58,7 +58,7 @@ std::unique_ptr ReaderActivity::loadXtc(const std::string& path) { } std::unique_ptr ReaderActivity::loadTxt(const std::string& path) { - if (!HAL_STORAGE.exists(path.c_str())) { + if (!SdMan.exists(path.c_str())) { Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str()); return nullptr; } diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 8bdcded5..b25198cc 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -538,7 +538,7 @@ void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int void TxtReaderActivity::saveProgress() const { FsFile f; - if (HAL_STORAGE.openFileForWrite("TRS", txt->getCachePath() + "/progress.bin", f)) { + if (SdMan.openFileForWrite("TRS", txt->getCachePath() + "/progress.bin", f)) { uint8_t data[4]; data[0] = currentPage & 0xFF; data[1] = (currentPage >> 8) & 0xFF; @@ -551,7 +551,7 @@ void TxtReaderActivity::saveProgress() const { void TxtReaderActivity::loadProgress() { FsFile f; - if (HAL_STORAGE.openFileForRead("TRS", txt->getCachePath() + "/progress.bin", f)) { + if (SdMan.openFileForRead("TRS", txt->getCachePath() + "/progress.bin", f)) { uint8_t data[4]; if (f.read(data, 4) == 4) { currentPage = data[0] + (data[1] << 8); @@ -582,7 +582,7 @@ bool TxtReaderActivity::loadPageIndexCache() { std::string cachePath = txt->getCachePath() + "/index.bin"; FsFile f; - if (!HAL_STORAGE.openFileForRead("TRS", cachePath, f)) { + if (!SdMan.openFileForRead("TRS", cachePath, f)) { Serial.printf("[%lu] [TRS] No page index cache found\n", millis()); return false; } @@ -674,7 +674,7 @@ bool TxtReaderActivity::loadPageIndexCache() { void TxtReaderActivity::savePageIndexCache() const { std::string cachePath = txt->getCachePath() + "/index.bin"; FsFile f; - if (!HAL_STORAGE.openFileForWrite("TRS", cachePath, f)) { + if (!SdMan.openFileForWrite("TRS", cachePath, f)) { Serial.printf("[%lu] [TRS] Failed to save page index cache\n", millis()); return; } diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index 046de5cc..0ec66e82 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -362,7 +362,7 @@ void XtcReaderActivity::renderPage() { void XtcReaderActivity::saveProgress() const { FsFile f; - if (HAL_STORAGE.openFileForWrite("XTR", xtc->getCachePath() + "/progress.bin", f)) { + if (SdMan.openFileForWrite("XTR", xtc->getCachePath() + "/progress.bin", f)) { uint8_t data[4]; data[0] = currentPage & 0xFF; data[1] = (currentPage >> 8) & 0xFF; @@ -375,7 +375,7 @@ void XtcReaderActivity::saveProgress() const { void XtcReaderActivity::loadProgress() { FsFile f; - if (HAL_STORAGE.openFileForRead("XTR", xtc->getCachePath() + "/progress.bin", f)) { + if (SdMan.openFileForRead("XTR", xtc->getCachePath() + "/progress.bin", f)) { uint8_t data[4]; if (f.read(data, 4) == 4) { currentPage = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24); diff --git a/src/activities/settings/ClearCacheActivity.cpp b/src/activities/settings/ClearCacheActivity.cpp index cd0ec846..368e07e9 100644 --- a/src/activities/settings/ClearCacheActivity.cpp +++ b/src/activities/settings/ClearCacheActivity.cpp @@ -106,7 +106,7 @@ void ClearCacheActivity::clearCache() { Serial.printf("[%lu] [CLEAR_CACHE] Clearing cache...\n", millis()); // Open .crosspoint directory - auto root = HAL_STORAGE.open("/.crosspoint"); + 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(); @@ -131,7 +131,7 @@ void ClearCacheActivity::clearCache() { file.close(); // Close before attempting to delete - if (HAL_STORAGE.removeDir(fullPath.c_str())) { + if (SdMan.removeDir(fullPath.c_str())) { clearedCount++; } else { Serial.printf("[%lu] [CLEAR_CACHE] Failed to remove: %s\n", millis(), fullPath.c_str()); diff --git a/src/main.cpp b/src/main.cpp index 32cafb72..00700f68 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -311,7 +311,7 @@ void setup() { // SD Card Initialization // We need 6 open files concurrently when parsing a new chapter - if (!HAL_STORAGE.begin()) { + if (!SdMan.begin()) { Serial.printf("[%lu] [ ] SD card initialization failed\n", millis()); setupDisplayAndFonts(); exitActivity(); diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index e98997bd..fe7ec8cd 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -231,7 +231,7 @@ void CrossPointWebServer::handleStatus() const { } void CrossPointWebServer::scanFiles(const char* path, const std::function& callback) const { - FsFile root = HAL_STORAGE.open(path); + FsFile root = SdMan.open(path); if (!root) { Serial.printf("[%lu] [WEB] Failed to open directory: %s\n", millis(), path); return; @@ -441,15 +441,15 @@ void CrossPointWebServer::handleUpload() const { // Check if file already exists - SD operations can be slow esp_task_wdt_reset(); - if (HAL_STORAGE.exists(filePath.c_str())) { + if (SdMan.exists(filePath.c_str())) { Serial.printf("[%lu] [WEB] [UPLOAD] Overwriting existing file: %s\n", millis(), filePath.c_str()); esp_task_wdt_reset(); - HAL_STORAGE.remove(filePath.c_str()); + SdMan.remove(filePath.c_str()); } // Open file for writing - this can be slow due to FAT cluster allocation esp_task_wdt_reset(); - if (!HAL_STORAGE.openFileForWrite("WEB", filePath, uploadFile)) { + if (!SdMan.openFileForWrite("WEB", filePath, uploadFile)) { uploadError = "Failed to create file on SD card"; Serial.printf("[%lu] [WEB] [UPLOAD] FAILED to create file: %s\n", millis(), filePath.c_str()); return; @@ -527,7 +527,7 @@ void CrossPointWebServer::handleUpload() const { String filePath = uploadPath; if (!filePath.endsWith("/")) filePath += "/"; filePath += uploadFileName; - HAL_STORAGE.remove(filePath.c_str()); + SdMan.remove(filePath.c_str()); } uploadError = "Upload aborted"; Serial.printf("[%lu] [WEB] Upload aborted\n", millis()); @@ -578,13 +578,13 @@ void CrossPointWebServer::handleCreateFolder() const { Serial.printf("[%lu] [WEB] Creating folder: %s\n", millis(), folderPath.c_str()); // Check if already exists - if (HAL_STORAGE.exists(folderPath.c_str())) { + if (SdMan.exists(folderPath.c_str())) { server->send(400, "text/plain", "Folder already exists"); return; } // Create the folder - if (HAL_STORAGE.mkdir(folderPath.c_str())) { + if (SdMan.mkdir(folderPath.c_str())) { Serial.printf("[%lu] [WEB] Folder created successfully: %s\n", millis(), folderPath.c_str()); server->send(200, "text/plain", "Folder created: " + folderName); } else { @@ -634,7 +634,7 @@ void CrossPointWebServer::handleDelete() const { } // Check if item exists - if (!HAL_STORAGE.exists(itemPath.c_str())) { + if (!SdMan.exists(itemPath.c_str())) { Serial.printf("[%lu] [WEB] Delete failed - item not found: %s\n", millis(), itemPath.c_str()); server->send(404, "text/plain", "Item not found"); return; @@ -646,7 +646,7 @@ void CrossPointWebServer::handleDelete() const { if (itemType == "folder") { // For folders, try to remove (will fail if not empty) - FsFile dir = HAL_STORAGE.open(itemPath.c_str()); + FsFile dir = SdMan.open(itemPath.c_str()); if (dir && dir.isDirectory()) { // Check if folder is empty FsFile entry = dir.openNextFile(); @@ -660,10 +660,10 @@ void CrossPointWebServer::handleDelete() const { } dir.close(); } - success = HAL_STORAGE.rmdir(itemPath.c_str()); + success = SdMan.rmdir(itemPath.c_str()); } else { // For files, use remove - success = HAL_STORAGE.remove(itemPath.c_str()); + success = SdMan.remove(itemPath.c_str()); } if (success) { @@ -699,7 +699,7 @@ void CrossPointWebServer::onWebSocketEvent(uint8_t num, WStype_t type, uint8_t* String filePath = wsUploadPath; if (!filePath.endsWith("/")) filePath += "/"; filePath += wsUploadFileName; - HAL_STORAGE.remove(filePath.c_str()); + SdMan.remove(filePath.c_str()); Serial.printf("[%lu] [WS] Deleted incomplete upload: %s\n", millis(), filePath.c_str()); } wsUploadInProgress = false; @@ -743,13 +743,13 @@ void CrossPointWebServer::onWebSocketEvent(uint8_t num, WStype_t type, uint8_t* // Check if file exists and remove it esp_task_wdt_reset(); - if (HAL_STORAGE.exists(filePath.c_str())) { - HAL_STORAGE.remove(filePath.c_str()); + if (SdMan.exists(filePath.c_str())) { + SdMan.remove(filePath.c_str()); } // Open file for writing esp_task_wdt_reset(); - if (!HAL_STORAGE.openFileForWrite("WS", filePath, wsUploadFile)) { + if (!SdMan.openFileForWrite("WS", filePath, wsUploadFile)) { wsServer->sendTXT(num, "ERROR:Failed to create file"); wsUploadInProgress = false; return; diff --git a/src/network/HttpDownloader.cpp b/src/network/HttpDownloader.cpp index c88f4264..fe65ea6b 100644 --- a/src/network/HttpDownloader.cpp +++ b/src/network/HttpDownloader.cpp @@ -83,13 +83,13 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& Serial.printf("[%lu] [HTTP] Content-Length: %zu\n", millis(), contentLength); // Remove existing file if present - if (HAL_STORAGE.exists(destPath.c_str())) { - HAL_STORAGE.remove(destPath.c_str()); + if (SdMan.exists(destPath.c_str())) { + SdMan.remove(destPath.c_str()); } // Open file for writing FsFile file; - if (!HAL_STORAGE.openFileForWrite("HTTP", destPath.c_str(), file)) { + if (!SdMan.openFileForWrite("HTTP", destPath.c_str(), file)) { Serial.printf("[%lu] [HTTP] Failed to open file for writing\n", millis()); http.end(); return FILE_ERROR; @@ -100,7 +100,7 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& if (!stream) { Serial.printf("[%lu] [HTTP] Failed to get stream\n", millis()); file.close(); - HAL_STORAGE.remove(destPath.c_str()); + SdMan.remove(destPath.c_str()); http.end(); return HTTP_ERROR; } @@ -128,7 +128,7 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& if (written != bytesRead) { Serial.printf("[%lu] [HTTP] Write failed: wrote %zu of %zu bytes\n", millis(), written, bytesRead); file.close(); - HAL_STORAGE.remove(destPath.c_str()); + SdMan.remove(destPath.c_str()); http.end(); return FILE_ERROR; } @@ -148,7 +148,7 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& // Verify download size if known if (contentLength > 0 && downloaded != contentLength) { Serial.printf("[%lu] [HTTP] Size mismatch: got %zu, expected %zu\n", millis(), downloaded, contentLength); - HAL_STORAGE.remove(destPath.c_str()); + SdMan.remove(destPath.c_str()); return HTTP_ERROR; } From db359b2798886fbe14042f3fdbc9b58372fcd833 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Fri, 23 Jan 2026 02:06:06 +0100 Subject: [PATCH 06/10] add storage emulation --- lib/hal/EmulationUtils.h | 179 +++++++++ lib/hal/HalDisplay.cpp | 65 +--- lib/hal/HalStorage.cpp | 327 +++++++++++++++++ lib/hal/HalStorage.h | 77 +++- scripts/emulation/docker-compose.yml | 3 +- scripts/emulation/server.py | 522 +++++++++++++++++++++++++++ scripts/emulation/web_server.py | 242 ------------- scripts/emulation/web_ui.html | 104 +++++- src/main.cpp | 6 + 9 files changed, 1206 insertions(+), 319 deletions(-) create mode 100644 lib/hal/EmulationUtils.h create mode 100644 scripts/emulation/server.py delete mode 100644 scripts/emulation/web_server.py diff --git a/lib/hal/EmulationUtils.h b/lib/hal/EmulationUtils.h new file mode 100644 index 00000000..c4ad48c0 --- /dev/null +++ b/lib/hal/EmulationUtils.h @@ -0,0 +1,179 @@ +#pragma once + +#include +#include + +namespace EmulationUtils { + +// Packet format (from device to host): +// $$CMD_(COMMAND):[(ARG0)][:(ARG1)][:(ARG2)]$$\n +// Host response with either a base64-encoded payload or int64, terminated by newline: +// (BASE64_ENCODED_PAYLOAD)\n +// 123\n + +static const char* CMD_DISPLAY = "DISPLAY"; // arg0: base64-encoded buffer -- no return value +static const char* CMD_FS_LIST = "FS_LIST"; // arg0: path, arg1: max files -- returns list of filenames, one per line, terminated by empty line +static const char* CMD_FS_READ = "FS_READ"; // arg0: path, arg1: offset, arg2: length (-1 means read all) -- returns base64-encoded file contents +static const char* CMD_FS_STAT = "FS_STAT"; // arg0: path -- return file size int64_t; -1 means not found; -2 means is a directory +static const char* CMD_FS_WRITE = "FS_WRITE"; // arg0: path, arg1: base64-encoded data, arg2: offset, arg3: is inplace (0/1) -- return int64_t bytes written +static const char* CMD_FS_MKDIR = "FS_MKDIR"; // arg0: path -- return int64_t: 0=success +static const char* CMD_FS_RM = "FS_RM"; // arg0: path -- return int64_t: 0=success + +static std::string base64_encode(const char* buf, unsigned int bufLen); +static std::vector base64_decode(const char* encoded_string, unsigned int in_len); + +static void sendCmd(const char* cmd, const char* arg0 = nullptr, const char* arg1 = nullptr, const char* arg2 = nullptr, const char* arg3 = nullptr) { + Serial.printf("[%lu] [EMU] Sending command: %s\n", millis(), cmd); + Serial.print("$$CMD_"); + Serial.print(cmd); + if (arg0 != nullptr) { + Serial.print(":"); + Serial.print(arg0); + } else { + Serial.print(":"); // ensure at least one colon + } + if (arg1 != nullptr) { + Serial.print(":"); + Serial.print(arg1); + } + if (arg2 != nullptr) { + Serial.print(":"); + Serial.print(arg2); + } + if (arg3 != nullptr) { + Serial.print(":"); + Serial.print(arg3); + } + Serial.print("$$\n"); +} + +static String recvRespStr() { + unsigned long startMillis = millis(); + String line; + const unsigned long timeoutMs = 5000; + while (millis() - startMillis < (unsigned long)timeoutMs) { + if (Serial.available()) { + char c = Serial.read(); + if (c == '\n') { + return line; + } else { + line += c; + } + } + } + // should never reach here + Serial.println("FATAL: Timeout waiting for response"); + return String(); +} + +static std::vector recvRespBuf() { + String respStr = recvRespStr(); + return base64_decode(respStr.c_str(), respStr.length()); +} + +static int64_t recvRespInt64() { + String respStr = recvRespStr(); + return respStr.toInt(); +} + +static const std::string base64_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + +static inline bool is_base64(char c) { + return (isalnum(c) || (c == '+') || (c == '/')); +} + +static std::string base64_encode(const char* buf, unsigned int bufLen) { + std::string ret; + ret.reserve(bufLen * 4 / 3 + 4); // reserve enough space + int i = 0; + int j = 0; + char char_array_3[3]; + char char_array_4[4]; + + while (bufLen--) { + char_array_3[i++] = *(buf++); + if (i == 3) { + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for(i = 0; (i < 4) ; i++) { + ret += base64_chars[char_array_4[i]]; + } + i = 0; + } + } + + if (i) { + for(j = i; j < 3; j++) { + char_array_3[j] = '\0'; + } + + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for (j = 0; (j < i + 1); j++) { + ret += base64_chars[char_array_4[j]]; + } + + while((i++ < 3)) { + ret += '='; + } + } + + return ret; +} + +static std::vector base64_decode(const char* encoded_string, unsigned int in_len) { + int i = 0; + int j = 0; + int in_ = 0; + char char_array_4[4], char_array_3[3]; + std::vector ret; + + while (in_len-- && ( encoded_string[in_] != '=') && is_base64(encoded_string[in_])) { + char_array_4[i++] = encoded_string[in_]; in_++; + if (i ==4) { + for (i = 0; i < 4; i++) { + char_array_4[i] = base64_chars.find(char_array_4[i]); + } + + char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); + char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; + + for (i = 0; (i < 3); i++) { + ret.push_back(char_array_3[i]); + } + i = 0; + } + } + + if (i) { + for (j = i; j < 4; j++) { + char_array_4[j] = 0; + } + + for (j = 0; j < 4; j++) { + char_array_4[j] = base64_chars.find(char_array_4[j]); + } + + char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); + char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; + + for (j = 0; (j < i - 1); j++) { + ret.push_back(char_array_3[j]); + } + } + + return ret; +} + +} // namespace EmulationUtils diff --git a/lib/hal/HalDisplay.cpp b/lib/hal/HalDisplay.cpp index 28bcfa64..fa8e6c1e 100644 --- a/lib/hal/HalDisplay.cpp +++ b/lib/hal/HalDisplay.cpp @@ -1,8 +1,7 @@ #include +#include #include -std::string base64_encode(char* buf, unsigned int bufLen); - HalDisplay::HalDisplay(int8_t sclk, int8_t mosi, int8_t cs, int8_t dc, int8_t rst, int8_t busy) : einkDisplay(sclk, mosi, cs, dc, rst, busy) { if (is_emulated) { emuFramebuffer0 = new uint8_t[BUFFER_SIZE]; @@ -71,10 +70,9 @@ void HalDisplay::displayBuffer(RefreshMode mode) { einkDisplay.displayBuffer(mode); } else { Serial.printf("[%lu] [ ] Emulated display buffer with mode %d\n", millis(), static_cast(mode)); - std::string b64 = base64_encode(reinterpret_cast(emuFramebuffer0), BUFFER_SIZE); - Serial.printf("$$DATA:DISPLAY:{\"mode\":%d,\"buffer\":\"", static_cast(mode)); - Serial.print(b64.c_str()); - Serial.print("\"}$$\n"); + std::string b64 = EmulationUtils::base64_encode(reinterpret_cast(emuFramebuffer0), BUFFER_SIZE); + EmulationUtils::sendCmd(EmulationUtils::CMD_DISPLAY, b64.c_str()); + // no response expected } } @@ -155,58 +153,3 @@ void HalDisplay::displayGrayBuffer() { // TODO: not sure what this does } } - -// -// Base64 utilities -// - -static const std::string base64_chars = - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789+/"; - -static inline bool is_base64(char c) { - return (isalnum(c) || (c == '+') || (c == '/')); -} - -std::string base64_encode(char* buf, unsigned int bufLen) { - std::string ret; - ret.reserve(bufLen * 4 / 3 + 4); // reserve enough space - int i = 0; - int j = 0; - char char_array_3[3]; - char char_array_4[4]; - - while (bufLen--) { - char_array_3[i++] = *(buf++); - if (i == 3) { - char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; - char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); - char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); - char_array_4[3] = char_array_3[2] & 0x3f; - - for(i = 0; (i < 4) ; i++) - ret += base64_chars[char_array_4[i]]; - i = 0; - } - } - - if (i) { - for(j = i; j < 3; j++) { - char_array_3[j] = '\0'; - } - - char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; - char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); - char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); - char_array_4[3] = char_array_3[2] & 0x3f; - - for (j = 0; (j < i + 1); j++) - ret += base64_chars[char_array_4[j]]; - - while((i++ < 3)) - ret += '='; - } - - return ret; -} diff --git a/lib/hal/HalStorage.cpp b/lib/hal/HalStorage.cpp index e69de29b..849c05bd 100644 --- a/lib/hal/HalStorage.cpp +++ b/lib/hal/HalStorage.cpp @@ -0,0 +1,327 @@ +#include "HalStorage.h" +#include "EmulationUtils.h" +#include + +#if CROSSPOINT_EMULATED == 0 +#include +#endif + +HalStorage HalStorage::instance; + +HalStorage::HalStorage() {} + +bool HalStorage::begin() { +#if CROSSPOINT_EMULATED == 0 + return SdMan.begin(); +#else + // no-op + return true; +#endif +} + +bool HalStorage::ready() const { +#if CROSSPOINT_EMULATED == 0 + return SdMan.ready(); +#else + // no-op + return true; +#endif +} + +std::vector HalStorage::listFiles(const char* path, int maxFiles) { +#if CROSSPOINT_EMULATED == 0 + return SdMan.listFiles(path, maxFiles); +#else + Serial.printf("[%lu] [FS ] Emulated listFiles: %s\n", millis(), path); + EmulationUtils::sendCmd(EmulationUtils::CMD_FS_LIST, path); + std::vector output; + for (int i = 0; i < maxFiles; ++i) { + auto resp = EmulationUtils::recvRespStr(); + if (resp.length() == 0) { + break; + } + output.push_back(resp); + } + return output; +#endif +} + +String HalStorage::readFile(const char* path) { +#if CROSSPOINT_EMULATED == 0 + return SdMan.readFile(path); +#else + Serial.printf("[%lu] [FS ] Emulated readFile: %s\n", millis(), path); + EmulationUtils::sendCmd(EmulationUtils::CMD_FS_READ, path, "0", "-1"); + return EmulationUtils::recvRespStr(); +#endif +} + +static int64_t getFileSizeEmulated(const char* path) { + EmulationUtils::sendCmd(EmulationUtils::CMD_FS_STAT, path); + return EmulationUtils::recvRespInt64(); +} + +bool HalStorage::readFileToStream(const char* path, Print& out, size_t chunkSize) { +#if CROSSPOINT_EMULATED == 0 + return SdMan.readFileToStream(path, out, chunkSize); +#else + Serial.printf("[%lu] [FS ] Emulated readFileToStream: %s\n", millis(), path); + auto size = getFileSizeEmulated(path); + if (size == -1) { + Serial.printf("[%lu] [FS ] File not found: %s\n", millis(), path); + return false; + } + if (size == -2) { + Serial.printf("[%lu] [FS ] Path is a directory, not a file: %s\n", millis(), path); + return false; + } + size_t bytesRead = 0; + while (bytesRead < static_cast(size)) { + size_t toRead = std::min(chunkSize, static_cast(size) - bytesRead); + EmulationUtils::sendCmd(EmulationUtils::CMD_FS_READ, path, String(bytesRead).c_str(), String(toRead).c_str()); + auto buf = EmulationUtils::recvRespBuf(); + out.write(buf.data(), buf.size()); + bytesRead += buf.size(); + } + return true; +#endif +} + +size_t HalStorage::readFileToBuffer(const char* path, char* buffer, size_t bufferSize, size_t maxBytes) { +#if CROSSPOINT_EMULATED == 0 + return SdMan.readFileToBuffer(path, buffer, bufferSize, maxBytes); +#else + Serial.printf("[%lu] [FS ] Emulated readFileToBuffer: %s\n", millis(), path); + auto size = getFileSizeEmulated(path); + if (size == -1) { + Serial.printf("[%lu] [FS ] File not found: %s\n", millis(), path); + return 0; + } + if (size == -2) { + Serial.printf("[%lu] [FS ] Path is a directory, not a file: %s\n", millis(), path); + return 0; + } + size_t toRead = static_cast(size); + if (maxBytes > 0 && maxBytes < toRead) { + toRead = maxBytes; + } + if (toRead >= bufferSize) { + toRead = bufferSize - 1; // leave space for null terminator + } + EmulationUtils::sendCmd(EmulationUtils::CMD_FS_READ, path, "0", String(toRead).c_str()); + auto buf = EmulationUtils::recvRespBuf(); + size_t bytesRead = buf.size(); + memcpy(buffer, buf.data(), bytesRead); + buffer[bytesRead] = '\0'; // null-terminate + return bytesRead; +#endif +} + +bool HalStorage::writeFile(const char* path, const String& content) { +#if CROSSPOINT_EMULATED == 0 + return SdMan.writeFile(path, content); +#else + Serial.printf("[%lu] [FS ] Emulated writeFile: %s\n", millis(), path); + std::string b64 = EmulationUtils::base64_encode((char*)content.c_str(), content.length()); + EmulationUtils::sendCmd(EmulationUtils::CMD_FS_WRITE, path, b64.c_str(), "0", "0"); + EmulationUtils::recvRespInt64(); // unused for now + return true; +#endif +} + +bool HalStorage::ensureDirectoryExists(const char* path) { +#if CROSSPOINT_EMULATED == 0 + return SdMan.ensureDirectoryExists(path); +#else + Serial.printf("[%lu] [FS ] Emulated ensureDirectoryExists: %s\n", millis(), path); + EmulationUtils::sendCmd(EmulationUtils::CMD_FS_MKDIR, path); + EmulationUtils::recvRespInt64(); // unused for now + return true; +#endif +} + +FsFile HalStorage::open(const char* path, const oflag_t oflag) { +#if CROSSPOINT_EMULATED == 0 + return SdMan.open(path, oflag); +#else + // TODO: do we need to check existence or create the file? + return FsFile(path, oflag); +#endif +} + +bool HalStorage::mkdir(const char* path, const bool pFlag) { +#if CROSSPOINT_EMULATED == 0 + return SdMan.mkdir(path, pFlag); +#else + EmulationUtils::sendCmd(EmulationUtils::CMD_FS_MKDIR, path); + EmulationUtils::recvRespInt64(); // unused for now + return true; +#endif +} + +bool HalStorage::exists(const char* path) { +#if CROSSPOINT_EMULATED == 0 + return SdMan.exists(path); +#else + Serial.printf("[%lu] [FS ] Emulated exists: %s\n", millis(), path); + auto size = getFileSizeEmulated(path); + return size != -1; +#endif +} + +bool HalStorage::remove(const char* path) { +#if CROSSPOINT_EMULATED == 0 + return SdMan.remove(path); +#else + Serial.printf("[%lu] [FS ] Emulated remove: %s\n", millis(), path); + EmulationUtils::sendCmd(EmulationUtils::CMD_FS_RM, path); + EmulationUtils::recvRespInt64(); // unused for now + return true; +#endif +} + +bool HalStorage::rmdir(const char* path) { +#if CROSSPOINT_EMULATED == 0 + return SdMan.rmdir(path); +#else + Serial.printf("[%lu] [FS ] Emulated rmdir: %s\n", millis(), path); + EmulationUtils::sendCmd(EmulationUtils::CMD_FS_RM, path); + EmulationUtils::recvRespInt64(); // unused for now + return true; +#endif +} + +bool HalStorage::openFileForRead(const char* moduleName, const char* path, FsFile& file) { +#if CROSSPOINT_EMULATED == 0 + return SdMan.openFileForRead(moduleName, path, file); +#else + Serial.printf("[%lu] [FS ] Emulated openFileForRead: %s\n", millis(), path); + auto size = getFileSizeEmulated(path); + if (size == -1) { + Serial.printf("[%lu] [FS ] File not found: %s\n", millis(), path); + return false; + } + if (size == -2) { + Serial.printf("[%lu] [FS ] Path is a directory, not a file: %s\n", millis(), path); + return false; + } + return true; +#endif +} + +bool HalStorage::openFileForRead(const char* moduleName, const std::string& path, FsFile& file) { + return openFileForRead(moduleName, path.c_str(), file); +} + +bool HalStorage::openFileForRead(const char* moduleName, const String& path, FsFile& file) { + return openFileForRead(moduleName, path.c_str(), file); +} + +bool HalStorage::openFileForWrite(const char* moduleName, const char* path, FsFile& file) { +#if CROSSPOINT_EMULATED == 0 + return SdMan.openFileForWrite(moduleName, path, file); +#else + Serial.printf("[%lu] [FS ] Emulated openFileForWrite: %s\n", millis(), path); + auto size = getFileSizeEmulated(path); + if (size == -1) { + Serial.printf("[%lu] [FS ] File not found: %s\n", millis(), path); + return false; + } + if (size == -2) { + Serial.printf("[%lu] [FS ] Path is a directory, not a file: %s\n", millis(), path); + return false; + } + return true; +#endif +} + +bool HalStorage::openFileForWrite(const char* moduleName, const std::string& path, FsFile& file) { + return openFileForWrite(moduleName, path.c_str(), file); +} + +bool HalStorage::openFileForWrite(const char* moduleName, const String& path, FsFile& file) { + return openFileForWrite(moduleName, path.c_str(), file); +} + +bool HalStorage::removeDir(const char* path) { +#if CROSSPOINT_EMULATED == 0 + return SdMan.removeDir(path); +#else + // to be implemented + return false; +#endif +} + + + +#if CROSSPOINT_EMULATED == 1 +// +// FsFile emulation methods +// + +FsFile::FsFile(const char* path, oflag_t oflag) : path(path), oflag(oflag) { + Serial.printf("[%lu] [FS ] Emulated FsFile open: %s\n", millis(), path); + auto size = getFileSizeEmulated(path); + if (size == -1) { + Serial.printf("[%lu] [FS ] File not found: %s\n", millis(), path); + open = false; + fileSizeBytes = 0; + } else if (isDir) { + Serial.printf("[%lu] [FS ] Path is a directory: %s\n", millis(), path); + open = true; + // get directory entries + EmulationUtils::sendCmd(EmulationUtils::CMD_FS_LIST, path); + dirEntries.clear(); + while (true) { + auto resp = EmulationUtils::recvRespStr(); + if (resp.length() == 0) { + break; + } + dirEntries.push_back(resp); + } + Serial.printf("[%lu] [FS ] Directory has %u entries\n", millis(), (unsigned)dirEntries.size()); + dirIndex = 0; + fileSizeBytes = 0; + } else { + open = true; + fileSizeBytes = static_cast(size); + } +} + +int FsFile::read(void* buf, size_t count) { + if (!open || isDir) return -1; + size_t bytesAvailable = (fileSizeBytes > filePos) ? (fileSizeBytes - filePos) : 0; + if (bytesAvailable == 0) return 0; + size_t toRead = std::min(count, bytesAvailable); + EmulationUtils::sendCmd(EmulationUtils::CMD_FS_READ, path.c_str(), String(filePos).c_str(), String(toRead).c_str()); + auto data = EmulationUtils::recvRespBuf(); + size_t bytesRead = data.size(); + memcpy(buf, data.data(), bytesRead); + filePos += bytesRead; + return static_cast(bytesRead); +} + +int FsFile::read() { + uint8_t b; + int result = read(&b, 1); + if (result <= 0) return -1; + return b; +} + +size_t FsFile::write(const uint8_t* buffer, size_t size) { + if (!open || isDir) return 0; + std::string b64 = EmulationUtils::base64_encode((const char*)buffer, size); + EmulationUtils::sendCmd(EmulationUtils::CMD_FS_WRITE, path.c_str(), b64.c_str(), String(filePos).c_str(), "1"); + EmulationUtils::recvRespInt64(); // unused for now + filePos += size; + if (filePos > fileSizeBytes) { + fileSizeBytes = filePos; + } + return size; +} + +size_t FsFile::write(uint8_t b) { + return write(&b, 1); +} + +#endif diff --git a/lib/hal/HalStorage.h b/lib/hal/HalStorage.h index 1ff1b763..f5cd82c3 100644 --- a/lib/hal/HalStorage.h +++ b/lib/hal/HalStorage.h @@ -1,8 +1,76 @@ #pragma once -#include +#include #include +#if CROSSPOINT_EMULATED == 0 +#include +#else +typedef int oflag_t; +#define O_RDONLY 0 /* +1 == FREAD */ +#define O_WRONLY 1 /* +1 == FWRITE */ +#define O_RDWR 2 /* +1 == FREAD|FWRITE */ + +class FsFile : public Print { + String path; + String name; + oflag_t oflag; // unused for now + bool open = false; + // directory state + bool isDir = false; + std::vector dirEntries; + size_t dirIndex = 0; + // file state + size_t filePos = 0; + size_t fileSizeBytes = 0; + public: + FsFile() = default; + FsFile(const char* path, oflag_t oflag); + ~FsFile() = default; + + void flush() { /* no-op */ } + size_t getName(char* name, size_t len) { + String n = path; + if (n.length() >= len) { + n = n.substring(0, len - 1); + } + n.toCharArray(name, len); + return n.length(); + } + size_t size() { return fileSizeBytes; } + size_t fileSize() { return size(); } + size_t seek(size_t pos) { + filePos = pos; + return filePos; + } + size_t seekCur(int64_t offset) { return seek(filePos + offset); } + size_t seekSet(size_t offset) { return seek(offset); } + int available() const { return (fileSizeBytes > filePos) ? (fileSizeBytes - filePos) : 0; } + size_t position() const { return filePos; } + int read(void* buf, size_t count); + int read(); // read a single byte + size_t write(const uint8_t* buffer, size_t size); + size_t write(uint8_t b) override; + bool isDirectory() const { return isDir; } + int rewindDirectory() { + if (!isDir) return -1; + dirIndex = 0; + return 0; + } + bool close() { open = false; return true; } + FsFile openNextFile() { + if (!isDir || dirIndex >= dirEntries.size()) { + return FsFile(); + } + FsFile f(dirEntries[dirIndex].c_str(), O_RDONLY); + dirIndex++; + return f; + } + bool isOpen() const { return open; } + operator bool() const { return isOpen(); } +}; +#endif + class HalStorage { public: HalStorage(); @@ -42,8 +110,9 @@ class HalStorage { private: static HalStorage instance; - bool initialized = false; - SdFat sd; + bool is_emulated = CROSSPOINT_EMULATED; }; -#define HAL_STORAGE HalStorage::getInstance() \ No newline at end of file +#if CROSSPOINT_EMULATED == 1 +#define SdMan HalStorage::getInstance() +#endif diff --git a/scripts/emulation/docker-compose.yml b/scripts/emulation/docker-compose.yml index 22d85dad..129b5e1c 100644 --- a/scripts/emulation/docker-compose.yml +++ b/scripts/emulation/docker-compose.yml @@ -4,6 +4,7 @@ services: container_name: crosspoint-emulator volumes: - ../..:/app:ro + - ../../.sdcard:/sdcard:rw tmpfs: - /tmp ports: @@ -31,4 +32,4 @@ services: python3 -m pip install websockets --break-system-packages cp /app/scripts/emulation/web_ui.html . - python3 /app/scripts/emulation/web_server.py + python3 /app/scripts/emulation/server.py diff --git a/scripts/emulation/server.py b/scripts/emulation/server.py new file mode 100644 index 00000000..737d6845 --- /dev/null +++ b/scripts/emulation/server.py @@ -0,0 +1,522 @@ +import asyncio +import base64 +import json +import os +import re +import shutil +import signal +import sys +from http import HTTPStatus +from pathlib import Path +from typing import Set + +import websockets +from websockets.http11 import Response +from websockets.server import WebSocketServerProtocol + + +""" +This script spawns a QEMU process, then forward its stdout and stderr to WebSocket clients. +""" + + + +# SD card emulation directory +sdcard_path = os.environ.get("SDCARD_PATH", "/sdcard") +if not os.path.exists(sdcard_path): + os.makedirs(sdcard_path) + + +class SdCardHandler: + """Handle SD card filesystem commands from the emulated device.""" + + def __init__(self, base_path: str): + self.base_path = base_path + + def _resolve_path(self, path: str) -> str: + """Resolve a device path to a host filesystem path.""" + # Remove leading slash and join with base path + if path.startswith("/"): + path = path[1:] + return os.path.join(self.base_path, path) + + def handle_fs_list(self, path: str, max_files: str = "200") -> list[str]: + """List files in a directory. Returns list of filenames.""" + resolved = self._resolve_path(path) + max_count = int(max_files) if max_files else 200 + + if not os.path.exists(resolved): + return [] + + if not os.path.isdir(resolved): + return [] + + try: + entries = [] + for entry in os.listdir(resolved): + if len(entries) >= max_count: + break + full_path = os.path.join(resolved, entry) + # Append "/" for directories to match device behavior + if os.path.isdir(full_path): + entries.append(entry + "/") + else: + entries.append(entry) + return entries + except Exception as e: + print(f"[SdCard] Error listing {path}: {e}", file=sys.stderr) + return [] + + def handle_fs_read(self, path: str, offset: str = "0", length: str = "-1") -> str: + """Read file contents. Returns base64-encoded data.""" + resolved = self._resolve_path(path) + offset_int = int(offset) if offset else 0 + length_int = int(length) if length else -1 + + if not os.path.exists(resolved) or os.path.isdir(resolved): + return "" + + try: + with open(resolved, "rb") as f: + if offset_int > 0: + f.seek(offset_int) + if length_int == -1: + data = f.read() + else: + data = f.read(length_int) + return base64.b64encode(data).decode("ascii") + except Exception as e: + print(f"[SdCard] Error reading {path}: {e}", file=sys.stderr) + return "" + + def handle_fs_stat(self, path: str) -> int: + """Get file size. Returns -1 if not found, -2 if directory.""" + resolved = self._resolve_path(path) + + if not os.path.exists(resolved): + return -1 + + if os.path.isdir(resolved): + return -2 + + try: + return os.path.getsize(resolved) + except Exception as e: + print(f"[SdCard] Error stat {path}: {e}", file=sys.stderr) + return -1 + + def handle_fs_write(self, path: str, b64_data: str, offset: str = "0", inplace: str = "0") -> int: + """Write data to file. Returns bytes written.""" + resolved = self._resolve_path(path) + offset_int = int(offset) if offset else 0 + is_inplace = inplace == "1" + + try: + data = base64.b64decode(b64_data) + except Exception as e: + print(f"[SdCard] Error decoding base64 for {path}: {e}", file=sys.stderr) + return 0 + + try: + # Ensure parent directory exists + parent = os.path.dirname(resolved) + if parent and not os.path.exists(parent): + os.makedirs(parent) + + if is_inplace and os.path.exists(resolved): + # In-place write at offset + with open(resolved, "r+b") as f: + f.seek(offset_int) + f.write(data) + else: + # Overwrite or create new file + mode = "r+b" if os.path.exists(resolved) and offset_int > 0 else "wb" + with open(resolved, mode) as f: + if offset_int > 0: + f.seek(offset_int) + f.write(data) + + return len(data) + except Exception as e: + print(f"[SdCard] Error writing {path}: {e}", file=sys.stderr) + return 0 + + def handle_fs_mkdir(self, path: str) -> int: + """Create directory. Returns 0 on success.""" + resolved = self._resolve_path(path) + + try: + os.makedirs(resolved, exist_ok=True) + return 0 + except Exception as e: + print(f"[SdCard] Error mkdir {path}: {e}", file=sys.stderr) + return -1 + + def handle_fs_rm(self, path: str) -> int: + """Remove file or directory. Returns 0 on success.""" + resolved = self._resolve_path(path) + + if not os.path.exists(resolved): + return -1 + + try: + if os.path.isdir(resolved): + shutil.rmtree(resolved) + else: + os.remove(resolved) + return 0 + except Exception as e: + print(f"[SdCard] Error removing {path}: {e}", file=sys.stderr) + return -1 + + +# Global SD card handler instance +sdcard_handler = SdCardHandler(sdcard_path) + + +# WebSocket clients +connected_clients: Set[WebSocketServerProtocol] = set() + +# QEMU process +qemu_process: asyncio.subprocess.Process | None = None + +# Command pattern: $$CMD_(COMMAND):(ARG0)[:(ARG1)][:(ARG2)][:(ARG3)]$$ +CMD_PATTERN = re.compile(r'^\$\$CMD_([A-Z_]+):(.+)\$\$$') + + +def parse_command(message: str) -> tuple[str, list[str]] | None: + """Parse a command message. Returns (command, args) or None if not a command.""" + match = CMD_PATTERN.match(message) + if not match: + return None + command = match.group(1) + args_str = match.group(2) + # Split args by ':' but the last arg might contain ':' + args = args_str.split(':') + return (command, args) + + +async def send_response(response: str): + """Send a response back to the QEMU process via stdin.""" + global qemu_process + if qemu_process and qemu_process.stdin: + qemu_process.stdin.write((response + "\n").encode()) + await qemu_process.stdin.drain() + + +LAST_DISPLAY_BUFFER = None + +async def handle_command(command: str, args: list[str]) -> bool: + """Handle a command from the device. Returns True if handled.""" + global sdcard_handler, LAST_DISPLAY_BUFFER + + try: + if command == "DISPLAY": + # Display command - no response needed + # arg0: base64-encoded buffer + print(f"[Emulator] DISPLAY command received (buffer size: {len(args[0]) if args else 0})", flush=True) + LAST_DISPLAY_BUFFER = args[0] if args else None + return True + + elif command == "FS_LIST": + # arg0: path, arg1: max files (optional) + path = args[0] if len(args) > 0 else "/" + max_files = args[1] if len(args) > 1 else "200" + entries = sdcard_handler.handle_fs_list(path, max_files) + print(f"[Emulator] FS_LIST {path} -> {len(entries)} entries", flush=True) + # Send each entry as a line, then empty line to terminate + for entry in entries: + await send_response(entry) + await send_response("") # Empty line terminates the list + return True + + elif command == "FS_READ": + # arg0: path, arg1: offset, arg2: length + path = args[0] if len(args) > 0 else "" + offset = args[1] if len(args) > 1 else "0" + length = args[2] if len(args) > 2 else "-1" + result = sdcard_handler.handle_fs_read(path, offset, length) + print(f"[Emulator] FS_READ {path} offset={offset} len={length} -> {len(result)} bytes (b64)", flush=True) + await send_response(result) + return True + + elif command == "FS_STAT": + # arg0: path + path = args[0] if len(args) > 0 else "" + result = sdcard_handler.handle_fs_stat(path) + print(f"[Emulator] FS_STAT {path} -> {result}", flush=True) + await send_response(str(result)) + return True + + elif command == "FS_WRITE": + # arg0: path, arg1: base64-encoded data, arg2: offset, arg3: is inplace + path = args[0] if len(args) > 0 else "" + b64_data = args[1] if len(args) > 1 else "" + offset = args[2] if len(args) > 2 else "0" + inplace = args[3] if len(args) > 3 else "0" + result = sdcard_handler.handle_fs_write(path, b64_data, offset, inplace) + print(f"[Emulator] FS_WRITE {path} offset={offset} inplace={inplace} -> {result} bytes", flush=True) + await send_response(str(result)) + return True + + elif command == "FS_MKDIR": + # arg0: path + path = args[0] if len(args) > 0 else "" + result = sdcard_handler.handle_fs_mkdir(path) + print(f"[Emulator] FS_MKDIR {path} -> {result}", flush=True) + await send_response(str(result)) + return True + + elif command == "FS_RM": + # arg0: path + path = args[0] if len(args) > 0 else "" + result = sdcard_handler.handle_fs_rm(path) + print(f"[Emulator] FS_RM {path} -> {result}", flush=True) + await send_response(str(result)) + return True + + else: + print(f"[Emulator] Unknown command: {command}", flush=True) + return False + + except Exception as e: + print(f"[Emulator] Error handling command {command}: {e}", file=sys.stderr) + return False + + +def process_message(message: str, for_ui: bool) -> str: + if message.startswith("$$CMD_"): + if for_ui: + return message + else: + # $$CMD_(COMMAND)[:(ARG0)][:(ARG1)][:(ARG2)]$$ + command = message[2:].split(':')[0] + return "[Emulator] Received command: " + command + return message + + +async def broadcast_message(msg_type: str, data: str): + """Broadcast a message to all connected WebSocket clients.""" + if not connected_clients: + return + + message = json.dumps({"type": msg_type, "data": data}) + + # Send to all clients, remove disconnected ones + disconnected = set() + for client in connected_clients: + try: + await client.send(message) + except websockets.exceptions.ConnectionClosed: + disconnected.add(client) + + connected_clients.difference_update(disconnected) + + +async def read_stream(stream: asyncio.StreamReader, stream_type: str): + """Read from a stream line by line and broadcast to clients.""" + buffer = b"" + + while True: + try: + chunk = await stream.read(1024) + if not chunk: + break + + buffer += chunk + + # Process complete lines + while b"\n" in buffer: + line, buffer = buffer.split(b"\n", 1) + try: + decoded_line = line.decode("utf-8", errors="replace") + except Exception: + decoded_line = line.decode("latin-1", errors="replace") + + # Check if this is a command that needs handling + parsed = parse_command(decoded_line) + if parsed: + command, args = parsed + await handle_command(command, args) + # Still broadcast to UI for visibility + await broadcast_message(stream_type, decoded_line) + continue + + # Forward to parent process + if stream_type == "stdout": + print(process_message(decoded_line, for_ui=False), flush=True) + else: + print(process_message(decoded_line, for_ui=False), file=sys.stderr, flush=True) + + # Broadcast to WebSocket clients + await broadcast_message(stream_type, process_message(decoded_line, for_ui=True)) + + except Exception as e: + print(f"Error reading {stream_type}: {e}", file=sys.stderr) + break + + # Process remaining buffer + if buffer: + try: + decoded_line = buffer.decode("utf-8", errors="replace") + except Exception: + decoded_line = buffer.decode("latin-1", errors="replace") + + if stream_type == "stdout": + print(decoded_line, flush=True) + else: + print(decoded_line, file=sys.stderr, flush=True) + + await broadcast_message(stream_type, decoded_line) + + +async def spawn_qemu(): + """Spawn the QEMU process and capture its output.""" + global qemu_process + + # Build the command + cmd = [ + "qemu-system-riscv32", + "-nographic", + "-M", "esp32c3", + "-drive", "file=flash.bin,if=mtd,format=raw", + "-global", "driver=timer.esp32c3.timg,property=wdt_disable,value=true", # got panic if we don't disable WDT, why? + ] + + # Get working directory from environment or use /tmp + work_dir = os.getcwd() + + print(f"Starting QEMU in {work_dir}...", flush=True) + print(f"Command: {' '.join(cmd)}", flush=True) + + try: + qemu_process = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + stdin=asyncio.subprocess.PIPE, + cwd=work_dir, + env=os.environ.copy() + ) + + # Read stdout and stderr concurrently + await asyncio.gather( + read_stream(qemu_process.stdout, "stdout"), + read_stream(qemu_process.stderr, "stderr") + ) + + # Wait for process to complete + await qemu_process.wait() + print(f"QEMU process exited with code {qemu_process.returncode}", flush=True) + + except FileNotFoundError: + print("Error: qemu-system-riscv32 not found. Make sure it's in PATH.", file=sys.stderr) + await broadcast_message("stderr", "Error: qemu-system-riscv32 not found") + except Exception as e: + print(f"Error spawning QEMU: {e}", file=sys.stderr) + await broadcast_message("stderr", f"Error spawning QEMU: {e}") + + +async def websocket_handler(websocket: WebSocketServerProtocol): + """Handle a WebSocket connection.""" + connected_clients.add(websocket) + print(f"Client connected. Total clients: {len(connected_clients)}", flush=True) + + try: + # Send a welcome message + await websocket.send(json.dumps({ + "type": "info", + "data": "Connected to CrossPoint emulator" + })) + + # Send the last display buffer if available + if LAST_DISPLAY_BUFFER is not None: + await websocket.send(json.dumps({ + "type": "stdout", + "data": f"$$CMD_DISPLAY:{LAST_DISPLAY_BUFFER}$$" + })) + + # Handle incoming messages (for stdin forwarding) + async for message in websocket: + try: + data = json.loads(message) + if data.get("type") == "stdin" and qemu_process and qemu_process.stdin: + input_data = data.get("data", "") + qemu_process.stdin.write((input_data + "\n").encode()) + await qemu_process.stdin.drain() + except json.JSONDecodeError: + pass + except Exception as e: + print(f"Error handling client message: {e}", file=sys.stderr) + + except websockets.exceptions.ConnectionClosed: + pass + finally: + connected_clients.discard(websocket) + print(f"Client disconnected. Total clients: {len(connected_clients)}", flush=True) + + +async def main(): + """Main entry point.""" + host = os.environ.get("HOST", "0.0.0.0") + port = int(os.environ.get("PORT", "8090")) + + print(f"Starting WebSocket server on {host}:{port}", flush=True) + + # Start WebSocket server + async with websockets.serve( + websocket_handler, host, port, + process_request=process_request, + origins=None, + ): + print(f"WebSocket server running on ws://{host}:{port}/ws", flush=True) + print(f"Web UI available at http://{host}:{port}/", flush=True) + + # Spawn QEMU process + await spawn_qemu() + + +def signal_handler(signum, frame): + """Handle shutdown signals.""" + print("\nShutting down...", flush=True) + if qemu_process: + qemu_process.terminate() + sys.exit(0) + + +def process_request(connection, request): + """Handle HTTP requests for serving static files.""" + path = request.path + + if path == "/" or path == "/web_ui.html": + # Serve the web_ui.html file + html_path = Path(__file__).parent / "web_ui.html" + try: + content = html_path.read_bytes() + return Response( + HTTPStatus.OK, + "OK", + websockets.Headers([ + ("Content-Type", "text/html; charset=utf-8"), + ("Content-Length", str(len(content))), + ]), + content + ) + except FileNotFoundError: + return Response(HTTPStatus.NOT_FOUND, "Not Found", websockets.Headers(), b"web_ui.html not found") + + if path == "/ws": + # Return None to continue with WebSocket handshake + return None + + # Return 404 for other paths + return Response(HTTPStatus.NOT_FOUND, "Not Found", websockets.Headers(), b"Not found") + + +if __name__ == "__main__": + # Set up signal handlers + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + # Run the main loop + asyncio.run(main()) diff --git a/scripts/emulation/web_server.py b/scripts/emulation/web_server.py deleted file mode 100644 index dea2f9b5..00000000 --- a/scripts/emulation/web_server.py +++ /dev/null @@ -1,242 +0,0 @@ -import asyncio -import json -import os -import signal -import sys -from http import HTTPStatus -from pathlib import Path -from typing import Set - -import websockets -from websockets.http11 import Response -from websockets.server import WebSocketServerProtocol - - -""" -This script spawns a QEMU process, then forward its stdout and stderr to WebSocket clients. -""" - - -# WebSocket clients -connected_clients: Set[WebSocketServerProtocol] = set() - -# QEMU process -qemu_process: asyncio.subprocess.Process | None = None - -def process_message(message: str, for_ui: bool) -> str: - if message.startswith("$$DATA:DISPLAY:"): - if for_ui: - return message - else: - return f"[DISPLAY DATA]" - return message - - -async def broadcast_message(msg_type: str, data: str): - """Broadcast a message to all connected WebSocket clients.""" - if not connected_clients: - return - - message = json.dumps({"type": msg_type, "data": data}) - - # Send to all clients, remove disconnected ones - disconnected = set() - for client in connected_clients: - try: - await client.send(message) - except websockets.exceptions.ConnectionClosed: - disconnected.add(client) - - connected_clients.difference_update(disconnected) - - -async def read_stream(stream: asyncio.StreamReader, stream_type: str): - """Read from a stream line by line and broadcast to clients.""" - buffer = b"" - - while True: - try: - chunk = await stream.read(1024) - if not chunk: - break - - buffer += chunk - - # Process complete lines - while b"\n" in buffer: - line, buffer = buffer.split(b"\n", 1) - try: - decoded_line = line.decode("utf-8", errors="replace") - except Exception: - decoded_line = line.decode("latin-1", errors="replace") - - # Forward to parent process - if stream_type == "stdout": - print(process_message(decoded_line, for_ui=False), flush=True) - else: - print(process_message(decoded_line, for_ui=False), file=sys.stderr, flush=True) - - # Broadcast to WebSocket clients - await broadcast_message(stream_type, process_message(decoded_line, for_ui=True)) - - except Exception as e: - print(f"Error reading {stream_type}: {e}", file=sys.stderr) - break - - # Process remaining buffer - if buffer: - try: - decoded_line = buffer.decode("utf-8", errors="replace") - except Exception: - decoded_line = buffer.decode("latin-1", errors="replace") - - if stream_type == "stdout": - print(decoded_line, flush=True) - else: - print(decoded_line, file=sys.stderr, flush=True) - - await broadcast_message(stream_type, decoded_line) - - -async def spawn_qemu(): - """Spawn the QEMU process and capture its output.""" - global qemu_process - - # Build the command - cmd = [ - "qemu-system-riscv32", - "-nographic", - "-M", "esp32c3", - "-drive", "file=flash.bin,if=mtd,format=raw" - ] - - # Get working directory from environment or use /tmp - work_dir = os.getcwd() - - print(f"Starting QEMU in {work_dir}...", flush=True) - print(f"Command: {' '.join(cmd)}", flush=True) - - try: - qemu_process = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - stdin=asyncio.subprocess.PIPE, - cwd=work_dir, - env=os.environ.copy() - ) - - # Read stdout and stderr concurrently - await asyncio.gather( - read_stream(qemu_process.stdout, "stdout"), - read_stream(qemu_process.stderr, "stderr") - ) - - # Wait for process to complete - await qemu_process.wait() - print(f"QEMU process exited with code {qemu_process.returncode}", flush=True) - - except FileNotFoundError: - print("Error: qemu-system-riscv32 not found. Make sure it's in PATH.", file=sys.stderr) - await broadcast_message("stderr", "Error: qemu-system-riscv32 not found") - except Exception as e: - print(f"Error spawning QEMU: {e}", file=sys.stderr) - await broadcast_message("stderr", f"Error spawning QEMU: {e}") - - -async def websocket_handler(websocket: WebSocketServerProtocol): - """Handle a WebSocket connection.""" - connected_clients.add(websocket) - print(f"Client connected. Total clients: {len(connected_clients)}", flush=True) - - try: - # Send a welcome message - await websocket.send(json.dumps({ - "type": "info", - "data": "Connected to CrossPoint emulator" - })) - - # Handle incoming messages (for stdin forwarding) - async for message in websocket: - try: - data = json.loads(message) - if data.get("type") == "stdin" and qemu_process and qemu_process.stdin: - input_data = data.get("data", "") - qemu_process.stdin.write((input_data + "\n").encode()) - await qemu_process.stdin.drain() - except json.JSONDecodeError: - pass - except Exception as e: - print(f"Error handling client message: {e}", file=sys.stderr) - - except websockets.exceptions.ConnectionClosed: - pass - finally: - connected_clients.discard(websocket) - print(f"Client disconnected. Total clients: {len(connected_clients)}", flush=True) - - -async def main(): - """Main entry point.""" - host = os.environ.get("HOST", "0.0.0.0") - port = int(os.environ.get("PORT", "8090")) - - print(f"Starting WebSocket server on {host}:{port}", flush=True) - - # Start WebSocket server - async with websockets.serve( - websocket_handler, host, port, - process_request=process_request, - origins=None, - ): - print(f"WebSocket server running on ws://{host}:{port}/ws", flush=True) - print(f"Web UI available at http://{host}:{port}/", flush=True) - - # Spawn QEMU process - await spawn_qemu() - - -def signal_handler(signum, frame): - """Handle shutdown signals.""" - print("\nShutting down...", flush=True) - if qemu_process: - qemu_process.terminate() - sys.exit(0) - - -def process_request(connection, request): - """Handle HTTP requests for serving static files.""" - path = request.path - - if path == "/" or path == "/web_ui.html": - # Serve the web_ui.html file - html_path = Path(__file__).parent / "web_ui.html" - try: - content = html_path.read_bytes() - return Response( - HTTPStatus.OK, - "OK", - websockets.Headers([ - ("Content-Type", "text/html; charset=utf-8"), - ("Content-Length", str(len(content))), - ]), - content - ) - except FileNotFoundError: - return Response(HTTPStatus.NOT_FOUND, "Not Found", websockets.Headers(), b"web_ui.html not found") - - if path == "/ws": - # Return None to continue with WebSocket handshake - return None - - # Return 404 for other paths - return Response(HTTPStatus.NOT_FOUND, "Not Found", websockets.Headers(), b"Not found") - - -if __name__ == "__main__": - # Set up signal handlers - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - # Run the main loop - asyncio.run(main()) diff --git a/scripts/emulation/web_ui.html b/scripts/emulation/web_ui.html index 7e69532f..2d1741a0 100644 --- a/scripts/emulation/web_ui.html +++ b/scripts/emulation/web_ui.html @@ -8,13 +8,85 @@ body { font-family: monospace; margin: 0; + height: 100vh; + background-color: #bbb; + display: flex; + justify-content: center; + } + + button { + font-size: 1.2em; + padding: 0.5em 1em; + margin: 0.2em; + } + + .container { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + max-width: 1200px; + } + + .display, .control { + width: 50%; + height: 95vh; + display: flex; + justify-content: center; + align-items: center; + padding-left: 2em; + padding-right: 2em; + } + + .display > canvas { + max-height: 100%; + } + + .control { + flex-direction: column; + gap: 1em; + } + + .control > .ctrl { + padding-top: 2em; + padding-bottom: 2em; + } + + .control > #output { + height: 100%; + width: 100%; + padding: 10px; + overflow-y: scroll; + border: 1px solid #555; } - +
+
+ + +
-
+
+ +
+ + +     + + +     + + +     + +
+
+
+
diff --git a/src/main.cpp b/src/main.cpp index 00700f68..89b800a0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -190,6 +190,9 @@ void verifyWakeupLongPress() { } void waitForPowerRelease() { + if (CROSSPOINT_EMULATED) { + return; + } inputManager.update(); while (inputManager.isPressed(InputManager::BTN_POWER)) { delay(50); @@ -282,6 +285,9 @@ bool isUsbConnected() { } bool isWakeupAfterFlashing() { + if (CROSSPOINT_EMULATED) { + return true; + } const auto wakeupCause = esp_sleep_get_wakeup_cause(); const auto resetReason = esp_reset_reason(); From 5783faed333bfd9a0e5b4a7854a06edd75c97a9d Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Fri, 23 Jan 2026 16:12:18 +0100 Subject: [PATCH 07/10] disable battery measurement on emulation --- scripts/emulation/Dockerfile | 29 +++++++++++++++++++++++++---- scripts/emulation/server.py | 2 +- src/Battery.h | 14 +++++++++++++- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/scripts/emulation/Dockerfile b/scripts/emulation/Dockerfile index 9439c9f2..bc348a46 100644 --- a/scripts/emulation/Dockerfile +++ b/scripts/emulation/Dockerfile @@ -3,21 +3,42 @@ FROM debian:12 WORKDIR /root ARG DEBIAN_FRONTEND=noninteractive +ARG ESP_IDF_VERSION=v5.5.2 +ARG ESP_QEMU_VERSION=esp-develop-9.2.2-20250817 # Update container RUN apt-get update && apt-get upgrade -y # Install dependencies -RUN apt-get install git wget flex bison gperf python3 python3-pip python3-setuptools python3-venv cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0 \ - libgcrypt20 libglib2.0-0 libpixman-1-0 libsdl2-2.0-0 libslirp0 -y +RUN apt-get install -y git wget flex bison gperf python3 python3-pip python3-setuptools python3-venv cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0 \ + libgcrypt20 libglib2.0-0 libpixman-1-0 libsdl2-2.0-0 libslirp0 \ + libglib2.0-dev libpixman-1-dev libgcrypt20-dev libslirp-dev libsdl2-dev # Install ESP-IDF ENV PATH="~/.local/bin:${PATH}" -RUN mkdir -p ~/esp && cd ~/esp && git clone --quiet --recursive https://github.com/espressif/esp-idf.git && ln -s /usr/bin/python3 /usr/bin/python +RUN mkdir -p ~/esp && \ + cd ~/esp && git clone --recursive https://github.com/espressif/esp-idf.git -b ${ESP_IDF_VERSION} --depth 1 -j $(nproc) && \ + ln -s /usr/bin/python3 /usr/bin/python RUN cd ~/esp/esp-idf && export IDF_GITHUB_ASSETS="dl.espressif.com/github_assets" && ./install.sh && /bin/bash -c "source ./export.sh" # Install qemu -RUN cd ~/esp/esp-idf && python tools/idf_tools.py install qemu-riscv32 +RUN cd ~/esp/esp-idf && python tools/idf_tools.py install qemu-riscv32 qemu-xtensa + +# Or, build qemu from source +# RUN git clone https://github.com/espressif/qemu.git -b ${ESP_QEMU_VERSION} --depth 1 \ +# && cd qemu \ +# && mkdir -p build \ +# && cd build \ +# && ../configure --target-list=riscv32-softmmu \ +# --enable-gcrypt \ +# --enable-slirp \ +# --enable-debug \ +# --enable-sdl \ +# --disable-strip --disable-user \ +# --disable-capstone --disable-vnc \ +# --disable-gtk \ +# && make -j $(nproc) vga=no \ +# && make install RUN echo "alias idf='source /root/esp/esp-idf/export.sh'" >> .bashrc diff --git a/scripts/emulation/server.py b/scripts/emulation/server.py index 737d6845..d0dd3dfe 100644 --- a/scripts/emulation/server.py +++ b/scripts/emulation/server.py @@ -380,7 +380,7 @@ async def spawn_qemu(): "-nographic", "-M", "esp32c3", "-drive", "file=flash.bin,if=mtd,format=raw", - "-global", "driver=timer.esp32c3.timg,property=wdt_disable,value=true", # got panic if we don't disable WDT, why? + # "-global", "driver=timer.esp32c3.timg,property=wdt_disable,value=true", # got panic if we don't disable WDT, why? ] # Get working directory from environment or use /tmp diff --git a/src/Battery.h b/src/Battery.h index dcfcbf79..4181f402 100644 --- a/src/Battery.h +++ b/src/Battery.h @@ -3,4 +3,16 @@ #define BAT_GPIO0 0 // Battery voltage -static BatteryMonitor battery(BAT_GPIO0); +class Battery { + public: + uint16_t readPercentage() const { +#if CROSSPOINT_EMULATED == 0 + static const BatteryMonitor hwBattery = BatteryMonitor(BAT_GPIO0); + return hwBattery.readPercentage(); +#else + return 100; +#endif + } +}; + +static Battery battery; From fa05eba8a1ca1fca07160a21c5f95eb6aa56c27c Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Fri, 23 Jan 2026 16:25:02 +0100 Subject: [PATCH 08/10] stub input HAL --- lib/hal/HalInput.cpp | 85 ++++++++++++++++++++++++++++++++++++++++ lib/hal/HalInput.h | 40 +++++++++++++++++++ src/MappedInputManager.h | 2 +- src/main.cpp | 11 ++---- 4 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 lib/hal/HalInput.cpp create mode 100644 lib/hal/HalInput.h diff --git a/lib/hal/HalInput.cpp b/lib/hal/HalInput.cpp new file mode 100644 index 00000000..0c55f1e7 --- /dev/null +++ b/lib/hal/HalInput.cpp @@ -0,0 +1,85 @@ +#include "HalInput.h" +#include + +void HalInput::begin() { +#if CROSSPOINT_EMULATED == 0 + inputMgr.begin(); +#endif +} + +void HalInput::update() { +#if CROSSPOINT_EMULATED == 0 + inputMgr.update(); +#else + // TODO +#endif +} + +bool HalInput::isPressed(uint8_t buttonIndex) const { +#if CROSSPOINT_EMULATED == 0 + return inputMgr.isPressed(buttonIndex); +#else + // TODO + return false; +#endif +} + +bool HalInput::wasPressed(uint8_t buttonIndex) const { +#if CROSSPOINT_EMULATED == 0 + return inputMgr.wasPressed(buttonIndex); +#else + // TODO + return false; +#endif +} + +bool HalInput::wasAnyPressed() const { +#if CROSSPOINT_EMULATED == 0 + return inputMgr.wasAnyPressed(); +#else + // TODO + return false; +#endif +} + +bool HalInput::wasReleased(uint8_t buttonIndex) const { +#if CROSSPOINT_EMULATED == 0 + return inputMgr.wasReleased(buttonIndex); +#else + // TODO + return false; +#endif +} + +bool HalInput::wasAnyReleased() const { +#if CROSSPOINT_EMULATED == 0 + return inputMgr.wasAnyReleased(); +#else + // TODO + return false; +#endif +} + +unsigned long HalInput::getHeldTime() const { +#if CROSSPOINT_EMULATED == 0 + return inputMgr.getHeldTime(); +#else + // TODO + return 0; +#endif +} + +void HalInput::setupGpioWakeup() { +#if CROSSPOINT_EMULATED == 0 + esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); + // Ensure that the power button has been released to avoid immediately turning back on if you're holding it + while (isPressed(InputManager::BTN_POWER)) { + delay(50); + update(); + } + // Enter Deep Sleep + esp_deep_sleep_start(); +#else + // TODO +#endif +} diff --git a/lib/hal/HalInput.h b/lib/hal/HalInput.h new file mode 100644 index 00000000..a06771b4 --- /dev/null +++ b/lib/hal/HalInput.h @@ -0,0 +1,40 @@ +#pragma once + +#include + +#if CROSSPOINT_EMULATED == 0 +#include +#endif + +class HalInput { +#if CROSSPOINT_EMULATED == 0 + InputManager inputMgr; +#endif + + public: + HalInput() = default; + void begin(); + + void update(); + bool isPressed(uint8_t buttonIndex) const; + bool wasPressed(uint8_t buttonIndex) const; + bool wasAnyPressed() const; + bool wasReleased(uint8_t buttonIndex) const; + bool wasAnyReleased() const; + unsigned long getHeldTime() const; + + void setupGpioWakeup(); + + // Button indices + static constexpr uint8_t BTN_BACK = 0; + static constexpr uint8_t BTN_CONFIRM = 1; + static constexpr uint8_t BTN_LEFT = 2; + static constexpr uint8_t BTN_RIGHT = 3; + static constexpr uint8_t BTN_UP = 4; + static constexpr uint8_t BTN_DOWN = 5; + static constexpr uint8_t BTN_POWER = 6; +}; + +#if CROSSPOINT_EMULATED == 1 +using InputManager = HalInput; +#endif diff --git a/src/MappedInputManager.h b/src/MappedInputManager.h index 62065fe9..66573915 100644 --- a/src/MappedInputManager.h +++ b/src/MappedInputManager.h @@ -1,6 +1,6 @@ #pragma once -#include +#include class MappedInputManager { public: diff --git a/src/main.cpp b/src/main.cpp index 89b800a0..82d9a0c9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,7 +2,7 @@ #include #include #include -#include +#include #include #include #include @@ -184,8 +184,7 @@ void verifyWakeupLongPress() { if (abort) { // Button released too early. Returning to sleep. // IMPORTANT: Re-arm the wakeup trigger before sleeping again - esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); - esp_deep_sleep_start(); + inputManager.setupGpioWakeup(); } } @@ -208,11 +207,7 @@ void enterDeepSleep() { einkDisplay.deepSleep(); Serial.printf("[%lu] [ ] Power button press calibration value: %lu ms\n", millis(), t2 - t1); Serial.printf("[%lu] [ ] Entering deep sleep.\n", millis()); - esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); - // Ensure that the power button has been released to avoid immediately turning back on if you're holding it - waitForPowerRelease(); - // Enter Deep Sleep - esp_deep_sleep_start(); + inputManager.setupGpioWakeup(); } void onGoHome(); From 72c2da971d5aea3183e12d4b52f37143672f762a Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Fri, 23 Jan 2026 16:32:21 +0100 Subject: [PATCH 09/10] fix startDeepSleep() --- lib/hal/HalInput.cpp | 8 ++++---- lib/hal/HalInput.h | 4 ++-- src/main.cpp | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/hal/HalInput.cpp b/lib/hal/HalInput.cpp index 0c55f1e7..bed3d861 100644 --- a/lib/hal/HalInput.cpp +++ b/lib/hal/HalInput.cpp @@ -69,17 +69,17 @@ unsigned long HalInput::getHeldTime() const { #endif } -void HalInput::setupGpioWakeup() { +void startDeepSleep(InputManager& inputMgr) { #if CROSSPOINT_EMULATED == 0 esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); // Ensure that the power button has been released to avoid immediately turning back on if you're holding it - while (isPressed(InputManager::BTN_POWER)) { + while (inputMgr.isPressed(InputManager::BTN_POWER)) { delay(50); - update(); + inputMgr.update(); } // Enter Deep Sleep esp_deep_sleep_start(); #else - // TODO + Serial.println("[ ] GPIO wakeup setup skipped in emulation mode."); #endif } diff --git a/lib/hal/HalInput.h b/lib/hal/HalInput.h index a06771b4..f1dd8ce5 100644 --- a/lib/hal/HalInput.h +++ b/lib/hal/HalInput.h @@ -23,8 +23,6 @@ class HalInput { bool wasAnyReleased() const; unsigned long getHeldTime() const; - void setupGpioWakeup(); - // Button indices static constexpr uint8_t BTN_BACK = 0; static constexpr uint8_t BTN_CONFIRM = 1; @@ -38,3 +36,5 @@ class HalInput { #if CROSSPOINT_EMULATED == 1 using InputManager = HalInput; #endif + +void startDeepSleep(InputManager& inputMgr); diff --git a/src/main.cpp b/src/main.cpp index 82d9a0c9..54ecade2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -184,7 +184,7 @@ void verifyWakeupLongPress() { if (abort) { // Button released too early. Returning to sleep. // IMPORTANT: Re-arm the wakeup trigger before sleeping again - inputManager.setupGpioWakeup(); + startDeepSleep(inputManager); } } @@ -207,7 +207,7 @@ void enterDeepSleep() { einkDisplay.deepSleep(); Serial.printf("[%lu] [ ] Power button press calibration value: %lu ms\n", millis(), t2 - t1); Serial.printf("[%lu] [ ] Entering deep sleep.\n", millis()); - inputManager.setupGpioWakeup(); + startDeepSleep(inputManager); } void onGoHome(); From 3532f90f1ef4d542431e182c9cecbe0816802c3b Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Fri, 23 Jan 2026 17:12:18 +0100 Subject: [PATCH 10/10] btn ok --- lib/hal/EmulationUtils.h | 9 ++++- lib/hal/HalDisplay.cpp | 6 ++-- lib/hal/HalInput.cpp | 63 +++++++++++++++++++++++++++-------- lib/hal/HalInput.h | 12 +++++++ lib/hal/HalStorage.h | 2 ++ scripts/emulation/server.py | 51 +++++++++++++++++++++------- scripts/emulation/web_ui.html | 40 ++++++++++++++++++++++ 7 files changed, 154 insertions(+), 29 deletions(-) diff --git a/lib/hal/EmulationUtils.h b/lib/hal/EmulationUtils.h index c4ad48c0..ea86a4b8 100644 --- a/lib/hal/EmulationUtils.h +++ b/lib/hal/EmulationUtils.h @@ -18,12 +18,18 @@ static const char* CMD_FS_STAT = "FS_STAT"; // arg0: path -- return file size static const char* CMD_FS_WRITE = "FS_WRITE"; // arg0: path, arg1: base64-encoded data, arg2: offset, arg3: is inplace (0/1) -- return int64_t bytes written static const char* CMD_FS_MKDIR = "FS_MKDIR"; // arg0: path -- return int64_t: 0=success static const char* CMD_FS_RM = "FS_RM"; // arg0: path -- return int64_t: 0=success +static const char* CMD_BUTTON = "BUTTON"; // arg0: action ("read") -- return int64_t: HalInput state bitmask static std::string base64_encode(const char* buf, unsigned int bufLen); static std::vector base64_decode(const char* encoded_string, unsigned int in_len); +static SemaphoreHandle_t sendMutex = xSemaphoreCreateMutex(); + static void sendCmd(const char* cmd, const char* arg0 = nullptr, const char* arg1 = nullptr, const char* arg2 = nullptr, const char* arg3 = nullptr) { - Serial.printf("[%lu] [EMU] Sending command: %s\n", millis(), cmd); + xSemaphoreTake(sendMutex, portMAX_DELAY); + if (cmd != CMD_BUTTON) { + Serial.printf("[%lu] [EMU] Sending command: %s\n", millis(), cmd); + } Serial.print("$$CMD_"); Serial.print(cmd); if (arg0 != nullptr) { @@ -45,6 +51,7 @@ static void sendCmd(const char* cmd, const char* arg0 = nullptr, const char* arg Serial.print(arg3); } Serial.print("$$\n"); + xSemaphoreGive(sendMutex); } static String recvRespStr() { diff --git a/lib/hal/HalDisplay.cpp b/lib/hal/HalDisplay.cpp index fa8e6c1e..088064b1 100644 --- a/lib/hal/HalDisplay.cpp +++ b/lib/hal/HalDisplay.cpp @@ -83,11 +83,11 @@ void HalDisplay::refreshDisplay(RefreshMode mode, bool turnOffScreen) { Serial.printf("[%lu] [ ] Emulated refresh display with mode %d, turnOffScreen %d\n", millis(), static_cast(mode), turnOffScreen); // emulated delay if (mode == RefreshMode::FAST_REFRESH) { - delay(50); + delay(500); } else if (mode == RefreshMode::HALF_REFRESH) { - delay(800); + delay(1000); } else if (mode == RefreshMode::FULL_REFRESH) { - delay(1500); + delay(2000); } } } diff --git a/lib/hal/HalInput.cpp b/lib/hal/HalInput.cpp index bed3d861..160e826f 100644 --- a/lib/hal/HalInput.cpp +++ b/lib/hal/HalInput.cpp @@ -1,4 +1,5 @@ #include "HalInput.h" +#include "EmulationUtils.h" #include void HalInput::begin() { @@ -11,7 +12,44 @@ void HalInput::update() { #if CROSSPOINT_EMULATED == 0 inputMgr.update(); #else - // TODO + const unsigned long currentTime = millis(); + + EmulationUtils::sendCmd(EmulationUtils::CMD_BUTTON, "read"); + auto res = EmulationUtils::recvRespInt64(); + assert(res >= 0); + + const uint8_t state = static_cast(res); + + // Always clear events first + pressedEvents = 0; + releasedEvents = 0; + + // Debounce + if (state != lastState) { + lastDebounceTime = currentTime; + lastState = state; + } + + static constexpr unsigned long DEBOUNCE_DELAY = 5; + if ((currentTime - lastDebounceTime) > DEBOUNCE_DELAY) { + if (state != currentState) { + // Calculate pressed and released events + pressedEvents = state & ~currentState; + releasedEvents = currentState & ~state; + + // If pressing buttons and wasn't before, start recording time + if (pressedEvents > 0 && currentState == 0) { + buttonPressStart = currentTime; + } + + // If releasing a button and no other buttons being pressed, record finish time + if (releasedEvents > 0 && state == 0) { + buttonPressFinish = currentTime; + } + + currentState = state; + } + } #endif } @@ -19,8 +57,7 @@ bool HalInput::isPressed(uint8_t buttonIndex) const { #if CROSSPOINT_EMULATED == 0 return inputMgr.isPressed(buttonIndex); #else - // TODO - return false; + return currentState & (1 << buttonIndex); #endif } @@ -28,8 +65,7 @@ bool HalInput::wasPressed(uint8_t buttonIndex) const { #if CROSSPOINT_EMULATED == 0 return inputMgr.wasPressed(buttonIndex); #else - // TODO - return false; + return currentState & (1 << buttonIndex); #endif } @@ -37,8 +73,7 @@ bool HalInput::wasAnyPressed() const { #if CROSSPOINT_EMULATED == 0 return inputMgr.wasAnyPressed(); #else - // TODO - return false; + return pressedEvents > 0; #endif } @@ -46,8 +81,7 @@ bool HalInput::wasReleased(uint8_t buttonIndex) const { #if CROSSPOINT_EMULATED == 0 return inputMgr.wasReleased(buttonIndex); #else - // TODO - return false; + return releasedEvents & (1 << buttonIndex); #endif } @@ -55,8 +89,7 @@ bool HalInput::wasAnyReleased() const { #if CROSSPOINT_EMULATED == 0 return inputMgr.wasAnyReleased(); #else - // TODO - return false; + return releasedEvents > 0; #endif } @@ -64,8 +97,12 @@ unsigned long HalInput::getHeldTime() const { #if CROSSPOINT_EMULATED == 0 return inputMgr.getHeldTime(); #else - // TODO - return 0; + // Still hold a button + if (currentState > 0) { + return millis() - buttonPressStart; + } + + return buttonPressFinish - buttonPressStart; #endif } diff --git a/lib/hal/HalInput.h b/lib/hal/HalInput.h index f1dd8ce5..b011c993 100644 --- a/lib/hal/HalInput.h +++ b/lib/hal/HalInput.h @@ -31,8 +31,20 @@ class HalInput { static constexpr uint8_t BTN_UP = 4; static constexpr uint8_t BTN_DOWN = 5; static constexpr uint8_t BTN_POWER = 6; + + private: + // emulation state + uint8_t currentState; + uint8_t lastState; + uint8_t pressedEvents; + uint8_t releasedEvents; + unsigned long lastDebounceTime; + unsigned long buttonPressStart; + unsigned long buttonPressFinish; }; +// TODO @ngxson : this is a trick to avoid changing too many files at once. +// consider refactoring in a dedicated PR later. #if CROSSPOINT_EMULATED == 1 using InputManager = HalInput; #endif diff --git a/lib/hal/HalStorage.h b/lib/hal/HalStorage.h index f5cd82c3..bb351df6 100644 --- a/lib/hal/HalStorage.h +++ b/lib/hal/HalStorage.h @@ -113,6 +113,8 @@ class HalStorage { bool is_emulated = CROSSPOINT_EMULATED; }; +// TODO @ngxson : this is a trick to avoid changing too many files at once. +// consider refactoring in a dedicated PR later. #if CROSSPOINT_EMULATED == 1 #define SdMan HalStorage::getInstance() #endif diff --git a/scripts/emulation/server.py b/scripts/emulation/server.py index d0dd3dfe..f5533dd6 100644 --- a/scripts/emulation/server.py +++ b/scripts/emulation/server.py @@ -205,10 +205,11 @@ async def send_response(response: str): LAST_DISPLAY_BUFFER = None +BUTTON_STATE = 0 async def handle_command(command: str, args: list[str]) -> bool: """Handle a command from the device. Returns True if handled.""" - global sdcard_handler, LAST_DISPLAY_BUFFER + global sdcard_handler, LAST_DISPLAY_BUFFER, BUTTON_STATE try: if command == "DISPLAY": @@ -274,6 +275,16 @@ async def handle_command(command: str, args: list[str]) -> bool: print(f"[Emulator] FS_RM {path} -> {result}", flush=True) await send_response(str(result)) return True + + elif command == "BUTTON": + # arg0: action + action = args[0] + if action != "read": + raise ValueError("Only read action is supported") + # no log (too frequent) + # print(f"[Emulator] BUTTON command received: {action}", flush=True) + await send_response(str(BUTTON_STATE)) + return True else: print(f"[Emulator] Unknown command: {command}", flush=True) @@ -284,7 +295,9 @@ async def handle_command(command: str, args: list[str]) -> bool: return False -def process_message(message: str, for_ui: bool) -> str: +def process_message(message: str, for_ui: bool) -> str | None: + if message.startswith("$$CMD_BUTTON:"): + return None # Too frequent, ignore if message.startswith("$$CMD_"): if for_ui: return message @@ -338,18 +351,23 @@ async def read_stream(stream: asyncio.StreamReader, stream_type: str): if parsed: command, args = parsed await handle_command(command, args) - # Still broadcast to UI for visibility - await broadcast_message(stream_type, decoded_line) + if command == "DISPLAY": + await broadcast_message(stream_type, decoded_line) continue + to_stdio = process_message(decoded_line, for_ui=False) + to_ws = process_message(decoded_line, for_ui=True) + # Forward to parent process - if stream_type == "stdout": - print(process_message(decoded_line, for_ui=False), flush=True) - else: - print(process_message(decoded_line, for_ui=False), file=sys.stderr, flush=True) + if to_stdio is not None: + if stream_type == "stdout": + print(to_stdio, flush=True) + else: + print(to_stdio, file=sys.stderr, flush=True) # Broadcast to WebSocket clients - await broadcast_message(stream_type, process_message(decoded_line, for_ui=True)) + if to_ws is not None: + await broadcast_message(stream_type, to_ws) except Exception as e: print(f"Error reading {stream_type}: {e}", file=sys.stderr) @@ -436,16 +454,25 @@ async def websocket_handler(websocket: WebSocketServerProtocol): "data": f"$$CMD_DISPLAY:{LAST_DISPLAY_BUFFER}$$" })) - # Handle incoming messages (for stdin forwarding) + # Handle incoming messages (for stdin forwarding and events) async for message in websocket: try: data = json.loads(message) - if data.get("type") == "stdin" and qemu_process and qemu_process.stdin: + msg_type = data.get("type") + + if msg_type == "stdin" and qemu_process and qemu_process.stdin: input_data = data.get("data", "") qemu_process.stdin.write((input_data + "\n").encode()) await qemu_process.stdin.drain() + elif msg_type == "button_state": + global BUTTON_STATE + BUTTON_STATE = int(data.get("state", "0")) + print(f"[WebUI] Button state updated to {BUTTON_STATE}", flush=True) + else: + # Print any other messages for debugging + print(f"[WebUI Message] {message}", flush=True) except json.JSONDecodeError: - pass + print(f"[WebUI] Invalid JSON received: {message}", file=sys.stderr) except Exception as e: print(f"Error handling client message: {e}", file=sys.stderr) diff --git a/scripts/emulation/web_ui.html b/scripts/emulation/web_ui.html index 2d1741a0..2c5b0408 100644 --- a/scripts/emulation/web_ui.html +++ b/scripts/emulation/web_ui.html @@ -153,6 +153,46 @@ ctx.putImageData(imageData, 0, 0); } + let btn0Pressed = false; + let btn1Pressed = false; + let btn2Pressed = false; + let btn3Pressed = false; + let btnUpPressed = false; + let btnDownPressed = false; + let btnPowerPressed = false; + + let buttonState = 0; + function sendButtonState() { + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ + type: 'button_state', + state: buttonState + })); + appendLog('info', `Sent button state: ${buttonState}`); + } else { + appendLog('error', 'WebSocket not connected'); + } + } + + function setupButtonHandler(buttonId, bitIndex) { + const button = document.getElementById(buttonId); + button.onmousedown = () => { + buttonState ^= (1 << bitIndex); + sendButtonState(); + }; + button.onmouseup = () => { + buttonState &= ~(1 << bitIndex); + sendButtonState(); + }; + } + setupButtonHandler('btn0', 0); + setupButtonHandler('btn1', 1); + setupButtonHandler('btn2', 2); + setupButtonHandler('btn3', 3); + setupButtonHandler('btnU', 4); + setupButtonHandler('btnD', 5); + setupButtonHandler('btnP', 6); + function connect() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws`;