feat: add HalDisplay and HalGPIO (#522)

## Summary

Extracted some changes from
https://github.com/crosspoint-reader/crosspoint-reader/pull/500 to make
reviewing easier

This PR adds HAL (Hardware Abstraction Layer) for display and GPIO
components, making it easier to write a stub or an emulated
implementation of the hardware.

SD card HAL will be added via another PR, because it's a bit more
tricky.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **NO**
This commit is contained in:
Xuan-Son Nguyen 2026-01-27 18:50:15 +01:00 committed by Uri Tauber
parent 8b8cca2d9b
commit 0e46822a8b
14 changed files with 322 additions and 148 deletions

View File

@ -10,19 +10,19 @@ void GfxRenderer::rotateCoordinates(const int x, const int y, int* rotatedX, int
// Logical portrait (480x800) → panel (800x480) // Logical portrait (480x800) → panel (800x480)
// Rotation: 90 degrees clockwise // Rotation: 90 degrees clockwise
*rotatedX = y; *rotatedX = y;
*rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x; *rotatedY = HalDisplay::DISPLAY_HEIGHT - 1 - x;
break; break;
} }
case LandscapeClockwise: { case LandscapeClockwise: {
// Logical landscape (800x480) rotated 180 degrees (swap top/bottom and left/right) // Logical landscape (800x480) rotated 180 degrees (swap top/bottom and left/right)
*rotatedX = EInkDisplay::DISPLAY_WIDTH - 1 - x; *rotatedX = HalDisplay::DISPLAY_WIDTH - 1 - x;
*rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - y; *rotatedY = HalDisplay::DISPLAY_HEIGHT - 1 - y;
break; break;
} }
case PortraitInverted: { case PortraitInverted: {
// Logical portrait (480x800) → panel (800x480) // Logical portrait (480x800) → panel (800x480)
// Rotation: 90 degrees counter-clockwise // Rotation: 90 degrees counter-clockwise
*rotatedX = EInkDisplay::DISPLAY_WIDTH - 1 - y; *rotatedX = HalDisplay::DISPLAY_WIDTH - 1 - y;
*rotatedY = x; *rotatedY = x;
break; 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 { 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 // Early return if no framebuffer is set
if (!frameBuffer) { if (!frameBuffer) {
@ -49,14 +49,13 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
rotateCoordinates(x, y, &rotatedX, &rotatedY); rotateCoordinates(x, y, &rotatedX, &rotatedY);
// Bounds checking against physical panel dimensions // Bounds checking against physical panel dimensions
if (rotatedX < 0 || rotatedX >= EInkDisplay::DISPLAY_WIDTH || rotatedY < 0 || if (rotatedX < 0 || rotatedX >= HalDisplay::DISPLAY_WIDTH || rotatedY < 0 || rotatedY >= HalDisplay::DISPLAY_HEIGHT) {
rotatedY >= EInkDisplay::DISPLAY_HEIGHT) {
Serial.printf("[%lu] [GFX] !! Outside range (%d, %d) -> (%d, %d)\n", millis(), x, y, rotatedX, rotatedY); Serial.printf("[%lu] [GFX] !! Outside range (%d, %d) -> (%d, %d)\n", millis(), x, y, rotatedX, rotatedY);
return; return;
} }
// Calculate byte position and bit position // 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 const uint8_t bitPosition = 7 - (rotatedX % 8); // MSB first
if (state) { if (state) {
@ -164,7 +163,7 @@ void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, co
break; break;
} }
// TODO: Rotate bits // TODO: Rotate bits
einkDisplay.drawImage(bitmap, rotatedX, rotatedY, width, height); display.drawImage(bitmap, rotatedX, rotatedY, width, height);
} }
void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight, void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight,
@ -399,22 +398,20 @@ void GfxRenderer::fillPolygon(const int* xPoints, const int* yPoints, int numPoi
free(nodeX); 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 { void GfxRenderer::invertScreen() const {
uint8_t* buffer = einkDisplay.getFrameBuffer(); uint8_t* buffer = display.getFrameBuffer();
if (!buffer) { if (!buffer) {
Serial.printf("[%lu] [GFX] !! No framebuffer in invertScreen\n", millis()); Serial.printf("[%lu] [GFX] !! No framebuffer in invertScreen\n", millis());
return; return;
} }
for (int i = 0; i < EInkDisplay::BUFFER_SIZE; i++) { for (int i = 0; i < HalDisplay::BUFFER_SIZE; i++) {
buffer[i] = ~buffer[i]; buffer[i] = ~buffer[i];
} }
} }
void GfxRenderer::displayBuffer(const EInkDisplay::RefreshMode refreshMode) const { void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const { display.displayBuffer(refreshMode); }
einkDisplay.displayBuffer(refreshMode);
}
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth, std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
const EpdFontFamily::Style style) const { const EpdFontFamily::Style style) const {
@ -433,13 +430,13 @@ int GfxRenderer::getScreenWidth() const {
case Portrait: case Portrait:
case PortraitInverted: case PortraitInverted:
// 480px wide in portrait logical coordinates // 480px wide in portrait logical coordinates
return EInkDisplay::DISPLAY_HEIGHT; return HalDisplay::DISPLAY_HEIGHT;
case LandscapeClockwise: case LandscapeClockwise:
case LandscapeCounterClockwise: case LandscapeCounterClockwise:
// 800px wide in landscape logical coordinates // 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 { int GfxRenderer::getScreenHeight() const {
@ -447,13 +444,13 @@ int GfxRenderer::getScreenHeight() const {
case Portrait: case Portrait:
case PortraitInverted: case PortraitInverted:
// 800px tall in portrait logical coordinates // 800px tall in portrait logical coordinates
return EInkDisplay::DISPLAY_WIDTH; return HalDisplay::DISPLAY_WIDTH;
case LandscapeClockwise: case LandscapeClockwise:
case LandscapeCounterClockwise: case LandscapeCounterClockwise:
// 480px tall in landscape logical coordinates // 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 { int GfxRenderer::getSpaceWidth(const int fontId) const {
@ -653,17 +650,18 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
} }
} }
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() { void GfxRenderer::freeBwBufferChunks() {
for (auto& bwBufferChunk : bwBufferChunks) { for (auto& bwBufferChunk : bwBufferChunks) {
@ -681,7 +679,7 @@ void GfxRenderer::freeBwBufferChunks() {
* Returns true if buffer was stored successfully, false if allocation failed. * Returns true if buffer was stored successfully, false if allocation failed.
*/ */
bool GfxRenderer::storeBwBuffer() { bool GfxRenderer::storeBwBuffer() {
const uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); const uint8_t* frameBuffer = display.getFrameBuffer();
if (!frameBuffer) { if (!frameBuffer) {
Serial.printf("[%lu] [GFX] !! No framebuffer in storeBwBuffer\n", millis()); Serial.printf("[%lu] [GFX] !! No framebuffer in storeBwBuffer\n", millis());
return false; return false;
@ -736,7 +734,7 @@ void GfxRenderer::restoreBwBuffer() {
return; return;
} }
uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); uint8_t* frameBuffer = display.getFrameBuffer();
if (!frameBuffer) { if (!frameBuffer) {
Serial.printf("[%lu] [GFX] !! No framebuffer in restoreBwBuffer\n", millis()); Serial.printf("[%lu] [GFX] !! No framebuffer in restoreBwBuffer\n", millis());
freeBwBufferChunks(); freeBwBufferChunks();
@ -755,7 +753,7 @@ void GfxRenderer::restoreBwBuffer() {
memcpy(frameBuffer + offset, bwBufferChunks[i], BW_BUFFER_CHUNK_SIZE); memcpy(frameBuffer + offset, bwBufferChunks[i], BW_BUFFER_CHUNK_SIZE);
} }
einkDisplay.cleanupGrayscaleBuffers(frameBuffer); display.cleanupGrayscaleBuffers(frameBuffer);
freeBwBufferChunks(); freeBwBufferChunks();
Serial.printf("[%lu] [GFX] Restored and freed BW buffer chunks\n", millis()); Serial.printf("[%lu] [GFX] Restored and freed BW buffer chunks\n", millis());
@ -766,9 +764,9 @@ void GfxRenderer::restoreBwBuffer() {
* Use this when BW buffer was re-rendered instead of stored/restored. * Use this when BW buffer was re-rendered instead of stored/restored.
*/ */
void GfxRenderer::cleanupGrayscaleWithFrameBuffer() const { void GfxRenderer::cleanupGrayscaleWithFrameBuffer() const {
uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); uint8_t* frameBuffer = display.getFrameBuffer();
if (frameBuffer) { if (frameBuffer) {
einkDisplay.cleanupGrayscaleBuffers(frameBuffer); display.cleanupGrayscaleBuffers(frameBuffer);
} }
} }

View File

@ -1,7 +1,7 @@
#pragma once #pragma once
#include <EInkDisplay.h>
#include <EpdFontFamily.h> #include <EpdFontFamily.h>
#include <HalDisplay.h>
#include <map> #include <map>
@ -21,11 +21,11 @@ class GfxRenderer {
private: private:
static constexpr size_t BW_BUFFER_CHUNK_SIZE = 8000; // 8KB chunks to allow for non-contiguous memory 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 constexpr size_t BW_BUFFER_NUM_CHUNKS = HalDisplay::BUFFER_SIZE / BW_BUFFER_CHUNK_SIZE;
static_assert(BW_BUFFER_CHUNK_SIZE * BW_BUFFER_NUM_CHUNKS == EInkDisplay::BUFFER_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"); "BW buffer chunking does not line up with display buffer size");
EInkDisplay& einkDisplay; HalDisplay& display;
RenderMode renderMode; RenderMode renderMode;
Orientation orientation; Orientation orientation;
uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr}; 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; void rotateCoordinates(int x, int y, int* rotatedX, int* rotatedY) const;
public: public:
explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), renderMode(BW), orientation(Portrait) {} explicit GfxRenderer(HalDisplay& halDisplay) : display(halDisplay), renderMode(BW), orientation(Portrait) {}
~GfxRenderer() { freeBwBufferChunks(); } ~GfxRenderer() { freeBwBufferChunks(); }
static constexpr int VIEWABLE_MARGIN_TOP = 9; static constexpr int VIEWABLE_MARGIN_TOP = 9;
@ -54,7 +54,7 @@ class GfxRenderer {
// Screen ops // Screen ops
int getScreenWidth() const; int getScreenWidth() const;
int getScreenHeight() 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 // EXPERIMENTAL: Windowed update - display only a rectangular region
void displayWindow(int x, int y, int width, int height) const; void displayWindow(int x, int y, int width, int height) const;
void invertScreen() const; void invertScreen() const;

51
lib/hal/HalDisplay.cpp Normal file
View File

@ -0,0 +1,51 @@
#include <HalDisplay.h>
#include <HalGPIO.h>
#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(); }

52
lib/hal/HalDisplay.h Normal file
View File

@ -0,0 +1,52 @@
#pragma once
#include <Arduino.h>
#include <EInkDisplay.h>
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;
};

55
lib/hal/HalGPIO.cpp Normal file
View File

@ -0,0 +1,55 @@
#include <HalGPIO.h>
#include <SPI.h>
#include <esp_sleep.h>
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);
}
}

61
lib/hal/HalGPIO.h Normal file
View File

@ -0,0 +1,61 @@
#pragma once
#include <Arduino.h>
#include <BatteryMonitor.h>
#include <InputManager.h>
// 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;
};

View File

@ -19,20 +19,20 @@ struct SideLayoutMap {
// Order matches CrossPointSettings::FRONT_BUTTON_LAYOUT. // Order matches CrossPointSettings::FRONT_BUTTON_LAYOUT.
constexpr FrontLayoutMap kFrontLayouts[] = { constexpr FrontLayoutMap kFrontLayouts[] = {
{InputManager::BTN_BACK, InputManager::BTN_CONFIRM, InputManager::BTN_LEFT, InputManager::BTN_RIGHT}, {HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM, HalGPIO::BTN_LEFT, HalGPIO::BTN_RIGHT},
{InputManager::BTN_LEFT, InputManager::BTN_RIGHT, InputManager::BTN_BACK, InputManager::BTN_CONFIRM}, {HalGPIO::BTN_LEFT, HalGPIO::BTN_RIGHT, HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM},
{InputManager::BTN_CONFIRM, InputManager::BTN_LEFT, InputManager::BTN_BACK, InputManager::BTN_RIGHT}, {HalGPIO::BTN_CONFIRM, HalGPIO::BTN_LEFT, HalGPIO::BTN_BACK, HalGPIO::BTN_RIGHT},
{InputManager::BTN_BACK, InputManager::BTN_CONFIRM, InputManager::BTN_RIGHT, InputManager::BTN_LEFT}, {HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM, HalGPIO::BTN_RIGHT, HalGPIO::BTN_LEFT},
}; };
// Order matches CrossPointSettings::SIDE_BUTTON_LAYOUT. // Order matches CrossPointSettings::SIDE_BUTTON_LAYOUT.
constexpr SideLayoutMap kSideLayouts[] = { constexpr SideLayoutMap kSideLayouts[] = {
{InputManager::BTN_UP, InputManager::BTN_DOWN}, {HalGPIO::BTN_UP, HalGPIO::BTN_DOWN},
{InputManager::BTN_DOWN, InputManager::BTN_UP}, {HalGPIO::BTN_DOWN, HalGPIO::BTN_UP},
}; };
} // namespace } // namespace
bool MappedInputManager::mapButton(const Button button, bool (InputManager::*fn)(uint8_t) const) const { bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint8_t) const) const {
const auto frontLayout = static_cast<CrossPointSettings::FRONT_BUTTON_LAYOUT>(SETTINGS.frontButtonLayout); const auto frontLayout = static_cast<CrossPointSettings::FRONT_BUTTON_LAYOUT>(SETTINGS.frontButtonLayout);
const auto sideLayout = static_cast<CrossPointSettings::SIDE_BUTTON_LAYOUT>(SETTINGS.sideButtonLayout); const auto sideLayout = static_cast<CrossPointSettings::SIDE_BUTTON_LAYOUT>(SETTINGS.sideButtonLayout);
const auto& front = kFrontLayouts[frontLayout]; const auto& front = kFrontLayouts[frontLayout];
@ -40,41 +40,39 @@ bool MappedInputManager::mapButton(const Button button, bool (InputManager::*fn)
switch (button) { switch (button) {
case Button::Back: case Button::Back:
return (inputManager.*fn)(front.back); return (gpio.*fn)(front.back);
case Button::Confirm: case Button::Confirm:
return (inputManager.*fn)(front.confirm); return (gpio.*fn)(front.confirm);
case Button::Left: case Button::Left:
return (inputManager.*fn)(front.left); return (gpio.*fn)(front.left);
case Button::Right: case Button::Right:
return (inputManager.*fn)(front.right); return (gpio.*fn)(front.right);
case Button::Up: case Button::Up:
return (inputManager.*fn)(InputManager::BTN_UP); return (gpio.*fn)(HalGPIO::BTN_UP);
case Button::Down: case Button::Down:
return (inputManager.*fn)(InputManager::BTN_DOWN); return (gpio.*fn)(HalGPIO::BTN_DOWN);
case Button::Power: case Button::Power:
return (inputManager.*fn)(InputManager::BTN_POWER); return (gpio.*fn)(HalGPIO::BTN_POWER);
case Button::PageBack: case Button::PageBack:
return (inputManager.*fn)(side.pageBack); return (gpio.*fn)(side.pageBack);
case Button::PageForward: case Button::PageForward:
return (inputManager.*fn)(side.pageForward); return (gpio.*fn)(side.pageForward);
} }
return false; return false;
} }
bool MappedInputManager::wasPressed(const Button button) const { return mapButton(button, &InputManager::wasPressed); } bool MappedInputManager::wasPressed(const Button button) const { return mapButton(button, &HalGPIO::wasPressed); }
bool MappedInputManager::wasReleased(const Button button) const { bool MappedInputManager::wasReleased(const Button button) const { return mapButton(button, &HalGPIO::wasReleased); }
return mapButton(button, &InputManager::wasReleased);
}
bool MappedInputManager::isPressed(const Button button) const { return mapButton(button, &InputManager::isPressed); } 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, MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const char* confirm, const char* previous,
const char* next) const { const char* next) const {

View File

@ -1,6 +1,6 @@
#pragma once #pragma once
#include <InputManager.h> #include <HalGPIO.h>
class MappedInputManager { class MappedInputManager {
public: public:
@ -13,7 +13,7 @@ class MappedInputManager {
const char* btn4; const char* btn4;
}; };
explicit MappedInputManager(InputManager& inputManager) : inputManager(inputManager) {} explicit MappedInputManager(HalGPIO& gpio) : gpio(gpio) {}
bool wasPressed(Button button) const; bool wasPressed(Button button) const;
bool wasReleased(Button button) const; bool wasReleased(Button button) const;
@ -24,7 +24,7 @@ class MappedInputManager {
Labels mapLabels(const char* back, const char* confirm, const char* previous, const char* next) const; Labels mapLabels(const char* back, const char* confirm, const char* previous, const char* next) const;
private: private:
InputManager& inputManager; HalGPIO& gpio;
bool mapButton(Button button, bool (InputManager::*fn)(uint8_t) const) const; bool mapButton(Button button, bool (HalGPIO::*fn)(uint8_t) const) const;
}; };

View File

@ -133,7 +133,7 @@ void SleepActivity::renderDefaultSleepScreen() const {
renderer.invertScreen(); renderer.invertScreen();
} }
renderer.displayBuffer(EInkDisplay::HALF_REFRESH); renderer.displayBuffer(HalDisplay::HALF_REFRESH);
} }
void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const { void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
@ -189,7 +189,7 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
renderer.invertScreen(); renderer.invertScreen();
} }
renderer.displayBuffer(EInkDisplay::HALF_REFRESH); renderer.displayBuffer(HalDisplay::HALF_REFRESH);
if (hasGreyscale) { if (hasGreyscale) {
bitmap.rewindToData(); bitmap.rewindToData();
@ -280,5 +280,5 @@ void SleepActivity::renderCoverSleepScreen() const {
void SleepActivity::renderBlankSleepScreen() const { void SleepActivity::renderBlankSleepScreen() const {
renderer.clearScreen(); renderer.clearScreen();
renderer.displayBuffer(EInkDisplay::HALF_REFRESH); renderer.displayBuffer(HalDisplay::HALF_REFRESH);
} }

View File

@ -360,7 +360,7 @@ void EpubReaderActivity::renderScreen() {
auto progressCallback = [this, barX, barY, barWidth, barHeight](int progress) { auto progressCallback = [this, barX, barY, barWidth, barHeight](int progress) {
const int fillWidth = (barWidth - 2) * progress / 100; const int fillWidth = (barWidth - 2) * progress / 100;
renderer.fillRect(barX + 1, barY + 1, fillWidth, barHeight - 2, true); 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(), if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
@ -459,7 +459,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
if (pagesUntilFullRefresh <= 1) { if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(EInkDisplay::HALF_REFRESH); renderer.displayBuffer(HalDisplay::HALF_REFRESH);
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
} else { } else {
renderer.displayBuffer(); renderer.displayBuffer();

View File

@ -256,7 +256,7 @@ void TxtReaderActivity::buildPageIndex() {
// Fill progress bar // Fill progress bar
const int fillWidth = (barWidth - 2) * progressPercent / 100; const int fillWidth = (barWidth - 2) * progressPercent / 100;
renderer.fillRect(barX + 1, barY + 1, fillWidth, barHeight - 2, true); 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 // Yield to other tasks periodically
@ -484,7 +484,7 @@ void TxtReaderActivity::renderPage() {
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
if (pagesUntilFullRefresh <= 1) { if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(EInkDisplay::HALF_REFRESH); renderer.displayBuffer(HalDisplay::HALF_REFRESH);
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
} else { } else {
renderer.displayBuffer(); renderer.displayBuffer();

View File

@ -276,7 +276,7 @@ void XtcReaderActivity::renderPage() {
// Display BW with conditional refresh based on pagesUntilFullRefresh // Display BW with conditional refresh based on pagesUntilFullRefresh
if (pagesUntilFullRefresh <= 1) { if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(EInkDisplay::HALF_REFRESH); renderer.displayBuffer(HalDisplay::HALF_REFRESH);
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
} else { } else {
renderer.displayBuffer(); renderer.displayBuffer();
@ -356,7 +356,7 @@ void XtcReaderActivity::renderPage() {
// Display with appropriate refresh // Display with appropriate refresh
if (pagesUntilFullRefresh <= 1) { if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(EInkDisplay::HALF_REFRESH); renderer.displayBuffer(HalDisplay::HALF_REFRESH);
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
} else { } else {
renderer.displayBuffer(); renderer.displayBuffer();

View File

@ -1,6 +1,6 @@
#pragma once #pragma once
#include <EInkDisplay.h>
#include <EpdFontFamily.h> #include <EpdFontFamily.h>
#include <HalDisplay.h>
#include <string> #include <string>
#include <utility> #include <utility>
@ -10,12 +10,12 @@
class FullScreenMessageActivity final : public Activity { class FullScreenMessageActivity final : public Activity {
std::string text; std::string text;
EpdFontFamily::Style style; EpdFontFamily::Style style;
EInkDisplay::RefreshMode refreshMode; HalDisplay::RefreshMode refreshMode;
public: public:
explicit FullScreenMessageActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string text, explicit FullScreenMessageActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string text,
const EpdFontFamily::Style style = EpdFontFamily::REGULAR, const EpdFontFamily::Style style = EpdFontFamily::REGULAR,
const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH)
: Activity("FullScreenMessage", renderer, mappedInput), : Activity("FullScreenMessage", renderer, mappedInput),
text(std::move(text)), text(std::move(text)),
style(style), style(style),

View File

@ -1,8 +1,8 @@
#include <Arduino.h> #include <Arduino.h>
#include <EInkDisplay.h>
#include <Epub.h> #include <Epub.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <InputManager.h> #include <HalDisplay.h>
#include <HalGPIO.h>
#include <SDCardManager.h> #include <SDCardManager.h>
#include <SPI.h> #include <SPI.h>
#include <builtinFonts/all.h> #include <builtinFonts/all.h>
@ -26,23 +26,10 @@
#include "activities/util/FullScreenMessageActivity.h" #include "activities/util/FullScreenMessageActivity.h"
#include "fontIds.h" #include "fontIds.h"
#define SPI_FQ 40000000 HalDisplay display;
// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults) HalGPIO gpio;
#define EPD_SCLK 8 // SPI Clock MappedInputManager mappedInputManager(gpio);
#define EPD_MOSI 10 // SPI MOSI (Master Out Slave In) GfxRenderer renderer(display);
#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);
Activity* currentActivity; Activity* currentActivity;
// Fonts // Fonts
@ -170,21 +157,20 @@ void verifyPowerButtonDuration() {
const uint16_t calibratedPressDuration = const uint16_t calibratedPressDuration =
(calibration < SETTINGS.getPowerButtonDuration()) ? SETTINGS.getPowerButtonDuration() - calibration : 1; (calibration < SETTINGS.getPowerButtonDuration()) ? SETTINGS.getPowerButtonDuration() - calibration : 1;
inputManager.update(); gpio.update();
// Verify the user has actually pressed
// Needed because inputManager.isPressed() may take up to ~500ms to return the correct state // 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. delay(10); // only wait 10ms each iteration to not delay too much in case of short configured duration.
inputManager.update(); gpio.update();
} }
t2 = millis(); t2 = millis();
if (inputManager.isPressed(InputManager::BTN_POWER)) { if (gpio.isPressed(HalGPIO::BTN_POWER)) {
do { do {
delay(10); delay(10);
inputManager.update(); gpio.update();
} while (inputManager.isPressed(InputManager::BTN_POWER) && inputManager.getHeldTime() < calibratedPressDuration); } while (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.getHeldTime() < calibratedPressDuration);
abort = inputManager.getHeldTime() < calibratedPressDuration; abort = gpio.getHeldTime() < calibratedPressDuration;
} else { } else {
abort = true; abort = true;
} }
@ -192,16 +178,15 @@ void verifyPowerButtonDuration() {
if (abort) { if (abort) {
// Button released too early. Returning to sleep. // Button released too early. Returning to sleep.
// IMPORTANT: Re-arm the wakeup trigger before sleeping again // 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); gpio.startDeepSleep();
esp_deep_sleep_start();
} }
} }
void waitForPowerRelease() { void waitForPowerRelease() {
inputManager.update(); gpio.update();
while (inputManager.isPressed(InputManager::BTN_POWER)) { while (gpio.isPressed(HalGPIO::BTN_POWER)) {
delay(50); delay(50);
inputManager.update(); gpio.update();
} }
} }
@ -210,14 +195,11 @@ void enterDeepSleep() {
exitActivity(); exitActivity();
enterNewActivity(new SleepActivity(renderer, mappedInputManager)); 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] [ ] Power button press calibration value: %lu ms\n", millis(), t2 - t1);
Serial.printf("[%lu] [ ] Entering deep sleep.\n", millis()); 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 gpio.startDeepSleep();
waitForPowerRelease();
// Enter Deep Sleep
esp_deep_sleep_start();
} }
void onGoHome(); void onGoHome();
@ -261,7 +243,7 @@ void onGoHome() {
} }
void setupDisplayAndFonts() { void setupDisplayAndFonts() {
einkDisplay.begin(); display.begin();
Serial.printf("[%lu] [ ] Display initialized\n", millis()); Serial.printf("[%lu] [ ] Display initialized\n", millis());
renderer.insertFont(BOOKERLY_14_FONT_ID, bookerly14FontFamily); renderer.insertFont(BOOKERLY_14_FONT_ID, bookerly14FontFamily);
#ifndef OMIT_FONTS #ifndef OMIT_FONTS
@ -284,27 +266,13 @@ void setupDisplayAndFonts() {
Serial.printf("[%lu] [ ] Fonts setup\n", millis()); 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() { void setup() {
t1 = millis(); t1 = millis();
gpio.begin();
// Only start serial if USB connected // Only start serial if USB connected
pinMode(UART0_RXD, INPUT); if (gpio.isUsbConnected()) {
if (isUsbConnected()) {
Serial.begin(115200); Serial.begin(115200);
// Wait up to 3 seconds for Serial to be ready to catch early logs // Wait up to 3 seconds for Serial to be ready to catch early logs
unsigned long start = millis(); unsigned long start = millis();
@ -313,13 +281,6 @@ void setup() {
} }
} }
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 // SD Card Initialization
// We need 6 open files concurrently when parsing a new chapter // We need 6 open files concurrently when parsing a new chapter
if (!SdMan.begin()) { if (!SdMan.begin()) {
@ -333,7 +294,7 @@ void setup() {
SETTINGS.loadFromFile(); SETTINGS.loadFromFile();
KOREADER_STORE.loadFromFile(); KOREADER_STORE.loadFromFile();
if (isWakeupByPowerButton()) { if (gpio.isWakeupByPowerButton()) {
// For normal wakeups, verify power button press duration // For normal wakeups, verify power button press duration
Serial.printf("[%lu] [ ] Verifying power button press duration\n", millis()); Serial.printf("[%lu] [ ] Verifying power button press duration\n", millis());
verifyPowerButtonDuration(); verifyPowerButtonDuration();
@ -370,7 +331,7 @@ void loop() {
const unsigned long loopStartTime = millis(); const unsigned long loopStartTime = millis();
static unsigned long lastMemPrint = 0; static unsigned long lastMemPrint = 0;
inputManager.update(); gpio.update();
if (Serial && millis() - lastMemPrint >= 10000) { if (Serial && millis() - lastMemPrint >= 10000) {
Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(), Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(),
@ -380,8 +341,7 @@ void loop() {
// Check for any user activity (button press or release) or active background work // Check for any user activity (button press or release) or active background work
static unsigned long lastActivityTime = millis(); static unsigned long lastActivityTime = millis();
if (inputManager.wasAnyPressed() || inputManager.wasAnyReleased() || if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || (currentActivity && currentActivity->preventAutoSleep())) {
(currentActivity && currentActivity->preventAutoSleep())) {
lastActivityTime = millis(); // Reset inactivity timer lastActivityTime = millis(); // Reset inactivity timer
} }
@ -393,8 +353,7 @@ void loop() {
return; return;
} }
if (inputManager.isPressed(InputManager::BTN_POWER) && if (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.getHeldTime() > SETTINGS.getPowerButtonDuration()) {
inputManager.getHeldTime() > SETTINGS.getPowerButtonDuration()) {
enterDeepSleep(); enterDeepSleep();
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start // This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start
return; return;