First pass at moving to SDK EInkDisplay library

This commit is contained in:
Dave Allie 2025-12-08 02:36:21 +11:00
parent de453fed1d
commit 9e046d7086
No known key found for this signature in database
GPG Key ID: F2FDDB3AD8D0276F
8 changed files with 166 additions and 77 deletions

View File

@ -9,18 +9,19 @@ inline int max(const int a, const int b) { return a > b ? a : b; }
template <typename Renderable>
class EpdFontRenderer {
Renderable& renderer;
void renderChar(uint32_t cp, int* x, const int* y, uint16_t color, EpdFontStyle style = REGULAR);
void renderChar(uint32_t cp, int* x, const int* y, bool pixelState, EpdFontStyle style = REGULAR);
public:
const EpdFontFamily* fontFamily;
explicit EpdFontRenderer(const EpdFontFamily* fontFamily, Renderable& renderer)
: fontFamily(fontFamily), renderer(renderer) {}
~EpdFontRenderer() = default;
void renderString(const char* string, int* x, int* y, uint16_t color, EpdFontStyle style = REGULAR);
void renderString(const char* string, int* x, int* y, bool pixelState = true, EpdFontStyle style = REGULAR);
void drawPixel(int x, int y, bool pixelState);
};
template <typename Renderable>
void EpdFontRenderer<Renderable>::renderString(const char* string, int* x, int* y, const uint16_t color,
void EpdFontRenderer<Renderable>::renderString(const char* string, int* x, int* y, const bool pixelState,
const EpdFontStyle style) {
// cannot draw a NULL / empty string
if (string == nullptr || *string == '\0') {
@ -34,14 +35,48 @@ void EpdFontRenderer<Renderable>::renderString(const char* string, int* x, int*
uint32_t cp;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
renderChar(cp, x, y, color, style);
renderChar(cp, x, y, pixelState, style);
}
*y += fontFamily->getData(style)->advanceY;
}
// TODO: Consolidate this with EpdRenderer implementation
template <typename Renderable>
void EpdFontRenderer<Renderable>::renderChar(const uint32_t cp, int* x, const int* y, uint16_t color,
void EpdFontRenderer<Renderable>::drawPixel(const int x, const int y, const bool pixelState) {
uint8_t* frameBuffer = renderer.getFrameBuffer();
// Early return if no framebuffer is set
if (!frameBuffer) {
Serial.printf("!!No framebuffer\n");
return;
}
// Bounds checking (portrait: 480x800)
if (x < 0 || x >= EInkDisplay::DISPLAY_HEIGHT || y < 0 || y >= EInkDisplay::DISPLAY_WIDTH) {
Serial.printf("!!Outside range (%d, %d)\n", x, y);
return;
}
// Rotate coordinates: portrait (480x800) -> landscape (800x480)
// Rotation: 90 degrees clockwise
const int16_t rotatedX = y;
const int16_t rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x;
// Calculate byte position and bit position
const uint16_t byteIndex = rotatedY * EInkDisplay::DISPLAY_WIDTH_BYTES + (rotatedX / 8);
const uint8_t bitPosition = 7 - (rotatedX % 8); // MSB first
// Set or clear the bit
if (pixelState) {
frameBuffer[byteIndex] &= ~(1 << bitPosition); // Clear bit
} else {
frameBuffer[byteIndex] |= (1 << bitPosition); // Set bit
}
}
template <typename Renderable>
void EpdFontRenderer<Renderable>::renderChar(const uint32_t cp, int* x, const int* y, const bool pixelState,
const EpdFontStyle style) {
const EpdGlyph* glyph = fontFamily->getGlyph(cp, style);
if (!glyph) {
@ -74,7 +109,7 @@ void EpdFontRenderer<Renderable>::renderChar(const uint32_t cp, int* x, const in
const uint8_t bit_index = 7 - (pixelPosition % 8);
if ((byte >> bit_index) & 1) {
renderer.drawPixel(screenX, screenY, color);
drawPixel(screenX, screenY, pixelState);
}
}
}

View File

@ -21,11 +21,16 @@ EpdFont ubuntu10Font(&ubuntu_10);
EpdFont ununtuBold10Font(&ubuntu_bold_10);
EpdFontFamily ubuntuFontFamily(&ubuntu10Font, &ununtuBold10Font);
EpdRenderer::EpdRenderer(XteinkDisplay& display)
: display(display), marginTop(11), marginBottom(30), marginLeft(10), marginRight(10), lineCompression(0.95f) {
this->regularFontRenderer = new EpdFontRenderer<XteinkDisplay>(&bookerlyFontFamily, display);
this->smallFontRenderer = new EpdFontRenderer<XteinkDisplay>(&smallFontFamily, display);
this->uiFontRenderer = new EpdFontRenderer<XteinkDisplay>(&ubuntuFontFamily, display);
EpdRenderer::EpdRenderer(EInkDisplay& einkDisplay)
: einkDisplay(einkDisplay),
marginTop(11),
marginBottom(30),
marginLeft(10),
marginRight(10),
lineCompression(0.95f) {
this->regularFontRenderer = new EpdFontRenderer<EInkDisplay>(&bookerlyFontFamily, einkDisplay);
this->smallFontRenderer = new EpdFontRenderer<EInkDisplay>(&smallFontFamily, einkDisplay);
this->uiFontRenderer = new EpdFontRenderer<EInkDisplay>(&ubuntuFontFamily, einkDisplay);
}
EpdRenderer::~EpdRenderer() {
@ -34,6 +39,40 @@ EpdRenderer::~EpdRenderer() {
delete uiFontRenderer;
}
void EpdRenderer::drawPixel(const int x, const int y, const bool state) const {
uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
// Early return if no framebuffer is set
if (!frameBuffer) {
Serial.printf("!!No framebuffer\n");
return;
}
const int adjX = x + marginLeft;
const int adjY = y + marginTop;
// Bounds checking (portrait: 480x800)
if (adjX < 0 || adjX >= EInkDisplay::DISPLAY_HEIGHT || adjY < 0 || adjY >= EInkDisplay::DISPLAY_WIDTH) {
Serial.printf("!!Outside range (%d, %d)\n", adjX, adjY);
return;
}
// Rotate coordinates: portrait (480x800) -> landscape (800x480)
// Rotation: 90 degrees clockwise
const int rotatedX = adjY;
const int rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - adjX;
// Calculate byte position and bit position
const uint16_t byteIndex = rotatedY * EInkDisplay::DISPLAY_WIDTH_BYTES + (rotatedX / 8);
const uint8_t bitPosition = 7 - (rotatedX % 8); // MSB first
if (state) {
frameBuffer[byteIndex] &= ~(1 << bitPosition); // Clear bit
} else {
frameBuffer[byteIndex] |= 1 << bitPosition; // Set bit
}
}
int EpdRenderer::getTextWidth(const char* text, const EpdFontStyle style) const {
int w = 0, h = 0;
@ -58,25 +97,25 @@ int EpdRenderer::getSmallTextWidth(const char* text, const EpdFontStyle style) c
return w;
}
void EpdRenderer::drawText(const int x, const int y, const char* text, const uint16_t color,
void EpdRenderer::drawText(const int x, const int y, const char* text, const bool state,
const EpdFontStyle style) const {
int ypos = y + getLineHeight() + marginTop;
int xpos = x + marginLeft;
regularFontRenderer->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE, style);
regularFontRenderer->renderString(text, &xpos, &ypos, state, style);
}
void EpdRenderer::drawUiText(const int x, const int y, const char* text, const uint16_t color,
void EpdRenderer::drawUiText(const int x, const int y, const char* text, const bool state,
const EpdFontStyle style) const {
int ypos = y + uiFontRenderer->fontFamily->getData(style)->advanceY + marginTop;
int xpos = x + marginLeft;
uiFontRenderer->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE, style);
uiFontRenderer->renderString(text, &xpos, &ypos, state, style);
}
void EpdRenderer::drawSmallText(const int x, const int y, const char* text, const uint16_t color,
void EpdRenderer::drawSmallText(const int x, const int y, const char* text, const bool state,
const EpdFontStyle style) const {
int ypos = y + smallFontRenderer->fontFamily->getData(style)->advanceY + marginTop;
int xpos = x + marginLeft;
smallFontRenderer->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE, style);
smallFontRenderer->renderString(text, &xpos, &ypos, state, style);
}
void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text, const int width, const int height,
@ -115,51 +154,67 @@ void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text,
}
}
void EpdRenderer::drawLine(int x1, int y1, int x2, int y2, uint16_t color) const {
display.drawLine(x1 + marginLeft, y1 + marginTop, x2 + marginLeft, y2 + marginTop,
color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
void EpdRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) const {
if (x1 == x2) {
if (y2 < y1) {
std::swap(y1, y2);
}
for (int y = y1; y <= y2; y++) {
drawPixel(x1, y, state);
}
} else if (y1 == y2) {
if (x2 < x1) {
std::swap(x1, x2);
}
for (int x = x1; x <= x2; x++) {
drawPixel(x, y1, state);
}
} else {
// TODO: Implement
Serial.println("Line drawing not supported");
}
}
void EpdRenderer::drawRect(const int x, const int y, const int width, const int height, const uint16_t color) const {
display.drawRect(x + marginLeft, y + marginTop, width, height, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
void EpdRenderer::drawRect(const int x, const int y, const int width, const int height, const bool state) const {
drawLine(x, y, x + width - 1, y, state);
drawLine(x + width - 1, y, x + width - 1, y + height - 1, state);
drawLine(x + width - 1, y + height - 1, x, y + height - 1, state);
drawLine(x, y, x, y + height - 1, state);
}
void EpdRenderer::fillRect(const int x, const int y, const int width, const int height, const uint16_t color) const {
display.fillRect(x + marginLeft, y + marginTop, width, height, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
void EpdRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const {
for (int fillY = y; fillY < y + height; fillY++) {
drawLine(x, fillY, x + width - 1, fillY, state);
}
}
void EpdRenderer::drawCircle(const int x, const int y, const int radius, const uint16_t color) const {
display.drawCircle(x + marginLeft, y + marginTop, radius, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
void EpdRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const {
drawImageNoMargin(bitmap, x + marginLeft, y + marginTop, width, height);
}
void EpdRenderer::fillCircle(const int x, const int y, const int radius, const uint16_t color) const {
display.fillCircle(x + marginLeft, y + marginTop, radius, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
// TODO: Support y-mirror?
void EpdRenderer::drawImageNoMargin(const uint8_t bitmap[], const int x, const int y, const int width,
const int height) const {
einkDisplay.drawImage(bitmap, x, y, width, height);
}
void EpdRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height,
const bool invert, const bool mirrorY) const {
drawImageNoMargin(bitmap, x + marginLeft, y + marginTop, width, height, invert, mirrorY);
}
void EpdRenderer::drawImageNoMargin(const uint8_t bitmap[], const int x, const int y, const int width, const int height,
const bool invert, const bool mirrorY) const {
display.drawImage(bitmap, x, y, width, height, invert, mirrorY);
}
void EpdRenderer::clearScreen(const bool black) const {
void EpdRenderer::clearScreen(const uint8_t color) const {
Serial.println("Clearing screen");
display.fillScreen(black ? GxEPD_BLACK : GxEPD_WHITE);
einkDisplay.clearScreen(color);
}
void EpdRenderer::flushDisplay(const bool partialUpdate) const { display.display(partialUpdate); }
void EpdRenderer::flushDisplay(const bool partialUpdate) const {
einkDisplay.displayBuffer(partialUpdate ? EInkDisplay::FAST_REFRESH : EInkDisplay::FULL_REFRESH);
}
// TODO: Support partial window update
void EpdRenderer::flushArea(const int x, const int y, const int width, const int height) const {
display.displayWindow(x + marginLeft, y + marginTop, width, height);
einkDisplay.displayBuffer();
}
int EpdRenderer::getPageWidth() const { return display.width() - marginLeft - marginRight; }
int EpdRenderer::getPageWidth() const { return EInkDisplay::DISPLAY_HEIGHT - marginLeft - marginRight; }
int EpdRenderer::getPageHeight() const { return display.height() - marginTop - marginBottom; }
int EpdRenderer::getPageHeight() const { return EInkDisplay::DISPLAY_WIDTH - marginTop - marginBottom; }
int EpdRenderer::getSpaceWidth() const { return regularFontRenderer->fontFamily->getGlyph(' ', REGULAR)->advanceX; }

View File

@ -1,16 +1,14 @@
#pragma once
#include <GxEPD2_BW.h>
#include <EInkDisplay.h>
#include <EpdFontRenderer.hpp>
#define XteinkDisplay GxEPD2_BW<GxEPD2_426_GDEQ0426T82, GxEPD2_426_GDEQ0426T82::HEIGHT>
class EpdRenderer {
XteinkDisplay& display;
EpdFontRenderer<XteinkDisplay>* regularFontRenderer;
EpdFontRenderer<XteinkDisplay>* smallFontRenderer;
EpdFontRenderer<XteinkDisplay>* uiFontRenderer;
EInkDisplay& einkDisplay;
EpdFontRenderer<EInkDisplay>* regularFontRenderer;
EpdFontRenderer<EInkDisplay>* smallFontRenderer;
EpdFontRenderer<EInkDisplay>* uiFontRenderer;
int marginTop;
int marginBottom;
int marginLeft;
@ -18,25 +16,23 @@ class EpdRenderer {
float lineCompression;
public:
explicit EpdRenderer(XteinkDisplay& display);
explicit EpdRenderer(EInkDisplay& einkDisplay);
~EpdRenderer();
void drawPixel(int x, int y, bool state = true) const;
int getTextWidth(const char* text, EpdFontStyle style = REGULAR) const;
int getUiTextWidth(const char* text, EpdFontStyle style = REGULAR) const;
int getSmallTextWidth(const char* text, EpdFontStyle style = REGULAR) const;
void drawText(int x, int y, const char* text, uint16_t color = 1, EpdFontStyle style = REGULAR) const;
void drawUiText(int x, int y, const char* text, uint16_t color = 1, EpdFontStyle style = REGULAR) const;
void drawSmallText(int x, int y, const char* text, uint16_t color = 1, EpdFontStyle style = REGULAR) const;
void drawText(int x, int y, const char* text, bool state = true, EpdFontStyle style = REGULAR) const;
void drawUiText(int x, int y, const char* text, bool state = true, EpdFontStyle style = REGULAR) const;
void drawSmallText(int x, int y, const char* text, bool state = true, EpdFontStyle style = REGULAR) const;
void drawTextBox(int x, int y, const std::string& text, int width, int height, EpdFontStyle style = REGULAR) const;
void drawLine(int x1, int y1, int x2, int y2, uint16_t color = 1) const;
void drawRect(int x, int y, int width, int height, uint16_t color = 1) const;
void fillRect(int x, int y, int width, int height, uint16_t color = 1) const;
void drawCircle(int x, int y, int radius, uint16_t color = 1) const;
void fillCircle(int x, int y, int radius, uint16_t color = 1) const;
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height, bool invert = false,
bool mirrorY = false) const;
void drawImageNoMargin(const uint8_t bitmap[], int x, int y, int width, int height, bool invert = false,
bool mirrorY = false) const;
void clearScreen(bool black = false) const;
void drawLine(int x1, int y1, int x2, int y2, bool state = true) const;
void drawRect(int x, int y, int width, int height, bool state = true) const;
void fillRect(int x, int y, int width, int height, bool state = true) const;
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
void drawImageNoMargin(const uint8_t bitmap[], int x, int y, int width, int height) const;
void clearScreen(uint8_t color = 0xFF) const;
void flushDisplay(bool partialUpdate = true) const;
void flushArea(int x, int y, int width, int height) const;
@ -44,6 +40,7 @@ class EpdRenderer {
int getPageHeight() const;
int getSpaceWidth() const;
int getLineHeight() const;
// set margins
void setMarginTop(const int newMarginTop) { this->marginTop = newMarginTop; }
void setMarginBottom(const int newMarginBottom) { this->marginBottom = newMarginBottom; }

@ -1 +1 @@
Subproject commit 8224d278c58e76abf781c2e015f28a09419f27b2
Subproject commit fe963b32821dec7fb22abbfa22daa8fc67f286d1

View File

@ -34,7 +34,7 @@ build_flags =
; Libraries
lib_deps =
zinggjm/GxEPD2@^1.6.5
https://github.com/leethomason/tinyxml2.git#11.0.0
BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor
InputManager=symlink://open-x4-sdk/libs/hardware/InputManager
EInkDisplay=symlink://open-x4-sdk/libs/display/EInkDisplay

View File

@ -1,7 +1,7 @@
#include <Arduino.h>
#include <EInkDisplay.h>
#include <EpdRenderer.h>
#include <Epub.h>
#include <GxEPD2_BW.h>
#include <InputManager.h>
#include <SD.h>
#include <SPI.h>
@ -28,10 +28,9 @@
#define SD_SPI_CS 12
#define SD_SPI_MISO 7
GxEPD2_BW<GxEPD2_426_GDEQ0426T82, GxEPD2_426_GDEQ0426T82::HEIGHT> display(GxEPD2_426_GDEQ0426T82(EPD_CS, EPD_DC,
EPD_RST, EPD_BUSY));
EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY);
InputManager inputManager;
EpdRenderer renderer(display);
EpdRenderer renderer(einkDisplay);
Screen* currentScreen;
CrossPointState appState;
@ -123,7 +122,7 @@ void enterDeepSleep() {
// Enable Wakeup on LOW (button press)
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
display.hibernate();
einkDisplay.deepSleep();
// Enter Deep Sleep
esp_deep_sleep_start();
@ -170,10 +169,7 @@ void setup() {
SPI.begin(EPD_SCLK, SD_SPI_MISO, EPD_MOSI, EPD_CS);
// Initialize display
const SPISettings spi_settings(SPI_FQ, MSBFIRST, SPI_MODE0);
display.init(115200, true, 2, false, SPI, spi_settings);
display.setRotation(3); // 270 degrees
display.setTextColor(GxEPD_BLACK);
einkDisplay.begin();
Serial.println("Display initialized");
exitScreen();

View File

@ -11,4 +11,5 @@ void BootLogoScreen::onEnter() {
renderer.clearScreen();
// Location for images is from top right in landscape orientation
renderer.drawImage(CrossLarge, (pageHeight - 128) / 2, (pageWidth - 128) / 2, 128, 128);
renderer.flushDisplay();
}

View File

@ -4,4 +4,9 @@
#include "images/SleepScreenImg.h"
void SleepScreen::onEnter() { renderer.drawImageNoMargin(SleepScreenImg, 0, 0, 800, 480, false, true); }
void SleepScreen::onEnter() {
renderer.clearScreen();
renderer.flushDisplay();
renderer.drawImageNoMargin(SleepScreenImg, 0, 0, 800, 480);
renderer.flushDisplay();
}