add HalDisplay

This commit is contained in:
Xuan Son Nguyen 2026-01-22 20:16:35 +01:00
parent 080a9bb1bb
commit 58232d2483
10 changed files with 306 additions and 39 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;
}
@ -49,14 +49,14 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
rotateCoordinates(x, y, &rotatedX, &rotatedY);
// Bounds checking against physical panel dimensions
if (rotatedX < 0 || rotatedX >= EInkDisplay::DISPLAY_WIDTH || rotatedY < 0 ||
rotatedY >= EInkDisplay::DISPLAY_HEIGHT) {
if (rotatedX < 0 || rotatedX >= HalDisplay::DISPLAY_WIDTH || rotatedY < 0 ||
rotatedY >= HalDisplay::DISPLAY_HEIGHT) {
Serial.printf("[%lu] [GFX] !! Outside range (%d, %d) -> (%d, %d)\n", millis(), x, y, rotatedX, rotatedY);
return;
}
// Calculate byte position and bit position
const uint16_t byteIndex = rotatedY * EInkDisplay::DISPLAY_WIDTH_BYTES + (rotatedX / 8);
const uint16_t byteIndex = rotatedY * HalDisplay::DISPLAY_WIDTH_BYTES + (rotatedX / 8);
const uint8_t bitPosition = 7 - (rotatedX % 8); // MSB first
if (state) {
@ -392,12 +392,12 @@ void GfxRenderer::invertScreen() const {
Serial.printf("[%lu] [GFX] !! No framebuffer in invertScreen\n", millis());
return;
}
for (int i = 0; i < EInkDisplay::BUFFER_SIZE; i++) {
for (int i = 0; i < HalDisplay::BUFFER_SIZE; i++) {
buffer[i] = ~buffer[i];
}
}
void GfxRenderer::displayBuffer(const EInkDisplay::RefreshMode refreshMode) const {
void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const {
einkDisplay.displayBuffer(refreshMode);
}
@ -418,13 +418,13 @@ int GfxRenderer::getScreenWidth() const {
case Portrait:
case PortraitInverted:
// 480px wide in portrait logical coordinates
return EInkDisplay::DISPLAY_HEIGHT;
return HalDisplay::DISPLAY_HEIGHT;
case LandscapeClockwise:
case LandscapeCounterClockwise:
// 800px wide in landscape logical coordinates
return EInkDisplay::DISPLAY_WIDTH;
return HalDisplay::DISPLAY_WIDTH;
}
return EInkDisplay::DISPLAY_HEIGHT;
return HalDisplay::DISPLAY_HEIGHT;
}
int GfxRenderer::getScreenHeight() const {
@ -432,13 +432,13 @@ int GfxRenderer::getScreenHeight() const {
case Portrait:
case PortraitInverted:
// 800px tall in portrait logical coordinates
return EInkDisplay::DISPLAY_WIDTH;
return HalDisplay::DISPLAY_WIDTH;
case LandscapeClockwise:
case LandscapeCounterClockwise:
// 480px tall in landscape logical coordinates
return EInkDisplay::DISPLAY_HEIGHT;
return HalDisplay::DISPLAY_HEIGHT;
}
return EInkDisplay::DISPLAY_WIDTH;
return HalDisplay::DISPLAY_WIDTH;
}
int GfxRenderer::getSpaceWidth(const int fontId) const {
@ -640,9 +640,7 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
uint8_t* GfxRenderer::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); }
size_t GfxRenderer::getBufferSize() { return EInkDisplay::BUFFER_SIZE; }
void GfxRenderer::grayscaleRevert() const { einkDisplay.grayscaleRevert(); }
size_t GfxRenderer::getBufferSize() { return HalDisplay::BUFFER_SIZE; }
void GfxRenderer::copyGrayscaleLsbBuffers() const { einkDisplay.copyGrayscaleLsbBuffers(einkDisplay.getFrameBuffer()); }

View File

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

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

@ -0,0 +1,212 @@
#include <HalDisplay.h>
#include <string>
std::string base64_encode(char* buf, unsigned int bufLen);
HalDisplay::HalDisplay(int8_t sclk, int8_t mosi, int8_t cs, int8_t dc, int8_t rst, int8_t busy) : einkDisplay(sclk, mosi, cs, dc, rst, busy) {
if (is_emulated) {
emuFramebuffer0 = new uint8_t[BUFFER_SIZE];
}
}
HalDisplay::~HalDisplay() {
if (emuFramebuffer0) {
delete[] emuFramebuffer0;
emuFramebuffer0 = nullptr;
}
}
void HalDisplay::begin() {
if (!is_emulated) {
einkDisplay.begin();
} else {
Serial.printf("[%lu] [ ] Emulated display initialized\n", millis());
// no-op
}
}
void HalDisplay::clearScreen(uint8_t color) const {
if (!is_emulated) {
einkDisplay.clearScreen(color);
} else {
Serial.printf("[%lu] [ ] Emulated clear screen with color 0x%02X\n", millis(), color);
memset(emuFramebuffer0, color, BUFFER_SIZE);
}
}
void HalDisplay::drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h, bool fromProgmem) const {
if (!is_emulated) {
einkDisplay.drawImage(imageData, x, y, w, h, fromProgmem);
} else {
Serial.printf("[%lu] [ ] Emulated draw image at (%u, %u) with size %ux%u\n", millis(), x, y, w, h);
// Calculate bytes per line for the image
const uint16_t imageWidthBytes = w / 8;
// Copy image data to frame buffer
for (uint16_t row = 0; row < h; row++) {
const uint16_t destY = y + row;
if (destY >= DISPLAY_HEIGHT)
break;
const uint16_t destOffset = destY * DISPLAY_WIDTH_BYTES + (x / 8);
const uint16_t srcOffset = row * imageWidthBytes;
for (uint16_t col = 0; col < imageWidthBytes; col++) {
if ((x / 8 + col) >= DISPLAY_WIDTH_BYTES)
break;
if (fromProgmem) {
emuFramebuffer0[destOffset + col] = pgm_read_byte(&imageData[srcOffset + col]);
} else {
emuFramebuffer0[destOffset + col] = imageData[srcOffset + col];
}
}
}
}
}
void HalDisplay::displayBuffer(RefreshMode mode) {
if (!is_emulated) {
einkDisplay.displayBuffer(mode);
} else {
Serial.printf("[%lu] [ ] Emulated display buffer with mode %d\n", millis(), static_cast<int>(mode));
std::string b64 = base64_encode(reinterpret_cast<char*>(emuFramebuffer0), BUFFER_SIZE);
Serial.printf("$$DATA:DISPLAY:{\"mode\":%d,\"buffer\":\"", static_cast<int>(mode));
Serial.print(b64.c_str());
Serial.print("\"}$$\n");
}
}
void HalDisplay::refreshDisplay(RefreshMode mode, bool turnOffScreen) {
if (!is_emulated) {
einkDisplay.refreshDisplay(mode, turnOffScreen);
} else {
Serial.printf("[%lu] [ ] Emulated refresh display with mode %d, turnOffScreen %d\n", millis(), static_cast<int>(mode), turnOffScreen);
// emulated delay
if (mode == RefreshMode::FAST_REFRESH) {
delay(50);
} else if (mode == RefreshMode::HALF_REFRESH) {
delay(800);
} else if (mode == RefreshMode::FULL_REFRESH) {
delay(1500);
}
}
}
void HalDisplay::deepSleep() {
if (!is_emulated) {
einkDisplay.deepSleep();
} else {
Serial.printf("[%lu] [ ] Emulated deep sleep\n", millis());
// no-op
}
}
uint8_t* HalDisplay::getFrameBuffer() const {
if (!is_emulated) {
return einkDisplay.getFrameBuffer();
} else {
return emuFramebuffer0;
}
}
void HalDisplay::copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer) {
if (!is_emulated) {
einkDisplay.copyGrayscaleBuffers(lsbBuffer, msbBuffer);
} else {
Serial.printf("[%lu] [ ] Emulated copy grayscale buffers\n", millis());
// TODO: not sure what this does
}
}
void HalDisplay::copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer) {
if (!is_emulated) {
einkDisplay.copyGrayscaleLsbBuffers(lsbBuffer);
} else {
Serial.printf("[%lu] [ ] Emulated copy grayscale LSB buffers\n", millis());
// TODO: not sure what this does
}
}
void HalDisplay::copyGrayscaleMsbBuffers(const uint8_t* msbBuffer) {
if (!is_emulated) {
einkDisplay.copyGrayscaleMsbBuffers(msbBuffer);
} else {
Serial.printf("[%lu] [ ] Emulated copy grayscale MSB buffers\n", millis());
// TODO: not sure what this does
}
}
void HalDisplay::cleanupGrayscaleBuffers(const uint8_t* bwBuffer) {
if (!is_emulated) {
einkDisplay.cleanupGrayscaleBuffers(bwBuffer);
} else {
Serial.printf("[%lu] [ ] Emulated cleanup grayscale buffers\n", millis());
// TODO: not sure what this does
}
}
void HalDisplay::displayGrayBuffer() {
if (!is_emulated) {
einkDisplay.displayGrayBuffer();
} else {
Serial.printf("[%lu] [ ] Emulated display gray buffer\n", millis());
// TODO: not sure what this does
}
}
//
// Base64 utilities
//
static const std::string base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
static inline bool is_base64(char c) {
return (isalnum(c) || (c == '+') || (c == '/'));
}
std::string base64_encode(char* buf, unsigned int bufLen) {
std::string ret;
ret.reserve(bufLen * 4 / 3 + 4); // reserve enough space
int i = 0;
int j = 0;
char char_array_3[3];
char char_array_4[4];
while (bufLen--) {
char_array_3[i++] = *(buf++);
if (i == 3) {
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
char_array_4[3] = char_array_3[2] & 0x3f;
for(i = 0; (i < 4) ; i++)
ret += base64_chars[char_array_4[i]];
i = 0;
}
}
if (i) {
for(j = i; j < 3; j++) {
char_array_3[j] = '\0';
}
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
char_array_4[3] = char_array_3[2] & 0x3f;
for (j = 0; (j < i + 1); j++)
ret += base64_chars[char_array_4[j]];
while((i++ < 3))
ret += '=';
}
return ret;
}

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

@ -0,0 +1,57 @@
#pragma once
#include <EInkDisplay.h>
#include <Arduino.h>
#include <SPI.h>
class HalDisplay {
public:
// Constructor with pin configuration
HalDisplay(int8_t sclk, int8_t mosi, int8_t cs, int8_t dc, int8_t rst, int8_t busy);
// Destructor
~HalDisplay();
// Refresh modes (guarded to avoid redefinition in test builds)
using RefreshMode = EInkDisplay::RefreshMode;
static constexpr EInkDisplay::RefreshMode FULL_REFRESH = EInkDisplay::FULL_REFRESH;
static constexpr EInkDisplay::RefreshMode HALF_REFRESH = EInkDisplay::HALF_REFRESH;
static constexpr EInkDisplay::RefreshMode FAST_REFRESH = EInkDisplay::FAST_REFRESH;
// Initialize the display hardware and driver
void begin();
// Display dimensions
static constexpr uint16_t DISPLAY_WIDTH = 800;
static constexpr uint16_t DISPLAY_HEIGHT = 480;
static constexpr uint16_t DISPLAY_WIDTH_BYTES = DISPLAY_WIDTH / 8;
static constexpr uint32_t BUFFER_SIZE = DISPLAY_WIDTH_BYTES * DISPLAY_HEIGHT;
// Frame buffer operations
void clearScreen(uint8_t color = 0xFF) const;
void drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h, bool fromProgmem = false) const;
void displayBuffer(RefreshMode mode = RefreshMode::FAST_REFRESH);
void refreshDisplay(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
// Power management
void deepSleep();
// Access to frame buffer
uint8_t* getFrameBuffer() const;
void copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer);
void copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer);
void copyGrayscaleMsbBuffers(const uint8_t* msbBuffer);
void cleanupGrayscaleBuffers(const uint8_t* bwBuffer);
void displayGrayBuffer();
private:
bool is_emulated = CROSSPOINT_EMULATED;
// real display implementation
EInkDisplay einkDisplay;
// emulated display implementation
uint8_t* emuFramebuffer0 = nullptr;
};

View File

@ -133,7 +133,7 @@ void SleepActivity::renderDefaultSleepScreen() const {
renderer.invertScreen();
}
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
}
void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
@ -180,7 +180,7 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
Serial.printf("[%lu] [SLP] drawing to %d x %d\n", millis(), x, y);
renderer.clearScreen();
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
if (bitmap.hasGreyscale()) {
bitmap.rewindToData();
@ -270,5 +270,5 @@ void SleepActivity::renderCoverSleepScreen() const {
void SleepActivity::renderBlankSleepScreen() const {
renderer.clearScreen();
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
}

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
#pragma once
#include <EInkDisplay.h>
#include <HalDisplay.h>
#include <EpdFontFamily.h>
#include <string>
@ -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,5 +1,5 @@
#include <Arduino.h>
#include <EInkDisplay.h>
#include <HalDisplay.h>
#include <Epub.h>
#include <GfxRenderer.h>
#include <InputManager.h>
@ -39,7 +39,7 @@
#define SD_SPI_MISO 7
EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY);
HalDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY);
InputManager inputManager;
MappedInputManager mappedInputManager(inputManager);
GfxRenderer renderer(einkDisplay);