From be8b02efd6cfedfbbc0ffce05f8f284baf329307 Mon Sep 17 00:00:00 2001 From: cottongin Date: Fri, 30 Jan 2026 22:49:52 -0500 Subject: [PATCH] feat: merge PR #522 - add HalDisplay and HalGPIO abstraction layer Cherry-picked upstream PR #522 (da4d3b5) with conflict resolution: - Added new lib/hal/ files (HalDisplay, HalGPIO) - Updated GfxRenderer to use HalDisplay, preserving base viewable margins - Adopted PR #522's MappedInputManager lookup table implementation - Updated main.cpp to use HAL while preserving custom Serial initialization - Updated all EInkDisplay::RefreshMode references to HalDisplay::RefreshMode This introduces a Hardware Abstraction Layer for display and GPIO, enabling easier emulation and testing. --- lib/GfxRenderer/GfxRenderer.cpp | 68 +++++----- lib/GfxRenderer/GfxRenderer.h | 12 +- lib/hal/HalDisplay.cpp | 51 +++++++ lib/hal/HalDisplay.h | 52 ++++++++ lib/hal/HalGPIO.cpp | 55 ++++++++ lib/hal/HalGPIO.h | 61 +++++++++ src/MappedInputManager.cpp | 124 +++++++----------- src/MappedInputManager.h | 9 +- src/activities/boot_sleep/SleepActivity.cpp | 6 +- .../dictionary/EpubWordSelectionActivity.cpp | 4 +- src/activities/reader/EpubReaderActivity.cpp | 8 +- src/activities/reader/TxtReaderActivity.cpp | 8 +- .../util/FullScreenMessageActivity.h | 6 +- src/main.cpp | 99 ++++---------- 14 files changed, 357 insertions(+), 206 deletions(-) create mode 100644 lib/hal/HalDisplay.cpp create mode 100644 lib/hal/HalDisplay.h create mode 100644 lib/hal/HalGPIO.cpp create mode 100644 lib/hal/HalGPIO.h diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index d24ad3b..d1225f0 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; } @@ -36,7 +36,7 @@ void GfxRenderer::rotateCoordinates(const int x, const int y, int* rotatedX, int } void GfxRenderer::drawPixel(const int x, const int y, const bool state) const { - uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); + uint8_t* frameBuffer = display.getFrameBuffer(); // Early return if no framebuffer is set if (!frameBuffer) { @@ -49,14 +49,13 @@ 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) { @@ -202,7 +201,7 @@ void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, co break; } // TODO: Rotate bits - einkDisplay.drawImage(bitmap, rotatedX, rotatedY, width, height); + display.drawImage(bitmap, rotatedX, rotatedY, width, height); } void GfxRenderer::drawImageRotated(const uint8_t bitmap[], const int x, const int y, const int width, const int height, @@ -519,22 +518,20 @@ void GfxRenderer::fillPolygon(const int* xPoints, const int* yPoints, int numPoi free(nodeX); } -void GfxRenderer::clearScreen(const uint8_t color) const { einkDisplay.clearScreen(color); } +void GfxRenderer::clearScreen(const uint8_t color) const { display.clearScreen(color); } void GfxRenderer::invertScreen() const { - uint8_t* buffer = einkDisplay.getFrameBuffer(); + uint8_t* buffer = display.getFrameBuffer(); if (!buffer) { 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 { - einkDisplay.displayBuffer(refreshMode); -} +void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const { display.displayBuffer(refreshMode); } std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth, const EpdFontFamily::Style style) const { @@ -553,13 +550,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 { @@ -567,13 +564,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 { @@ -902,17 +899,18 @@ void GfxRenderer::drawTextRotated90CCW(const int fontId, const int x, const int } } -uint8_t* GfxRenderer::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); } +uint8_t* GfxRenderer::getFrameBuffer() const { return display.getFrameBuffer(); } -size_t GfxRenderer::getBufferSize() { return EInkDisplay::BUFFER_SIZE; } +size_t GfxRenderer::getBufferSize() { return HalDisplay::BUFFER_SIZE; } -void GfxRenderer::grayscaleRevert() const { einkDisplay.grayscaleRevert(); } +// unused +// void GfxRenderer::grayscaleRevert() const { display.grayscaleRevert(); } -void GfxRenderer::copyGrayscaleLsbBuffers() const { einkDisplay.copyGrayscaleLsbBuffers(einkDisplay.getFrameBuffer()); } +void GfxRenderer::copyGrayscaleLsbBuffers() const { display.copyGrayscaleLsbBuffers(display.getFrameBuffer()); } -void GfxRenderer::copyGrayscaleMsbBuffers() const { einkDisplay.copyGrayscaleMsbBuffers(einkDisplay.getFrameBuffer()); } +void GfxRenderer::copyGrayscaleMsbBuffers() const { display.copyGrayscaleMsbBuffers(display.getFrameBuffer()); } -void GfxRenderer::displayGrayBuffer() const { einkDisplay.displayGrayBuffer(); } +void GfxRenderer::displayGrayBuffer() const { display.displayGrayBuffer(); } void GfxRenderer::freeBwBufferChunks() { for (auto& bwBufferChunk : bwBufferChunks) { @@ -930,7 +928,7 @@ void GfxRenderer::freeBwBufferChunks() { * Returns true if buffer was stored successfully, false if allocation failed. */ bool GfxRenderer::storeBwBuffer() { - const uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); + const uint8_t* frameBuffer = display.getFrameBuffer(); if (!frameBuffer) { Serial.printf("[%lu] [GFX] !! No framebuffer in storeBwBuffer\n", millis()); return false; @@ -985,14 +983,14 @@ void GfxRenderer::restoreBwBuffer() { // CRITICAL: Even if restore fails, we must clean up the grayscale state // to prevent grayscaleRevert() from being called with corrupted RAM state // Use the current framebuffer content (which may not be ideal but prevents worse issues) - uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); + uint8_t* frameBuffer = display.getFrameBuffer(); if (frameBuffer) { - einkDisplay.cleanupGrayscaleBuffers(frameBuffer); + display.cleanupGrayscaleBuffers(frameBuffer); } return; } - uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); + uint8_t* frameBuffer = display.getFrameBuffer(); if (!frameBuffer) { Serial.printf("[%lu] [GFX] !! No framebuffer in restoreBwBuffer\n", millis()); freeBwBufferChunks(); @@ -1005,7 +1003,7 @@ void GfxRenderer::restoreBwBuffer() { Serial.printf("[%lu] [GFX] !! BW buffer chunks not stored - this is likely a bug\n", millis()); freeBwBufferChunks(); // CRITICAL: Clean up grayscale state even on mid-restore failure - einkDisplay.cleanupGrayscaleBuffers(frameBuffer); + display.cleanupGrayscaleBuffers(frameBuffer); return; } @@ -1013,7 +1011,7 @@ void GfxRenderer::restoreBwBuffer() { memcpy(frameBuffer + offset, bwBufferChunks[i], BW_BUFFER_CHUNK_SIZE); } - einkDisplay.cleanupGrayscaleBuffers(frameBuffer); + display.cleanupGrayscaleBuffers(frameBuffer); freeBwBufferChunks(); Serial.printf("[%lu] [GFX] Restored and freed BW buffer chunks\n", millis()); @@ -1024,9 +1022,9 @@ void GfxRenderer::restoreBwBuffer() { * Use this when BW buffer was re-rendered instead of stored/restored. */ void GfxRenderer::cleanupGrayscaleWithFrameBuffer() const { - uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); + uint8_t* frameBuffer = display.getFrameBuffer(); if (frameBuffer) { - einkDisplay.cleanupGrayscaleBuffers(frameBuffer); + display.cleanupGrayscaleBuffers(frameBuffer); } } diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index d25e05e..9e38638 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -1,7 +1,7 @@ #pragma once -#include #include +#include #include @@ -24,8 +24,8 @@ 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"); // Base viewable margins (hardware-specific, before bezel compensation) @@ -34,7 +34,7 @@ class GfxRenderer { static constexpr int BASE_VIEWABLE_MARGIN_BOTTOM = 3; static constexpr int BASE_VIEWABLE_MARGIN_LEFT = 3; - EInkDisplay& einkDisplay; + HalDisplay& display; RenderMode renderMode; Orientation orientation; int bezelCompensation = 0; // Pixels to add for bezel defect compensation @@ -47,7 +47,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& halDisplay) : display(halDisplay), renderMode(BW), orientation(Portrait) {} ~GfxRenderer() { freeBwBufferChunks(); } // Viewable margins (includes bezel compensation applied to the configured edge) @@ -79,7 +79,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; diff --git a/lib/hal/HalDisplay.cpp b/lib/hal/HalDisplay.cpp new file mode 100644 index 0000000..6f69d7f --- /dev/null +++ b/lib/hal/HalDisplay.cpp @@ -0,0 +1,51 @@ +#include +#include + +#define SD_SPI_MISO 7 + +HalDisplay::HalDisplay() : einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY) {} + +HalDisplay::~HalDisplay() {} + +void HalDisplay::begin() { einkDisplay.begin(); } + +void HalDisplay::clearScreen(uint8_t color) const { einkDisplay.clearScreen(color); } + +void HalDisplay::drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h, + bool fromProgmem) const { + einkDisplay.drawImage(imageData, x, y, w, h, fromProgmem); +} + +EInkDisplay::RefreshMode convertRefreshMode(HalDisplay::RefreshMode mode) { + switch (mode) { + case HalDisplay::FULL_REFRESH: + return EInkDisplay::FULL_REFRESH; + case HalDisplay::HALF_REFRESH: + return EInkDisplay::HALF_REFRESH; + case HalDisplay::FAST_REFRESH: + default: + return EInkDisplay::FAST_REFRESH; + } +} + +void HalDisplay::displayBuffer(HalDisplay::RefreshMode mode) { einkDisplay.displayBuffer(convertRefreshMode(mode)); } + +void HalDisplay::refreshDisplay(HalDisplay::RefreshMode mode, bool turnOffScreen) { + einkDisplay.refreshDisplay(convertRefreshMode(mode), turnOffScreen); +} + +void HalDisplay::deepSleep() { einkDisplay.deepSleep(); } + +uint8_t* HalDisplay::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); } + +void HalDisplay::copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer) { + einkDisplay.copyGrayscaleBuffers(lsbBuffer, msbBuffer); +} + +void HalDisplay::copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer) { einkDisplay.copyGrayscaleLsbBuffers(lsbBuffer); } + +void HalDisplay::copyGrayscaleMsbBuffers(const uint8_t* msbBuffer) { einkDisplay.copyGrayscaleMsbBuffers(msbBuffer); } + +void HalDisplay::cleanupGrayscaleBuffers(const uint8_t* bwBuffer) { einkDisplay.cleanupGrayscaleBuffers(bwBuffer); } + +void HalDisplay::displayGrayBuffer() { einkDisplay.displayGrayBuffer(); } diff --git a/lib/hal/HalDisplay.h b/lib/hal/HalDisplay.h new file mode 100644 index 0000000..6eb7156 --- /dev/null +++ b/lib/hal/HalDisplay.h @@ -0,0 +1,52 @@ +#pragma once +#include +#include + +class HalDisplay { + public: + // Constructor with pin configuration + HalDisplay(); + + // Destructor + ~HalDisplay(); + + // Refresh modes + enum RefreshMode { + FULL_REFRESH, // Full refresh with complete waveform + HALF_REFRESH, // Half refresh (1720ms) - balanced quality and speed + FAST_REFRESH // Fast refresh using custom LUT + }; + + // Initialize the display hardware and driver + void begin(); + + // Display dimensions + static constexpr uint16_t DISPLAY_WIDTH = EInkDisplay::DISPLAY_WIDTH; + static constexpr uint16_t DISPLAY_HEIGHT = EInkDisplay::DISPLAY_HEIGHT; + 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: + EInkDisplay einkDisplay; +}; diff --git a/lib/hal/HalGPIO.cpp b/lib/hal/HalGPIO.cpp new file mode 100644 index 0000000..803efba --- /dev/null +++ b/lib/hal/HalGPIO.cpp @@ -0,0 +1,55 @@ +#include +#include +#include + +void HalGPIO::begin() { + inputMgr.begin(); + SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS); + pinMode(BAT_GPIO0, INPUT); + pinMode(UART0_RXD, INPUT); +} + +void HalGPIO::update() { inputMgr.update(); } + +bool HalGPIO::isPressed(uint8_t buttonIndex) const { return inputMgr.isPressed(buttonIndex); } + +bool HalGPIO::wasPressed(uint8_t buttonIndex) const { return inputMgr.wasPressed(buttonIndex); } + +bool HalGPIO::wasAnyPressed() const { return inputMgr.wasAnyPressed(); } + +bool HalGPIO::wasReleased(uint8_t buttonIndex) const { return inputMgr.wasReleased(buttonIndex); } + +bool HalGPIO::wasAnyReleased() const { return inputMgr.wasAnyReleased(); } + +unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); } + +void HalGPIO::startDeepSleep() { + 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 (inputMgr.isPressed(BTN_POWER)) { + delay(50); + inputMgr.update(); + } + // Enter Deep Sleep + esp_deep_sleep_start(); +} + +int HalGPIO::getBatteryPercentage() const { + static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0); + return battery.readPercentage(); +} + +bool HalGPIO::isUsbConnected() const { + // U0RXD/GPIO20 reads HIGH when USB is connected + return digitalRead(UART0_RXD) == HIGH; +} + +bool HalGPIO::isWakeupByPowerButton() const { + const auto wakeupCause = esp_sleep_get_wakeup_cause(); + const auto resetReason = esp_reset_reason(); + if (isUsbConnected()) { + return wakeupCause == ESP_SLEEP_WAKEUP_GPIO; + } else { + return (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED) && (resetReason == ESP_RST_POWERON); + } +} diff --git a/lib/hal/HalGPIO.h b/lib/hal/HalGPIO.h new file mode 100644 index 0000000..11ffb22 --- /dev/null +++ b/lib/hal/HalGPIO.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults) +#define EPD_SCLK 8 // SPI Clock +#define EPD_MOSI 10 // SPI MOSI (Master Out Slave In) +#define EPD_CS 21 // Chip Select +#define EPD_DC 4 // Data/Command +#define EPD_RST 5 // Reset +#define EPD_BUSY 6 // Busy + +#define SPI_MISO 7 // SPI MISO, shared between SD card and display (Master In Slave Out) + +#define BAT_GPIO0 0 // Battery voltage + +#define UART0_RXD 20 // Used for USB connection detection + +class HalGPIO { +#if CROSSPOINT_EMULATED == 0 + InputManager inputMgr; +#endif + + public: + HalGPIO() = default; + + // Start button GPIO and setup SPI for screen and SD card + void begin(); + + // Button input methods + 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; + + // Setup wake up GPIO and enter deep sleep + void startDeepSleep(); + + // Get battery percentage (range 0-100) + int getBatteryPercentage() const; + + // Check if USB is connected + bool isUsbConnected() const; + + // Check if wakeup was caused by power button press + bool isWakeupByPowerButton() const; + + // 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; +}; diff --git a/src/MappedInputManager.cpp b/src/MappedInputManager.cpp index c66a5c0..e37995e 100644 --- a/src/MappedInputManager.cpp +++ b/src/MappedInputManager.cpp @@ -2,103 +2,77 @@ #include "CrossPointSettings.h" -decltype(InputManager::BTN_BACK) MappedInputManager::mapButton(const Button button) const { +namespace { +using ButtonIndex = uint8_t; + +struct FrontLayoutMap { + ButtonIndex back; + ButtonIndex confirm; + ButtonIndex left; + ButtonIndex right; +}; + +struct SideLayoutMap { + ButtonIndex pageBack; + ButtonIndex pageForward; +}; + +// Order matches CrossPointSettings::FRONT_BUTTON_LAYOUT. +constexpr FrontLayoutMap kFrontLayouts[] = { + {HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM, HalGPIO::BTN_LEFT, HalGPIO::BTN_RIGHT}, + {HalGPIO::BTN_LEFT, HalGPIO::BTN_RIGHT, HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM}, + {HalGPIO::BTN_CONFIRM, HalGPIO::BTN_LEFT, HalGPIO::BTN_BACK, HalGPIO::BTN_RIGHT}, + {HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM, HalGPIO::BTN_RIGHT, HalGPIO::BTN_LEFT}, +}; + +// Order matches CrossPointSettings::SIDE_BUTTON_LAYOUT. +constexpr SideLayoutMap kSideLayouts[] = { + {HalGPIO::BTN_UP, HalGPIO::BTN_DOWN}, + {HalGPIO::BTN_DOWN, HalGPIO::BTN_UP}, +}; +} // namespace + +bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint8_t) const) const { const auto frontLayout = static_cast(SETTINGS.frontButtonLayout); const auto sideLayout = static_cast(SETTINGS.sideButtonLayout); + const auto& front = kFrontLayouts[frontLayout]; + const auto& side = kSideLayouts[sideLayout]; switch (button) { case Button::Back: - switch (frontLayout) { - case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM: - return InputManager::BTN_LEFT; - case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT: - return InputManager::BTN_CONFIRM; - case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT: - /* fall through */ - case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT: - /* fall through */ - default: - return InputManager::BTN_BACK; - } + return (gpio.*fn)(front.back); case Button::Confirm: - switch (frontLayout) { - case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM: - return InputManager::BTN_RIGHT; - case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT: - return InputManager::BTN_LEFT; - case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT: - /* fall through */ - case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT: - /* fall through */ - default: - return InputManager::BTN_CONFIRM; - } + return (gpio.*fn)(front.confirm); case Button::Left: - switch (frontLayout) { - case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM: - /* fall through */ - case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT: - return InputManager::BTN_BACK; - case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT: - return InputManager::BTN_RIGHT; - case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT: - /* fall through */ - default: - return InputManager::BTN_LEFT; - } + return (gpio.*fn)(front.left); case Button::Right: - switch (frontLayout) { - case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM: - return InputManager::BTN_CONFIRM; - case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT: - return InputManager::BTN_LEFT; - case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT: - /* fall through */ - case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT: - /* fall through */ - default: - return InputManager::BTN_RIGHT; - } + return (gpio.*fn)(front.right); case Button::Up: - return InputManager::BTN_UP; + return (gpio.*fn)(HalGPIO::BTN_UP); case Button::Down: - return InputManager::BTN_DOWN; + return (gpio.*fn)(HalGPIO::BTN_DOWN); case Button::Power: - return InputManager::BTN_POWER; + return (gpio.*fn)(HalGPIO::BTN_POWER); case Button::PageBack: - switch (sideLayout) { - case CrossPointSettings::NEXT_PREV: - return InputManager::BTN_DOWN; - case CrossPointSettings::PREV_NEXT: - /* fall through */ - default: - return InputManager::BTN_UP; - } + return (gpio.*fn)(side.pageBack); case Button::PageForward: - switch (sideLayout) { - case CrossPointSettings::NEXT_PREV: - return InputManager::BTN_UP; - case CrossPointSettings::PREV_NEXT: - /* fall through */ - default: - return InputManager::BTN_DOWN; - } + return (gpio.*fn)(side.pageForward); } - return InputManager::BTN_BACK; + return false; } -bool MappedInputManager::wasPressed(const Button button) const { return inputManager.wasPressed(mapButton(button)); } +bool MappedInputManager::wasPressed(const Button button) const { return mapButton(button, &HalGPIO::wasPressed); } -bool MappedInputManager::wasReleased(const Button button) const { return inputManager.wasReleased(mapButton(button)); } +bool MappedInputManager::wasReleased(const Button button) const { return mapButton(button, &HalGPIO::wasReleased); } -bool MappedInputManager::isPressed(const Button button) const { return inputManager.isPressed(mapButton(button)); } +bool MappedInputManager::isPressed(const Button button) const { return mapButton(button, &HalGPIO::isPressed); } -bool MappedInputManager::wasAnyPressed() const { return inputManager.wasAnyPressed(); } +bool MappedInputManager::wasAnyPressed() const { return gpio.wasAnyPressed(); } -bool MappedInputManager::wasAnyReleased() const { return inputManager.wasAnyReleased(); } +bool MappedInputManager::wasAnyReleased() const { return gpio.wasAnyReleased(); } -unsigned long MappedInputManager::getHeldTime() const { return inputManager.getHeldTime(); } +unsigned long MappedInputManager::getHeldTime() const { return gpio.getHeldTime(); } MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const char* confirm, const char* previous, const char* next) const { diff --git a/src/MappedInputManager.h b/src/MappedInputManager.h index 62065fe..f507a92 100644 --- a/src/MappedInputManager.h +++ b/src/MappedInputManager.h @@ -1,6 +1,6 @@ #pragma once -#include +#include class MappedInputManager { public: @@ -13,7 +13,7 @@ class MappedInputManager { const char* btn4; }; - explicit MappedInputManager(InputManager& inputManager) : inputManager(inputManager) {} + explicit MappedInputManager(HalGPIO& gpio) : gpio(gpio) {} bool wasPressed(Button button) const; bool wasReleased(Button button) const; @@ -24,6 +24,7 @@ class MappedInputManager { Labels mapLabels(const char* back, const char* confirm, const char* previous, const char* next) const; private: - InputManager& inputManager; - decltype(InputManager::BTN_BACK) mapButton(Button button) const; + HalGPIO& gpio; + + bool mapButton(Button button, bool (HalGPIO::*fn)(uint8_t) const) const; }; diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 0ad9d8b..09aa96d 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -154,7 +154,7 @@ void SleepActivity::renderDefaultSleepScreen() const { renderer.invertScreen(); } - renderer.displayBuffer(EInkDisplay::HALF_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); } void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::string& bmpPath) const { @@ -269,7 +269,7 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str renderer.invertScreen(); } - renderer.displayBuffer(EInkDisplay::HALF_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); if (hasGreyscale) { // Grayscale LSB pass @@ -400,7 +400,7 @@ void SleepActivity::renderCoverSleepScreen() const { void SleepActivity::renderBlankSleepScreen() const { renderer.clearScreen(); - renderer.displayBuffer(EInkDisplay::HALF_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); } std::string SleepActivity::getEdgeCachePath(const std::string& bmpPath) { diff --git a/src/activities/dictionary/EpubWordSelectionActivity.cpp b/src/activities/dictionary/EpubWordSelectionActivity.cpp index d1bf7c0..ee8eee0 100644 --- a/src/activities/dictionary/EpubWordSelectionActivity.cpp +++ b/src/activities/dictionary/EpubWordSelectionActivity.cpp @@ -1,6 +1,6 @@ #include "EpubWordSelectionActivity.h" -#include +#include #include #include @@ -263,5 +263,5 @@ void EpubWordSelectionActivity::render() const { const char* sideBottomHint = (currentLineIndex < lastLine) ? "DOWN" : ""; renderer.drawSideButtonHints(SMALL_FONT_ID, sideTopHint, sideBottomHint, false); // No border - renderer.displayBuffer(EInkDisplay::FAST_REFRESH); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); } diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 583fa93..2db93ca 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -89,7 +89,7 @@ void EpubReaderActivity::onEnter() { renderer.fillRect(boxX, boxY, boxWidth, boxHeight, false); renderer.drawRect(boxX + 5, boxY + 5, boxWidth - 10, boxHeight - 10); renderer.drawText(UI_12_FONT_ID, boxX + boxMargin, boxY + boxMargin, "Preparing book... [0%]"); - renderer.displayBuffer(EInkDisplay::FAST_REFRESH); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); // Generate covers with progress callback epub->generateAllCovers([&](int percent) { @@ -103,7 +103,7 @@ void EpubReaderActivity::onEnter() { char progressStr[32]; snprintf(progressStr, sizeof(progressStr), "Preparing book... [%d%%]", percent); renderer.drawText(UI_12_FONT_ID, boxX + boxMargin, boxY + boxMargin, progressStr); - renderer.displayBuffer(EInkDisplay::FAST_REFRESH); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); } }); } @@ -717,7 +717,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(), @@ -835,7 +835,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or 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 de81d10..9c05656 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -85,7 +85,7 @@ void TxtReaderActivity::onEnter() { renderer.fillRect(boxX, boxY, boxWidth, boxHeight, false); renderer.drawRect(boxX + 5, boxY + 5, boxWidth - 10, boxHeight - 10); renderer.drawText(UI_12_FONT_ID, boxX + boxMargin, boxY + boxMargin, "Preparing book... [0%]"); - renderer.displayBuffer(EInkDisplay::FAST_REFRESH); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); // Generate covers with progress callback (void)txt->generateAllCovers([&](int percent) { @@ -99,7 +99,7 @@ void TxtReaderActivity::onEnter() { char progressStr[32]; snprintf(progressStr, sizeof(progressStr), "Preparing book... [%d%%]", percent); renderer.drawText(UI_12_FONT_ID, boxX + boxMargin, boxY + boxMargin, progressStr); - renderer.displayBuffer(EInkDisplay::FAST_REFRESH); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); } }); } @@ -339,7 +339,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 @@ -571,7 +571,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/util/FullScreenMessageActivity.h b/src/activities/util/FullScreenMessageActivity.h index 3e975c9..9390950 100644 --- a/src/activities/util/FullScreenMessageActivity.h +++ b/src/activities/util/FullScreenMessageActivity.h @@ -1,6 +1,6 @@ #pragma once -#include #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 f3dcb1c..04c3bc0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,9 +1,9 @@ #include #include -#include #include #include -#include +#include +#include #include #include #include @@ -32,23 +32,10 @@ #include "fontIds.h" #include "images/LockIcon.h" -#define SPI_FQ 40000000 -// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults) -#define EPD_SCLK 8 // SPI Clock -#define EPD_MOSI 10 // SPI MOSI (Master Out Slave In) -#define EPD_CS 21 // Chip Select -#define EPD_DC 4 // Data/Command -#define EPD_RST 5 // Reset -#define EPD_BUSY 6 // Busy - -#define UART0_RXD 20 // Used for USB connection detection - -#define SD_SPI_MISO 7 - -EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY); -InputManager inputManager; -MappedInputManager mappedInputManager(inputManager); -GfxRenderer renderer(einkDisplay); +HalDisplay display; +HalGPIO gpio; +MappedInputManager mappedInputManager(gpio); +GfxRenderer renderer(display); Activity* currentActivity; // Fonts @@ -221,7 +208,7 @@ void checkForFlashCommand() { renderer.drawImageRotated(LockIcon, iconX, iconY, LOCK_ICON_WIDTH, LOCK_ICON_HEIGHT, rotation); // Use full refresh for clean display before flash overwrites firmware - renderer.displayBuffer(EInkDisplay::HALF_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); } flashCmdBuffer = ""; } else if (c != '\r') { @@ -272,21 +259,20 @@ void verifyPowerButtonDuration() { const uint16_t calibratedPressDuration = (calibration < SETTINGS.getPowerButtonDuration()) ? SETTINGS.getPowerButtonDuration() - calibration : 1; - inputManager.update(); - // Verify the user has actually pressed + gpio.update(); // Needed because inputManager.isPressed() may take up to ~500ms to return the correct state - while (!inputManager.isPressed(InputManager::BTN_POWER) && millis() - start < 1000) { + while (!gpio.isPressed(HalGPIO::BTN_POWER) && millis() - start < 1000) { delay(10); // only wait 10ms each iteration to not delay too much in case of short configured duration. - inputManager.update(); + gpio.update(); } t2 = millis(); - if (inputManager.isPressed(InputManager::BTN_POWER)) { + if (gpio.isPressed(HalGPIO::BTN_POWER)) { do { delay(10); - inputManager.update(); - } while (inputManager.isPressed(InputManager::BTN_POWER) && inputManager.getHeldTime() < calibratedPressDuration); - abort = inputManager.getHeldTime() < calibratedPressDuration; + gpio.update(); + } while (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.getHeldTime() < calibratedPressDuration); + abort = gpio.getHeldTime() < calibratedPressDuration; } else { abort = true; } @@ -294,16 +280,15 @@ void verifyPowerButtonDuration() { 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(); + gpio.startDeepSleep(); } } void waitForPowerRelease() { - inputManager.update(); - while (inputManager.isPressed(InputManager::BTN_POWER)) { + gpio.update(); + while (gpio.isPressed(HalGPIO::BTN_POWER)) { delay(50); - inputManager.update(); + gpio.update(); } } @@ -312,14 +297,11 @@ void enterDeepSleep() { exitActivity(); enterNewActivity(new SleepActivity(renderer, mappedInputManager)); - einkDisplay.deepSleep(); + display.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(); + + gpio.startDeepSleep(); } void onGoHome(); @@ -444,7 +426,7 @@ void onGoHome() { } void setupDisplayAndFonts() { - einkDisplay.begin(); + display.begin(); Serial.printf("[%lu] [ ] Display initialized\n", millis()); renderer.insertFont(BOOKERLY_14_FONT_ID, bookerly14FontFamily); #ifndef OMIT_FONTS @@ -466,45 +448,24 @@ void setupDisplayAndFonts() { Serial.printf("[%lu] [ ] Fonts setup\n", millis()); } -bool isUsbConnected() { - // U0RXD/GPIO20 reads HIGH when USB is connected - return digitalRead(UART0_RXD) == HIGH; -} - -bool isWakeupByPowerButton() { - const auto wakeupCause = esp_sleep_get_wakeup_cause(); - const auto resetReason = esp_reset_reason(); - if (isUsbConnected()) { - return wakeupCause == ESP_SLEEP_WAKEUP_GPIO; - } else { - return (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED) && (resetReason == ESP_RST_POWERON); - } -} - void setup() { t1 = millis(); + gpio.begin(); + // Always initialize Serial - safe on ESP32-C3 USB CDC even without USB connected // (the peripheral just remains idle). - pinMode(UART0_RXD, INPUT); Serial.begin(115200); // Only wait for terminal connection if USB is physically connected // This allows catching early debug logs when a serial monitor is attached - if (isUsbConnected()) { + if (gpio.isUsbConnected()) { unsigned long start = millis(); while (!Serial && (millis() - start) < 3000) { delay(10); } } - inputManager.begin(); - // Initialize pins - pinMode(BAT_GPIO0, INPUT); - - // Initialize SPI with custom pins - SPI.begin(EPD_SCLK, SD_SPI_MISO, EPD_MOSI, EPD_CS); - // SD Card Initialization // We need 6 open files concurrently when parsing a new chapter if (!SdMan.begin()) { @@ -521,7 +482,7 @@ void setup() { // Apply bezel compensation from settings renderer.setBezelCompensation(SETTINGS.bezelCompensation, SETTINGS.bezelCompensationEdge); - if (isWakeupByPowerButton()) { + if (gpio.isWakeupByPowerButton()) { // For normal wakeups, verify power button press duration Serial.printf("[%lu] [ ] Verifying power button press duration\n", millis()); verifyPowerButtonDuration(); @@ -561,7 +522,7 @@ void loop() { const unsigned long loopStartTime = millis(); static unsigned long lastMemPrint = 0; - inputManager.update(); + gpio.update(); if (Serial && millis() - lastMemPrint >= 10000) { // Basic heap info @@ -579,8 +540,7 @@ void loop() { // Check for any user activity (button press or release) or active background work static unsigned long lastActivityTime = millis(); - if (inputManager.wasAnyPressed() || inputManager.wasAnyReleased() || - (currentActivity && currentActivity->preventAutoSleep())) { + if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || (currentActivity && currentActivity->preventAutoSleep())) { lastActivityTime = millis(); // Reset inactivity timer } @@ -592,8 +552,7 @@ void loop() { return; } - if (inputManager.isPressed(InputManager::BTN_POWER) && - inputManager.getHeldTime() > SETTINGS.getPowerButtonDuration()) { + if (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.getHeldTime() > SETTINGS.getPowerButtonDuration()) { enterDeepSleep(); // This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start return;