26 Commits
0.1.0 ... 0.2.3

Author SHA1 Message Date
Dave Allie
021f77eab3 Sort items on FileSelectionScreen 2025-12-06 13:01:16 +11:00
Dave Allie
6d3d25a288 Fix bug with selectin epubs inside of folders 2025-12-06 12:57:17 +11:00
Dave Allie
9a33030623 Use reference passing for EpdRenderer 2025-12-06 12:56:39 +11:00
Dave Allie
6414f85257 Use InputManager from community-sdk 2025-12-06 12:35:41 +11:00
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
Dave Allie
2f9f86b3dd Update README.md features 2025-12-04 00:14:47 +11:00
Dave Allie
47eb1157ef Support left and right buttons in reader and file picker 2025-12-04 00:11:51 +11:00
Dave Allie
aee239a931 Adjust input button thresholds to support more devices 2025-12-04 00:11:51 +11:00
Dave Allie
1ee8b728f9 Add file selection screen 2025-12-04 00:11:51 +11:00
Dave Allie
2c80aca7b5 Use correct current page on reader screen
Fixes the page counter not updating
2025-12-03 22:34:16 +11:00
Dave Allie
7704772ebe Handle nested navpoint elements in nxc TOC 2025-12-03 22:30:50 +11:00
Dave Allie
4186c7da9e Remove debug lines 2025-12-03 22:30:13 +11:00
Dave Allie
802c9d0a30 Add web flashing instructions 2025-12-03 22:21:11 +11:00
39 changed files with 7464 additions and 376 deletions

View File

@@ -31,7 +31,9 @@ This project is **not affiliated with Xteink**; it's built as a community projec
- [x] EPUB parsing and rendering - [x] EPUB parsing and rendering
- [x] Saved reading position - [x] Saved reading position
- [ ] File explorer with file picker - [ ] File explorer with file picker
- Currently CrossPoint will just open the first EPUB it finds at the root of the SD card - [x] Basic EPUB picker from root directory
- [x] Support nested folders
- [ ] 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
- [ ] WiFi connectivity - [ ] WiFi connectivity
@@ -46,14 +48,37 @@ 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
Connect your Xteink X4 to your computer via USB-C and run the following command.
```sh ```sh
pio run --target upload pio run --target upload
``` ```
#### Web
1. Connect your Xteink X4 to your computer via USB-C
2. Download the `firmware.bin` file from the latest release via the [releases page](https://github.com/daveallie/crosspoint-reader/releases)
3. Go to https://xteink.dve.al/ and flash the firmware file using the "OTA fast flash controls" section
4. Press the reset button on the Xteink X4 to restart the device
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
back to the other partition using the "Swap boot partition" button here https://xteink.dve.al/debug.
## Internals ## Internals
CrossPoint Reader is pretty aggressive about caching data down to the SD card to minimise RAM usage. The ESP32-C3 only CrossPoint Reader is pretty aggressive about caching data down to the SD card to minimise RAM usage. The ESP32-C3 only

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 { 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, GxEPD_BLACK); 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) 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

@@ -3,7 +3,6 @@
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <SD.h> #include <SD.h>
#include <ZipFile.h> #include <ZipFile.h>
#include <tinyxml2.h>
#include <map> #include <map>
@@ -162,14 +161,14 @@ bool Epub::parseContentOpf(ZipFile& zip, std::string& content_opf_file) {
return true; return true;
} }
bool Epub::parseTocNcxFile(ZipFile& zip) { bool Epub::parseTocNcxFile(const ZipFile& zip) {
// the ncx file should have been specified in the content.opf file // the ncx file should have been specified in the content.opf file
if (tocNcxItem.empty()) { if (tocNcxItem.empty()) {
Serial.println("No ncx file specified"); Serial.println("No ncx file specified");
return false; return false;
} }
auto ncxData = zip.readTextFileToMemory(tocNcxItem.c_str()); const auto ncxData = zip.readTextFileToMemory(tocNcxItem.c_str());
if (!ncxData) { if (!ncxData) {
Serial.printf("Could not find %s\n", tocNcxItem.c_str()); Serial.printf("Could not find %s\n", tocNcxItem.c_str());
return false; return false;
@@ -177,7 +176,7 @@ bool Epub::parseTocNcxFile(ZipFile& zip) {
// Parse the Toc contents // Parse the Toc contents
tinyxml2::XMLDocument doc; tinyxml2::XMLDocument doc;
auto result = doc.Parse(ncxData); const auto result = doc.Parse(ncxData);
free(ncxData); free(ncxData);
if (result != tinyxml2::XML_SUCCESS) { if (result != tinyxml2::XML_SUCCESS) {
@@ -185,27 +184,30 @@ bool Epub::parseTocNcxFile(ZipFile& zip) {
return false; return false;
} }
auto ncx = doc.FirstChildElement("ncx"); const auto ncx = doc.FirstChildElement("ncx");
if (!ncx) { if (!ncx) {
Serial.println("Could not find first child ncx in toc"); Serial.println("Could not find first child ncx in toc");
return false; return false;
} }
auto navMap = ncx->FirstChildElement("navMap"); const auto navMap = ncx->FirstChildElement("navMap");
if (!navMap) { if (!navMap) {
Serial.println("Could not find navMap child in ncx"); Serial.println("Could not find navMap child in ncx");
return false; return false;
} }
auto navPoint = navMap->FirstChildElement("navPoint"); recursivelyParseNavMap(navMap->FirstChildElement("navPoint"));
return true;
}
void Epub::recursivelyParseNavMap(tinyxml2::XMLElement* element) {
// Fills toc map // Fills toc map
while (navPoint) { while (element) {
std::string navTitle = navPoint->FirstChildElement("navLabel")->FirstChildElement("text")->FirstChild()->Value(); std::string navTitle = element->FirstChildElement("navLabel")->FirstChildElement("text")->FirstChild()->Value();
auto content = navPoint->FirstChildElement("content"); const auto content = element->FirstChildElement("content");
std::string href = contentBasePath + content->Attribute("src"); std::string href = contentBasePath + content->Attribute("src");
// split the href on the # to get the href and the anchor // split the href on the # to get the href and the anchor
size_t pos = href.find('#'); const size_t pos = href.find('#');
std::string anchor; std::string anchor;
if (pos != std::string::npos) { if (pos != std::string::npos) {
@@ -214,10 +216,13 @@ bool Epub::parseTocNcxFile(ZipFile& zip) {
} }
toc.emplace_back(navTitle, href, anchor, 0); toc.emplace_back(navTitle, href, anchor, 0);
navPoint = navPoint->NextSiblingElement("navPoint");
}
return true; tinyxml2::XMLElement* nestedNavPoint = element->FirstChildElement("navPoint");
if (nestedNavPoint) {
recursivelyParseNavMap(nestedNavPoint);
}
element = element->NextSiblingElement("navPoint");
}
} }
// load in the meta data for the epub file // load in the meta data for the epub file
@@ -369,9 +374,7 @@ int Epub::getSpineIndexForTocIndex(const int tocIndex) const {
int Epub::getTocIndexForSpineIndex(const int spineIndex) const { int Epub::getTocIndexForSpineIndex(const int spineIndex) const {
// the toc entry should have an href that matches the spine item // the toc entry should have an href that matches the spine item
// so we can find the toc index by looking for the href // so we can find the toc index by looking for the href
Serial.printf("Looking for %s\n", spine[spineIndex].second.c_str());
for (int i = 0; i < toc.size(); i++) { for (int i = 0; i < toc.size(); i++) {
Serial.printf("Looking at %s\n", toc[i].href.c_str());
if (toc[i].href == spine[spineIndex].second) { if (toc[i].href == spine[spineIndex].second) {
return i; return i;
} }

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <tinyxml2.h>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
@@ -38,36 +39,29 @@ class Epub {
// find the path for the content.opf file // find the path for the content.opf file
static bool findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile); static bool findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile);
bool parseContentOpf(ZipFile& zip, std::string& content_opf_file); bool parseContentOpf(ZipFile& zip, std::string& content_opf_file);
bool parseTocNcxFile(ZipFile& zip); bool parseTocNcxFile(const ZipFile& zip);
void recursivelyParseNavMap(tinyxml2::XMLElement* element);
public: public:
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) { explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
// create a cache key based on the filepath // create a cache key based on the filepath
cachePath = cacheDir + "/epub_" + std::to_string(std::hash<std::string>{}(this->filepath)); cachePath = cacheDir + "/epub_" + std::to_string(std::hash<std::string>{}(this->filepath));
} }
~Epub() = default; ~Epub() = default;
std::string& getBasePath() { return contentBasePath; } std::string& getBasePath() { return contentBasePath; }
bool load(); bool load();
void clearCache() const; void clearCache() const;
void setupCacheDir() const; void setupCacheDir() const;
const std::string& getCachePath() const; const std::string& getCachePath() const;
const std::string& getPath() const; const std::string& getPath() const;
const std::string& getTitle() const; const std::string& getTitle() const;
const std::string& getCoverImageItem() const; const std::string& getCoverImageItem() const;
uint8_t* getItemContents(const std::string& itemHref, size_t* size = nullptr) const; uint8_t* getItemContents(const std::string& itemHref, size_t* size = nullptr) const;
char* getTextItemContents(const std::string& itemHref, size_t* size = nullptr) const; char* getTextItemContents(const std::string& itemHref, size_t* size = nullptr) const;
std::string& getSpineItem(int spineIndex); std::string& getSpineItem(int spineIndex);
int getSpineItemsCount() const; int getSpineItemsCount() const;
EpubTocEntry& getTocItem(int tocTndex); EpubTocEntry& getTocItem(int tocTndex);
int getTocItemsCount() const; int getTocItemsCount() const;
// work out the section index for a toc index
int getSpineIndexForTocIndex(int tocIndex) const; int getSpineIndexForTocIndex(int tocIndex) const;
int getTocIndexForSpineIndex(int spineIndex) const; int getTocIndexForSpineIndex(int spineIndex) const;
}; };

View File

@@ -146,8 +146,8 @@ void EpubHtmlParser::makePages() {
currentPage = new Page(); currentPage = new Page();
} }
const int lineHeight = renderer->getLineHeight(); const int lineHeight = renderer.getLineHeight();
const int pageHeight = renderer->getPageHeight(); const int pageHeight = renderer.getPageHeight();
// Long running task, make sure to let other things happen // Long running task, make sure to let other things happen
vTaskDelay(1); vTaskDelay(1);

View File

@@ -10,7 +10,7 @@ class EpdRenderer;
class EpubHtmlParser final : public tinyxml2::XMLVisitor { class EpubHtmlParser final : public tinyxml2::XMLVisitor {
const char* filepath; const char* filepath;
EpdRenderer* renderer; EpdRenderer& renderer;
std::function<void(Page*)> completePageFn; std::function<void(Page*)> completePageFn;
bool insideBoldTag = false; bool insideBoldTag = false;
@@ -27,7 +27,7 @@ class EpubHtmlParser final : public tinyxml2::XMLVisitor {
bool VisitExit(const tinyxml2::XMLElement& element) override; bool VisitExit(const tinyxml2::XMLElement& element) override;
// xml parser callbacks // xml parser callbacks
public: public:
explicit EpubHtmlParser(const char* filepath, EpdRenderer* renderer, const std::function<void(Page*)>& completePageFn) explicit EpubHtmlParser(const char* filepath, EpdRenderer& renderer, const std::function<void(Page*)>& completePageFn)
: filepath(filepath), renderer(renderer), completePageFn(completePageFn) {} : filepath(filepath), renderer(renderer), completePageFn(completePageFn) {}
~EpubHtmlParser() override = default; ~EpubHtmlParser() override = default;
bool parseAndBuildPages(); bool parseAndBuildPages();

View File

@@ -3,7 +3,7 @@
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <Serialization.h> #include <Serialization.h>
void PageLine::render(EpdRenderer* renderer) { block->render(renderer, 0, yPos); } void PageLine::render(EpdRenderer& renderer) { block->render(renderer, 0, yPos); }
void PageLine::serialize(std::ostream& os) { void PageLine::serialize(std::ostream& os) {
serialization::writePod(os, yPos); serialization::writePod(os, yPos);
@@ -20,7 +20,7 @@ PageLine* PageLine::deserialize(std::istream& is) {
return new PageLine(tb, yPos); return new PageLine(tb, yPos);
} }
void Page::render(EpdRenderer* renderer) const { void Page::render(EpdRenderer& renderer) const {
const auto start = millis(); const auto start = millis();
for (const auto element : elements) { for (const auto element : elements) {
element->render(renderer); element->render(renderer);

View File

@@ -11,7 +11,7 @@ class PageElement {
int yPos; int yPos;
explicit PageElement(const int yPos) : yPos(yPos) {} explicit PageElement(const int yPos) : yPos(yPos) {}
virtual ~PageElement() = default; virtual ~PageElement() = default;
virtual void render(EpdRenderer* renderer) = 0; virtual void render(EpdRenderer& renderer) = 0;
virtual void serialize(std::ostream& os) = 0; virtual void serialize(std::ostream& os) = 0;
}; };
@@ -22,7 +22,7 @@ class PageLine final : public PageElement {
public: public:
PageLine(const TextBlock* block, const int yPos) : PageElement(yPos), block(block) {} PageLine(const TextBlock* block, const int yPos) : PageElement(yPos), block(block) {}
~PageLine() override { delete block; } ~PageLine() override { delete block; }
void render(EpdRenderer* renderer) override; void render(EpdRenderer& renderer) override;
void serialize(std::ostream& os) override; void serialize(std::ostream& os) override;
static PageLine* deserialize(std::istream& is); static PageLine* deserialize(std::istream& is);
}; };
@@ -32,7 +32,7 @@ class Page {
int nextY = 0; int nextY = 0;
// the list of block index and line numbers on this page // the list of block index and line numbers on this page
std::vector<PageElement*> elements; std::vector<PageElement*> elements;
void render(EpdRenderer* renderer) const; void render(EpdRenderer& renderer) const;
~Page() { ~Page() {
for (const auto element : elements) { for (const auto element : elements) {
delete element; delete element;

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

@@ -7,7 +7,7 @@ class EpdRenderer;
class Section { class Section {
Epub* epub; Epub* epub;
const int spineIndex; const int spineIndex;
EpdRenderer* renderer; EpdRenderer& renderer;
std::string cachePath; std::string cachePath;
void onPageComplete(const Page* page); void onPageComplete(const Page* page);
@@ -16,7 +16,7 @@ class Section {
int pageCount = 0; int pageCount = 0;
int currentPage = 0; int currentPage = 0;
explicit Section(Epub* epub, const int spineIndex, EpdRenderer* renderer) explicit Section(Epub* epub, const int spineIndex, EpdRenderer& renderer)
: epub(epub), spineIndex(spineIndex), renderer(renderer) { : epub(epub), spineIndex(spineIndex), renderer(renderer) {
cachePath = epub->getCachePath() + "/" + std::to_string(spineIndex); cachePath = epub->getCachePath() + "/" + std::to_string(spineIndex);
} }

View File

@@ -8,7 +8,7 @@ typedef enum { TEXT_BLOCK, IMAGE_BLOCK } BlockType;
class Block { class Block {
public: public:
virtual ~Block() = default; virtual ~Block() = default;
virtual void layout(EpdRenderer* renderer) = 0; virtual void layout(EpdRenderer& renderer) = 0;
virtual BlockType getType() = 0; virtual BlockType getType() = 0;
virtual bool isEmpty() = 0; virtual bool isEmpty() = 0;
virtual void finish() {} virtual void finish() {}

View File

@@ -43,10 +43,10 @@ void TextBlock::addSpan(const std::string& span, const bool is_bold, const bool
} }
} }
std::list<TextBlock*> TextBlock::splitIntoLines(const EpdRenderer* renderer) { std::list<TextBlock*> TextBlock::splitIntoLines(const EpdRenderer& renderer) {
const int totalWordCount = words.size(); const int totalWordCount = words.size();
const int pageWidth = renderer->getPageWidth(); const int pageWidth = renderer.getPageWidth();
const int spaceWidth = renderer->getSpaceWidth(); const int spaceWidth = renderer.getSpaceWidth();
words.shrink_to_fit(); words.shrink_to_fit();
wordStyles.shrink_to_fit(); wordStyles.shrink_to_fit();
@@ -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;
} }
@@ -177,12 +187,23 @@ std::list<TextBlock*> TextBlock::splitIntoLines(const EpdRenderer* renderer) {
return lines; return lines;
} }
void TextBlock::render(const EpdRenderer* renderer, const int x, const int y) const { void TextBlock::render(const EpdRenderer& renderer, const int x, const int y) const {
for (int i = 0; i < words.size(); i++) { for (int i = 0; i < words.size(); i++) {
// 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

@@ -40,10 +40,10 @@ class TextBlock final : public Block {
void set_style(const BLOCK_STYLE style) { this->style = style; } void set_style(const BLOCK_STYLE style) { this->style = style; }
BLOCK_STYLE get_style() const { return style; } BLOCK_STYLE get_style() const { return style; }
bool isEmpty() override { return words.empty(); } bool isEmpty() override { return words.empty(); }
void layout(EpdRenderer* renderer) override {}; void layout(EpdRenderer& renderer) override {};
// given a renderer works out where to break the words into lines // given a renderer works out where to break the words into lines
std::list<TextBlock*> splitIntoLines(const EpdRenderer* renderer); std::list<TextBlock*> splitIntoLines(const EpdRenderer& renderer);
void render(const EpdRenderer* renderer, int x, int y) const; void render(const EpdRenderer& renderer, int x, int y) const;
BlockType getType() override { return TEXT_BLOCK; } BlockType getType() override { return TEXT_BLOCK; }
void serialize(std::ostream& os) const; void serialize(std::ostream& os) const;
static TextBlock* deserialize(std::istream& is); static TextBlock* deserialize(std::istream& is);

View File

@@ -34,3 +34,4 @@ lib_deps =
zinggjm/GxEPD2@^1.6.5 zinggjm/GxEPD2@^1.6.5
https://github.com/leethomason/tinyxml2.git#11.0.0 https://github.com/leethomason/tinyxml2.git#11.0.0
BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor
InputManager=symlink://open-x4-sdk/libs/hardware/InputManager

35
src/CrossPointState.cpp Normal file
View File

@@ -0,0 +1,35 @@
#include "CrossPointState.h"
#include <HardwareSerial.h>
#include <SD.h>
#include <Serialization.h>
#include <fstream>
constexpr uint8_t STATE_VERSION = 1;
constexpr char STATE_FILE[] = "/sd/.crosspoint/state.bin";
bool CrossPointState::saveToFile() const {
std::ofstream outputFile(STATE_FILE);
serialization::writePod(outputFile, STATE_VERSION);
serialization::writeString(outputFile, openEpubPath);
outputFile.close();
return true;
}
bool CrossPointState::loadFromFile() {
std::ifstream inputFile(STATE_FILE);
uint8_t version;
serialization::readPod(inputFile, version);
if (version != STATE_VERSION) {
Serial.printf("CrossPointState: Unknown version %u\n", version);
inputFile.close();
return false;
}
serialization::readString(inputFile, openEpubPath);
inputFile.close();
return true;
}

13
src/CrossPointState.h Normal file
View File

@@ -0,0 +1,13 @@
#pragma once
#include <iosfwd>
#include <string>
class CrossPointState {
public:
std::string openEpubPath;
~CrossPointState() = default;
bool saveToFile() const;
bool loadFromFile();
};

View File

@@ -1,43 +0,0 @@
#include "Input.h"
#include <esp32-hal-adc.h>
void setupInputPinModes() {
pinMode(BTN_GPIO1, INPUT);
pinMode(BTN_GPIO2, INPUT);
pinMode(BTN_GPIO3, INPUT_PULLUP); // Power button
}
// Get currently pressed button by reading ADC values (and digital for power
// button)
Button getPressedButton() {
// Check BTN_GPIO3 (Power button) - digital read
if (digitalRead(BTN_GPIO3) == LOW) return POWER;
// Check BTN_GPIO1 (4 buttons on resistor ladder)
const int btn1 = analogRead(BTN_GPIO1);
if (btn1 < BTN_RIGHT_VAL + BTN_THRESHOLD) return RIGHT;
if (btn1 < BTN_LEFT_VAL + BTN_THRESHOLD) return LEFT;
if (btn1 < BTN_CONFIRM_VAL + BTN_THRESHOLD) return CONFIRM;
if (btn1 < BTN_BACK_VAL + BTN_THRESHOLD) return BACK;
// Check BTN_GPIO2 (2 buttons on resistor ladder)
const int btn2 = analogRead(BTN_GPIO2);
if (btn2 < BTN_VOLUME_DOWN_VAL + BTN_THRESHOLD) return VOLUME_DOWN;
if (btn2 < BTN_VOLUME_UP_VAL + BTN_THRESHOLD) return VOLUME_UP;
return NONE;
}
Input getInput(const bool skipWait) {
const Button button = getPressedButton();
if (button == NONE) return {NONE, 0};
if (skipWait) {
return {button, 0};
}
const auto start = millis();
while (getPressedButton() == button) delay(50);
return {button, millis() - start};
}

View File

@@ -1,28 +0,0 @@
#pragma once
// 4 buttons on ADC resistor ladder: Back, Confirm, Left, Right
#define BTN_GPIO1 1
// 2 buttons on ADC resistor ladder: Volume Up, Volume Down
#define BTN_GPIO2 2
// Power button (digital)
#define BTN_GPIO3 3
// Button ADC thresholds
#define BTN_THRESHOLD 100 // Threshold tolerance
#define BTN_RIGHT_VAL 3
#define BTN_LEFT_VAL 1470
#define BTN_CONFIRM_VAL 2655
#define BTN_BACK_VAL 3470
#define BTN_VOLUME_DOWN_VAL 3
#define BTN_VOLUME_UP_VAL 2305
enum Button { NONE = 0, RIGHT, LEFT, CONFIRM, BACK, VOLUME_UP, VOLUME_DOWN, POWER };
struct Input {
Button button;
unsigned long pressTime;
};
void setupInputPinModes();
Button getPressedButton();
Input getInput(bool skipWait = false);

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

@@ -2,13 +2,17 @@
#include <EpdRenderer.h> #include <EpdRenderer.h>
#include <Epub.h> #include <Epub.h>
#include <GxEPD2_BW.h> #include <GxEPD2_BW.h>
#include <InputManager.h>
#include <SD.h> #include <SD.h>
#include <SPI.h> #include <SPI.h>
#include "Battery.h" #include "Battery.h"
#include "Input.h" #include "CrossPointState.h"
#include "screens/BootLogoScreen.h"
#include "screens/EpubReaderScreen.h" #include "screens/EpubReaderScreen.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)
@@ -26,18 +30,20 @@
GxEPD2_BW<GxEPD2_426_GDEQ0426T82, GxEPD2_426_GDEQ0426T82::HEIGHT> display(GxEPD2_426_GDEQ0426T82(EPD_CS, EPD_DC, GxEPD2_BW<GxEPD2_426_GDEQ0426T82, GxEPD2_426_GDEQ0426T82::HEIGHT> display(GxEPD2_426_GDEQ0426T82(EPD_CS, EPD_DC,
EPD_RST, EPD_BUSY)); EPD_RST, EPD_BUSY));
auto renderer = new EpdRenderer(&display); InputManager inputManager;
EpdRenderer renderer(&display);
Screen* currentScreen; Screen* currentScreen;
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;
Epub* loadEpub(const std::string& path) { Epub* loadEpub(const std::string& path) {
if (!SD.exists(path.c_str())) { if (!SD.exists(path.c_str())) {
Serial.println("File does not exist"); Serial.printf("File does not exist: %s\n", path.c_str());
return nullptr; return nullptr;
} }
@@ -47,40 +53,75 @@ 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();
bool abort = false;
if (input.button == POWER && input.pressTime > POWER_BUTTON_WAKEUP_MS) { Serial.println("Verifying power button press");
inputManager.update();
while (!inputManager.isPressed(InputManager::BTN_POWER) && millis() - start < 1000) {
delay(50);
inputManager.update();
Serial.println("Waiting...");
}
Serial.printf("Made it? %s\n", inputManager.isPressed(InputManager::BTN_POWER) ? "yes" : "no");
if (inputManager.isPressed(InputManager::BTN_POWER)) {
do {
delay(50);
inputManager.update();
} while (inputManager.isPressed(InputManager::BTN_POWER) && inputManager.getHeldTime() < POWER_BUTTON_WAKEUP_MS);
abort = inputManager.getHeldTime() < POWER_BUTTON_WAKEUP_MS;
} else {
abort = true;
}
Serial.printf("held for %lu\n", inputManager.getHeldTime());
if (abort) {
// 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 << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
esp_deep_sleep_start(); esp_deep_sleep_start();
} }
} }
void waitForPowerRelease() {
inputManager.update();
while (inputManager.isPressed(InputManager::BTN_POWER)) {
delay(50);
inputManager.update();
}
}
// 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, inputManager));
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 << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
display.hibernate(); display.hibernate();
@@ -88,32 +129,40 @@ void enterDeepSleep() {
esp_deep_sleep_start(); esp_deep_sleep_start();
} }
void setupSerial() { void onGoHome();
Serial.begin(115200); void onSelectEpubFile(const std::string& path) {
// Wait for serial monitor exitScreen();
const unsigned long start = millis(); enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Loading..."));
while (!Serial && (millis() - start) < 3000) {
delay(10);
}
if (Serial) { Epub* epub = loadEpub(path);
// delay for monitor to start reading if (epub) {
delay(1000); appState.openEpubPath = path;
appState.saveToFile();
exitScreen();
enterNewScreen(new EpubReaderScreen(renderer, inputManager, epub, onGoHome));
} else {
exitScreen();
enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Failed to load epub", REGULAR, false, false));
delay(2000);
onGoHome();
} }
} }
void onGoHome() {
exitScreen();
enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile));
}
void setup() { void setup() {
setupInputPinModes(); inputManager.begin();
verifyWakeupLongPress();
// Check if boot was triggered by the Power Button (Deep Sleep Wakeup) // Begin serial only if USB connected
// If triggered by RST pin or Battery insertion, this will be false, allowing pinMode(UART0_RXD, INPUT);
// normal boot. if (digitalRead(UART0_RXD) == HIGH) {
if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_GPIO) { Serial.begin(115200);
verifyWakeupLongPress();
} }
setupSerial();
// Initialize pins // Initialize pins
pinMode(BAT_GPIO0, INPUT); pinMode(BAT_GPIO0, INPUT);
@@ -127,56 +176,49 @@ void setup() {
display.setTextColor(GxEPD_BLACK); display.setTextColor(GxEPD_BLACK);
Serial.println("Display initialized"); Serial.println("Display initialized");
enterNewScreen(new FullScreenMessageScreen(renderer, "Loading...", true)); exitScreen();
enterNewScreen(new BootLogoScreen(renderer, inputManager));
// SD Card Initialization // SD Card Initialization
SD.begin(SD_SPI_CS, SPI, SPI_FQ); SD.begin(SD_SPI_CS, SPI, SPI_FQ);
// TODO: Add a file selection screen, for now just load the first file appState.loadFromFile();
File root = SD.open("/"); if (!appState.openEpubPath.empty()) {
String filename; Epub* epub = loadEpub(appState.openEpubPath);
while (true) { if (epub) {
filename = root.getNextFileName(); exitScreen();
if (!filename) { enterNewScreen(new EpubReaderScreen(renderer, inputManager, epub, onGoHome));
break; // Ensure we're not still holding the power button before leaving setup
} waitForPowerRelease();
return;
if (filename.substring(filename.length() - 5) == ".epub") {
Serial.printf("Found epub: %s\n", filename.c_str());
break;
} }
} }
if (!filename) { exitScreen();
enterNewScreen(new FullScreenMessageScreen(renderer, "Could not find epub")); enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile));
return;
}
Epub* epub = loadEpub(std::string(filename.c_str())); // Ensure we're not still holding the power button before leaving setup
if (epub) { waitForPowerRelease();
enterNewScreen(new EpubReaderScreen(renderer, epub));
} else {
enterNewScreen(new FullScreenMessageScreen(renderer, "Failed to load epub"));
}
} }
void loop() { void loop() {
delay(50); delay(10);
const Input input = getInput(); static unsigned long lastMemPrint = 0;
if (Serial && millis() - lastMemPrint >= 2000) {
if (input.button == NONE) { Serial.printf("[%lu] Memory - Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(),
return; ESP.getHeapSize(), ESP.getMinFreeHeap());
lastMemPrint = millis();
} }
if (input.button == POWER && input.pressTime > POWER_BUTTON_SLEEP_MS) { inputManager.update();
if (inputManager.wasReleased(InputManager::BTN_POWER) && inputManager.getHeldTime() > POWER_BUTTON_WAKEUP_MS) {
enterDeepSleep(); enterDeepSleep();
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start // This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start
delay(1000);
return; return;
} }
if (currentScreen) { if (currentScreen) {
currentScreen->handleInput(input); currentScreen->handleInput();
} }
} }

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, InputManager& inputManager) : Screen(renderer, inputManager) {}
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,56 +45,82 @@ void EpubReaderScreen::onEnter() {
} }
void EpubReaderScreen::onExit() { void EpubReaderScreen::onExit() {
vTaskDelete(displayTaskHandle); // Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(sectionMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
vSemaphoreDelete(sectionMutex); if (displayTaskHandle) {
sectionMutex = nullptr; vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
delete section;
section = nullptr;
delete epub;
epub = nullptr;
} }
void EpubReaderScreen::handleInput(const Input input) { void EpubReaderScreen::handleInput() {
if (input.button == VOLUME_UP || input.button == VOLUME_DOWN) { if (inputManager.wasPressed(InputManager::BTN_BACK)) {
const bool skipChapter = input.pressTime > SKIP_CHAPTER_MS; onGoHome();
return;
}
// No current section, attempt to rerender the book const bool prevReleased =
if (!section) { inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT);
updateRequired = true; const bool nextReleased =
return; inputManager.wasReleased(InputManager::BTN_DOWN) || inputManager.wasReleased(InputManager::BTN_RIGHT);
}
if (input.button == VOLUME_UP && skipChapter) { if (!prevReleased && !nextReleased) {
nextPageNumber = 0; return;
}
Serial.printf("Prev released: %d, Next released: %d\n", prevReleased, nextReleased);
const bool skipChapter = inputManager.getHeldTime() > SKIP_CHAPTER_MS;
if (skipChapter) {
// We don't want to delete the section mid-render, so grab the semaphore
xSemaphoreTake(renderingMutex, portMAX_DELAY);
nextPageNumber = 0;
currentSpineIndex = nextReleased ? currentSpineIndex + 1 : currentSpineIndex - 1;
delete section;
section = nullptr;
xSemaphoreGive(renderingMutex);
updateRequired = true;
return;
}
// No current section, attempt to rerender the book
if (!section) {
updateRequired = true;
return;
}
if (prevReleased) {
if (section->currentPage > 0) {
section->currentPage--;
} else {
// We don't want to delete the section mid-render, so grab the semaphore
xSemaphoreTake(renderingMutex, portMAX_DELAY);
nextPageNumber = UINT16_MAX;
currentSpineIndex--; currentSpineIndex--;
delete section; delete section;
section = nullptr; section = nullptr;
} else if (input.button == VOLUME_DOWN && skipChapter) { xSemaphoreGive(renderingMutex);
}
updateRequired = true;
} else {
if (section->currentPage < section->pageCount - 1) {
section->currentPage++;
} else {
// 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;
} else if (input.button == VOLUME_UP) { xSemaphoreGive(renderingMutex);
if (section->currentPage > 0) {
section->currentPage--;
} else {
xSemaphoreTake(sectionMutex, portMAX_DELAY);
nextPageNumber = UINT16_MAX;
currentSpineIndex--;
delete section;
section = nullptr;
xSemaphoreGive(sectionMutex);
}
} else if (input.button == VOLUME_DOWN) {
if (section->currentPage < section->pageCount - 1) {
section->currentPage++;
} else {
xSemaphoreTake(sectionMutex, portMAX_DELAY);
nextPageNumber = 0;
currentSpineIndex++;
delete section;
section = nullptr;
xSemaphoreGive(sectionMutex);
}
} }
updateRequired = true; updateRequired = true;
} }
} }
@@ -98,7 +129,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);
} }
@@ -113,19 +146,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 {
@@ -139,10 +184,16 @@ void EpubReaderScreen::renderPage() {
} }
} }
renderer->clearScreen(); renderer.clearScreen();
section->renderPage(); section->renderPage();
renderStatusBar(); renderStatusBar();
renderer->flushDisplay(); if (pagesUntilFullRefresh <= 1) {
renderer.flushDisplay(false);
pagesUntilFullRefresh = PAGES_PER_REFRESH;
} else {
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];
@@ -152,46 +203,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(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;
@@ -199,11 +248,11 @@ void EpubReaderScreen::renderStatusBar() const {
const int availableTextWidth = pageWidth - leftMargin - rightMargin; const int availableTextWidth = pageWidth - leftMargin - rightMargin;
const auto tocItem = epub->getTocItem(epub->getTocIndexForSpineIndex(currentSpineIndex)); const auto tocItem = epub->getTocItem(epub->getTocIndexForSpineIndex(currentSpineIndex));
auto title = tocItem.title; auto title = tocItem.title;
int titleWidth = renderer->getSmallTextWidth(title.c_str()); int titleWidth = renderer.getSmallTextWidth(title.c_str());
while (titleWidth > availableTextWidth) { while (titleWidth > availableTextWidth) {
title = title.substr(0, title.length() - 8) + "..."; title = title.substr(0, title.length() - 8) + "...";
titleWidth = renderer->getSmallTextWidth(title.c_str()); titleWidth = renderer.getSmallTextWidth(title.c_str());
} }
renderer->drawSmallText(leftMargin + (availableTextWidth - titleWidth) / 2, 765, title.c_str()); renderer.drawSmallText(leftMargin + (availableTextWidth - titleWidth) / 2, 765, title.c_str());
} }

View File

@@ -11,11 +11,12 @@ 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 currentPage = 0; int pagesUntilFullRefresh = 0;
bool updateRequired = false; bool updateRequired = false;
const std::function<void()> onGoHome;
static void taskTrampoline(void* param); static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop(); [[noreturn]] void displayTaskLoop();
@@ -23,9 +24,10 @@ class EpubReaderScreen final : public Screen {
void renderStatusBar() const; void renderStatusBar() const;
public: public:
explicit EpubReaderScreen(EpdRenderer* renderer, Epub* epub) : Screen(renderer), epub(epub) {} explicit EpubReaderScreen(EpdRenderer& renderer, InputManager& inputManager, Epub* epub,
~EpubReaderScreen() override { free(section); } const std::function<void()>& onGoHome)
: Screen(renderer, inputManager), epub(epub), onGoHome(onGoHome) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void handleInput(Input input) override; void handleInput() override;
}; };

View File

@@ -0,0 +1,136 @@
#include "FileSelectionScreen.h"
#include <EpdRenderer.h>
#include <SD.h>
void caseInsensitiveSort(std::vector<std::string>& strs) {
std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) {
return lexicographical_compare(
begin(str1), end(str1), begin(str2), end(str2),
[](const char& char1, const char& char2) { return tolower(char1) < tolower(char2); });
});
}
void FileSelectionScreen::taskTrampoline(void* param) {
auto* self = static_cast<FileSelectionScreen*>(param);
self->displayTaskLoop();
}
void FileSelectionScreen::loadFiles() {
files.clear();
selectorIndex = 0;
auto root = SD.open(basepath.c_str());
for (File file = root.openNextFile(); file; file = root.openNextFile()) {
auto filename = std::string(file.name());
if (filename[0] == '.') {
file.close();
continue;
}
if (file.isDirectory()) {
files.emplace_back(filename + "/");
} else if (filename.substr(filename.length() - 5) == ".epub") {
files.emplace_back(filename);
}
file.close();
}
root.close();
caseInsensitiveSort(files);
}
void FileSelectionScreen::onEnter() {
renderingMutex = xSemaphoreCreateMutex();
basepath = "/";
loadFiles();
selectorIndex = 0;
// Trigger first update
updateRequired = true;
xTaskCreate(&FileSelectionScreen::taskTrampoline, "FileSelectionScreenTask",
1024, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
}
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);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
files.clear();
}
void FileSelectionScreen::handleInput() {
const bool prevPressed =
inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT);
const bool nextPressed =
inputManager.wasPressed(InputManager::BTN_DOWN) || inputManager.wasPressed(InputManager::BTN_RIGHT);
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
if (files.empty()) {
return;
}
if (basepath.back() != '/') basepath += "/";
if (files[selectorIndex].back() == '/') {
basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1);
loadFiles();
updateRequired = true;
} else {
onSelect(basepath + files[selectorIndex]);
}
} else if (inputManager.wasPressed(InputManager::BTN_BACK) && basepath != "/") {
basepath = basepath.substr(0, basepath.rfind('/'));
if (basepath.empty()) basepath = "/";
loadFiles();
updateRequired = true;
} else if (prevPressed) {
selectorIndex = (selectorIndex + files.size() - 1) % files.size();
updateRequired = true;
} else if (nextPressed) {
selectorIndex = (selectorIndex + 1) % files.size();
updateRequired = true;
}
}
void FileSelectionScreen::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void FileSelectionScreen::render() const {
renderer.clearScreen();
const auto pageWidth = renderer.getPageWidth();
const auto titleWidth = renderer.getTextWidth("CrossPoint Reader", BOLD);
renderer.drawText((pageWidth - titleWidth) / 2, 0, "CrossPoint Reader", 1, BOLD);
if (files.empty()) {
renderer.drawUiText(10, 50, "No EPUBs found");
} else {
// Draw selection
renderer.fillRect(0, 50 + selectorIndex * 30 + 2, pageWidth - 1, 30);
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.flushDisplay();
}

View File

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

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,23 @@
#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, InputManager& inputManager, std::string text,
const bool italic = false, const bool invert = false) const EpdFontStyle style = REGULAR, const bool invert = false,
: Screen(renderer), text(std::move(text)), bold(bold), italic(italic), invert(invert) {} const bool partialUpdate = true)
: Screen(renderer, inputManager),
text(std::move(text)),
style(style),
invert(invert),
partialUpdate(partialUpdate) {}
void onEnter() override; void onEnter() override;
}; };

View File

@@ -1,16 +1,17 @@
#pragma once #pragma once
#include "Input.h" #include <InputManager.h>
class EpdRenderer; class EpdRenderer;
class Screen { class Screen {
protected: protected:
EpdRenderer* renderer; EpdRenderer& renderer;
InputManager& inputManager;
public: public:
explicit Screen(EpdRenderer* renderer) : renderer(renderer) {} explicit Screen(EpdRenderer& renderer, InputManager& inputManager) : renderer(renderer), inputManager(inputManager) {}
virtual ~Screen() = default; virtual ~Screen() = default;
virtual void onEnter() {} virtual void onEnter() {}
virtual void onExit() {} virtual void onExit() {}
virtual void handleInput(Input input) {} virtual void handleInput() {}
}; };

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, InputManager& inputManager) : Screen(renderer, inputManager) {}
void onEnter() override;
};