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.
This commit is contained in:
cottongin 2026-01-30 22:49:52 -05:00
parent 448ce55bb4
commit be8b02efd6
No known key found for this signature in database
GPG Key ID: 0ECC91FE4655C262
14 changed files with 357 additions and 206 deletions

View File

@ -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);
}
}

View File

@ -1,7 +1,7 @@
#pragma once
#include <EInkDisplay.h>
#include <EpdFontFamily.h>
#include <HalDisplay.h>
#include <map>
@ -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;

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

@ -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<CrossPointSettings::FRONT_BUTTON_LAYOUT>(SETTINGS.frontButtonLayout);
const auto sideLayout = static_cast<CrossPointSettings::SIDE_BUTTON_LAYOUT>(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 {

View File

@ -1,6 +1,6 @@
#pragma once
#include <InputManager.h>
#include <HalGPIO.h>
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;
};

View File

@ -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) {

View File

@ -1,6 +1,6 @@
#include "EpubWordSelectionActivity.h"
#include <EInkDisplay.h>
#include <HalDisplay.h>
#include <GfxRenderer.h>
#include <algorithm>
@ -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);
}

View File

@ -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> 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();

View File

@ -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();

View File

@ -1,6 +1,6 @@
#pragma once
#include <EInkDisplay.h>
#include <EpdFontFamily.h>
#include <HalDisplay.h>
#include <string>
#include <utility>
@ -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),

View File

@ -1,9 +1,9 @@
#include <Arduino.h>
#include <BitmapHelpers.h>
#include <EInkDisplay.h>
#include <Epub.h>
#include <GfxRenderer.h>
#include <InputManager.h>
#include <HalDisplay.h>
#include <HalGPIO.h>
#include <SDCardManager.h>
#include <SPI.h>
#include <builtinFonts/all.h>
@ -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;