Compare commits

...

30 Commits

Author SHA1 Message Date
Arthur Tazhitdinov
b1fe750739
Merge 5869b1539e into da4d3b5ea5 2026-01-27 13:08:59 -08:00
Arthur Tazhitdinov
5869b1539e fix 2026-01-28 02:08:55 +05:00
Arthur Tazhitdinov
d80b879ba7 Merge branch 'master' into refactor-popups 2026-01-28 01:39:33 +05:00
Xuan-Son Nguyen
da4d3b5ea5
feat: add HalDisplay and HalGPIO (#522)
Some checks failed
CI / build (push) Has been cancelled
## Summary

Extracted some changes from
https://github.com/crosspoint-reader/crosspoint-reader/pull/500 to make
reviewing easier

This PR adds HAL (Hardware Abstraction Layer) for display and GPIO
components, making it easier to write a stub or an emulated
implementation of the hardware.

SD card HAL will be added via another PR, because it's a bit more
tricky.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **NO**
2026-01-28 04:50:15 +11:00
Eliz
172916afd4
feat: Display epub metadata on Recents (#511)
* **What is the goal of this PR?** Implement a metadata viewer for the
Recents screen
* **What changes are included?**

| Recents | Files |
| --- | --- |
| <img alt="image"
src="https://github.com/user-attachments/assets/e0f2d816-ddce-4a2e-bd4a-cd431d0e6532"
/> | <img alt="image"
src="https://github.com/user-attachments/assets/3225cdce-d501-4175-bc92-73cb8bfe7a41"
/> |

For the Files screen, I have not made any changes on purpose. For the
Recents screen, we now display the Book title and author. If it is a
file with no epub metadata like txt or md, we display the file name
without the file extension.

---

Did you use AI tools to help write this code? _**< YES  >**_

Although I went trough all the code manually and made changes as well,
please be aware the majority of the code is AI generated.

---------

Co-authored-by: Eliz Kilic <elizk@google.com>
2026-01-28 04:25:42 +11:00
Dave Allie
ebcd813ff6
chore: Cut release 0.16.0 2026-01-28 04:08:04 +11:00
Dave Allie
712c566664
fix: Correctly render italics on image alt placeholders (#569)
## Summary

* Correctly render italics on image alt placeholders
  * Parser incorrectly handled depth of self-closing tags
  * Self-closing tags immediately call start and end tag

## Additional Context

* Previously, it would incorrectly make the whole chapter bold/italics,
or not italicised the image alt

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? No
2026-01-28 03:33:36 +11:00
Boris Faure
5894ae5afe
chore: .gitignore: add compile_commands.json & .cache (#568)
## Summary

* **What is the goal of this PR?**
Quality of Life

* **What changes are included?**
Add compile_commands.json & .cache to .gitignore .

Both are use by clangd that can help IDE support.

Run `pio run --target compiledb` to generate `compile_commands.json`.


---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-01-28 03:32:33 +11:00
Dave Allie
8c1c80787a
fix: Render keyboard entry over multiple lines (#567)
Some checks are pending
CI / build (push) Waiting to run
## Summary

* Render keyboard entry over multiple lines
  * Grows display areas based on input text
  * Shown on OPDS entry, but applies everywhere

## Additional Context

* Fixes
https://github.com/crosspoint-reader/crosspoint-reader/issues/554

| One line | Multi-line |
| --- | --- |
|
![IMG_5925](https://github.com/user-attachments/assets/28be00a8-7b90-4bf6-9ebf-4d4ad6642bc9)
|
![IMG_5926](https://github.com/user-attachments/assets/1c69a96f-d868-49a1-866c-546ca7b784ab)
|

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? No
2026-01-28 02:43:04 +11:00
Arthur Tazhitdinov
d528e7a1a9 reorder 2026-01-27 19:51:03 +05:00
Arthur Tazhitdinov
56e54a5209 Merge branch 'master' into refactor-popups 2026-01-27 19:49:38 +05:00
Arthur Tazhitdinov
6873404f50 refactor: restore wording 2026-01-26 15:39:07 +05:00
Arthur Tazhitdinov
adda1954b9 refactor: simplify popup logic in renderScreen function 2026-01-26 15:12:21 +05:00
Arthur Tazhitdinov
192acba809 refactor: add popup callback to section file creation for indexing feedback 2026-01-26 15:09:35 +05:00
Arthur Tazhitdinov
d9006b2991 refactor: remove progress callback and related logic from section file creation and indexing 2026-01-26 15:01:09 +05:00
Arthur Tazhitdinov
11366862cb refactor: remove unnecessary blank line in platformio.ini 2026-01-26 14:29:17 +05:00
Arthur Tazhitdinov
c78a0db558 refactor: revert build flags changes 2026-01-26 14:28:53 +05:00
Arthur Tazhitdinov
43cd44c061 refactor: Remove MIN_SIZE_FOR_PROGRESS constant from ChapterHtmlSlimParser 2026-01-26 14:27:12 +05:00
Arthur Tazhitdinov
aa2e6ad0b3 refactor: Move MIN_SIZE_FOR_PROGRESS constant to the correct location and remove unused include 2026-01-26 14:26:38 +05:00
Arthur Tazhitdinov
f4625b56e1 refactor: Add grayscale revert function and remove unused variable in renderScreen 2026-01-26 14:23:55 +05:00
Arthur Tazhitdinov
492e79f9c0 rectangular frame for popups 2026-01-26 14:18:12 +05:00
Arthur Tazhitdinov
1facf55fbd refactor: Update popup dimensions and styles; add rounded rectangle drawing functions 2026-01-23 08:23:19 +05:00
Arthur Tazhitdinov
d418948a76 refactor: Update popup dimensions and styles; add sleep entry message 2026-01-22 18:35:28 +05:00
Arthur Tazhitdinov
93ce00aed9 clang format fix 2026-01-21 22:12:48 +05:00
Arthur Tazhitdinov
a4e335f0aa Merge branch 'master' into refactor-popups 2026-01-21 22:09:23 +05:00
Arthur Tazhitdinov
e23091a109 clang format fix 2026-01-20 09:28:37 +05:00
Arthur Tazhitdinov
21304ec72d refactor: simplify PopupLayout by removing unused progress bar attributes 2026-01-20 09:18:52 +05:00
Arthur Tazhitdinov
4bd7232948 clang format fix 2026-01-20 08:58:38 +05:00
Arthur Tazhitdinov
27176da811 refactor: remove unused ScreenComponents include from main.cpp 2026-01-20 08:53:34 +05:00
Arthur Tazhitdinov
400b92e788 refactor: streamline popup rendering and remove unused BootActivity 2026-01-20 08:42:35 +05:00
28 changed files with 557 additions and 378 deletions

4
.gitignore vendored
View File

@ -6,4 +6,6 @@ lib/EpdFont/fontsrc
*.generated.h *.generated.h
.vs .vs
build build
**/__pycache__/ **/__pycache__/
/compile_commands.json
/.cache

View File

@ -123,9 +123,7 @@ bool Section::clearCache() const {
bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const uint16_t viewportHeight, const bool hyphenationEnabled,
const std::function<void()>& progressSetupFn, const std::function<void()>& popupFn) {
const std::function<void(int)>& progressFn) {
constexpr uint32_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
const auto localPath = epub->getSpineItem(spineIndex).href; const auto localPath = epub->getSpineItem(spineIndex).href;
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html"; const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
@ -171,11 +169,6 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
Serial.printf("[%lu] [SCT] Streamed temp HTML to %s (%d bytes)\n", millis(), tmpHtmlPath.c_str(), fileSize); Serial.printf("[%lu] [SCT] Streamed temp HTML to %s (%d bytes)\n", millis(), tmpHtmlPath.c_str(), fileSize);
// Only show progress bar for larger chapters where rendering overhead is worth it
if (progressSetupFn && fileSize >= MIN_SIZE_FOR_PROGRESS) {
progressSetupFn();
}
if (!SdMan.openFileForWrite("SCT", filePath, file)) { if (!SdMan.openFileForWrite("SCT", filePath, file)) {
return false; return false;
} }
@ -186,8 +179,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
ChapterHtmlSlimParser visitor( ChapterHtmlSlimParser visitor(
tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
viewportHeight, hyphenationEnabled, viewportHeight, hyphenationEnabled,
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, [this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, popupFn);
progressFn);
Hyphenator::setPreferredLanguage(epub->getLanguage()); Hyphenator::setPreferredLanguage(epub->getLanguage());
success = visitor.parseAndBuildPages(); success = visitor.parseAndBuildPages();

View File

@ -33,7 +33,6 @@ class Section {
bool clearCache() const; bool clearCache() const;
bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled,
const std::function<void()>& progressSetupFn = nullptr, const std::function<void()>& popupFn = nullptr);
const std::function<void(int)>& progressFn = nullptr);
std::unique_ptr<Page> loadPageFromSectionFile(); std::unique_ptr<Page> loadPageFromSectionFile();
}; };

View File

@ -10,8 +10,8 @@
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"}; const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]); constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
// Minimum file size (in bytes) to show progress bar - smaller chapters don't benefit from it // Minimum file size (in bytes) to show indexing popup - smaller chapters don't benefit from it
constexpr size_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB constexpr size_t MIN_SIZE_FOR_POPUP = 50 * 1024; // 50KB
const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"}; const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]); constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
@ -84,41 +84,42 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
if (strcmp(name, "table") == 0) { if (strcmp(name, "table") == 0) {
// Add placeholder text // Add placeholder text
self->startNewTextBlock(TextBlock::CENTER_ALIGN); self->startNewTextBlock(TextBlock::CENTER_ALIGN);
if (self->currentTextBlock) {
self->currentTextBlock->addWord("[Table omitted]", EpdFontFamily::ITALIC);
}
// Skip table contents self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
self->skipUntilDepth = self->depth; // Advance depth before processing character data (like you would for a element with text)
self->depth += 1; self->depth += 1;
self->characterData(userData, "[Table omitted]", strlen("[Table omitted]"));
// Skip table contents (skip until parent as we pre-advanced depth above)
self->skipUntilDepth = self->depth - 1;
return; return;
} }
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) { if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
// TODO: Start processing image tags // TODO: Start processing image tags
std::string alt; std::string alt = "[Image]";
if (atts != nullptr) { if (atts != nullptr) {
for (int i = 0; atts[i]; i += 2) { for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], "alt") == 0) { if (strcmp(atts[i], "alt") == 0) {
// add " " (counts as whitespace) at the end of alt if (strlen(atts[i + 1]) > 0) {
// so the corresponding text block ends. alt = "[Image: " + std::string(atts[i + 1]) + "]";
// TODO: A zero-width breaking space would be more appropriate (once/if we support it) }
alt = "[Image: " + std::string(atts[i + 1]) + "] "; break;
} }
} }
Serial.printf("[%lu] [EHP] Image alt: %s\n", millis(), alt.c_str());
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
self->depth += 1;
self->characterData(userData, alt.c_str(), alt.length());
return;
} else {
// Skip for now
self->skipUntilDepth = self->depth;
self->depth += 1;
return;
} }
Serial.printf("[%lu] [EHP] Image alt: %s\n", millis(), alt.c_str());
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
// Advance depth before processing character data (like you would for a element with text)
self->depth += 1;
self->characterData(userData, alt.c_str(), alt.length());
// Skip table contents (skip until parent as we pre-advanced depth above)
self->skipUntilDepth = self->depth - 1;
return;
} }
if (matches(name, SKIP_TAGS, NUM_SKIP_TAGS)) { if (matches(name, SKIP_TAGS, NUM_SKIP_TAGS)) {
@ -143,25 +144,43 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) { if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
self->startNewTextBlock(TextBlock::CENTER_ALIGN); self->startNewTextBlock(TextBlock::CENTER_ALIGN);
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth); self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
} else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) { self->depth += 1;
return;
}
if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
if (strcmp(name, "br") == 0) { if (strcmp(name, "br") == 0) {
if (self->partWordBufferIndex > 0) { if (self->partWordBufferIndex > 0) {
// flush word preceding <br/> to currentTextBlock before calling startNewTextBlock // flush word preceding <br/> to currentTextBlock before calling startNewTextBlock
self->flushPartWordBuffer(); self->flushPartWordBuffer();
} }
self->startNewTextBlock(self->currentTextBlock->getStyle()); self->startNewTextBlock(self->currentTextBlock->getStyle());
} else { self->depth += 1;
self->startNewTextBlock((TextBlock::Style)self->paragraphAlignment); return;
if (strcmp(name, "li") == 0) {
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
}
} }
} else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth); self->startNewTextBlock(static_cast<TextBlock::Style>(self->paragraphAlignment));
} else if (matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS)) { if (strcmp(name, "li") == 0) {
self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth); self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
}
self->depth += 1;
return;
} }
if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
self->depth += 1;
return;
}
if (matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS)) {
self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth);
self->depth += 1;
return;
}
// Unprocessed tag, just increasing depth and continue forward
self->depth += 1; self->depth += 1;
} }
@ -227,7 +246,8 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
// text styling needs to be overhauled to fix it. // text styling needs to be overhauled to fix it.
const bool shouldBreakText = const bool shouldBreakText =
matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS) || matches(name, HEADER_TAGS, NUM_HEADER_TAGS) || matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS) || matches(name, HEADER_TAGS, NUM_HEADER_TAGS) ||
matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) || self->depth == 1; matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) ||
strcmp(name, "table") == 0 || matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) || self->depth == 1;
if (shouldBreakText) { if (shouldBreakText) {
self->flushPartWordBuffer(); self->flushPartWordBuffer();
@ -269,10 +289,10 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
return false; return false;
} }
// Get file size for progress calculation // Get file size to decide whether to show indexing popup.
const size_t totalSize = file.size(); if (popupFn && file.size() >= MIN_SIZE_FOR_POPUP) {
size_t bytesRead = 0; popupFn();
int lastProgress = -1; }
XML_SetUserData(parser, this); XML_SetUserData(parser, this);
XML_SetElementHandler(parser, startElement, endElement); XML_SetElementHandler(parser, startElement, endElement);
@ -302,17 +322,6 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
return false; return false;
} }
// Update progress (call every 10% change to avoid too frequent updates)
// Only show progress for larger chapters where rendering overhead is worth it
bytesRead += len;
if (progressFn && totalSize >= MIN_SIZE_FOR_PROGRESS) {
const int progress = static_cast<int>((bytesRead * 100) / totalSize);
if (lastProgress / 10 != progress / 10) {
lastProgress = progress;
progressFn(progress);
}
}
done = file.available() == 0; done = file.available() == 0;
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) { if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {

View File

@ -18,7 +18,7 @@ class ChapterHtmlSlimParser {
const std::string& filepath; const std::string& filepath;
GfxRenderer& renderer; GfxRenderer& renderer;
std::function<void(std::unique_ptr<Page>)> completePageFn; std::function<void(std::unique_ptr<Page>)> completePageFn;
std::function<void(int)> progressFn; // Progress callback (0-100) std::function<void()> popupFn; // Popup callback
int depth = 0; int depth = 0;
int skipUntilDepth = INT_MAX; int skipUntilDepth = INT_MAX;
int boldUntilDepth = INT_MAX; int boldUntilDepth = INT_MAX;
@ -52,7 +52,7 @@ class ChapterHtmlSlimParser {
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const uint16_t viewportHeight, const bool hyphenationEnabled,
const std::function<void(std::unique_ptr<Page>)>& completePageFn, const std::function<void(std::unique_ptr<Page>)>& completePageFn,
const std::function<void(int)>& progressFn = nullptr) const std::function<void()>& popupFn = nullptr)
: filepath(filepath), : filepath(filepath),
renderer(renderer), renderer(renderer),
fontId(fontId), fontId(fontId),
@ -63,7 +63,7 @@ class ChapterHtmlSlimParser {
viewportHeight(viewportHeight), viewportHeight(viewportHeight),
hyphenationEnabled(hyphenationEnabled), hyphenationEnabled(hyphenationEnabled),
completePageFn(completePageFn), completePageFn(completePageFn),
progressFn(progressFn) {} popupFn(popupFn) {}
~ChapterHtmlSlimParser() = default; ~ChapterHtmlSlimParser() = default;
bool parseAndBuildPages(); bool parseAndBuildPages();
void addLineToPage(std::shared_ptr<TextBlock> line); void addLineToPage(std::shared_ptr<TextBlock> line);

View File

@ -10,19 +10,19 @@ void GfxRenderer::rotateCoordinates(const int x, const int y, int* rotatedX, int
// Logical portrait (480x800) → panel (800x480) // Logical portrait (480x800) → panel (800x480)
// Rotation: 90 degrees clockwise // Rotation: 90 degrees clockwise
*rotatedX = y; *rotatedX = y;
*rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x; *rotatedY = HalDisplay::DISPLAY_HEIGHT - 1 - x;
break; break;
} }
case LandscapeClockwise: { case LandscapeClockwise: {
// Logical landscape (800x480) rotated 180 degrees (swap top/bottom and left/right) // Logical landscape (800x480) rotated 180 degrees (swap top/bottom and left/right)
*rotatedX = EInkDisplay::DISPLAY_WIDTH - 1 - x; *rotatedX = HalDisplay::DISPLAY_WIDTH - 1 - x;
*rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - y; *rotatedY = HalDisplay::DISPLAY_HEIGHT - 1 - y;
break; break;
} }
case PortraitInverted: { case PortraitInverted: {
// Logical portrait (480x800) → panel (800x480) // Logical portrait (480x800) → panel (800x480)
// Rotation: 90 degrees counter-clockwise // Rotation: 90 degrees counter-clockwise
*rotatedX = EInkDisplay::DISPLAY_WIDTH - 1 - y; *rotatedX = HalDisplay::DISPLAY_WIDTH - 1 - y;
*rotatedY = x; *rotatedY = x;
break; break;
} }
@ -36,7 +36,7 @@ void GfxRenderer::rotateCoordinates(const int x, const int y, int* rotatedX, int
} }
void GfxRenderer::drawPixel(const int x, const int y, const bool state) const { void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); uint8_t* frameBuffer = display.getFrameBuffer();
// Early return if no framebuffer is set // Early return if no framebuffer is set
if (!frameBuffer) { if (!frameBuffer) {
@ -49,14 +49,13 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
rotateCoordinates(x, y, &rotatedX, &rotatedY); rotateCoordinates(x, y, &rotatedX, &rotatedY);
// Bounds checking against physical panel dimensions // Bounds checking against physical panel dimensions
if (rotatedX < 0 || rotatedX >= EInkDisplay::DISPLAY_WIDTH || rotatedY < 0 || if (rotatedX < 0 || rotatedX >= HalDisplay::DISPLAY_WIDTH || rotatedY < 0 || rotatedY >= HalDisplay::DISPLAY_HEIGHT) {
rotatedY >= EInkDisplay::DISPLAY_HEIGHT) {
Serial.printf("[%lu] [GFX] !! Outside range (%d, %d) -> (%d, %d)\n", millis(), x, y, rotatedX, rotatedY); Serial.printf("[%lu] [GFX] !! Outside range (%d, %d) -> (%d, %d)\n", millis(), x, y, rotatedX, rotatedY);
return; return;
} }
// Calculate byte position and bit position // Calculate byte position and bit position
const uint16_t byteIndex = rotatedY * EInkDisplay::DISPLAY_WIDTH_BYTES + (rotatedX / 8); const uint16_t byteIndex = rotatedY * HalDisplay::DISPLAY_WIDTH_BYTES + (rotatedX / 8);
const uint8_t bitPosition = 7 - (rotatedX % 8); // MSB first const uint8_t bitPosition = 7 - (rotatedX % 8); // MSB first
if (state) { if (state) {
@ -164,7 +163,7 @@ void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, co
break; break;
} }
// TODO: Rotate bits // TODO: Rotate bits
einkDisplay.drawImage(bitmap, rotatedX, rotatedY, width, height); display.drawImage(bitmap, rotatedX, rotatedY, width, height);
} }
void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight, void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight,
@ -399,22 +398,20 @@ void GfxRenderer::fillPolygon(const int* xPoints, const int* yPoints, int numPoi
free(nodeX); free(nodeX);
} }
void GfxRenderer::clearScreen(const uint8_t color) const { einkDisplay.clearScreen(color); } void GfxRenderer::clearScreen(const uint8_t color) const { display.clearScreen(color); }
void GfxRenderer::invertScreen() const { void GfxRenderer::invertScreen() const {
uint8_t* buffer = einkDisplay.getFrameBuffer(); uint8_t* buffer = display.getFrameBuffer();
if (!buffer) { if (!buffer) {
Serial.printf("[%lu] [GFX] !! No framebuffer in invertScreen\n", millis()); Serial.printf("[%lu] [GFX] !! No framebuffer in invertScreen\n", millis());
return; return;
} }
for (int i = 0; i < EInkDisplay::BUFFER_SIZE; i++) { for (int i = 0; i < HalDisplay::BUFFER_SIZE; i++) {
buffer[i] = ~buffer[i]; buffer[i] = ~buffer[i];
} }
} }
void GfxRenderer::displayBuffer(const EInkDisplay::RefreshMode refreshMode) const { void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const { display.displayBuffer(refreshMode); }
einkDisplay.displayBuffer(refreshMode);
}
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth, std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
const EpdFontFamily::Style style) const { const EpdFontFamily::Style style) const {
@ -433,13 +430,13 @@ int GfxRenderer::getScreenWidth() const {
case Portrait: case Portrait:
case PortraitInverted: case PortraitInverted:
// 480px wide in portrait logical coordinates // 480px wide in portrait logical coordinates
return EInkDisplay::DISPLAY_HEIGHT; return HalDisplay::DISPLAY_HEIGHT;
case LandscapeClockwise: case LandscapeClockwise:
case LandscapeCounterClockwise: case LandscapeCounterClockwise:
// 800px wide in landscape logical coordinates // 800px wide in landscape logical coordinates
return EInkDisplay::DISPLAY_WIDTH; return HalDisplay::DISPLAY_WIDTH;
} }
return EInkDisplay::DISPLAY_HEIGHT; return HalDisplay::DISPLAY_HEIGHT;
} }
int GfxRenderer::getScreenHeight() const { int GfxRenderer::getScreenHeight() const {
@ -447,13 +444,13 @@ int GfxRenderer::getScreenHeight() const {
case Portrait: case Portrait:
case PortraitInverted: case PortraitInverted:
// 800px tall in portrait logical coordinates // 800px tall in portrait logical coordinates
return EInkDisplay::DISPLAY_WIDTH; return HalDisplay::DISPLAY_WIDTH;
case LandscapeClockwise: case LandscapeClockwise:
case LandscapeCounterClockwise: case LandscapeCounterClockwise:
// 480px tall in landscape logical coordinates // 480px tall in landscape logical coordinates
return EInkDisplay::DISPLAY_HEIGHT; return HalDisplay::DISPLAY_HEIGHT;
} }
return EInkDisplay::DISPLAY_WIDTH; return HalDisplay::DISPLAY_WIDTH;
} }
int GfxRenderer::getSpaceWidth(const int fontId) const { int GfxRenderer::getSpaceWidth(const int fontId) const {
@ -653,17 +650,18 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
} }
} }
uint8_t* GfxRenderer::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); } uint8_t* GfxRenderer::getFrameBuffer() const { return display.getFrameBuffer(); }
size_t GfxRenderer::getBufferSize() { return EInkDisplay::BUFFER_SIZE; } size_t GfxRenderer::getBufferSize() { return HalDisplay::BUFFER_SIZE; }
void GfxRenderer::grayscaleRevert() const { einkDisplay.grayscaleRevert(); } // unused
// void GfxRenderer::grayscaleRevert() const { display.grayscaleRevert(); }
void GfxRenderer::copyGrayscaleLsbBuffers() const { einkDisplay.copyGrayscaleLsbBuffers(einkDisplay.getFrameBuffer()); } void GfxRenderer::copyGrayscaleLsbBuffers() const { display.copyGrayscaleLsbBuffers(display.getFrameBuffer()); }
void GfxRenderer::copyGrayscaleMsbBuffers() const { einkDisplay.copyGrayscaleMsbBuffers(einkDisplay.getFrameBuffer()); } void GfxRenderer::copyGrayscaleMsbBuffers() const { display.copyGrayscaleMsbBuffers(display.getFrameBuffer()); }
void GfxRenderer::displayGrayBuffer() const { einkDisplay.displayGrayBuffer(); } void GfxRenderer::displayGrayBuffer() const { display.displayGrayBuffer(); }
void GfxRenderer::freeBwBufferChunks() { void GfxRenderer::freeBwBufferChunks() {
for (auto& bwBufferChunk : bwBufferChunks) { for (auto& bwBufferChunk : bwBufferChunks) {
@ -681,7 +679,7 @@ void GfxRenderer::freeBwBufferChunks() {
* Returns true if buffer was stored successfully, false if allocation failed. * Returns true if buffer was stored successfully, false if allocation failed.
*/ */
bool GfxRenderer::storeBwBuffer() { bool GfxRenderer::storeBwBuffer() {
const uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); const uint8_t* frameBuffer = display.getFrameBuffer();
if (!frameBuffer) { if (!frameBuffer) {
Serial.printf("[%lu] [GFX] !! No framebuffer in storeBwBuffer\n", millis()); Serial.printf("[%lu] [GFX] !! No framebuffer in storeBwBuffer\n", millis());
return false; return false;
@ -736,7 +734,7 @@ void GfxRenderer::restoreBwBuffer() {
return; return;
} }
uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); uint8_t* frameBuffer = display.getFrameBuffer();
if (!frameBuffer) { if (!frameBuffer) {
Serial.printf("[%lu] [GFX] !! No framebuffer in restoreBwBuffer\n", millis()); Serial.printf("[%lu] [GFX] !! No framebuffer in restoreBwBuffer\n", millis());
freeBwBufferChunks(); freeBwBufferChunks();
@ -755,7 +753,7 @@ void GfxRenderer::restoreBwBuffer() {
memcpy(frameBuffer + offset, bwBufferChunks[i], BW_BUFFER_CHUNK_SIZE); memcpy(frameBuffer + offset, bwBufferChunks[i], BW_BUFFER_CHUNK_SIZE);
} }
einkDisplay.cleanupGrayscaleBuffers(frameBuffer); display.cleanupGrayscaleBuffers(frameBuffer);
freeBwBufferChunks(); freeBwBufferChunks();
Serial.printf("[%lu] [GFX] Restored and freed BW buffer chunks\n", millis()); Serial.printf("[%lu] [GFX] Restored and freed BW buffer chunks\n", millis());
@ -766,9 +764,9 @@ void GfxRenderer::restoreBwBuffer() {
* Use this when BW buffer was re-rendered instead of stored/restored. * Use this when BW buffer was re-rendered instead of stored/restored.
*/ */
void GfxRenderer::cleanupGrayscaleWithFrameBuffer() const { void GfxRenderer::cleanupGrayscaleWithFrameBuffer() const {
uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); uint8_t* frameBuffer = display.getFrameBuffer();
if (frameBuffer) { if (frameBuffer) {
einkDisplay.cleanupGrayscaleBuffers(frameBuffer); display.cleanupGrayscaleBuffers(frameBuffer);
} }
} }

View File

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

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

@ -0,0 +1,51 @@
#include <HalDisplay.h>
#include <HalGPIO.h>
#define SD_SPI_MISO 7
HalDisplay::HalDisplay() : einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY) {}
HalDisplay::~HalDisplay() {}
void HalDisplay::begin() { einkDisplay.begin(); }
void HalDisplay::clearScreen(uint8_t color) const { einkDisplay.clearScreen(color); }
void HalDisplay::drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h,
bool fromProgmem) const {
einkDisplay.drawImage(imageData, x, y, w, h, fromProgmem);
}
EInkDisplay::RefreshMode convertRefreshMode(HalDisplay::RefreshMode mode) {
switch (mode) {
case HalDisplay::FULL_REFRESH:
return EInkDisplay::FULL_REFRESH;
case HalDisplay::HALF_REFRESH:
return EInkDisplay::HALF_REFRESH;
case HalDisplay::FAST_REFRESH:
default:
return EInkDisplay::FAST_REFRESH;
}
}
void HalDisplay::displayBuffer(HalDisplay::RefreshMode mode) { einkDisplay.displayBuffer(convertRefreshMode(mode)); }
void HalDisplay::refreshDisplay(HalDisplay::RefreshMode mode, bool turnOffScreen) {
einkDisplay.refreshDisplay(convertRefreshMode(mode), turnOffScreen);
}
void HalDisplay::deepSleep() { einkDisplay.deepSleep(); }
uint8_t* HalDisplay::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); }
void HalDisplay::copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer) {
einkDisplay.copyGrayscaleBuffers(lsbBuffer, msbBuffer);
}
void HalDisplay::copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer) { einkDisplay.copyGrayscaleLsbBuffers(lsbBuffer); }
void HalDisplay::copyGrayscaleMsbBuffers(const uint8_t* msbBuffer) { einkDisplay.copyGrayscaleMsbBuffers(msbBuffer); }
void HalDisplay::cleanupGrayscaleBuffers(const uint8_t* bwBuffer) { einkDisplay.cleanupGrayscaleBuffers(bwBuffer); }
void HalDisplay::displayGrayBuffer() { einkDisplay.displayGrayBuffer(); }

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

@ -0,0 +1,52 @@
#pragma once
#include <Arduino.h>
#include <EInkDisplay.h>
class HalDisplay {
public:
// Constructor with pin configuration
HalDisplay();
// Destructor
~HalDisplay();
// Refresh modes
enum RefreshMode {
FULL_REFRESH, // Full refresh with complete waveform
HALF_REFRESH, // Half refresh (1720ms) - balanced quality and speed
FAST_REFRESH // Fast refresh using custom LUT
};
// Initialize the display hardware and driver
void begin();
// Display dimensions
static constexpr uint16_t DISPLAY_WIDTH = EInkDisplay::DISPLAY_WIDTH;
static constexpr uint16_t DISPLAY_HEIGHT = EInkDisplay::DISPLAY_HEIGHT;
static constexpr uint16_t DISPLAY_WIDTH_BYTES = DISPLAY_WIDTH / 8;
static constexpr uint32_t BUFFER_SIZE = DISPLAY_WIDTH_BYTES * DISPLAY_HEIGHT;
// Frame buffer operations
void clearScreen(uint8_t color = 0xFF) const;
void drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h,
bool fromProgmem = false) const;
void displayBuffer(RefreshMode mode = RefreshMode::FAST_REFRESH);
void refreshDisplay(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
// Power management
void deepSleep();
// Access to frame buffer
uint8_t* getFrameBuffer() const;
void copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer);
void copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer);
void copyGrayscaleMsbBuffers(const uint8_t* msbBuffer);
void cleanupGrayscaleBuffers(const uint8_t* bwBuffer);
void displayGrayBuffer();
private:
EInkDisplay einkDisplay;
};

55
lib/hal/HalGPIO.cpp Normal file
View File

@ -0,0 +1,55 @@
#include <HalGPIO.h>
#include <SPI.h>
#include <esp_sleep.h>
void HalGPIO::begin() {
inputMgr.begin();
SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS);
pinMode(BAT_GPIO0, INPUT);
pinMode(UART0_RXD, INPUT);
}
void HalGPIO::update() { inputMgr.update(); }
bool HalGPIO::isPressed(uint8_t buttonIndex) const { return inputMgr.isPressed(buttonIndex); }
bool HalGPIO::wasPressed(uint8_t buttonIndex) const { return inputMgr.wasPressed(buttonIndex); }
bool HalGPIO::wasAnyPressed() const { return inputMgr.wasAnyPressed(); }
bool HalGPIO::wasReleased(uint8_t buttonIndex) const { return inputMgr.wasReleased(buttonIndex); }
bool HalGPIO::wasAnyReleased() const { return inputMgr.wasAnyReleased(); }
unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); }
void HalGPIO::startDeepSleep() {
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
while (inputMgr.isPressed(BTN_POWER)) {
delay(50);
inputMgr.update();
}
// Enter Deep Sleep
esp_deep_sleep_start();
}
int HalGPIO::getBatteryPercentage() const {
static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0);
return battery.readPercentage();
}
bool HalGPIO::isUsbConnected() const {
// U0RXD/GPIO20 reads HIGH when USB is connected
return digitalRead(UART0_RXD) == HIGH;
}
bool HalGPIO::isWakeupByPowerButton() const {
const auto wakeupCause = esp_sleep_get_wakeup_cause();
const auto resetReason = esp_reset_reason();
if (isUsbConnected()) {
return wakeupCause == ESP_SLEEP_WAKEUP_GPIO;
} else {
return (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED) && (resetReason == ESP_RST_POWERON);
}
}

61
lib/hal/HalGPIO.h Normal file
View File

@ -0,0 +1,61 @@
#pragma once
#include <Arduino.h>
#include <BatteryMonitor.h>
#include <InputManager.h>
// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults)
#define EPD_SCLK 8 // SPI Clock
#define EPD_MOSI 10 // SPI MOSI (Master Out Slave In)
#define EPD_CS 21 // Chip Select
#define EPD_DC 4 // Data/Command
#define EPD_RST 5 // Reset
#define EPD_BUSY 6 // Busy
#define SPI_MISO 7 // SPI MISO, shared between SD card and display (Master In Slave Out)
#define BAT_GPIO0 0 // Battery voltage
#define UART0_RXD 20 // Used for USB connection detection
class HalGPIO {
#if CROSSPOINT_EMULATED == 0
InputManager inputMgr;
#endif
public:
HalGPIO() = default;
// Start button GPIO and setup SPI for screen and SD card
void begin();
// Button input methods
void update();
bool isPressed(uint8_t buttonIndex) const;
bool wasPressed(uint8_t buttonIndex) const;
bool wasAnyPressed() const;
bool wasReleased(uint8_t buttonIndex) const;
bool wasAnyReleased() const;
unsigned long getHeldTime() const;
// Setup wake up GPIO and enter deep sleep
void startDeepSleep();
// Get battery percentage (range 0-100)
int getBatteryPercentage() const;
// Check if USB is connected
bool isUsbConnected() const;
// Check if wakeup was caused by power button press
bool isWakeupByPowerButton() const;
// Button indices
static constexpr uint8_t BTN_BACK = 0;
static constexpr uint8_t BTN_CONFIRM = 1;
static constexpr uint8_t BTN_LEFT = 2;
static constexpr uint8_t BTN_RIGHT = 3;
static constexpr uint8_t BTN_UP = 4;
static constexpr uint8_t BTN_DOWN = 5;
static constexpr uint8_t BTN_POWER = 6;
};

View File

@ -2,7 +2,7 @@
default_envs = default default_envs = default
[crosspoint] [crosspoint]
version = 0.15.0 version = 0.16.0
[base] [base]
platform = espressif32 @ 6.12.0 platform = espressif32 @ 6.12.0

View File

@ -19,20 +19,20 @@ struct SideLayoutMap {
// Order matches CrossPointSettings::FRONT_BUTTON_LAYOUT. // Order matches CrossPointSettings::FRONT_BUTTON_LAYOUT.
constexpr FrontLayoutMap kFrontLayouts[] = { constexpr FrontLayoutMap kFrontLayouts[] = {
{InputManager::BTN_BACK, InputManager::BTN_CONFIRM, InputManager::BTN_LEFT, InputManager::BTN_RIGHT}, {HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM, HalGPIO::BTN_LEFT, HalGPIO::BTN_RIGHT},
{InputManager::BTN_LEFT, InputManager::BTN_RIGHT, InputManager::BTN_BACK, InputManager::BTN_CONFIRM}, {HalGPIO::BTN_LEFT, HalGPIO::BTN_RIGHT, HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM},
{InputManager::BTN_CONFIRM, InputManager::BTN_LEFT, InputManager::BTN_BACK, InputManager::BTN_RIGHT}, {HalGPIO::BTN_CONFIRM, HalGPIO::BTN_LEFT, HalGPIO::BTN_BACK, HalGPIO::BTN_RIGHT},
{InputManager::BTN_BACK, InputManager::BTN_CONFIRM, InputManager::BTN_RIGHT, InputManager::BTN_LEFT}, {HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM, HalGPIO::BTN_RIGHT, HalGPIO::BTN_LEFT},
}; };
// Order matches CrossPointSettings::SIDE_BUTTON_LAYOUT. // Order matches CrossPointSettings::SIDE_BUTTON_LAYOUT.
constexpr SideLayoutMap kSideLayouts[] = { constexpr SideLayoutMap kSideLayouts[] = {
{InputManager::BTN_UP, InputManager::BTN_DOWN}, {HalGPIO::BTN_UP, HalGPIO::BTN_DOWN},
{InputManager::BTN_DOWN, InputManager::BTN_UP}, {HalGPIO::BTN_DOWN, HalGPIO::BTN_UP},
}; };
} // namespace } // namespace
bool MappedInputManager::mapButton(const Button button, bool (InputManager::*fn)(uint8_t) const) const { bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint8_t) const) const {
const auto frontLayout = static_cast<CrossPointSettings::FRONT_BUTTON_LAYOUT>(SETTINGS.frontButtonLayout); const auto frontLayout = static_cast<CrossPointSettings::FRONT_BUTTON_LAYOUT>(SETTINGS.frontButtonLayout);
const auto sideLayout = static_cast<CrossPointSettings::SIDE_BUTTON_LAYOUT>(SETTINGS.sideButtonLayout); const auto sideLayout = static_cast<CrossPointSettings::SIDE_BUTTON_LAYOUT>(SETTINGS.sideButtonLayout);
const auto& front = kFrontLayouts[frontLayout]; const auto& front = kFrontLayouts[frontLayout];
@ -40,41 +40,39 @@ bool MappedInputManager::mapButton(const Button button, bool (InputManager::*fn)
switch (button) { switch (button) {
case Button::Back: case Button::Back:
return (inputManager.*fn)(front.back); return (gpio.*fn)(front.back);
case Button::Confirm: case Button::Confirm:
return (inputManager.*fn)(front.confirm); return (gpio.*fn)(front.confirm);
case Button::Left: case Button::Left:
return (inputManager.*fn)(front.left); return (gpio.*fn)(front.left);
case Button::Right: case Button::Right:
return (inputManager.*fn)(front.right); return (gpio.*fn)(front.right);
case Button::Up: case Button::Up:
return (inputManager.*fn)(InputManager::BTN_UP); return (gpio.*fn)(HalGPIO::BTN_UP);
case Button::Down: case Button::Down:
return (inputManager.*fn)(InputManager::BTN_DOWN); return (gpio.*fn)(HalGPIO::BTN_DOWN);
case Button::Power: case Button::Power:
return (inputManager.*fn)(InputManager::BTN_POWER); return (gpio.*fn)(HalGPIO::BTN_POWER);
case Button::PageBack: case Button::PageBack:
return (inputManager.*fn)(side.pageBack); return (gpio.*fn)(side.pageBack);
case Button::PageForward: case Button::PageForward:
return (inputManager.*fn)(side.pageForward); return (gpio.*fn)(side.pageForward);
} }
return false; return false;
} }
bool MappedInputManager::wasPressed(const Button button) const { return mapButton(button, &InputManager::wasPressed); } bool MappedInputManager::wasPressed(const Button button) const { return mapButton(button, &HalGPIO::wasPressed); }
bool MappedInputManager::wasReleased(const Button button) const { bool MappedInputManager::wasReleased(const Button button) const { return mapButton(button, &HalGPIO::wasReleased); }
return mapButton(button, &InputManager::wasReleased);
}
bool MappedInputManager::isPressed(const Button button) const { return mapButton(button, &InputManager::isPressed); } bool MappedInputManager::isPressed(const Button button) const { return mapButton(button, &HalGPIO::isPressed); }
bool MappedInputManager::wasAnyPressed() const { return inputManager.wasAnyPressed(); } bool MappedInputManager::wasAnyPressed() const { return gpio.wasAnyPressed(); }
bool MappedInputManager::wasAnyReleased() const { return inputManager.wasAnyReleased(); } bool MappedInputManager::wasAnyReleased() const { return gpio.wasAnyReleased(); }
unsigned long MappedInputManager::getHeldTime() const { return inputManager.getHeldTime(); } unsigned long MappedInputManager::getHeldTime() const { return gpio.getHeldTime(); }
MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const char* confirm, const char* previous, MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const char* confirm, const char* previous,
const char* next) const { const char* next) const {
@ -91,4 +89,4 @@ MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const
default: default:
return {back, confirm, previous, next}; return {back, confirm, previous, next};
} }
} }

View File

@ -1,6 +1,6 @@
#pragma once #pragma once
#include <InputManager.h> #include <HalGPIO.h>
class MappedInputManager { class MappedInputManager {
public: public:
@ -13,7 +13,7 @@ class MappedInputManager {
const char* btn4; const char* btn4;
}; };
explicit MappedInputManager(InputManager& inputManager) : inputManager(inputManager) {} explicit MappedInputManager(HalGPIO& gpio) : gpio(gpio) {}
bool wasPressed(Button button) const; bool wasPressed(Button button) const;
bool wasReleased(Button button) const; bool wasReleased(Button button) const;
@ -24,7 +24,7 @@ class MappedInputManager {
Labels mapLabels(const char* back, const char* confirm, const char* previous, const char* next) const; Labels mapLabels(const char* back, const char* confirm, const char* previous, const char* next) const;
private: private:
InputManager& inputManager; HalGPIO& gpio;
bool mapButton(Button button, bool (InputManager::*fn)(uint8_t) const) const; bool mapButton(Button button, bool (HalGPIO::*fn)(uint8_t) const) const;
}; };

View File

@ -7,22 +7,23 @@
#include <algorithm> #include <algorithm>
namespace { namespace {
constexpr uint8_t RECENT_BOOKS_FILE_VERSION = 1; constexpr uint8_t RECENT_BOOKS_FILE_VERSION = 2;
constexpr char RECENT_BOOKS_FILE[] = "/.crosspoint/recent.bin"; constexpr char RECENT_BOOKS_FILE[] = "/.crosspoint/recent.bin";
constexpr int MAX_RECENT_BOOKS = 10; constexpr int MAX_RECENT_BOOKS = 10;
} // namespace } // namespace
RecentBooksStore RecentBooksStore::instance; RecentBooksStore RecentBooksStore::instance;
void RecentBooksStore::addBook(const std::string& path) { void RecentBooksStore::addBook(const std::string& path, const std::string& title, const std::string& author) {
// Remove existing entry if present // Remove existing entry if present
auto it = std::find(recentBooks.begin(), recentBooks.end(), path); auto it =
std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; });
if (it != recentBooks.end()) { if (it != recentBooks.end()) {
recentBooks.erase(it); recentBooks.erase(it);
} }
// Add to front // Add to front
recentBooks.insert(recentBooks.begin(), path); recentBooks.insert(recentBooks.begin(), {path, title, author});
// Trim to max size // Trim to max size
if (recentBooks.size() > MAX_RECENT_BOOKS) { if (recentBooks.size() > MAX_RECENT_BOOKS) {
@ -46,7 +47,9 @@ bool RecentBooksStore::saveToFile() const {
serialization::writePod(outputFile, count); serialization::writePod(outputFile, count);
for (const auto& book : recentBooks) { for (const auto& book : recentBooks) {
serialization::writeString(outputFile, book); serialization::writeString(outputFile, book.path);
serialization::writeString(outputFile, book.title);
serialization::writeString(outputFile, book.author);
} }
outputFile.close(); outputFile.close();
@ -63,24 +66,41 @@ bool RecentBooksStore::loadFromFile() {
uint8_t version; uint8_t version;
serialization::readPod(inputFile, version); serialization::readPod(inputFile, version);
if (version != RECENT_BOOKS_FILE_VERSION) { if (version != RECENT_BOOKS_FILE_VERSION) {
Serial.printf("[%lu] [RBS] Deserialization failed: Unknown version %u\n", millis(), version); if (version == 1) {
inputFile.close(); // Old version, just read paths
return false; uint8_t count;
} serialization::readPod(inputFile, count);
recentBooks.clear();
recentBooks.reserve(count);
for (uint8_t i = 0; i < count; i++) {
std::string path;
serialization::readString(inputFile, path);
// Title and author will be empty, they will be filled when the book is
// opened again
recentBooks.push_back({path, "", ""});
}
} else {
Serial.printf("[%lu] [RBS] Deserialization failed: Unknown version %u\n", millis(), version);
inputFile.close();
return false;
}
} else {
uint8_t count;
serialization::readPod(inputFile, count);
uint8_t count; recentBooks.clear();
serialization::readPod(inputFile, count); recentBooks.reserve(count);
recentBooks.clear(); for (uint8_t i = 0; i < count; i++) {
recentBooks.reserve(count); std::string path, title, author;
serialization::readString(inputFile, path);
for (uint8_t i = 0; i < count; i++) { serialization::readString(inputFile, title);
std::string path; serialization::readString(inputFile, author);
serialization::readString(inputFile, path); recentBooks.push_back({path, title, author});
recentBooks.push_back(path); }
} }
inputFile.close(); inputFile.close();
Serial.printf("[%lu] [RBS] Recent books loaded from file (%d entries)\n", millis(), count); Serial.printf("[%lu] [RBS] Recent books loaded from file (%d entries)\n", millis(), recentBooks.size());
return true; return true;
} }

View File

@ -2,11 +2,19 @@
#include <string> #include <string>
#include <vector> #include <vector>
struct RecentBook {
std::string path;
std::string title;
std::string author;
bool operator==(const RecentBook& other) const { return path == other.path; }
};
class RecentBooksStore { class RecentBooksStore {
// Static instance // Static instance
static RecentBooksStore instance; static RecentBooksStore instance;
std::vector<std::string> recentBooks; std::vector<RecentBook> recentBooks;
public: public:
~RecentBooksStore() = default; ~RecentBooksStore() = default;
@ -14,11 +22,11 @@ class RecentBooksStore {
// Get singleton instance // Get singleton instance
static RecentBooksStore& getInstance() { return instance; } static RecentBooksStore& getInstance() { return instance; }
// Add a book path to the recent list (moves to front if already exists) // Add a book to the recent list (moves to front if already exists)
void addBook(const std::string& path); void addBook(const std::string& path, const std::string& title, const std::string& author);
// Get the list of recent book paths (most recent first) // Get the list of recent books (most recent first)
const std::vector<std::string>& getBooks() const { return recentBooks; } const std::vector<RecentBook>& getBooks() const { return recentBooks; }
// Get the count of recent books // Get the count of recent books
int getCount() const { return static_cast<int>(recentBooks.size()); } int getCount() const { return static_cast<int>(recentBooks.size()); }

View File

@ -42,6 +42,38 @@ void ScreenComponents::drawBattery(const GfxRenderer& renderer, const int left,
renderer.fillRect(x + 2, y + 2, filledWidth, batteryHeight - 4); renderer.fillRect(x + 2, y + 2, filledWidth, batteryHeight - 4);
} }
ScreenComponents::PopupLayout ScreenComponents::drawPopup(const GfxRenderer& renderer, const char* message) {
constexpr int margin = 15;
constexpr int y = 60;
const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, message, EpdFontFamily::BOLD);
const int textHeight = renderer.getLineHeight(UI_12_FONT_ID);
const int w = textWidth + margin * 2;
const int h = textHeight + margin * 2;
const int x = (renderer.getScreenWidth() - w) / 2;
renderer.fillRect(x - 2, y - 2, w + 4, h + 4, true); // frame thickness 2
renderer.fillRect(x, y, w, h, false);
const int textX = x + (w - textWidth) / 2;
const int textY = y + margin - 2;
renderer.drawText(UI_12_FONT_ID, textX, textY, message, true, EpdFontFamily::BOLD);
renderer.displayBuffer();
return {x, y, w, h};
}
void ScreenComponents::fillPopupProgress(const GfxRenderer& renderer, const PopupLayout& layout, const int progress) {
constexpr int barHeight = 4;
const int barWidth = layout.width - 30; // twice the margin in drawPopup to match text width
const int barX = layout.x + (layout.width - barWidth) / 2;
const int barY = layout.y + layout.height - 10;
int fillWidth = barWidth * progress / 100;
renderer.fillRect(barX, barY, fillWidth, barHeight, true);
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
}
void ScreenComponents::drawBookProgressBar(const GfxRenderer& renderer, const size_t bookProgress) { void ScreenComponents::drawBookProgressBar(const GfxRenderer& renderer, const size_t bookProgress) {
int vieweableMarginTop, vieweableMarginRight, vieweableMarginBottom, vieweableMarginLeft; int vieweableMarginTop, vieweableMarginRight, vieweableMarginBottom, vieweableMarginLeft;
renderer.getOrientedViewableTRBL(&vieweableMarginTop, &vieweableMarginRight, &vieweableMarginBottom, renderer.getOrientedViewableTRBL(&vieweableMarginTop, &vieweableMarginRight, &vieweableMarginBottom,

View File

@ -15,9 +15,20 @@ class ScreenComponents {
public: public:
static const int BOOK_PROGRESS_BAR_HEIGHT = 4; static const int BOOK_PROGRESS_BAR_HEIGHT = 4;
struct PopupLayout {
int x;
int y;
int width;
int height;
};
static void drawBattery(const GfxRenderer& renderer, int left, int top, bool showPercentage = true); static void drawBattery(const GfxRenderer& renderer, int left, int top, bool showPercentage = true);
static void drawBookProgressBar(const GfxRenderer& renderer, size_t bookProgress); static void drawBookProgressBar(const GfxRenderer& renderer, size_t bookProgress);
static PopupLayout drawPopup(const GfxRenderer& renderer, const char* message);
static void fillPopupProgress(const GfxRenderer& renderer, const PopupLayout& layout, int progress);
// Draw a horizontal tab bar with underline indicator for selected tab // Draw a horizontal tab bar with underline indicator for selected tab
// Returns the height of the tab bar (for positioning content below) // Returns the height of the tab bar (for positioning content below)
static int drawTabBar(const GfxRenderer& renderer, int y, const std::vector<TabInfo>& tabs); static int drawTabBar(const GfxRenderer& renderer, int y, const std::vector<TabInfo>& tabs);

View File

@ -8,13 +8,15 @@
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "CrossPointState.h" #include "CrossPointState.h"
#include "ScreenComponents.h"
#include "fontIds.h" #include "fontIds.h"
#include "images/CrossLarge.h" #include "images/CrossLarge.h"
#include "util/StringUtils.h" #include "util/StringUtils.h"
void SleepActivity::onEnter() { void SleepActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
renderPopup("Entering Sleep...");
ScreenComponents::drawPopup(renderer, "Entering Sleep...");
if (SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::BLANK) { if (SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::BLANK) {
return renderBlankSleepScreen(); return renderBlankSleepScreen();
@ -31,20 +33,6 @@ void SleepActivity::onEnter() {
renderDefaultSleepScreen(); renderDefaultSleepScreen();
} }
void SleepActivity::renderPopup(const char* message) const {
const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, message, EpdFontFamily::BOLD);
constexpr int margin = 20;
const int x = (renderer.getScreenWidth() - textWidth - margin * 2) / 2;
constexpr int y = 117;
const int w = textWidth + margin * 2;
const int h = renderer.getLineHeight(UI_12_FONT_ID) + margin * 2;
// renderer.clearScreen();
renderer.fillRect(x - 5, y - 5, w + 10, h + 10, true);
renderer.fillRect(x + 5, y + 5, w - 10, h - 10, false);
renderer.drawText(UI_12_FONT_ID, x + margin, y + margin, message, true, EpdFontFamily::BOLD);
renderer.displayBuffer();
}
void SleepActivity::renderCustomSleepScreen() const { void SleepActivity::renderCustomSleepScreen() const {
// Check if we have a /sleep directory // Check if we have a /sleep directory
auto dir = SdMan.open("/sleep"); auto dir = SdMan.open("/sleep");
@ -133,7 +121,7 @@ void SleepActivity::renderDefaultSleepScreen() const {
renderer.invertScreen(); renderer.invertScreen();
} }
renderer.displayBuffer(EInkDisplay::HALF_REFRESH); renderer.displayBuffer(HalDisplay::HALF_REFRESH);
} }
void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const { void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
@ -189,7 +177,7 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
renderer.invertScreen(); renderer.invertScreen();
} }
renderer.displayBuffer(EInkDisplay::HALF_REFRESH); renderer.displayBuffer(HalDisplay::HALF_REFRESH);
if (hasGreyscale) { if (hasGreyscale) {
bitmap.rewindToData(); bitmap.rewindToData();
@ -280,5 +268,5 @@ void SleepActivity::renderCoverSleepScreen() const {
void SleepActivity::renderBlankSleepScreen() const { void SleepActivity::renderBlankSleepScreen() const {
renderer.clearScreen(); renderer.clearScreen();
renderer.displayBuffer(EInkDisplay::HALF_REFRESH); renderer.displayBuffer(HalDisplay::HALF_REFRESH);
} }

View File

@ -10,7 +10,6 @@ class SleepActivity final : public Activity {
void onEnter() override; void onEnter() override;
private: private:
void renderPopup(const char* message) const;
void renderDefaultSleepScreen() const; void renderDefaultSleepScreen() const;
void renderCustomSleepScreen() const; void renderCustomSleepScreen() const;
void renderCoverSleepScreen() const; void renderCoverSleepScreen() const;

View File

@ -16,6 +16,7 @@ namespace {
constexpr int TAB_BAR_Y = 15; constexpr int TAB_BAR_Y = 15;
constexpr int CONTENT_START_Y = 60; constexpr int CONTENT_START_Y = 60;
constexpr int LINE_HEIGHT = 30; constexpr int LINE_HEIGHT = 30;
constexpr int RECENTS_LINE_HEIGHT = 65; // Increased for two-line items
constexpr int LEFT_MARGIN = 20; constexpr int LEFT_MARGIN = 20;
constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator
@ -47,7 +48,7 @@ int MyLibraryActivity::getPageItems() const {
int MyLibraryActivity::getCurrentItemCount() const { int MyLibraryActivity::getCurrentItemCount() const {
if (currentTab == Tab::Recent) { if (currentTab == Tab::Recent) {
return static_cast<int>(bookTitles.size()); return static_cast<int>(recentBooks.size());
} }
return static_cast<int>(files.size()); return static_cast<int>(files.size());
} }
@ -65,34 +66,16 @@ int MyLibraryActivity::getCurrentPage() const {
} }
void MyLibraryActivity::loadRecentBooks() { void MyLibraryActivity::loadRecentBooks() {
constexpr size_t MAX_RECENT_BOOKS = 20; recentBooks.clear();
bookTitles.clear();
bookPaths.clear();
const auto& books = RECENT_BOOKS.getBooks(); const auto& books = RECENT_BOOKS.getBooks();
bookTitles.reserve(std::min(books.size(), MAX_RECENT_BOOKS)); recentBooks.reserve(books.size());
bookPaths.reserve(std::min(books.size(), MAX_RECENT_BOOKS));
for (const auto& path : books) {
// Limit to maximum number of recent books
if (bookTitles.size() >= MAX_RECENT_BOOKS) {
break;
}
for (const auto& book : books) {
// Skip if file no longer exists // Skip if file no longer exists
if (!SdMan.exists(path.c_str())) { if (!SdMan.exists(book.path.c_str())) {
continue; continue;
} }
recentBooks.push_back(book);
// Extract filename from path for display
std::string title = path;
const size_t lastSlash = title.find_last_of('/');
if (lastSlash != std::string::npos) {
title = title.substr(lastSlash + 1);
}
bookTitles.push_back(title);
bookPaths.push_back(path);
} }
} }
@ -176,8 +159,6 @@ void MyLibraryActivity::onExit() {
vSemaphoreDelete(renderingMutex); vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr; renderingMutex = nullptr;
bookTitles.clear();
bookPaths.clear();
files.clear(); files.clear();
} }
@ -207,8 +188,8 @@ void MyLibraryActivity::loop() {
// Confirm button - open selected item // Confirm button - open selected item
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (currentTab == Tab::Recent) { if (currentTab == Tab::Recent) {
if (!bookPaths.empty() && selectorIndex < static_cast<int>(bookPaths.size())) { if (!recentBooks.empty() && selectorIndex < static_cast<int>(recentBooks.size())) {
onSelectBook(bookPaths[selectorIndex], currentTab); onSelectBook(recentBooks[selectorIndex].path, currentTab);
} }
} else { } else {
// Files tab // Files tab
@ -333,7 +314,7 @@ void MyLibraryActivity::render() const {
void MyLibraryActivity::renderRecentTab() const { void MyLibraryActivity::renderRecentTab() const {
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const int pageItems = getPageItems(); const int pageItems = getPageItems();
const int bookCount = static_cast<int>(bookTitles.size()); const int bookCount = static_cast<int>(recentBooks.size());
if (bookCount == 0) { if (bookCount == 0) {
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No recent books"); renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No recent books");
@ -343,14 +324,37 @@ void MyLibraryActivity::renderRecentTab() const {
const auto pageStartIndex = selectorIndex / pageItems * pageItems; const auto pageStartIndex = selectorIndex / pageItems * pageItems;
// Draw selection highlight // Draw selection highlight
renderer.fillRect(0, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN, renderer.fillRect(0, CONTENT_START_Y + (selectorIndex % pageItems) * RECENTS_LINE_HEIGHT - 2,
LINE_HEIGHT); pageWidth - RIGHT_MARGIN, RECENTS_LINE_HEIGHT);
// Draw items // Draw items
for (int i = pageStartIndex; i < bookCount && i < pageStartIndex + pageItems; i++) { for (int i = pageStartIndex; i < bookCount && i < pageStartIndex + pageItems; i++) {
auto item = renderer.truncatedText(UI_10_FONT_ID, bookTitles[i].c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN); const auto& book = recentBooks[i];
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, item.c_str(), const int y = CONTENT_START_Y + (i % pageItems) * RECENTS_LINE_HEIGHT;
i != selectorIndex);
// Line 1: Title
std::string title = book.title;
if (title.empty()) {
// Fallback for older entries or files without metadata
title = book.path;
const size_t lastSlash = title.find_last_of('/');
if (lastSlash != std::string::npos) {
title = title.substr(lastSlash + 1);
}
const size_t dot = title.find_last_of('.');
if (dot != std::string::npos) {
title.resize(dot);
}
}
auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title.c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN);
renderer.drawText(UI_12_FONT_ID, LEFT_MARGIN, y + 2, truncatedTitle.c_str(), i != selectorIndex);
// Line 2: Author
if (!book.author.empty()) {
auto truncatedAuthor =
renderer.truncatedText(UI_10_FONT_ID, book.author.c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN);
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, y + 32, truncatedAuthor.c_str(), i != selectorIndex);
}
} }
} }

View File

@ -8,6 +8,7 @@
#include <vector> #include <vector>
#include "../Activity.h" #include "../Activity.h"
#include "RecentBooksStore.h"
class MyLibraryActivity final : public Activity { class MyLibraryActivity final : public Activity {
public: public:
@ -22,8 +23,7 @@ class MyLibraryActivity final : public Activity {
bool updateRequired = false; bool updateRequired = false;
// Recent tab state // Recent tab state
std::vector<std::string> bookTitles; // Display titles for each book std::vector<RecentBook> recentBooks;
std::vector<std::string> bookPaths; // Paths for each visible book (excludes missing)
// Files tab state (from FileSelectionActivity) // Files tab state (from FileSelectionActivity)
std::string basepath = "/"; std::string basepath = "/";

View File

@ -85,7 +85,7 @@ void EpubReaderActivity::onEnter() {
// Save current epub as last opened epub and add to recent books // Save current epub as last opened epub and add to recent books
APP_STATE.openEpubPath = epub->getPath(); APP_STATE.openEpubPath = epub->getPath();
APP_STATE.saveToFile(); APP_STATE.saveToFile();
RECENT_BOOKS.addBook(epub->getPath()); RECENT_BOOKS.addBook(epub->getPath(), epub->getTitle(), epub->getAuthor());
// Trigger first update // Trigger first update
updateRequired = true; updateRequired = true;
@ -308,49 +308,11 @@ void EpubReaderActivity::renderScreen() {
viewportHeight, SETTINGS.hyphenationEnabled)) { viewportHeight, SETTINGS.hyphenationEnabled)) {
Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis()); Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis());
// Progress bar dimensions const auto popupFn = [this]() { ScreenComponents::drawPopup(renderer, "Indexing..."); };
constexpr int barWidth = 200;
constexpr int barHeight = 10;
constexpr int boxMargin = 20;
const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, "Indexing...");
const int boxWidthWithBar = (barWidth > textWidth ? barWidth : textWidth) + boxMargin * 2;
const int boxWidthNoBar = textWidth + boxMargin * 2;
const int boxHeightWithBar = renderer.getLineHeight(UI_12_FONT_ID) + barHeight + boxMargin * 3;
const int boxHeightNoBar = renderer.getLineHeight(UI_12_FONT_ID) + boxMargin * 2;
const int boxXWithBar = (renderer.getScreenWidth() - boxWidthWithBar) / 2;
const int boxXNoBar = (renderer.getScreenWidth() - boxWidthNoBar) / 2;
constexpr int boxY = 50;
const int barX = boxXWithBar + (boxWidthWithBar - barWidth) / 2;
const int barY = boxY + renderer.getLineHeight(UI_12_FONT_ID) + boxMargin * 2;
// Always show "Indexing..." text first
{
renderer.fillRect(boxXNoBar, boxY, boxWidthNoBar, boxHeightNoBar, false);
renderer.drawText(UI_12_FONT_ID, boxXNoBar + boxMargin, boxY + boxMargin, "Indexing...");
renderer.drawRect(boxXNoBar + 5, boxY + 5, boxWidthNoBar - 10, boxHeightNoBar - 10);
renderer.displayBuffer();
pagesUntilFullRefresh = 0;
}
// Setup callback - only called for chapters >= 50KB, redraws with progress bar
auto progressSetup = [this, boxXWithBar, boxWidthWithBar, boxHeightWithBar, barX, barY] {
renderer.fillRect(boxXWithBar, boxY, boxWidthWithBar, boxHeightWithBar, false);
renderer.drawText(UI_12_FONT_ID, boxXWithBar + boxMargin, boxY + boxMargin, "Indexing...");
renderer.drawRect(boxXWithBar + 5, boxY + 5, boxWidthWithBar - 10, boxHeightWithBar - 10);
renderer.drawRect(barX, barY, barWidth, barHeight);
renderer.displayBuffer();
};
// Progress callback to update progress bar
auto progressCallback = [this, barX, barY, barWidth, barHeight](int progress) {
const int fillWidth = (barWidth - 2) * progress / 100;
renderer.fillRect(barX + 1, barY + 1, fillWidth, barHeight - 2, true);
renderer.displayBuffer(EInkDisplay::FAST_REFRESH);
};
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, progressSetup, progressCallback)) { viewportHeight, SETTINGS.hyphenationEnabled, popupFn)) {
Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis()); Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis());
section.reset(); section.reset();
return; return;
@ -428,7 +390,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
if (pagesUntilFullRefresh <= 1) { if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(EInkDisplay::HALF_REFRESH); renderer.displayBuffer(HalDisplay::HALF_REFRESH);
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
} else { } else {
renderer.displayBuffer(); renderer.displayBuffer();

View File

@ -60,7 +60,7 @@ void TxtReaderActivity::onEnter() {
// Save current txt as last opened file and add to recent books // Save current txt as last opened file and add to recent books
APP_STATE.openEpubPath = txt->getPath(); APP_STATE.openEpubPath = txt->getPath();
APP_STATE.saveToFile(); APP_STATE.saveToFile();
RECENT_BOOKS.addBook(txt->getPath()); RECENT_BOOKS.addBook(txt->getPath(), "", "");
// Trigger first update // Trigger first update
updateRequired = true; updateRequired = true;
@ -207,28 +207,10 @@ void TxtReaderActivity::buildPageIndex() {
size_t offset = 0; size_t offset = 0;
const size_t fileSize = txt->getFileSize(); const size_t fileSize = txt->getFileSize();
int lastProgressPercent = -1;
Serial.printf("[%lu] [TRS] Building page index for %zu bytes...\n", millis(), fileSize); Serial.printf("[%lu] [TRS] Building page index for %zu bytes...\n", millis(), fileSize);
// Progress bar dimensions (matching EpubReaderActivity style) ScreenComponents::drawPopup(renderer, "Indexing...");
constexpr int barWidth = 200;
constexpr int barHeight = 10;
constexpr int boxMargin = 20;
const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, "Indexing...");
const int boxWidth = (barWidth > textWidth ? barWidth : textWidth) + boxMargin * 2;
const int boxHeight = renderer.getLineHeight(UI_12_FONT_ID) + barHeight + boxMargin * 3;
const int boxX = (renderer.getScreenWidth() - boxWidth) / 2;
constexpr int boxY = 50;
const int barX = boxX + (boxWidth - barWidth) / 2;
const int barY = boxY + renderer.getLineHeight(UI_12_FONT_ID) + boxMargin * 2;
// Draw initial progress box
renderer.fillRect(boxX, boxY, boxWidth, boxHeight, false);
renderer.drawText(UI_12_FONT_ID, boxX + boxMargin, boxY + boxMargin, "Indexing...");
renderer.drawRect(boxX + 5, boxY + 5, boxWidth - 10, boxHeight - 10);
renderer.drawRect(barX, barY, barWidth, barHeight);
renderer.displayBuffer();
while (offset < fileSize) { while (offset < fileSize) {
std::vector<std::string> tempLines; std::vector<std::string> tempLines;
@ -248,17 +230,6 @@ void TxtReaderActivity::buildPageIndex() {
pageOffsets.push_back(offset); pageOffsets.push_back(offset);
} }
// Update progress bar every 10% (matching EpubReaderActivity logic)
int progressPercent = (offset * 100) / fileSize;
if (lastProgressPercent / 10 != progressPercent / 10) {
lastProgressPercent = progressPercent;
// Fill progress bar
const int fillWidth = (barWidth - 2) * progressPercent / 100;
renderer.fillRect(barX + 1, barY + 1, fillWidth, barHeight - 2, true);
renderer.displayBuffer(EInkDisplay::FAST_REFRESH);
}
// Yield to other tasks periodically // Yield to other tasks periodically
if (pageOffsets.size() % 20 == 0) { if (pageOffsets.size() % 20 == 0) {
vTaskDelay(1); vTaskDelay(1);
@ -402,9 +373,6 @@ void TxtReaderActivity::renderScreen() {
// Initialize reader if not done // Initialize reader if not done
if (!initialized) { if (!initialized) {
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Indexing...", true, EpdFontFamily::BOLD);
renderer.displayBuffer();
initializeReader(); initializeReader();
} }
@ -484,7 +452,7 @@ void TxtReaderActivity::renderPage() {
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
if (pagesUntilFullRefresh <= 1) { if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(EInkDisplay::HALF_REFRESH); renderer.displayBuffer(HalDisplay::HALF_REFRESH);
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
} else { } else {
renderer.displayBuffer(); renderer.displayBuffer();

View File

@ -45,7 +45,7 @@ void XtcReaderActivity::onEnter() {
// Save current XTC as last opened book and add to recent books // Save current XTC as last opened book and add to recent books
APP_STATE.openEpubPath = xtc->getPath(); APP_STATE.openEpubPath = xtc->getPath();
APP_STATE.saveToFile(); APP_STATE.saveToFile();
RECENT_BOOKS.addBook(xtc->getPath()); RECENT_BOOKS.addBook(xtc->getPath(), xtc->getTitle(), xtc->getAuthor());
// Trigger first update // Trigger first update
updateRequired = true; updateRequired = true;
@ -276,7 +276,7 @@ void XtcReaderActivity::renderPage() {
// Display BW with conditional refresh based on pagesUntilFullRefresh // Display BW with conditional refresh based on pagesUntilFullRefresh
if (pagesUntilFullRefresh <= 1) { if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(EInkDisplay::HALF_REFRESH); renderer.displayBuffer(HalDisplay::HALF_REFRESH);
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
} else { } else {
renderer.displayBuffer(); renderer.displayBuffer();
@ -356,7 +356,7 @@ void XtcReaderActivity::renderPage() {
// Display with appropriate refresh // Display with appropriate refresh
if (pagesUntilFullRefresh <= 1) { if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(EInkDisplay::HALF_REFRESH); renderer.displayBuffer(HalDisplay::HALF_REFRESH);
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
} else { } else {
renderer.displayBuffer(); renderer.displayBuffer();

View File

@ -1,6 +1,6 @@
#pragma once #pragma once
#include <EInkDisplay.h>
#include <EpdFontFamily.h> #include <EpdFontFamily.h>
#include <HalDisplay.h>
#include <string> #include <string>
#include <utility> #include <utility>
@ -10,12 +10,12 @@
class FullScreenMessageActivity final : public Activity { class FullScreenMessageActivity final : public Activity {
std::string text; std::string text;
EpdFontFamily::Style style; EpdFontFamily::Style style;
EInkDisplay::RefreshMode refreshMode; HalDisplay::RefreshMode refreshMode;
public: public:
explicit FullScreenMessageActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string text, explicit FullScreenMessageActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string text,
const EpdFontFamily::Style style = EpdFontFamily::REGULAR, const EpdFontFamily::Style style = EpdFontFamily::REGULAR,
const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH)
: Activity("FullScreenMessage", renderer, mappedInput), : Activity("FullScreenMessage", renderer, mappedInput),
text(std::move(text)), text(std::move(text)),
style(style), style(style),

View File

@ -256,8 +256,9 @@ void KeyboardEntryActivity::render() const {
renderer.drawCenteredText(UI_10_FONT_ID, startY, title.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, startY, title.c_str());
// Draw input field // Draw input field
const int inputY = startY + 22; const int inputStartY = startY + 22;
renderer.drawText(UI_10_FONT_ID, 10, inputY, "["); int inputEndY = startY + 22;
renderer.drawText(UI_10_FONT_ID, 10, inputStartY, "[");
std::string displayText; std::string displayText;
if (isPassword) { if (isPassword) {
@ -269,19 +270,29 @@ void KeyboardEntryActivity::render() const {
// Show cursor at end // Show cursor at end
displayText += "_"; displayText += "_";
// Truncate if too long for display - use actual character width from font // Render input text across multiple lines
int approxCharWidth = renderer.getSpaceWidth(UI_10_FONT_ID); int lineStartIdx = 0;
if (approxCharWidth < 1) approxCharWidth = 8; // Fallback to approximate width int lineEndIdx = displayText.length();
const int maxDisplayLen = (pageWidth - 40) / approxCharWidth; while (true) {
if (displayText.length() > static_cast<size_t>(maxDisplayLen)) { std::string lineText = displayText.substr(lineStartIdx, lineEndIdx - lineStartIdx);
displayText = "..." + displayText.substr(displayText.length() - maxDisplayLen + 3); const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, lineText.c_str());
} if (textWidth <= pageWidth - 40) {
renderer.drawText(UI_10_FONT_ID, 20, inputEndY, lineText.c_str());
if (lineEndIdx == displayText.length()) {
break;
}
renderer.drawText(UI_10_FONT_ID, 20, inputY, displayText.c_str()); inputEndY += renderer.getLineHeight(UI_10_FONT_ID);
renderer.drawText(UI_10_FONT_ID, pageWidth - 15, inputY, "]"); lineStartIdx = lineEndIdx;
lineEndIdx = displayText.length();
} else {
lineEndIdx -= 1;
}
}
renderer.drawText(UI_10_FONT_ID, pageWidth - 15, inputEndY, "]");
// Draw keyboard - use compact spacing to fit 5 rows on screen // Draw keyboard - use compact spacing to fit 5 rows on screen
const int keyboardStartY = inputY + 25; const int keyboardStartY = inputEndY + 25;
constexpr int keyWidth = 18; constexpr int keyWidth = 18;
constexpr int keyHeight = 18; constexpr int keyHeight = 18;
constexpr int keySpacing = 3; constexpr int keySpacing = 3;

View File

@ -1,8 +1,8 @@
#include <Arduino.h> #include <Arduino.h>
#include <EInkDisplay.h>
#include <Epub.h> #include <Epub.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <InputManager.h> #include <HalDisplay.h>
#include <HalGPIO.h>
#include <SDCardManager.h> #include <SDCardManager.h>
#include <SPI.h> #include <SPI.h>
#include <builtinFonts/all.h> #include <builtinFonts/all.h>
@ -26,23 +26,10 @@
#include "activities/util/FullScreenMessageActivity.h" #include "activities/util/FullScreenMessageActivity.h"
#include "fontIds.h" #include "fontIds.h"
#define SPI_FQ 40000000 HalDisplay display;
// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults) HalGPIO gpio;
#define EPD_SCLK 8 // SPI Clock MappedInputManager mappedInputManager(gpio);
#define EPD_MOSI 10 // SPI MOSI (Master Out Slave In) GfxRenderer renderer(display);
#define EPD_CS 21 // Chip Select
#define EPD_DC 4 // Data/Command
#define EPD_RST 5 // Reset
#define EPD_BUSY 6 // Busy
#define UART0_RXD 20 // Used for USB connection detection
#define SD_SPI_MISO 7
EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY);
InputManager inputManager;
MappedInputManager mappedInputManager(inputManager);
GfxRenderer renderer(einkDisplay);
Activity* currentActivity; Activity* currentActivity;
// Fonts // Fonts
@ -170,21 +157,20 @@ void verifyPowerButtonDuration() {
const uint16_t calibratedPressDuration = const uint16_t calibratedPressDuration =
(calibration < SETTINGS.getPowerButtonDuration()) ? SETTINGS.getPowerButtonDuration() - calibration : 1; (calibration < SETTINGS.getPowerButtonDuration()) ? SETTINGS.getPowerButtonDuration() - calibration : 1;
inputManager.update(); gpio.update();
// Verify the user has actually pressed
// Needed because inputManager.isPressed() may take up to ~500ms to return the correct state // Needed because inputManager.isPressed() may take up to ~500ms to return the correct state
while (!inputManager.isPressed(InputManager::BTN_POWER) && millis() - start < 1000) { while (!gpio.isPressed(HalGPIO::BTN_POWER) && millis() - start < 1000) {
delay(10); // only wait 10ms each iteration to not delay too much in case of short configured duration. delay(10); // only wait 10ms each iteration to not delay too much in case of short configured duration.
inputManager.update(); gpio.update();
} }
t2 = millis(); t2 = millis();
if (inputManager.isPressed(InputManager::BTN_POWER)) { if (gpio.isPressed(HalGPIO::BTN_POWER)) {
do { do {
delay(10); delay(10);
inputManager.update(); gpio.update();
} while (inputManager.isPressed(InputManager::BTN_POWER) && inputManager.getHeldTime() < calibratedPressDuration); } while (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.getHeldTime() < calibratedPressDuration);
abort = inputManager.getHeldTime() < calibratedPressDuration; abort = gpio.getHeldTime() < calibratedPressDuration;
} else { } else {
abort = true; abort = true;
} }
@ -192,16 +178,15 @@ void verifyPowerButtonDuration() {
if (abort) { 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 << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); gpio.startDeepSleep();
esp_deep_sleep_start();
} }
} }
void waitForPowerRelease() { void waitForPowerRelease() {
inputManager.update(); gpio.update();
while (inputManager.isPressed(InputManager::BTN_POWER)) { while (gpio.isPressed(HalGPIO::BTN_POWER)) {
delay(50); delay(50);
inputManager.update(); gpio.update();
} }
} }
@ -210,14 +195,11 @@ void enterDeepSleep() {
exitActivity(); exitActivity();
enterNewActivity(new SleepActivity(renderer, mappedInputManager)); enterNewActivity(new SleepActivity(renderer, mappedInputManager));
einkDisplay.deepSleep(); display.deepSleep();
Serial.printf("[%lu] [ ] Power button press calibration value: %lu ms\n", millis(), t2 - t1); Serial.printf("[%lu] [ ] Power button press calibration value: %lu ms\n", millis(), t2 - t1);
Serial.printf("[%lu] [ ] Entering deep sleep.\n", millis()); Serial.printf("[%lu] [ ] Entering deep sleep.\n", millis());
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it gpio.startDeepSleep();
waitForPowerRelease();
// Enter Deep Sleep
esp_deep_sleep_start();
} }
void onGoHome(); void onGoHome();
@ -261,7 +243,7 @@ void onGoHome() {
} }
void setupDisplayAndFonts() { void setupDisplayAndFonts() {
einkDisplay.begin(); display.begin();
Serial.printf("[%lu] [ ] Display initialized\n", millis()); Serial.printf("[%lu] [ ] Display initialized\n", millis());
renderer.insertFont(BOOKERLY_14_FONT_ID, bookerly14FontFamily); renderer.insertFont(BOOKERLY_14_FONT_ID, bookerly14FontFamily);
#ifndef OMIT_FONTS #ifndef OMIT_FONTS
@ -284,27 +266,13 @@ void setupDisplayAndFonts() {
Serial.printf("[%lu] [ ] Fonts setup\n", millis()); Serial.printf("[%lu] [ ] Fonts setup\n", millis());
} }
bool isUsbConnected() {
// U0RXD/GPIO20 reads HIGH when USB is connected
return digitalRead(UART0_RXD) == HIGH;
}
bool isWakeupByPowerButton() {
const auto wakeupCause = esp_sleep_get_wakeup_cause();
const auto resetReason = esp_reset_reason();
if (isUsbConnected()) {
return wakeupCause == ESP_SLEEP_WAKEUP_GPIO;
} else {
return (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED) && (resetReason == ESP_RST_POWERON);
}
}
void setup() { void setup() {
t1 = millis(); t1 = millis();
gpio.begin();
// Only start serial if USB connected // Only start serial if USB connected
pinMode(UART0_RXD, INPUT); if (gpio.isUsbConnected()) {
if (isUsbConnected()) {
Serial.begin(115200); Serial.begin(115200);
// Wait up to 3 seconds for Serial to be ready to catch early logs // Wait up to 3 seconds for Serial to be ready to catch early logs
unsigned long start = millis(); unsigned long start = millis();
@ -313,13 +281,6 @@ void setup() {
} }
} }
inputManager.begin();
// Initialize pins
pinMode(BAT_GPIO0, INPUT);
// Initialize SPI with custom pins
SPI.begin(EPD_SCLK, SD_SPI_MISO, EPD_MOSI, EPD_CS);
// SD Card Initialization // SD Card Initialization
// We need 6 open files concurrently when parsing a new chapter // We need 6 open files concurrently when parsing a new chapter
if (!SdMan.begin()) { if (!SdMan.begin()) {
@ -333,7 +294,7 @@ void setup() {
SETTINGS.loadFromFile(); SETTINGS.loadFromFile();
KOREADER_STORE.loadFromFile(); KOREADER_STORE.loadFromFile();
if (isWakeupByPowerButton()) { if (gpio.isWakeupByPowerButton()) {
// For normal wakeups, verify power button press duration // For normal wakeups, verify power button press duration
Serial.printf("[%lu] [ ] Verifying power button press duration\n", millis()); Serial.printf("[%lu] [ ] Verifying power button press duration\n", millis());
verifyPowerButtonDuration(); verifyPowerButtonDuration();
@ -370,7 +331,7 @@ void loop() {
const unsigned long loopStartTime = millis(); const unsigned long loopStartTime = millis();
static unsigned long lastMemPrint = 0; static unsigned long lastMemPrint = 0;
inputManager.update(); gpio.update();
if (Serial && millis() - lastMemPrint >= 10000) { if (Serial && millis() - lastMemPrint >= 10000) {
Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(), Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(),
@ -380,8 +341,7 @@ void loop() {
// Check for any user activity (button press or release) or active background work // Check for any user activity (button press or release) or active background work
static unsigned long lastActivityTime = millis(); static unsigned long lastActivityTime = millis();
if (inputManager.wasAnyPressed() || inputManager.wasAnyReleased() || if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || (currentActivity && currentActivity->preventAutoSleep())) {
(currentActivity && currentActivity->preventAutoSleep())) {
lastActivityTime = millis(); // Reset inactivity timer lastActivityTime = millis(); // Reset inactivity timer
} }
@ -393,8 +353,7 @@ void loop() {
return; return;
} }
if (inputManager.isPressed(InputManager::BTN_POWER) && if (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.getHeldTime() > SETTINGS.getPowerButtonDuration()) {
inputManager.getHeldTime() > SETTINGS.getPowerButtonDuration()) {
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
return; return;