14 Commits
0.2.0 ... 0.2.2

Author SHA1 Message Date
Dave Allie
f0d92da8f2 Update README.md with checkout instructions
Fixes https://github.com/daveallie/crosspoint-reader/issues/1
2025-12-06 04:53:58 +11:00
Dave Allie
8679c8f57c Update sleep screen 2025-12-06 04:20:41 +11:00
Dave Allie
899caab70c Avoid leaving screens mid-display update
Was leave the EPD in a bad state, blocking further actions
2025-12-06 03:02:52 +11:00
Dave Allie
98c8e7e77c Fix memory leak with Epub object getting orphaned 2025-12-06 02:49:10 +11:00
Dave Allie
7198d943b0 Add UI font 2025-12-06 01:44:14 +11:00
Dave Allie
248af4b8fb Add new boot logo screen 2025-12-06 01:44:14 +11:00
Dave Allie
05a027e2bf Wrap up multiple font styles into EpdFontFamily 2025-12-06 01:44:14 +11:00
Dave Allie
fa0f27df6a Full screen refresh of EpubReaderScreen every 10 renders
This is done in an effort to minimise ghosting
2025-12-05 22:19:44 +11:00
Dave Allie
2631613b8d Add directory picking to home screen 2025-12-05 22:12:28 +11:00
Dave Allie
72aa7ba3f6 Upgrade open-x4-sdk 2025-12-05 21:14:08 +11:00
Dave Allie
e08bac2e10 Show indexing text when indexing 2025-12-05 21:12:15 +11:00
Dave Allie
12d28e2148 Avoid ghosting on sleep screen by doing a full screen update 2025-12-05 17:55:17 +11:00
Dave Allie
85502b417e Speedup boot by not waiting for Serial 2025-12-05 17:47:23 +11:00
Dave Allie
ddec7f78dd Fix hold to wake logic
esp_sleep_get_wakeup_cause does not seem to be set when not connected to USB power
2025-12-04 00:57:32 +11:00
26 changed files with 7133 additions and 212 deletions

View File

@@ -32,7 +32,7 @@ This project is **not affiliated with Xteink**; it's built as a community projec
- [x] Saved reading position - [x] Saved reading position
- [ ] File explorer with file picker - [ ] File explorer with file picker
- [x] Basic EPUB picker from root directory - [x] Basic EPUB picker from root directory
- [ ] Support nested folders - [x] Support nested folders
- [ ] EPUB picker with cover art - [ ] EPUB picker with cover art
- [ ] Image support within EPUB - [ ] Image support within EPUB
- [ ] Configurable font, layout, and display options - [ ] Configurable font, layout, and display options
@@ -48,6 +48,17 @@ This project is **not affiliated with Xteink**; it's built as a community projec
* USB-C cable for flashing the ESP32-C3 * USB-C cable for flashing the ESP32-C3
* Xteink X4 * Xteink X4
### Checking out the code
CrossPoint uses PlatformIO for building and flashing the firmware. To get started, clone the repository:
```
git clone --recursive https://github.com/daveallie/crosspoint-reader
# Or, if you've already cloned without --recursive:
git submodule update --init --recursive
```
### Flashing your device ### Flashing your device
#### Command line #### Command line

View File

@@ -0,0 +1,37 @@
#include "EpdFontFamily.h"
const EpdFont* EpdFontFamily::getFont(const EpdFontStyle style) const {
if (style == BOLD && bold) {
return bold;
}
if (style == ITALIC && italic) {
return italic;
}
if (style == BOLD_ITALIC) {
if (boldItalic) {
return boldItalic;
}
if (bold) {
return bold;
}
if (italic) {
return italic;
}
}
return regular;
}
void EpdFontFamily::getTextDimensions(const char* string, int* w, int* h, const EpdFontStyle style) const {
getFont(style)->getTextDimensions(string, w, h);
}
bool EpdFontFamily::hasPrintableChars(const char* string, const EpdFontStyle style) const {
return getFont(style)->hasPrintableChars(string);
}
const EpdFontData* EpdFontFamily::getData(const EpdFontStyle style) const { return getFont(style)->data; }
const EpdGlyph* EpdFontFamily::getGlyph(const uint32_t cp, const EpdFontStyle style) const {
return getFont(style)->getGlyph(cp);
};

View File

@@ -0,0 +1,24 @@
#pragma once
#include "EpdFont.h"
enum EpdFontStyle { REGULAR, BOLD, ITALIC, BOLD_ITALIC };
class EpdFontFamily {
const EpdFont* regular;
const EpdFont* bold;
const EpdFont* italic;
const EpdFont* boldItalic;
const EpdFont* getFont(EpdFontStyle style) const;
public:
explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
const EpdFont* boldItalic = nullptr)
: regular(regular), bold(bold), italic(italic), boldItalic(boldItalic) {}
~EpdFontFamily() = default;
void getTextDimensions(const char* string, int* w, int* h, EpdFontStyle style = REGULAR) const;
bool hasPrintableChars(const char* string, EpdFontStyle style = REGULAR) const;
const EpdFontData* getData(EpdFontStyle style = REGULAR) const;
const EpdGlyph* getGlyph(uint32_t cp, EpdFontStyle style = REGULAR) const;
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
#pragma once #pragma once
#include <EpdFont.h> #include <EpdFontFamily.h>
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <Utf8.h> #include <Utf8.h>
#include <miniz.h> #include <miniz.h>
@@ -12,13 +12,14 @@ static tinfl_decompressor decomp;
template <typename Renderable> template <typename Renderable>
class EpdFontRenderer { class EpdFontRenderer {
Renderable* renderer; Renderable* renderer;
void renderChar(uint32_t cp, int* x, const int* y, uint16_t color); void renderChar(uint32_t cp, int* x, const int* y, uint16_t color, EpdFontStyle style = REGULAR);
public: public:
const EpdFont* font; const EpdFontFamily* fontFamily;
explicit EpdFontRenderer(const EpdFont* font, Renderable* renderer); explicit EpdFontRenderer(const EpdFontFamily* fontFamily, Renderable* renderer)
: fontFamily(fontFamily), renderer(renderer) {}
~EpdFontRenderer() = default; ~EpdFontRenderer() = default;
void renderString(const char* string, int* x, int* y, uint16_t color); void renderString(const char* string, int* x, int* y, uint16_t color, EpdFontStyle style = REGULAR);
}; };
inline int uncompress(uint8_t* dest, size_t uncompressedSize, const uint8_t* source, size_t sourceSize) { inline int uncompress(uint8_t* dest, size_t uncompressedSize, const uint8_t* source, size_t sourceSize) {
@@ -38,37 +39,33 @@ inline int uncompress(uint8_t* dest, size_t uncompressedSize, const uint8_t* sou
} }
template <typename Renderable> template <typename Renderable>
EpdFontRenderer<Renderable>::EpdFontRenderer(const EpdFont* font, Renderable* renderer) { void EpdFontRenderer<Renderable>::renderString(const char* string, int* x, int* y, const uint16_t color,
this->font = font; const EpdFontStyle style) {
this->renderer = renderer;
}
template <typename Renderable>
void EpdFontRenderer<Renderable>::renderString(const char* string, int* x, int* y, const uint16_t color) {
// cannot draw a NULL / empty string // cannot draw a NULL / empty string
if (string == nullptr || *string == '\0') { if (string == nullptr || *string == '\0') {
return; return;
} }
// no printable characters // no printable characters
if (!font->hasPrintableChars(string)) { if (!fontFamily->hasPrintableChars(string, style)) {
return; return;
} }
uint32_t cp; uint32_t cp;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) { while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
renderChar(cp, x, y, color); renderChar(cp, x, y, color, style);
} }
*y += font->data->advanceY; *y += fontFamily->getData(style)->advanceY;
} }
template <typename Renderable> template <typename Renderable>
void EpdFontRenderer<Renderable>::renderChar(const uint32_t cp, int* x, const int* y, uint16_t color) { void EpdFontRenderer<Renderable>::renderChar(const uint32_t cp, int* x, const int* y, uint16_t color,
const EpdGlyph* glyph = font->getGlyph(cp); const EpdFontStyle style) {
const EpdGlyph* glyph = fontFamily->getGlyph(cp, style);
if (!glyph) { if (!glyph) {
// TODO: Replace with fallback glyph property? // TODO: Replace with fallback glyph property?
glyph = font->getGlyph('?'); glyph = fontFamily->getGlyph('?', style);
} }
// no glyph? // no glyph?
@@ -86,17 +83,17 @@ void EpdFontRenderer<Renderable>::renderChar(const uint32_t cp, int* x, const in
const unsigned long bitmapSize = byteWidth * height; const unsigned long bitmapSize = byteWidth * height;
const uint8_t* bitmap = nullptr; const uint8_t* bitmap = nullptr;
if (font->data->compressed) { if (fontFamily->getData(style)->compressed) {
auto* tmpBitmap = static_cast<uint8_t*>(malloc(bitmapSize)); auto* tmpBitmap = static_cast<uint8_t*>(malloc(bitmapSize));
if (tmpBitmap == nullptr && bitmapSize) { if (tmpBitmap == nullptr && bitmapSize) {
// ESP_LOGE("font", "malloc failed."); Serial.println("Failed to allocate memory for decompression buffer");
return; return;
} }
uncompress(tmpBitmap, bitmapSize, &font->data->bitmap[offset], glyph->compressedSize); uncompress(tmpBitmap, bitmapSize, &fontFamily->getData(style)->bitmap[offset], glyph->compressedSize);
bitmap = tmpBitmap; bitmap = tmpBitmap;
} else { } else {
bitmap = &font->data->bitmap[offset]; bitmap = &fontFamily->getData(style)->bitmap[offset];
} }
if (bitmap != nullptr) { if (bitmap != nullptr) {
@@ -123,7 +120,7 @@ void EpdFontRenderer<Renderable>::renderChar(const uint32_t cp, int* x, const in
} }
} }
if (font->data->compressed) { if (fontFamily->getData(style)->compressed) {
free(const_cast<uint8_t*>(bitmap)); free(const_cast<uint8_t*>(bitmap));
} }
} }

View File

@@ -5,14 +5,18 @@
#include "builtinFonts/bookerly_bold.h" #include "builtinFonts/bookerly_bold.h"
#include "builtinFonts/bookerly_bold_italic.h" #include "builtinFonts/bookerly_bold_italic.h"
#include "builtinFonts/bookerly_italic.h" #include "builtinFonts/bookerly_italic.h"
#include "builtinFonts/ubuntu_10.h"
#include "builtinFonts/ubuntu_bold_10.h"
EpdRenderer::EpdRenderer(XteinkDisplay* display) { EpdRenderer::EpdRenderer(XteinkDisplay* display) {
const auto bookerlyFontFamily = new EpdFontFamily(new EpdFont(&bookerly), new EpdFont(&bookerly_bold),
new EpdFont(&bookerly_italic), new EpdFont(&bookerly_bold_italic));
const auto ubuntuFontFamily = new EpdFontFamily(new EpdFont(&ubuntu_10), new EpdFont(&ubuntu_bold_10));
this->display = display; this->display = display;
this->regularFont = new EpdFontRenderer<XteinkDisplay>(new EpdFont(&bookerly), display); this->regularFontRenderer = new EpdFontRenderer<XteinkDisplay>(bookerlyFontFamily, display);
this->boldFont = new EpdFontRenderer<XteinkDisplay>(new EpdFont(&bookerly_bold), display); this->smallFontRenderer = new EpdFontRenderer<XteinkDisplay>(new EpdFontFamily(new EpdFont(&babyblue)), display);
this->italicFont = new EpdFontRenderer<XteinkDisplay>(new EpdFont(&bookerly_italic), display); this->uiFontRenderer = new EpdFontRenderer<XteinkDisplay>(ubuntuFontFamily, display);
this->bold_italicFont = new EpdFontRenderer<XteinkDisplay>(new EpdFont(&bookerly_bold_italic), display);
this->smallFont = new EpdFontRenderer<XteinkDisplay>(new EpdFont(&babyblue), display);
this->marginTop = 11; this->marginTop = 11;
this->marginBottom = 30; this->marginBottom = 30;
@@ -21,50 +25,53 @@ EpdRenderer::EpdRenderer(XteinkDisplay* display) {
this->lineCompression = 0.95f; this->lineCompression = 0.95f;
} }
EpdFontRenderer<XteinkDisplay>* EpdRenderer::getFontRenderer(const bool bold, const bool italic) const { int EpdRenderer::getTextWidth(const char* text, const EpdFontStyle style) const {
if (bold && italic) {
return bold_italicFont;
}
if (bold) {
return boldFont;
}
if (italic) {
return italicFont;
}
return regularFont;
}
int EpdRenderer::getTextWidth(const char* text, const bool bold, const bool italic) const {
int w = 0, h = 0; int w = 0, h = 0;
getFontRenderer(bold, italic)->font->getTextDimensions(text, &w, &h); regularFontRenderer->fontFamily->getTextDimensions(text, &w, &h, style);
return w; return w;
} }
int EpdRenderer::getSmallTextWidth(const char* text) const { int EpdRenderer::getUiTextWidth(const char* text, const EpdFontStyle style) const {
int w = 0, h = 0; int w = 0, h = 0;
smallFont->font->getTextDimensions(text, &w, &h); uiFontRenderer->fontFamily->getTextDimensions(text, &w, &h, style);
return w; return w;
} }
void EpdRenderer::drawText(const int x, const int y, const char* text, const bool bold, const bool italic, int EpdRenderer::getSmallTextWidth(const char* text, const EpdFontStyle style) const {
const uint16_t color) const { int w = 0, h = 0;
smallFontRenderer->fontFamily->getTextDimensions(text, &w, &h, style);
return w;
}
void EpdRenderer::drawText(const int x, const int y, const char* text, const uint16_t color,
const EpdFontStyle style) const {
int ypos = y + getLineHeight() + marginTop; int ypos = y + getLineHeight() + marginTop;
int xpos = x + marginLeft; int xpos = x + marginLeft;
getFontRenderer(bold, italic)->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE); regularFontRenderer->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE, style);
} }
void EpdRenderer::drawSmallText(const int x, const int y, const char* text, const uint16_t color) const { void EpdRenderer::drawUiText(const int x, const int y, const char* text, const uint16_t color,
int ypos = y + smallFont->font->data->advanceY + marginTop; const EpdFontStyle style) const {
int ypos = y + uiFontRenderer->fontFamily->getData(style)->advanceY + marginTop;
int xpos = x + marginLeft; int xpos = x + marginLeft;
smallFont->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE); uiFontRenderer->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE, style);
}
void EpdRenderer::drawSmallText(const int x, const int y, const char* text, const uint16_t color,
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);
} }
void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text, const int width, const int height, void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text, const int width, const int height,
const bool bold, const bool italic) const { const EpdFontStyle style) const {
const size_t length = text.length(); const size_t length = text.length();
// fit the text into the box // fit the text into the box
int start = 0; int start = 0;
@@ -72,7 +79,7 @@ void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text,
int ypos = 0; int ypos = 0;
while (true) { while (true) {
if (end >= length) { if (end >= length) {
drawText(x, y + ypos, text.substr(start, length - start).c_str(), bold, italic); drawText(x, y + ypos, text.substr(start, length - start).c_str(), 1, style);
break; break;
} }
@@ -81,15 +88,15 @@ void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text,
} }
if (text[end - 1] == '\n') { if (text[end - 1] == '\n') {
drawText(x, y + ypos, text.substr(start, end - start).c_str(), bold, italic); drawText(x, y + ypos, text.substr(start, end - start).c_str(), 1, style);
ypos += getLineHeight(); ypos += getLineHeight();
start = end; start = end;
end = start + 1; end = start + 1;
continue; continue;
} }
if (getTextWidth(text.substr(start, end - start).c_str(), bold, italic) > width) { if (getTextWidth(text.substr(start, end - start).c_str(), style) > width) {
drawText(x, y + ypos, text.substr(start, end - start - 1).c_str(), bold, italic); drawText(x, y + ypos, text.substr(start, end - start - 1).c_str(), 1, style);
ypos += getLineHeight(); ypos += getLineHeight();
start = end - 1; start = end - 1;
continue; continue;
@@ -108,44 +115,45 @@ void EpdRenderer::drawRect(const int x, const int y, const int width, const int
display->drawRect(x + marginLeft, y + marginTop, width, height, color > 0 ? GxEPD_BLACK : GxEPD_WHITE); display->drawRect(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, void EpdRenderer::fillRect(const int x, const int y, const int width, const int height, const uint16_t color) const {
const uint16_t color = 0) const {
display->fillRect(x + marginLeft, y + marginTop, width, height, color > 0 ? GxEPD_BLACK : GxEPD_WHITE); display->fillRect(x + marginLeft, y + marginTop, width, height, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
} }
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::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);
}
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 bool black) const {
Serial.println("Clearing screen"); Serial.println("Clearing screen");
display->fillScreen(black ? GxEPD_BLACK : GxEPD_WHITE); display->fillScreen(black ? GxEPD_BLACK : GxEPD_WHITE);
} }
void EpdRenderer::flushDisplay() const { display->display(true); } void EpdRenderer::flushDisplay(const bool partialUpdate) const { display->display(partialUpdate); }
void EpdRenderer::flushArea(int x, int y, int width, int height) const { void EpdRenderer::flushArea(const int x, const int y, const int width, const int height) const {
// TODO: Fix display->displayWindow(x + marginLeft, y + marginTop, width, height);
display->display(true);
} }
int EpdRenderer::getPageWidth() const { return display->width() - marginLeft - marginRight; } int EpdRenderer::getPageWidth() const { return display->width() - marginLeft - marginRight; }
int EpdRenderer::getPageHeight() const { return display->height() - marginTop - marginBottom; } int EpdRenderer::getPageHeight() const { return display->height() - marginTop - marginBottom; }
int EpdRenderer::getSpaceWidth() const { return regularFont->font->getGlyph(' ')->advanceX; } int EpdRenderer::getSpaceWidth() const { return regularFontRenderer->fontFamily->getGlyph(' ', REGULAR)->advanceX; }
int EpdRenderer::getLineHeight() const { return regularFont->font->data->advanceY * lineCompression; } int EpdRenderer::getLineHeight() const {
return regularFontRenderer->fontFamily->getData(REGULAR)->advanceY * lineCompression;
// deep sleep helper - persist any state to disk that may be needed on wake }
bool EpdRenderer::dehydrate() {
// TODO: Implement
return false;
};
// deep sleep helper - retrieve any state from disk after wake
bool EpdRenderer::hydrate() {
// TODO: Implement
return false;
};
// really really clear the screen
void EpdRenderer::reset() {
// TODO: Implement
};

View File

@@ -8,32 +8,36 @@
class EpdRenderer { class EpdRenderer {
XteinkDisplay* display; XteinkDisplay* display;
EpdFontRenderer<XteinkDisplay>* regularFont; EpdFontRenderer<XteinkDisplay>* regularFontRenderer;
EpdFontRenderer<XteinkDisplay>* boldFont; EpdFontRenderer<XteinkDisplay>* smallFontRenderer;
EpdFontRenderer<XteinkDisplay>* italicFont; EpdFontRenderer<XteinkDisplay>* uiFontRenderer;
EpdFontRenderer<XteinkDisplay>* bold_italicFont;
EpdFontRenderer<XteinkDisplay>* smallFont;
int marginTop; int marginTop;
int marginBottom; int marginBottom;
int marginLeft; int marginLeft;
int marginRight; int marginRight;
float lineCompression; float lineCompression;
EpdFontRenderer<XteinkDisplay>* getFontRenderer(bool bold, bool italic) const;
public: public:
explicit EpdRenderer(XteinkDisplay* display); explicit EpdRenderer(XteinkDisplay* display);
~EpdRenderer() = default; ~EpdRenderer() = default;
int getTextWidth(const char* text, bool bold = false, bool italic = false) const; int getTextWidth(const char* text, EpdFontStyle style = REGULAR) const;
int getSmallTextWidth(const char* text) const; int getUiTextWidth(const char* text, EpdFontStyle style = REGULAR) const;
void drawText(int x, int y, const char* text, bool bold = false, bool italic = false, uint16_t color = 1) const; int getSmallTextWidth(const char* text, EpdFontStyle style = REGULAR) const;
void drawSmallText(int x, int y, const char* text, uint16_t color = 1) const; void drawText(int x, int y, const char* text, uint16_t color = 1, EpdFontStyle style = REGULAR) const;
void drawTextBox(int x, int y, const std::string& text, int width, int height, bool bold = false, void drawUiText(int x, int y, const char* text, uint16_t color = 1, EpdFontStyle style = REGULAR) const;
bool italic = false) const; void drawSmallText(int x, int y, const char* text, uint16_t color = 1, EpdFontStyle style = REGULAR) const;
void drawLine(int x1, int y1, int x2, int y2, uint16_t color) const; void drawTextBox(int x, int y, const std::string& text, int width, int height, EpdFontStyle style = REGULAR) const;
void drawRect(int x, int y, int width, int height, uint16_t color) const; void drawLine(int x1, int y1, int x2, int y2, uint16_t color = 1) const;
void fillRect(int x, int y, int width, int height, uint16_t color) 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 clearScreen(bool black = false) const;
void flushDisplay() const; void flushDisplay(bool partialUpdate = true) const;
void flushArea(int x, int y, int width, int height) const; void flushArea(int x, int y, int width, int height) const;
int getPageWidth() const; int getPageWidth() const;
@@ -45,12 +49,4 @@ class EpdRenderer {
void setMarginBottom(const int newMarginBottom) { this->marginBottom = newMarginBottom; } void setMarginBottom(const int newMarginBottom) { this->marginBottom = newMarginBottom; }
void setMarginLeft(const int newMarginLeft) { this->marginLeft = newMarginLeft; } void setMarginLeft(const int newMarginLeft) { this->marginLeft = newMarginLeft; }
void setMarginRight(const int newMarginRight) { this->marginRight = newMarginRight; } void setMarginRight(const int newMarginRight) { this->marginRight = newMarginRight; }
// deep sleep helper - persist any state to disk that may be needed on wake
bool dehydrate();
// deep sleep helper - retrieve any state from disk after wake
bool hydrate();
// really really clear the screen
void reset();
uint8_t temperature = 20;
}; };

View File

@@ -107,11 +107,11 @@ void Section::renderPage() {
delete p; delete p;
} else if (pageCount == 0) { } else if (pageCount == 0) {
Serial.println("No pages to render"); Serial.println("No pages to render");
const int width = renderer->getTextWidth("Empty chapter", true); const int width = renderer->getTextWidth("Empty chapter", BOLD);
renderer->drawText((renderer->getPageWidth() - width) / 2, 300, "Empty chapter", true); renderer->drawText((renderer->getPageWidth() - width) / 2, 300, "Empty chapter", 1, BOLD);
} else { } else {
Serial.printf("Page out of bounds: %d (max %d)\n", currentPage, pageCount); Serial.printf("Page out of bounds: %d (max %d)\n", currentPage, pageCount);
const int width = renderer->getTextWidth("Out of bounds", true); const int width = renderer->getTextWidth("Out of bounds", BOLD);
renderer->drawText((renderer->getPageWidth() - width) / 2, 300, "Out of bounds", true); renderer->drawText((renderer->getPageWidth() - width) / 2, 300, "Out of bounds", 1, BOLD);
} }
} }

View File

@@ -56,7 +56,17 @@ std::list<TextBlock*> TextBlock::splitIntoLines(const EpdRenderer* renderer) {
uint16_t wordWidths[totalWordCount]; uint16_t wordWidths[totalWordCount];
for (int i = 0; i < words.size(); i++) { for (int i = 0; i < words.size(); i++) {
// measure the word // measure the word
const int width = renderer->getTextWidth(words[i].c_str(), wordStyles[i] & BOLD_SPAN, wordStyles[i] & ITALIC_SPAN); EpdFontStyle fontStyle = REGULAR;
if (wordStyles[i] & BOLD_SPAN) {
if (wordStyles[i] & ITALIC_SPAN) {
fontStyle = BOLD_ITALIC;
} else {
fontStyle = BOLD;
}
} else if (wordStyles[i] & ITALIC_SPAN) {
fontStyle = ITALIC;
}
const int width = renderer->getTextWidth(words[i].c_str(), fontStyle);
wordWidths[i] = width; wordWidths[i] = width;
} }
@@ -182,7 +192,18 @@ void TextBlock::render(const EpdRenderer* renderer, const int x, const int y) co
// get the style // get the style
const uint8_t wordStyle = wordStyles[i]; const uint8_t wordStyle = wordStyles[i];
// render the word // render the word
renderer->drawText(x + wordXpos[i], y, words[i].c_str(), wordStyle & BOLD_SPAN, wordStyle & ITALIC_SPAN); EpdFontStyle fontStyle = REGULAR;
if (wordStyles[i] & BOLD_SPAN) {
if (wordStyles[i] & ITALIC_SPAN) {
fontStyle = BOLD_ITALIC;
} else {
fontStyle = BOLD;
}
} else if (wordStyles[i] & ITALIC_SPAN) {
fontStyle = ITALIC;
}
renderer->drawText(x + wordXpos[i], y, words[i].c_str(), 1, fontStyle);
} }
} }

View File

@@ -29,15 +29,15 @@ Button getPressedButton() {
return NONE; return NONE;
} }
Input getInput(const bool skipWait) { Input getInput(const long maxHoldMs) {
const Button button = getPressedButton(); const Button button = getPressedButton();
if (button == NONE) return {NONE, 0}; if (button == NONE) return {NONE, 0};
if (skipWait) {
return {button, 0};
}
const auto start = millis(); const auto start = millis();
while (getPressedButton() == button) delay(50); unsigned long held = 0;
return {button, millis() - start}; while (getPressedButton() == button && (maxHoldMs < 0 || held < maxHoldMs)) {
delay(50);
held = millis() - start;
}
return {button, held};
} }

View File

@@ -25,4 +25,4 @@ struct Input {
void setupInputPinModes(); void setupInputPinModes();
Button getPressedButton(); Button getPressedButton();
Input getInput(bool skipWait = false); Input getInput(long maxHoldMs = -1);

113
src/images/CrossLarge.h Normal file
View File

@@ -0,0 +1,113 @@
#pragma once
#include <cstdint>
extern 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,
0xFF, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x7F, 0xFF,
0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF,
0xF8, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x1F,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x0F, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xE0, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x01,
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0xFF, 0xFF, 0xFF,
0xFE, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFC, 0x00, 0x00,
0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x3F, 0xFF,
0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF,
0xF8, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00,
0x00, 0x07, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x03, 0xFF,
0xFF, 0x80, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF,
0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFC, 0x00, 0x00,
0x00, 0x00, 0x00, 0x1F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0F, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xC0, 0x00,
0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0x00,
0x00, 0x03, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF,
0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x07, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFC,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFE, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x07, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00,
0x00, 0x03, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF,
0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xF8, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF,
0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF,
0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF,
0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF,
0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x3F, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF,
0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xE0, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xE0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F,
0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x3F, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x7F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0x80, 0x00, 0x00,
0x00, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xE0, 0x00,
0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xF0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x1F, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF,
0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFE, 0x00,
0x00, 0x00, 0x00, 0x00, 0x3F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
0x00, 0x7F, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0x80, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xC0, 0x00, 0x00,
0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x0F, 0xFF,
0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF,
0xF8, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x00,
0x00, 0x1F, 0xFF, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x3F, 0xFF,
0xFF, 0xFE, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0x00,
0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x03, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xE0, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0,
0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x0F, 0xFF,
0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF,
0xFC, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE3, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
};

2532
src/images/SleepScreenImg.h Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -8,9 +8,11 @@
#include "Battery.h" #include "Battery.h"
#include "CrossPointState.h" #include "CrossPointState.h"
#include "Input.h" #include "Input.h"
#include "screens/BootLogoScreen.h"
#include "screens/EpubReaderScreen.h" #include "screens/EpubReaderScreen.h"
#include "screens/FileSelectionScreen.h" #include "screens/FileSelectionScreen.h"
#include "screens/FullScreenMessageScreen.h" #include "screens/FullScreenMessageScreen.h"
#include "screens/SleepScreen.h"
#define SPI_FQ 40000000 #define SPI_FQ 40000000
// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults) // Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults)
@@ -34,7 +36,7 @@ CrossPointState* appState;
// Power button timing // Power button timing
// Time required to confirm boot from sleep // Time required to confirm boot from sleep
constexpr unsigned long POWER_BUTTON_WAKEUP_MS = 1500; constexpr unsigned long POWER_BUTTON_WAKEUP_MS = 1000;
// Time required to enter sleep mode // Time required to enter sleep mode
constexpr unsigned long POWER_BUTTON_SLEEP_MS = 1000; constexpr unsigned long POWER_BUTTON_SLEEP_MS = 1000;
@@ -50,24 +52,33 @@ Epub* loadEpub(const std::string& path) {
} }
Serial.println("Failed to load epub"); Serial.println("Failed to load epub");
free(epub); delete epub;
return nullptr; return nullptr;
} }
void enterNewScreen(Screen* screen) { void exitScreen() {
if (currentScreen) { if (currentScreen) {
currentScreen->onExit(); currentScreen->onExit();
delete currentScreen; delete currentScreen;
} }
}
void enterNewScreen(Screen* screen) {
currentScreen = screen; currentScreen = screen;
currentScreen->onEnter(); currentScreen->onEnter();
} }
// Verify long press on wake-up from deep sleep // Verify long press on wake-up from deep sleep
void verifyWakeupLongPress() { void verifyWakeupLongPress() {
const auto input = getInput(); // Give the user up to 1000ms to start holding the power button, and must hold for POWER_BUTTON_WAKEUP_MS
const auto start = millis();
auto input = getInput(POWER_BUTTON_WAKEUP_MS);
while (input.button != POWER && millis() - start < 1000) {
delay(50);
input = getInput(POWER_BUTTON_WAKEUP_MS);
}
if (input.button == POWER && input.pressTime > POWER_BUTTON_WAKEUP_MS) { if (input.button != POWER || input.pressTime < POWER_BUTTON_WAKEUP_MS) {
// Button released too early. Returning to sleep. // Button released too early. Returning to sleep.
// IMPORTANT: Re-arm the wakeup trigger before sleeping again // IMPORTANT: Re-arm the wakeup trigger before sleeping again
esp_deep_sleep_enable_gpio_wakeup(1ULL << BTN_GPIO3, ESP_GPIO_WAKEUP_GPIO_LOW); esp_deep_sleep_enable_gpio_wakeup(1ULL << BTN_GPIO3, ESP_GPIO_WAKEUP_GPIO_LOW);
@@ -75,12 +86,19 @@ void verifyWakeupLongPress() {
} }
} }
void waitForNoButton() {
while (getInput().button != NONE) {
delay(50);
}
}
// Enter deep sleep mode // Enter deep sleep mode
void enterDeepSleep() { void enterDeepSleep() {
enterNewScreen(new FullScreenMessageScreen(renderer, "Sleeping", true, false, true)); exitScreen();
enterNewScreen(new SleepScreen(renderer));
Serial.println("Power button released after a long press. Entering deep sleep."); Serial.println("Power button released after a long press. Entering deep sleep.");
delay(2000); // Allow Serial buffer to empty and display to update delay(1000); // Allow Serial buffer to empty and display to update
// Enable Wakeup on LOW (button press) // Enable Wakeup on LOW (button press)
esp_deep_sleep_enable_gpio_wakeup(1ULL << BTN_GPIO3, ESP_GPIO_WAKEUP_GPIO_LOW); esp_deep_sleep_enable_gpio_wakeup(1ULL << BTN_GPIO3, ESP_GPIO_WAKEUP_GPIO_LOW);
@@ -91,46 +109,39 @@ void enterDeepSleep() {
esp_deep_sleep_start(); esp_deep_sleep_start();
} }
void setupSerial() {
Serial.begin(115200);
// Wait for serial monitor
const unsigned long start = millis();
while (!Serial && (millis() - start) < 3000) {
delay(10);
}
if (Serial) {
// delay for monitor to start reading
delay(1000);
}
}
void onGoHome(); void onGoHome();
void onSelectEpubFile(const std::string& path) { void onSelectEpubFile(const std::string& path) {
exitScreen();
enterNewScreen(new FullScreenMessageScreen(renderer, "Loading...")); enterNewScreen(new FullScreenMessageScreen(renderer, "Loading..."));
Epub* epub = loadEpub(path); Epub* epub = loadEpub(path);
if (epub) { if (epub) {
appState->openEpubPath = path; appState->openEpubPath = path;
appState->saveToFile(); appState->saveToFile();
exitScreen();
enterNewScreen(new EpubReaderScreen(renderer, epub, onGoHome)); enterNewScreen(new EpubReaderScreen(renderer, epub, onGoHome));
} else { } else {
enterNewScreen(new FullScreenMessageScreen(renderer, "Failed to load epub")); exitScreen();
enterNewScreen(new FullScreenMessageScreen(renderer, "Failed to load epub", REGULAR, false, false));
delay(2000);
onGoHome();
} }
} }
void onGoHome() { enterNewScreen(new FileSelectionScreen(renderer, onSelectEpubFile)); }
void onGoHome() {
exitScreen();
enterNewScreen(new FileSelectionScreen(renderer, onSelectEpubFile));
}
void setup() { void setup() {
setupInputPinModes(); setupInputPinModes();
// Check if boot was triggered by the Power Button (Deep Sleep Wakeup)
// If triggered by RST pin or Battery insertion, this will be false, allowing
// normal boot.
if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_GPIO) {
verifyWakeupLongPress(); verifyWakeupLongPress();
}
setupSerial(); // Begin serial only if USB connected
pinMode(UART0_RXD, INPUT);
if (digitalRead(UART0_RXD) == HIGH) {
Serial.begin(115200);
}
// Initialize pins // Initialize pins
pinMode(BAT_GPIO0, INPUT); pinMode(BAT_GPIO0, INPUT);
@@ -145,7 +156,8 @@ void setup() {
display.setTextColor(GxEPD_BLACK); display.setTextColor(GxEPD_BLACK);
Serial.println("Display initialized"); Serial.println("Display initialized");
enterNewScreen(new FullScreenMessageScreen(renderer, "Booting...", true)); exitScreen();
enterNewScreen(new BootLogoScreen(renderer));
// SD Card Initialization // SD Card Initialization
SD.begin(SD_SPI_CS, SPI, SPI_FQ); SD.begin(SD_SPI_CS, SPI, SPI_FQ);
@@ -154,12 +166,19 @@ void setup() {
if (!appState->openEpubPath.empty()) { if (!appState->openEpubPath.empty()) {
Epub* epub = loadEpub(appState->openEpubPath); Epub* epub = loadEpub(appState->openEpubPath);
if (epub) { if (epub) {
exitScreen();
enterNewScreen(new EpubReaderScreen(renderer, epub, onGoHome)); enterNewScreen(new EpubReaderScreen(renderer, epub, onGoHome));
// Ensure we're not still holding the power button before leaving setup
waitForNoButton();
return; return;
} }
} }
exitScreen();
enterNewScreen(new FileSelectionScreen(renderer, onSelectEpubFile)); enterNewScreen(new FileSelectionScreen(renderer, onSelectEpubFile));
// Ensure we're not still holding the power button before leaving setup
waitForNoButton();
} }
void loop() { void loop() {

View File

@@ -0,0 +1,14 @@
#include "BootLogoScreen.h"
#include <EpdRenderer.h>
#include "images/CrossLarge.h"
void BootLogoScreen::onEnter() {
const auto pageWidth = renderer->getPageWidth();
const auto pageHeight = renderer->getPageHeight();
renderer->clearScreen();
// Location for images is from top right in landscape orientation
renderer->drawImage(CrossLarge, (pageHeight - 128) / 2, (pageWidth - 128) / 2, 128, 128);
}

View File

@@ -0,0 +1,8 @@
#pragma once
#include "Screen.h"
class BootLogoScreen final : public Screen {
public:
explicit BootLogoScreen(EpdRenderer* renderer) : Screen(renderer) {}
void onEnter() override;
};

View File

@@ -5,6 +5,7 @@
#include "Battery.h" #include "Battery.h"
constexpr int PAGES_PER_REFRESH = 10;
constexpr unsigned long SKIP_CHAPTER_MS = 700; constexpr unsigned long SKIP_CHAPTER_MS = 700;
void EpubReaderScreen::taskTrampoline(void* param) { void EpubReaderScreen::taskTrampoline(void* param) {
@@ -13,7 +14,11 @@ void EpubReaderScreen::taskTrampoline(void* param) {
} }
void EpubReaderScreen::onEnter() { void EpubReaderScreen::onEnter() {
sectionMutex = xSemaphoreCreateMutex(); if (!epub) {
return;
}
renderingMutex = xSemaphoreCreateMutex();
epub->setupCacheDir(); epub->setupCacheDir();
@@ -40,11 +45,18 @@ void EpubReaderScreen::onEnter() {
} }
void EpubReaderScreen::onExit() { void EpubReaderScreen::onExit() {
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle); vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr; displayTaskHandle = nullptr;
xSemaphoreTake(sectionMutex, portMAX_DELAY); }
vSemaphoreDelete(sectionMutex); vSemaphoreDelete(renderingMutex);
sectionMutex = nullptr; renderingMutex = nullptr;
delete section;
section = nullptr;
delete epub;
epub = nullptr;
} }
void EpubReaderScreen::handleInput(const Input input) { void EpubReaderScreen::handleInput(const Input input) {
@@ -71,23 +83,25 @@ void EpubReaderScreen::handleInput(const Input input) {
if (section->currentPage > 0) { if (section->currentPage > 0) {
section->currentPage--; section->currentPage--;
} else { } else {
xSemaphoreTake(sectionMutex, portMAX_DELAY); // We don't want to delete the section mid-render, so grab the semaphore
xSemaphoreTake(renderingMutex, portMAX_DELAY);
nextPageNumber = UINT16_MAX; nextPageNumber = UINT16_MAX;
currentSpineIndex--; currentSpineIndex--;
delete section; delete section;
section = nullptr; section = nullptr;
xSemaphoreGive(sectionMutex); xSemaphoreGive(renderingMutex);
} }
} else if (input.button == VOLUME_DOWN || input.button == RIGHT) { } else if (input.button == VOLUME_DOWN || input.button == RIGHT) {
if (section->currentPage < section->pageCount - 1) { if (section->currentPage < section->pageCount - 1) {
section->currentPage++; section->currentPage++;
} else { } else {
xSemaphoreTake(sectionMutex, portMAX_DELAY); // We don't want to delete the section mid-render, so grab the semaphore
xSemaphoreTake(renderingMutex, portMAX_DELAY);
nextPageNumber = 0; nextPageNumber = 0;
currentSpineIndex++; currentSpineIndex++;
delete section; delete section;
section = nullptr; section = nullptr;
xSemaphoreGive(sectionMutex); xSemaphoreGive(renderingMutex);
} }
} }
@@ -101,7 +115,9 @@ void EpubReaderScreen::displayTaskLoop() {
while (true) { while (true) {
if (updateRequired) { if (updateRequired) {
updateRequired = false; updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderPage(); renderPage();
xSemaphoreGive(renderingMutex);
} }
vTaskDelay(10 / portTICK_PERIOD_MS); vTaskDelay(10 / portTICK_PERIOD_MS);
} }
@@ -116,19 +132,31 @@ void EpubReaderScreen::renderPage() {
currentSpineIndex = 0; currentSpineIndex = 0;
} }
xSemaphoreTake(sectionMutex, portMAX_DELAY);
if (!section) { if (!section) {
const auto filepath = epub->getSpineItem(currentSpineIndex); const auto filepath = epub->getSpineItem(currentSpineIndex);
Serial.printf("Loading file: %s, index: %d\n", filepath.c_str(), currentSpineIndex); Serial.printf("Loading file: %s, index: %d\n", filepath.c_str(), currentSpineIndex);
section = new Section(epub, currentSpineIndex, renderer); section = new Section(epub, currentSpineIndex, renderer);
if (!section->hasCache()) { if (!section->hasCache()) {
Serial.println("Cache not found, building..."); Serial.println("Cache not found, building...");
{
const int textWidth = renderer->getTextWidth("Indexing...");
constexpr int margin = 20;
const int x = (renderer->getPageWidth() - textWidth - margin * 2) / 2;
constexpr int y = 50;
const int w = textWidth + margin * 2;
const int h = renderer->getLineHeight() + margin * 2;
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);
}
section->setupCacheDir(); section->setupCacheDir();
if (!section->persistPageDataToSD()) { if (!section->persistPageDataToSD()) {
Serial.println("Failed to persist page data to SD"); Serial.println("Failed to persist page data to SD");
free(section); delete section;
section = nullptr; section = nullptr;
xSemaphoreGive(sectionMutex);
return; return;
} }
} else { } else {
@@ -145,7 +173,13 @@ void EpubReaderScreen::renderPage() {
renderer->clearScreen(); renderer->clearScreen();
section->renderPage(); section->renderPage();
renderStatusBar(); renderStatusBar();
if (pagesUntilFullRefresh <= 1) {
renderer->flushDisplay(false);
pagesUntilFullRefresh = PAGES_PER_REFRESH;
} else {
renderer->flushDisplay(); renderer->flushDisplay();
pagesUntilFullRefresh--;
}
File f = SD.open((epub->getCachePath() + "/progress.bin").c_str(), FILE_WRITE); File f = SD.open((epub->getCachePath() + "/progress.bin").c_str(), FILE_WRITE);
uint8_t data[4]; uint8_t data[4];
@@ -155,46 +189,44 @@ void EpubReaderScreen::renderPage() {
data[3] = (section->currentPage >> 8) & 0xFF; data[3] = (section->currentPage >> 8) & 0xFF;
f.write(data, 4); f.write(data, 4);
f.close(); f.close();
xSemaphoreGive(sectionMutex);
} }
void EpubReaderScreen::renderStatusBar() const { void EpubReaderScreen::renderStatusBar() const {
const auto pageWidth = renderer->getPageWidth(); const auto pageWidth = renderer->getPageWidth();
std::string progress = std::to_string(section->currentPage + 1) + " / " + std::to_string(section->pageCount); const std::string progress = std::to_string(section->currentPage + 1) + " / " + std::to_string(section->pageCount);
const auto progressTextWidth = renderer->getSmallTextWidth(progress.c_str()); const auto progressTextWidth = renderer->getSmallTextWidth(progress.c_str());
renderer->drawSmallText(pageWidth - progressTextWidth, 765, progress.c_str()); renderer->drawSmallText(pageWidth - progressTextWidth, 765, progress.c_str());
const uint16_t percentage = battery.readPercentage(); const uint16_t percentage = battery.readPercentage();
auto percentageText = std::to_string(percentage) + "%"; const auto percentageText = std::to_string(percentage) + "%";
const auto percentageTextWidth = renderer->getSmallTextWidth(percentageText.c_str()); const auto percentageTextWidth = renderer->getSmallTextWidth(percentageText.c_str());
renderer->drawSmallText(20, 765, percentageText.c_str()); renderer->drawSmallText(20, 765, percentageText.c_str());
// 1 column on left, 2 columns on right, 5 columns of battery body // 1 column on left, 2 columns on right, 5 columns of battery body
constexpr int batteryWidth = 15; constexpr int batteryWidth = 15;
constexpr int batteryHeight = 10; constexpr int batteryHeight = 10;
const int x = 0; constexpr int x = 0;
const int y = 772; constexpr int y = 772;
// Top line // Top line
renderer->drawLine(x, y, x + batteryWidth - 4, y, 1); renderer->drawLine(x, y, x + batteryWidth - 4, y);
// Bottom line // Bottom line
renderer->drawLine(x, y + batteryHeight - 1, x + batteryWidth - 4, y + batteryHeight - 1, 1); renderer->drawLine(x, y + batteryHeight - 1, x + batteryWidth - 4, y + batteryHeight - 1);
// Left line // Left line
renderer->drawLine(x, y, x, y + batteryHeight - 1, 1); renderer->drawLine(x, y, x, y + batteryHeight - 1);
// Battery end // Battery end
renderer->drawLine(x + batteryWidth - 4, y, x + batteryWidth - 4, y + batteryHeight - 1, 1); renderer->drawLine(x + batteryWidth - 4, y, x + batteryWidth - 4, y + batteryHeight - 1);
renderer->drawLine(x + batteryWidth - 3, y + 2, x + batteryWidth - 3, y + batteryHeight - 3, 1); renderer->drawLine(x + batteryWidth - 3, y + 2, x + batteryWidth - 3, y + batteryHeight - 3);
renderer->drawLine(x + batteryWidth - 2, y + 2, x + batteryWidth - 2, y + batteryHeight - 3, 1); renderer->drawLine(x + batteryWidth - 2, y + 2, x + batteryWidth - 2, y + batteryHeight - 3);
renderer->drawLine(x + batteryWidth - 1, y + 2, x + batteryWidth - 1, y + batteryHeight - 3, 1); renderer->drawLine(x + batteryWidth - 1, y + 2, x + batteryWidth - 1, y + batteryHeight - 3);
// The +1 is to round up, so that we always fill at least one pixel // The +1 is to round up, so that we always fill at least one pixel
int filledWidth = percentage * (batteryWidth - 5) / 100 + 1; int filledWidth = percentage * (batteryWidth - 5) / 100 + 1;
if (filledWidth > batteryWidth - 5) { if (filledWidth > batteryWidth - 5) {
filledWidth = batteryWidth - 5; // Ensure we don't overflow filledWidth = batteryWidth - 5; // Ensure we don't overflow
} }
renderer->fillRect(x + 1, y + 1, filledWidth, batteryHeight - 2, 1); renderer->fillRect(x + 1, y + 1, filledWidth, batteryHeight - 2);
// Page width minus existing content with 30px padding on each side // Page width minus existing content with 30px padding on each side
const int leftMargin = 20 + percentageTextWidth + 30; const int leftMargin = 20 + percentageTextWidth + 30;

View File

@@ -11,9 +11,10 @@ class EpubReaderScreen final : public Screen {
Epub* epub; Epub* epub;
Section* section = nullptr; Section* section = nullptr;
TaskHandle_t displayTaskHandle = nullptr; TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t sectionMutex = nullptr; SemaphoreHandle_t renderingMutex = nullptr;
int currentSpineIndex = 0; int currentSpineIndex = 0;
int nextPageNumber = 0; int nextPageNumber = 0;
int pagesUntilFullRefresh = 0;
bool updateRequired = false; bool updateRequired = false;
const std::function<void()> onGoHome; const std::function<void()> onGoHome;
@@ -25,7 +26,6 @@ class EpubReaderScreen final : public Screen {
public: public:
explicit EpubReaderScreen(EpdRenderer* renderer, Epub* epub, const std::function<void()>& onGoHome) explicit EpubReaderScreen(EpdRenderer* renderer, Epub* epub, const std::function<void()>& onGoHome)
: Screen(renderer), epub(epub), onGoHome(onGoHome) {} : Screen(renderer), epub(epub), onGoHome(onGoHome) {}
~EpubReaderScreen() override { free(section); }
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void handleInput(Input input) override; void handleInput(Input input) override;

View File

@@ -8,26 +8,33 @@ void FileSelectionScreen::taskTrampoline(void* param) {
self->displayTaskLoop(); self->displayTaskLoop();
} }
void FileSelectionScreen::onEnter() { void FileSelectionScreen::loadFiles() {
files.clear(); files.clear();
auto root = SD.open("/"); selectorIndex = 0;
File file; auto root = SD.open(basepath.c_str());
while ((file = root.openNextFile())) { for (File file = root.openNextFile(); file; file = root.openNextFile()) {
if (file.isDirectory()) {
file.close();
continue;
}
auto filename = std::string(file.name()); auto filename = std::string(file.name());
if (filename.substr(filename.length() - 5) != ".epub" || filename[0] == '.') { if (filename[0] == '.') {
file.close(); file.close();
continue; continue;
} }
if (file.isDirectory()) {
files.emplace_back(filename + "/");
} else if (filename.substr(filename.length() - 5) == ".epub") {
files.emplace_back(filename); files.emplace_back(filename);
}
file.close(); file.close();
} }
root.close(); root.close();
}
void FileSelectionScreen::onEnter() {
renderingMutex = xSemaphoreCreateMutex();
basepath = "/";
loadFiles();
selectorIndex = 0;
// Trigger first update // Trigger first update
updateRequired = true; updateRequired = true;
@@ -41,9 +48,16 @@ void FileSelectionScreen::onEnter() {
} }
void FileSelectionScreen::onExit() { void FileSelectionScreen::onExit() {
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle); vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr; displayTaskHandle = nullptr;
} }
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
files.clear();
}
void FileSelectionScreen::handleInput(const Input input) { void FileSelectionScreen::handleInput(const Input input) {
if (input.button == VOLUME_DOWN || input.button == RIGHT) { if (input.button == VOLUME_DOWN || input.button == RIGHT) {
@@ -53,8 +67,23 @@ void FileSelectionScreen::handleInput(const Input input) {
selectorIndex = (selectorIndex + files.size() - 1) % files.size(); selectorIndex = (selectorIndex + files.size() - 1) % files.size();
updateRequired = true; updateRequired = true;
} else if (input.button == CONFIRM) { } else if (input.button == CONFIRM) {
Serial.printf("Selected file: %s\n", files[selectorIndex].c_str()); if (files.empty()) {
onSelect("/" + files[selectorIndex]); return;
}
if (files[selectorIndex].back() == '/') {
if (basepath.back() != '/') basepath += "/";
basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1);
loadFiles();
updateRequired = true;
} else {
onSelect(basepath + files[selectorIndex]);
}
} else if (input.button == BACK && basepath != "/") {
basepath = basepath.substr(0, basepath.rfind('/'));
if (basepath.empty()) basepath = "/";
loadFiles();
updateRequired = true;
} }
} }
@@ -62,7 +91,9 @@ void FileSelectionScreen::displayTaskLoop() {
while (true) { while (true) {
if (updateRequired) { if (updateRequired) {
updateRequired = false; updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render(); render();
xSemaphoreGive(renderingMutex);
} }
vTaskDelay(10 / portTICK_PERIOD_MS); vTaskDelay(10 / portTICK_PERIOD_MS);
} }
@@ -72,15 +103,19 @@ void FileSelectionScreen::render() const {
renderer->clearScreen(); renderer->clearScreen();
const auto pageWidth = renderer->getPageWidth(); const auto pageWidth = renderer->getPageWidth();
const auto titleWidth = renderer->getTextWidth("CrossPoint Reader", true); const auto titleWidth = renderer->getTextWidth("CrossPoint Reader", BOLD);
renderer->drawText((pageWidth - titleWidth) / 2, 0, "CrossPoint Reader", true); renderer->drawText((pageWidth - titleWidth) / 2, 0, "CrossPoint Reader", 1, BOLD);
if (files.empty()) {
renderer->drawUiText(10, 50, "No EPUBs found");
} else {
// Draw selection // Draw selection
renderer->fillRect(0, 50 + selectorIndex * 20 + 2, pageWidth - 1, 20, 1); renderer->fillRect(0, 50 + selectorIndex * 30 + 2, pageWidth - 1, 30);
for (size_t i = 0; i < files.size(); i++) { for (size_t i = 0; i < files.size(); i++) {
const auto file = files[i]; const auto file = files[i];
renderer->drawSmallText(50, 50 + i * 20, file.c_str(), i == selectorIndex ? 0 : 1); renderer->drawUiText(10, 50 + i * 30, file.c_str(), i == selectorIndex ? 0 : 1);
}
} }
renderer->flushDisplay(); renderer->flushDisplay();

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h> #include <freertos/task.h>
#include <functional> #include <functional>
@@ -10,6 +11,8 @@
class FileSelectionScreen final : public Screen { class FileSelectionScreen final : public Screen {
TaskHandle_t displayTaskHandle = nullptr; TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
std::string basepath = "/";
std::vector<std::string> files; std::vector<std::string> files;
int selectorIndex = 0; int selectorIndex = 0;
bool updateRequired = false; bool updateRequired = false;
@@ -18,6 +21,7 @@ class FileSelectionScreen final : public Screen {
static void taskTrampoline(void* param); static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop(); [[noreturn]] void displayTaskLoop();
void render() const; void render() const;
void loadFiles();
public: public:
explicit FileSelectionScreen(EpdRenderer* renderer, const std::function<void(const std::string&)>& onSelect) explicit FileSelectionScreen(EpdRenderer* renderer, const std::function<void(const std::string&)>& onSelect)

View File

@@ -3,12 +3,12 @@
#include <EpdRenderer.h> #include <EpdRenderer.h>
void FullScreenMessageScreen::onEnter() { void FullScreenMessageScreen::onEnter() {
const auto width = renderer->getTextWidth(text.c_str(), bold, italic); const auto width = renderer->getUiTextWidth(text.c_str(), style);
const auto height = renderer->getLineHeight(); const auto height = renderer->getLineHeight();
const auto left = (renderer->getPageWidth() - width) / 2; const auto left = (renderer->getPageWidth() - width) / 2;
const auto top = (renderer->getPageHeight() - height) / 2; const auto top = (renderer->getPageHeight() - height) / 2;
renderer->clearScreen(invert); renderer->clearScreen(invert);
renderer->drawText(left, top, text.c_str(), bold, italic, invert ? 0 : 1); renderer->drawUiText(left, top, text.c_str(), invert ? 0 : 1, style);
renderer->flushDisplay(); renderer->flushDisplay(partialUpdate);
} }

View File

@@ -2,17 +2,18 @@
#include <string> #include <string>
#include <utility> #include <utility>
#include "EpdFontFamily.h"
#include "Screen.h" #include "Screen.h"
class FullScreenMessageScreen final : public Screen { class FullScreenMessageScreen final : public Screen {
std::string text; std::string text;
bool bold; EpdFontStyle style;
bool italic;
bool invert; bool invert;
bool partialUpdate;
public: public:
explicit FullScreenMessageScreen(EpdRenderer* renderer, std::string text, const bool bold = false, explicit FullScreenMessageScreen(EpdRenderer* renderer, std::string text, const EpdFontStyle style = REGULAR,
const bool italic = false, const bool invert = false) const bool invert = false, const bool partialUpdate = true)
: Screen(renderer), text(std::move(text)), bold(bold), italic(italic), invert(invert) {} : Screen(renderer), text(std::move(text)), style(style), invert(invert), partialUpdate(partialUpdate) {}
void onEnter() override; void onEnter() override;
}; };

View File

@@ -0,0 +1,7 @@
#include "SleepScreen.h"
#include <EpdRenderer.h>
#include "images/SleepScreenImg.h"
void SleepScreen::onEnter() { renderer->drawImageNoMargin(SleepScreenImg, 0, 0, 800, 480, false, true); }

View File

@@ -0,0 +1,8 @@
#pragma once
#include "Screen.h"
class SleepScreen final : public Screen {
public:
explicit SleepScreen(EpdRenderer* renderer) : Screen(renderer) {}
void onEnter() override;
};