Move to SDK EInkDisplay and enable anti-aliased 2-bit text (#5)

* First pass at moving to SDK EInkDisplay library

* Add 2-bit grayscale text and anti-aliased rendering of text

* Render status bar for empty chapters

* Refresh screen every 15 pages to avoid ghosting

* Simplify boot and sleep screens

* Give FileSelectionScreen task more stack memory

* Move text around slightly on Boot and Sleep screens

* Re-use existing buffer and write to whole screen for 'partial update'
This commit is contained in:
Dave Allie 2025-12-08 19:48:49 +11:00 committed by GitHub
parent de453fed1d
commit 2ed8017aa2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 11300 additions and 9243 deletions

View File

@ -31,4 +31,5 @@ typedef struct {
uint8_t advanceY; ///< Newline distance (y axis)
int ascender; ///< Maximal height of a glyph above the base line
int descender; ///< Maximal height of a glyph below the base line
bool is2Bit;
} EpdFontData;

View File

@ -2,6 +2,7 @@
* generated by fontconvert.py
* name: babyblue
* size: 8
* mode: 1-bit
*/
#pragma once
#include "EpdFontData.h"
@ -500,5 +501,5 @@ static const EpdUnicodeInterval babyblueIntervals[] = {
};
static const EpdFontData babyblue = {
babyblueBitmaps, babyblueGlyphs, babyblueIntervals, 5, 17, 13, -4,
babyblueBitmaps, babyblueGlyphs, babyblueIntervals, 5, 17, 13, -4, false,
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@
* generated by fontconvert.py
* name: ubuntu_10
* size: 10
* mode: 1-bit
*/
#pragma once
#include "EpdFontData.h"
@ -762,5 +763,5 @@ static const EpdUnicodeInterval ubuntu_10Intervals[] = {
};
static const EpdFontData ubuntu_10 = {
ubuntu_10Bitmaps, ubuntu_10Glyphs, ubuntu_10Intervals, 31, 24, 20, -4,
ubuntu_10Bitmaps, ubuntu_10Glyphs, ubuntu_10Intervals, 31, 24, 20, -4, false,
};

View File

@ -2,6 +2,7 @@
* generated by fontconvert.py
* name: ubuntu_bold_10
* size: 10
* mode: 1-bit
*/
#pragma once
#include "EpdFontData.h"
@ -806,5 +807,5 @@ static const EpdUnicodeInterval ubuntu_bold_10Intervals[] = {
};
static const EpdFontData ubuntu_bold_10 = {
ubuntu_bold_10Bitmaps, ubuntu_bold_10Glyphs, ubuntu_bold_10Intervals, 31, 24, 20, -4,
ubuntu_bold_10Bitmaps, ubuntu_bold_10Glyphs, ubuntu_bold_10Intervals, 31, 24, 20, -4, false,
};

View File

@ -13,12 +13,14 @@ parser = argparse.ArgumentParser(description="Generate a header file from a font
parser.add_argument("name", action="store", help="name of the font.")
parser.add_argument("size", type=int, help="font size to use.")
parser.add_argument("fontstack", action="store", nargs='+', help="list of font files, ordered by descending priority.")
parser.add_argument("--2bit", dest="is2Bit", action="store_true", help="generate 2-bit greyscale bitmap instead of 1-bit black and white.")
parser.add_argument("--additional-intervals", dest="additional_intervals", action="append", help="Additional code point intervals to export as min,max. This argument can be repeated.")
args = parser.parse_args()
GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "data_length", "data_offset", "code_point"])
font_stack = [freetype.Face(f) for f in args.fontstack]
is2Bit = args.is2Bit
size = args.size
font_name = args.name
@ -173,26 +175,73 @@ for i_start, i_end in intervals:
pixels4g.append(px)
px = 0
# Downsample to 1-bit bitmap - treat any non-zero as black
pixelsbw = []
px = 0
pitch = (bitmap.width // 2) + (bitmap.width % 2)
for y in range(bitmap.rows):
for x in range(bitmap.width):
px = px << 1
bm = pixels4g[y * pitch + (x // 2)]
px += 1 if ((x & 1) == 0 and bm & 0xF > 0) or ((x & 1) == 1 and bm & 0xF0 > 0) else 0
if is2Bit:
# 0 = white, 15 black, 8+ dark grey, 7- light grey
# Downsample to 2-bit bitmap
pixels2b = []
px = 0
pitch = (bitmap.width // 2) + (bitmap.width % 2)
for y in range(bitmap.rows):
for x in range(bitmap.width):
px = px << 2
bm = pixels4g[y * pitch + (x // 2)]
bm = (bm >> ((x % 2) * 4)) & 0xF
if (y * bitmap.width + x) % 8 == 7:
pixelsbw.append(px)
px = 0
if (bitmap.width * bitmap.rows) % 8 != 0:
px = px << (8 - (bitmap.width * bitmap.rows) % 8)
pixelsbw.append(px)
if bm == 15:
px += 3
elif bm >= 8:
px += 2
elif bm > 0:
px += 1
if (y * bitmap.width + x) % 4 == 3:
pixels2b.append(px)
px = 0
if (bitmap.width * bitmap.rows) % 4 != 0:
px = px << (4 - (bitmap.width * bitmap.rows) % 4) * 2
pixels2b.append(px)
# for y in range(bitmap.rows):
# line = ''
# for x in range(bitmap.width):
# pixelPosition = y * bitmap.width + x
# byte = pixels2b[pixelPosition // 4]
# bit_index = (3 - (pixelPosition % 4)) * 2
# line += '#' if ((byte >> bit_index) & 3) > 0 else '.'
# print(line)
# print('')
else:
# Downsample to 1-bit bitmap - treat any non-zero as black
pixelsbw = []
px = 0
pitch = (bitmap.width // 2) + (bitmap.width % 2)
for y in range(bitmap.rows):
for x in range(bitmap.width):
px = px << 1
bm = pixels4g[y * pitch + (x // 2)]
px += 1 if ((x & 1) == 0 and bm & 0xF > 0) or ((x & 1) == 1 and bm & 0xF0 > 0) else 0
if (y * bitmap.width + x) % 8 == 7:
pixelsbw.append(px)
px = 0
if (bitmap.width * bitmap.rows) % 8 != 0:
px = px << (8 - (bitmap.width * bitmap.rows) % 8)
pixelsbw.append(px)
# for y in range(bitmap.rows):
# line = ''
# for x in range(bitmap.width):
# pixelPosition = y * bitmap.width + x
# byte = pixelsbw[pixelPosition // 8]
# bit_index = 7 - (pixelPosition % 8)
# line += '#' if (byte >> bit_index) & 1 else '.'
# print(line)
# print('')
pixels = pixels2b if is2Bit else pixelsbw
# Build output data
packed = bytes(pixelsbw)
packed = bytes(pixels)
glyph = GlyphProps(
width = bitmap.width,
height = bitmap.rows,
@ -216,7 +265,7 @@ for index, glyph in enumerate(all_glyphs):
glyph_data.extend([b for b in packed])
glyph_props.append(props)
print(f"/**\n * generated by fontconvert.py\n * name: {font_name}\n * size: {size}\n */")
print(f"/**\n * generated by fontconvert.py\n * name: {font_name}\n * size: {size}\n * mode: {'2-bit' if is2Bit else '1-bit'}\n */")
print("#pragma once")
print("#include \"EpdFontData.h\"\n")
print(f"static const uint8_t {font_name}Bitmaps[{len(glyph_data)}] = {{")
@ -244,4 +293,5 @@ print(f" {len(intervals)},")
print(f" {norm_ceil(face.size.height)},")
print(f" {norm_ceil(face.size.ascender)},")
print(f" {norm_floor(face.size.descender)},")
print(f" {'true' if is2Bit else 'false'},")
print("};")

View File

@ -6,22 +6,27 @@
inline int min(const int a, const int b) { return a < b ? a : b; }
inline int max(const int a, const int b) { return a > b ? a : b; }
enum EpdFontRendererMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB };
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,
EpdFontRendererMode mode = BW);
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,
EpdFontRendererMode mode = BW);
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,
const EpdFontStyle style) {
void EpdFontRenderer<Renderable>::renderString(const char* string, int* x, int* y, const bool pixelState,
const EpdFontStyle style, const EpdFontRendererMode mode) {
// cannot draw a NULL / empty string
if (string == nullptr || *string == '\0') {
return;
@ -34,15 +39,49 @@ 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, mode);
}
*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,
const EpdFontStyle style) {
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 EpdFontRendererMode mode) {
const EpdGlyph* glyph = fontFamily->getGlyph(cp, style);
if (!glyph) {
// TODO: Replace with fallback glyph property?
@ -55,6 +94,7 @@ void EpdFontRenderer<Renderable>::renderChar(const uint32_t cp, int* x, const in
return;
}
const int is2Bit = fontFamily->getData(style)->is2Bit;
const uint32_t offset = glyph->dataOffset;
const uint8_t width = glyph->width;
const uint8_t height = glyph->height;
@ -70,11 +110,26 @@ void EpdFontRenderer<Renderable>::renderChar(const uint32_t cp, int* x, const in
const int pixelPosition = glyphY * width + glyphX;
int screenX = *x + left + glyphX;
const uint8_t byte = bitmap[pixelPosition / 8];
const uint8_t bit_index = 7 - (pixelPosition % 8);
if (is2Bit) {
const uint8_t byte = bitmap[pixelPosition / 4];
const uint8_t bit_index = (3 - pixelPosition % 4) * 2;
if ((byte >> bit_index) & 1) {
renderer.drawPixel(screenX, screenY, color);
const uint8_t val = (byte >> bit_index) & 0x3;
if (mode == BW && val > 0) {
drawPixel(screenX, screenY, pixelState);
} else if (mode == GRAYSCALE_MSB && val == 1) {
// TODO: Not sure how this anti-aliasing goes on black backgrounds
drawPixel(screenX, screenY, false);
} else if (mode == GRAYSCALE_LSB && val == 2) {
drawPixel(screenX, screenY, false);
}
} else {
const uint8_t byte = bitmap[pixelPosition / 8];
const uint8_t bit_index = 7 - (pixelPosition % 8);
if ((byte >> bit_index) & 1) {
drawPixel(screenX, screenY, pixelState);
}
}
}
}

View File

@ -1,17 +1,17 @@
#include "EpdRenderer.h"
#include "builtinFonts/babyblue.h"
#include "builtinFonts/bookerly.h"
#include "builtinFonts/bookerly_bold.h"
#include "builtinFonts/bookerly_bold_italic.h"
#include "builtinFonts/bookerly_italic.h"
#include "builtinFonts/bookerly_2b.h"
#include "builtinFonts/bookerly_bold_2b.h"
#include "builtinFonts/bookerly_bold_italic_2b.h"
#include "builtinFonts/bookerly_italic_2b.h"
#include "builtinFonts/ubuntu_10.h"
#include "builtinFonts/ubuntu_bold_10.h"
EpdFont bookerlyFont(&bookerly);
EpdFont bookerlyBoldFont(&bookerly_bold);
EpdFont bookerlyItalicFont(&bookerly_italic);
EpdFont bookerlyBoldItalicFont(&bookerly_bold_italic);
EpdFont bookerlyFont(&bookerly_2b);
EpdFont bookerlyBoldFont(&bookerly_bold_2b);
EpdFont bookerlyItalicFont(&bookerly_italic_2b);
EpdFont bookerlyBoldItalicFont(&bookerly_bold_italic_2b);
EpdFontFamily bookerlyFontFamily(&bookerlyFont, &bookerlyBoldFont, &bookerlyItalicFont, &bookerlyBoldItalicFont);
EpdFont smallFont(&babyblue);
@ -21,11 +21,17 @@ 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),
fontRendererMode(BW),
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 +40,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 +98,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, fontRendererMode);
}
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, fontRendererMode);
}
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, fontRendererMode);
}
void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text, const int width, const int height,
@ -88,7 +128,7 @@ void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text,
int ypos = 0;
while (true) {
if (end >= length) {
drawText(x, y + ypos, text.substr(start, length - start).c_str(), 1, style);
drawText(x, y + ypos, text.substr(start, length - start).c_str(), true, style);
break;
}
@ -97,7 +137,7 @@ void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text,
}
if (text[end - 1] == '\n') {
drawText(x, y + ypos, text.substr(start, end - start).c_str(), 1, style);
drawText(x, y + ypos, text.substr(start, end - start).c_str(), true, style);
ypos += getLineHeight();
start = end;
end = start + 1;
@ -105,7 +145,7 @@ void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text,
}
if (getTextWidth(text.substr(start, end - start).c_str(), style) > width) {
drawText(x, y + ypos, text.substr(start, end - start - 1).c_str(), 1, style);
drawText(x, y + ypos, text.substr(start, end - start - 1).c_str(), true, style);
ypos += getLineHeight();
start = end - 1;
continue;
@ -115,54 +155,88 @@ 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::flushArea(const int x, const int y, const int width, const int height) const {
display.displayWindow(x + marginLeft, y + marginTop, width, height);
void EpdRenderer::invertScreen() const {
uint8_t *buffer = einkDisplay.getFrameBuffer();
for (int i = 0; i < EInkDisplay::BUFFER_SIZE; i++) {
buffer[i] = ~buffer[i];
}
}
int EpdRenderer::getPageWidth() const { return display.width() - marginLeft - marginRight; }
void EpdRenderer::flushDisplay(const EInkDisplay::RefreshMode refreshMode) const {
einkDisplay.displayBuffer(refreshMode);
}
int EpdRenderer::getPageHeight() const { return display.height() - marginTop - marginBottom; }
// TODO: Support partial window update
// void EpdRenderer::flushArea(const int x, const int y, const int width, const int height) const {
// const int rotatedX = y;
// const int rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x;
//
// einkDisplay.displayBuffer(EInkDisplay::FAST_REFRESH, rotatedX, rotatedY, height, width);
// }
int EpdRenderer::getPageWidth() const { return EInkDisplay::DISPLAY_HEIGHT - marginLeft - marginRight; }
int EpdRenderer::getPageHeight() const { return EInkDisplay::DISPLAY_WIDTH - marginTop - marginBottom; }
int EpdRenderer::getSpaceWidth() const { return regularFontRenderer->fontFamily->getGlyph(' ', REGULAR)->advanceX; }
int EpdRenderer::getLineHeight() const {
return regularFontRenderer->fontFamily->getData(REGULAR)->advanceY * lineCompression;
}
void EpdRenderer::swapBuffers() const { einkDisplay.swapBuffers(); }
void EpdRenderer::copyGrayscaleLsbBuffers() const { einkDisplay.copyGrayscaleLsbBuffers(einkDisplay.getFrameBuffer()); }
void EpdRenderer::copyGrayscaleMsbBuffers() const { einkDisplay.copyGrayscaleMsbBuffers(einkDisplay.getFrameBuffer()); }
void EpdRenderer::displayGrayBuffer() const { einkDisplay.displayGrayBuffer(); }

View File

@ -1,52 +1,58 @@
#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;
int marginRight;
EpdFontRendererMode fontRendererMode;
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 flushDisplay(bool partialUpdate = true) const;
void flushArea(int x, int y, int width, int height) 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 swapBuffers() const;
void copyGrayscaleLsbBuffers() const;
void copyGrayscaleMsbBuffers() const;
void displayGrayBuffer() const;
void clearScreen(uint8_t color = 0xFF) const;
void invertScreen() const;
void flushDisplay(EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const;
// void flushArea(int x, int y, int width, int height) const;
int getPageWidth() const;
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; }
void setMarginLeft(const int newMarginLeft) { this->marginLeft = newMarginLeft; }
void setMarginRight(const int newMarginRight) { this->marginRight = newMarginRight; }
void setFontRendererMode(const EpdFontRendererMode mode) { this->fontRendererMode = mode; }
};

View File

@ -98,21 +98,15 @@ bool Section::persistPageDataToSD() {
return true;
}
void Section::renderPage() const {
if (0 <= currentPage && currentPage < pageCount) {
const auto filePath = "/sd" + cachePath + "/page_" + std::to_string(currentPage) + ".bin";
std::ifstream inputFile(filePath);
const Page* p = Page::deserialize(inputFile);
inputFile.close();
p->render(renderer);
delete p;
} else if (pageCount == 0) {
Serial.println("No pages to render");
const int width = renderer.getTextWidth("Empty chapter", BOLD);
renderer.drawText((renderer.getPageWidth() - width) / 2, 300, "Empty chapter", 1, BOLD);
} else {
Serial.printf("Page out of bounds: %d (max %d)\n", currentPage, pageCount);
const int width = renderer.getTextWidth("Out of bounds", BOLD);
renderer.drawText((renderer.getPageWidth() - width) / 2, 300, "Out of bounds", 1, BOLD);
Page* Section::loadPageFromSD() const {
const auto filePath = "/sd" + cachePath + "/page_" + std::to_string(currentPage) + ".bin";
if (!SD.exists(filePath.c_str() + 3)) {
Serial.printf("Page file does not exist: %s\n", filePath.c_str());
return nullptr;
}
std::ifstream inputFile(filePath);
Page* p = Page::deserialize(inputFile);
inputFile.close();
return p;
}

View File

@ -26,5 +26,5 @@ class Section {
void setupCacheDir() const;
void clearCache() const;
bool persistPageDataToSD();
void renderPage() const;
Page* loadPageFromSD() const;
};

View File

@ -165,7 +165,7 @@ void TextBlock::render(const EpdRenderer& renderer, const int x, const int y) co
} else if (wordStyles[i] & ITALIC_SPAN) {
fontStyle = ITALIC;
}
renderer.drawText(x + wordXpos[i], y, words[i].c_str(), 1, fontStyle);
renderer.drawText(x + wordXpos[i], y, words[i].c_str(), true, fontStyle);
}
}

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

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 @@
#pragma once
#include <cstdint>
extern const uint8_t CrossLarge[] = {
static const uint8_t CrossLarge[] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,

File diff suppressed because it is too large Load Diff

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();
@ -142,7 +141,7 @@ void onSelectEpubFile(const std::string& path) {
enterNewScreen(new EpubReaderScreen(renderer, inputManager, epub, onGoHome));
} else {
exitScreen();
enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Failed to load epub", REGULAR, false, false));
enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Failed to load epub", REGULAR, EInkDisplay::HALF_REFRESH));
delay(2000);
onGoHome();
}
@ -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,9 @@ 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);
const int width = renderer.getUiTextWidth("CrossPoint", BOLD);
renderer.drawUiText((pageWidth - width)/ 2, pageHeight / 2 + 70, "CrossPoint", true, BOLD);
const int bootingWidth = renderer.getSmallTextWidth("BOOTING");
renderer.drawSmallText((pageWidth - bootingWidth) / 2, pageHeight / 2 + 95, "BOOTING");
renderer.flushDisplay();
}

View File

@ -1,11 +1,12 @@
#include "EpubReaderScreen.h"
#include <EpdRenderer.h>
#include <Epub/Page.h>
#include <SD.h>
#include "Battery.h"
constexpr int PAGES_PER_REFRESH = 20;
constexpr int PAGES_PER_REFRESH = 15;
constexpr unsigned long SKIP_CHAPTER_MS = 700;
void EpubReaderScreen::taskTrampoline(void* param) {
@ -128,7 +129,7 @@ void EpubReaderScreen::displayTaskLoop() {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderPage();
renderScreen();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
@ -136,7 +137,7 @@ void EpubReaderScreen::displayTaskLoop() {
}
// TODO: Failure handling
void EpubReaderScreen::renderPage() {
void EpubReaderScreen::renderScreen() {
if (!epub) {
return;
}
@ -159,10 +160,12 @@ void EpubReaderScreen::renderPage() {
constexpr int y = 50;
const int w = textWidth + margin * 2;
const int h = renderer.getLineHeight() + margin * 2;
renderer.swapBuffers();
renderer.fillRect(x, y, w, h, 0);
renderer.drawText(x + margin, y + margin, "Indexing...");
renderer.drawRect(x + 5, y + 5, w - 10, h - 10);
renderer.flushArea(x, y, w, h);
renderer.flushDisplay(EInkDisplay::HALF_REFRESH);
pagesUntilFullRefresh = 0;
}
section->setupCacheDir();
@ -184,16 +187,29 @@ void EpubReaderScreen::renderPage() {
}
renderer.clearScreen();
section->renderPage();
renderStatusBar();
if (pagesUntilFullRefresh <= 1) {
renderer.flushDisplay(false);
pagesUntilFullRefresh = PAGES_PER_REFRESH;
} else {
if (section->pageCount == 0) {
Serial.println("No pages to render");
const int width = renderer.getTextWidth("Empty chapter", BOLD);
renderer.drawText((renderer.getPageWidth() - width) / 2, 300, "Empty chapter", true, BOLD);
renderStatusBar();
renderer.flushDisplay();
pagesUntilFullRefresh--;
return;
}
if (section->currentPage < 0 || section->currentPage >= section->pageCount) {
Serial.printf("Page out of bounds: %d (max %d)\n", section->currentPage, section->pageCount);
const int width = renderer.getTextWidth("Out of bounds", BOLD);
renderer.drawText((renderer.getPageWidth() - width) / 2, 300, "Out of bounds", true, BOLD);
renderStatusBar();
renderer.flushDisplay();
return;
}
const Page* p = section->loadPageFromSD();
renderContents(p);
delete p;
File f = SD.open((epub->getCachePath() + "/progress.bin").c_str(), FILE_WRITE);
uint8_t data[4];
data[0] = currentSpineIndex & 0xFF;
@ -204,6 +220,36 @@ void EpubReaderScreen::renderPage() {
f.close();
}
void EpubReaderScreen::renderContents(const Page* p) {
p->render(renderer);
renderStatusBar();
if (pagesUntilFullRefresh <= 1) {
renderer.flushDisplay(EInkDisplay::HALF_REFRESH);
pagesUntilFullRefresh = PAGES_PER_REFRESH;
} else {
renderer.flushDisplay();
pagesUntilFullRefresh--;
}
// grayscale rendering
{
renderer.clearScreen(0x00);
renderer.setFontRendererMode(GRAYSCALE_LSB);
p->render(renderer);
renderer.copyGrayscaleLsbBuffers();
// Render and copy to MSB buffer
renderer.clearScreen(0x00);
renderer.setFontRendererMode(GRAYSCALE_MSB);
p->render(renderer);
renderer.copyGrayscaleMsbBuffers();
// display grayscale part
renderer.displayGrayBuffer();
renderer.setFontRendererMode(BW);
}
}
void EpubReaderScreen::renderStatusBar() const {
const auto pageWidth = renderer.getPageWidth();

View File

@ -20,7 +20,8 @@ class EpubReaderScreen final : public Screen {
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void renderPage();
void renderScreen();
void renderContents(const Page *p);
void renderStatusBar() const;
public:

View File

@ -51,7 +51,7 @@ void FileSelectionScreen::onEnter() {
updateRequired = true;
xTaskCreate(&FileSelectionScreen::taskTrampoline, "FileSelectionScreenTask",
1024, // Stack size
2048, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
@ -120,7 +120,7 @@ void FileSelectionScreen::render() const {
const auto pageWidth = renderer.getPageWidth();
const auto titleWidth = renderer.getTextWidth("CrossPoint Reader", BOLD);
renderer.drawText((pageWidth - titleWidth) / 2, 0, "CrossPoint Reader", 1, BOLD);
renderer.drawText((pageWidth - titleWidth) / 2, 0, "CrossPoint Reader", true, BOLD);
if (files.empty()) {
renderer.drawUiText(10, 50, "No EPUBs found");
@ -130,7 +130,7 @@ void FileSelectionScreen::render() const {
for (size_t i = 0; i < files.size(); i++) {
const auto file = files[i];
renderer.drawUiText(10, 50 + i * 30, file.c_str(), i == selectorIndex ? 0 : 1);
renderer.drawUiText(10, 50 + i * 30, file.c_str(), i != selectorIndex);
}
}

View File

@ -8,7 +8,7 @@ void FullScreenMessageScreen::onEnter() {
const auto left = (renderer.getPageWidth() - width) / 2;
const auto top = (renderer.getPageHeight() - height) / 2;
renderer.clearScreen(invert);
renderer.drawUiText(left, top, text.c_str(), invert ? 0 : 1, style);
renderer.flushDisplay(partialUpdate);
renderer.clearScreen();
renderer.drawUiText(left, top, text.c_str(), true, style);
renderer.flushDisplay(refreshMode);
}

View File

@ -2,23 +2,22 @@
#include <string>
#include <utility>
#include "EpdFontFamily.h"
#include <EInkDisplay.h>
#include <EpdFontFamily.h>
#include "Screen.h"
class FullScreenMessageScreen final : public Screen {
std::string text;
EpdFontStyle style;
bool invert;
bool partialUpdate;
EInkDisplay::RefreshMode refreshMode;
public:
explicit FullScreenMessageScreen(EpdRenderer& renderer, InputManager& inputManager, std::string text,
const EpdFontStyle style = REGULAR, const bool invert = false,
const bool partialUpdate = true)
const EpdFontStyle style = REGULAR,
const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH)
: Screen(renderer, inputManager),
text(std::move(text)),
style(style),
invert(invert),
partialUpdate(partialUpdate) {}
refreshMode(refreshMode) {}
void onEnter() override;
};

View File

@ -2,6 +2,18 @@
#include <EpdRenderer.h>
#include "images/SleepScreenImg.h"
#include "images/CrossLarge.h"
void SleepScreen::onEnter() { renderer.drawImageNoMargin(SleepScreenImg, 0, 0, 800, 480, false, true); }
void SleepScreen::onEnter() {
const auto pageWidth = renderer.getPageWidth();
const auto pageHeight = renderer.getPageHeight();
renderer.clearScreen();
renderer.drawImage(CrossLarge, (pageHeight - 128) / 2, (pageWidth - 128) / 2, 128, 128);
const int width = renderer.getUiTextWidth("CrossPoint", BOLD);
renderer.drawUiText((pageWidth - width)/ 2, pageHeight / 2 + 70, "CrossPoint", true, BOLD);
const int bootingWidth = renderer.getSmallTextWidth("SLEEPING");
renderer.drawSmallText((pageWidth - bootingWidth) / 2, pageHeight / 2 + 95, "SLEEPING");
renderer.invertScreen();
renderer.flushDisplay(EInkDisplay::FULL_REFRESH);
}