Compare commits

..

No commits in common. "140d8749a65ee4362a5c03e27c225026416c11c3" and "b1763821b57ab411941d557789e377754b2826b4" have entirely different histories.

45 changed files with 283 additions and 447 deletions

View File

@ -98,9 +98,9 @@ CrossPoint Reader is pretty aggressive about caching data down to the SD card to
has ~380KB of usable RAM, so we have to be careful. A lot of the decisions made in the design of the firmware were based has ~380KB of usable RAM, so we have to be careful. A lot of the decisions made in the design of the firmware were based
on this constraint. on this constraint.
### Data caching ### EPUB caching
The first time chapters of a book are loaded, they are cached to the SD card. Subsequent loads are served from the The first time chapters of an EPUB are loaded, they are cached to the SD card. Subsequent loads are served from the
cache. This cache directory exists at `.crosspoint` on the SD card. The structure is as follows: cache. This cache directory exists at `.crosspoint` on the SD card. The structure is as follows:
@ -108,22 +108,25 @@ cache. This cache directory exists at `.crosspoint` on the SD card. The structur
.crosspoint/ .crosspoint/
├── epub_12471232/ # Each EPUB is cached to a subdirectory named `epub_<hash>` ├── epub_12471232/ # Each EPUB is cached to a subdirectory named `epub_<hash>`
│ ├── progress.bin # Stores reading progress (chapter, page, etc.) │ ├── progress.bin # Stores reading progress (chapter, page, etc.)
│ ├── cover.bmp # Book cover image (once generated) │ ├── 0/ # Each chapter is stored in a subdirectory named by its index (based on the spine order)
│ ├── book.bin # Book metadata (title, author, spine, table of contents, etc.) │ │ ├── section.bin # Section metadata (page count)
│ └── sections/ # All chapter data is stored in the sections subdirectory │ │ ├── page_0.bin # Each page is stored in a separate file, it
│ ├── 0.bin # Chapter data (screen count, all text layout info, etc.) │ │ ├── page_1.bin # contains the position (x, y) and text for each word
│ ├── 1.bin # files are named by their index in the spine │ │ └── ...
│ └── ... │ ├── 1/
│ │ ├── section.bin
│ │ ├── page_0.bin
│ │ ├── page_1.bin
│ │ └── ...
│ └── ...
└── epub_189013891/ └── epub_189013891/
``` ```
Deleting the `.crosspoint` directory will clear the entire cache. Deleting the `.crosspoint` directory will clear the cache.
Due the way it's currently implemented, the cache is not automatically cleared when a book is deleted and moving a book Due the way it's currently implemented, the cache is not automatically cleared when the EPUB is deleted and moving an
file will use a new cache directory, resetting the reading progress. EPUB file will reset the reading progress.
For more details on the internal file structures, see the [file formats document](./docs/file-formats.md).
## Contributing ## Contributing

View File

@ -67,7 +67,6 @@ The Settings screen allows you to configure the device's behavior. There are a f
- **Extra Paragraph Spacing**: If enabled, vertical space will be added between paragraphs in the book, if disabled, - **Extra Paragraph Spacing**: If enabled, vertical space will be added between paragraphs in the book, if disabled,
paragraphs will not have vertical space between them, but will have first word indentation. paragraphs will not have vertical space between them, but will have first word indentation.
- **Short Power Button Click**: Whether to trigger the power button on a short press or a long press. - **Short Power Button Click**: Whether to trigger the power button on a short press or a long press.
- **Front Button Layout**: Swap the order of the bottom edge buttons from Back/Confirm/Left/Right to Left/Right/Back/Confirm.
### 3.6 Sleep Screen ### 3.6 Sleep Screen

View File

@ -1,9 +0,0 @@
# File Formats
## `book.bin`
![](./images/file-formats/book-bin.png)
## `section.bin`
![](./images/file-formats/section-bin.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 539 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

View File

@ -3,16 +3,20 @@
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <Serialization.h> #include <Serialization.h>
namespace {
constexpr uint8_t PAGE_FILE_VERSION = 3;
}
void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) { void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
block->render(renderer, fontId, xPos + xOffset, yPos + yOffset); block->render(renderer, fontId, xPos + xOffset, yPos + yOffset);
} }
bool PageLine::serialize(File& file) { void PageLine::serialize(File& file) {
serialization::writePod(file, xPos); serialization::writePod(file, xPos);
serialization::writePod(file, yPos); serialization::writePod(file, yPos);
// serialize TextBlock pointed to by PageLine // serialize TextBlock pointed to by PageLine
return block->serialize(file); block->serialize(file);
} }
std::unique_ptr<PageLine> PageLine::deserialize(File& file) { std::unique_ptr<PageLine> PageLine::deserialize(File& file) {
@ -31,22 +35,27 @@ void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, co
} }
} }
bool Page::serialize(File& file) const { void Page::serialize(File& file) const {
serialization::writePod(file, PAGE_FILE_VERSION);
const uint32_t count = elements.size(); const uint32_t count = elements.size();
serialization::writePod(file, count); serialization::writePod(file, count);
for (const auto& el : elements) { for (const auto& el : elements) {
// Only PageLine exists currently // Only PageLine exists currently
serialization::writePod(file, static_cast<uint8_t>(TAG_PageLine)); serialization::writePod(file, static_cast<uint8_t>(TAG_PageLine));
if (!el->serialize(file)) { el->serialize(file);
return false;
}
} }
return true;
} }
std::unique_ptr<Page> Page::deserialize(File& file) { std::unique_ptr<Page> Page::deserialize(File& file) {
uint8_t version;
serialization::readPod(file, version);
if (version != PAGE_FILE_VERSION) {
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown version %u\n", millis(), version);
return nullptr;
}
auto page = std::unique_ptr<Page>(new Page()); auto page = std::unique_ptr<Page>(new Page());
uint32_t count; uint32_t count;

View File

@ -18,7 +18,7 @@ class PageElement {
explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {} explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {}
virtual ~PageElement() = default; virtual ~PageElement() = default;
virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0; virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0;
virtual bool serialize(File& file) = 0; virtual void serialize(File& file) = 0;
}; };
// a line from a block element // a line from a block element
@ -29,7 +29,7 @@ class PageLine final : public PageElement {
PageLine(std::shared_ptr<TextBlock> block, const int16_t xPos, const int16_t yPos) PageLine(std::shared_ptr<TextBlock> block, const int16_t xPos, const int16_t yPos)
: PageElement(xPos, yPos), block(std::move(block)) {} : PageElement(xPos, yPos), block(std::move(block)) {}
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override; void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
bool serialize(File& file) override; void serialize(File& file) override;
static std::unique_ptr<PageLine> deserialize(File& file); static std::unique_ptr<PageLine> deserialize(File& file);
}; };
@ -38,6 +38,6 @@ class Page {
// the list of block index and line numbers on this page // the list of block index and line numbers on this page
std::vector<std::shared_ptr<PageElement>> elements; std::vector<std::shared_ptr<PageElement>> elements;
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const; void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
bool serialize(File& file) const; void serialize(File& file) const;
static std::unique_ptr<Page> deserialize(File& file); static std::unique_ptr<Page> deserialize(File& file);
}; };

View File

@ -8,60 +8,54 @@
#include "parsers/ChapterHtmlSlimParser.h" #include "parsers/ChapterHtmlSlimParser.h"
namespace { namespace {
constexpr uint8_t SECTION_FILE_VERSION = 7; constexpr uint8_t SECTION_FILE_VERSION = 6;
constexpr size_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(int) +
sizeof(int) + sizeof(int) + sizeof(size_t);
} // namespace } // namespace
size_t Section::onPageComplete(std::unique_ptr<Page> page) { void Section::onPageComplete(std::unique_ptr<Page> page) {
if (!file) { const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin";
Serial.printf("[%lu] [SCT] File not open for writing page %d\n", millis(), pageCount);
return 0;
}
const auto position = file.position(); File outputFile;
if (!page->serialize(file)) { if (!FsHelpers::openFileForWrite("SCT", filePath, outputFile)) {
Serial.printf("[%lu] [SCT] Failed to serialize page %d\n", millis(), pageCount); return;
return 0;
} }
page->serialize(outputFile);
outputFile.close();
Serial.printf("[%lu] [SCT] Page %d processed\n", millis(), pageCount); Serial.printf("[%lu] [SCT] Page %d processed\n", millis(), pageCount);
pageCount++; pageCount++;
return position;
} }
void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing, void Section::writeCacheMetadata(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const int viewportWidth, const int viewportHeight) { const int viewportWidth, const int viewportHeight) const {
if (!file) { File outputFile;
Serial.printf("[%lu] [SCT] File not open for writing header\n", millis()); if (!FsHelpers::openFileForWrite("SCT", cachePath + "/section.bin", outputFile)) {
return; return;
} }
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) + serialization::writePod(outputFile, SECTION_FILE_VERSION);
sizeof(extraParagraphSpacing) + sizeof(viewportWidth) + sizeof(viewportHeight) + serialization::writePod(outputFile, fontId);
sizeof(pageCount) + sizeof(size_t), serialization::writePod(outputFile, lineCompression);
"Header size mismatch"); serialization::writePod(outputFile, extraParagraphSpacing);
serialization::writePod(file, SECTION_FILE_VERSION); serialization::writePod(outputFile, viewportWidth);
serialization::writePod(file, fontId); serialization::writePod(outputFile, viewportHeight);
serialization::writePod(file, lineCompression); serialization::writePod(outputFile, pageCount);
serialization::writePod(file, extraParagraphSpacing); outputFile.close();
serialization::writePod(file, viewportWidth);
serialization::writePod(file, viewportHeight);
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0 when written)
serialization::writePod(file, static_cast<size_t>(0)); // Placeholder for LUT offset
} }
bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, bool Section::loadCacheMetadata(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const int viewportWidth, const int viewportHeight) { const int viewportWidth, const int viewportHeight) {
if (!FsHelpers::openFileForRead("SCT", filePath, file)) { const auto sectionFilePath = cachePath + "/section.bin";
File inputFile;
if (!FsHelpers::openFileForRead("SCT", sectionFilePath, inputFile)) {
return false; return false;
} }
// Match parameters // Match parameters
{ {
uint8_t version; uint8_t version;
serialization::readPod(file, version); serialization::readPod(inputFile, version);
if (version != SECTION_FILE_VERSION) { if (version != SECTION_FILE_VERSION) {
file.close(); inputFile.close();
Serial.printf("[%lu] [SCT] Deserialization failed: Unknown version %u\n", millis(), version); Serial.printf("[%lu] [SCT] Deserialization failed: Unknown version %u\n", millis(), version);
clearCache(); clearCache();
return false; return false;
@ -70,36 +64,41 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
int fileFontId, fileViewportWidth, fileViewportHeight; int fileFontId, fileViewportWidth, fileViewportHeight;
float fileLineCompression; float fileLineCompression;
bool fileExtraParagraphSpacing; bool fileExtraParagraphSpacing;
serialization::readPod(file, fileFontId); serialization::readPod(inputFile, fileFontId);
serialization::readPod(file, fileLineCompression); serialization::readPod(inputFile, fileLineCompression);
serialization::readPod(file, fileExtraParagraphSpacing); serialization::readPod(inputFile, fileExtraParagraphSpacing);
serialization::readPod(file, fileViewportWidth); serialization::readPod(inputFile, fileViewportWidth);
serialization::readPod(file, fileViewportHeight); serialization::readPod(inputFile, fileViewportHeight);
if (fontId != fileFontId || lineCompression != fileLineCompression || if (fontId != fileFontId || lineCompression != fileLineCompression ||
extraParagraphSpacing != fileExtraParagraphSpacing || viewportWidth != fileViewportWidth || extraParagraphSpacing != fileExtraParagraphSpacing || viewportWidth != fileViewportWidth ||
viewportHeight != fileViewportHeight) { viewportHeight != fileViewportHeight) {
file.close(); inputFile.close();
Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis()); Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis());
clearCache(); clearCache();
return false; return false;
} }
} }
serialization::readPod(file, pageCount); serialization::readPod(inputFile, pageCount);
file.close(); inputFile.close();
Serial.printf("[%lu] [SCT] Deserialization succeeded: %d pages\n", millis(), pageCount); Serial.printf("[%lu] [SCT] Deserialization succeeded: %d pages\n", millis(), pageCount);
return true; return true;
} }
void Section::setupCacheDir() const {
epub->setupCacheDir();
SD.mkdir(cachePath.c_str());
}
// Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem) // Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem)
bool Section::clearCache() const { bool Section::clearCache() const {
if (!SD.exists(filePath.c_str())) { if (!SD.exists(cachePath.c_str())) {
Serial.printf("[%lu] [SCT] Cache does not exist, no action needed\n", millis()); Serial.printf("[%lu] [SCT] Cache does not exist, no action needed\n", millis());
return true; return true;
} }
if (!SD.remove(filePath.c_str())) { if (!FsHelpers::removeDir(cachePath.c_str())) {
Serial.printf("[%lu] [SCT] Failed to clear cache\n", millis()); Serial.printf("[%lu] [SCT] Failed to clear cache\n", millis());
return false; return false;
} }
@ -108,10 +107,10 @@ bool Section::clearCache() const {
return true; return true;
} }
bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const int viewportWidth, const int viewportHeight, const int viewportWidth, const int viewportHeight,
const std::function<void()>& progressSetupFn, const std::function<void()>& progressSetupFn,
const std::function<void(int)>& progressFn) { const std::function<void(int)>& progressFn) {
constexpr size_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB constexpr size_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";
@ -157,66 +156,30 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
progressSetupFn(); progressSetupFn();
} }
if (!FsHelpers::openFileForWrite("SCT", filePath, file)) {
return false;
}
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, viewportWidth, viewportHeight);
std::vector<size_t> lut = {};
ChapterHtmlSlimParser visitor( ChapterHtmlSlimParser visitor(
tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, viewportWidth, viewportHeight, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, viewportWidth, viewportHeight,
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, [this](std::unique_ptr<Page> page) { this->onPageComplete(std::move(page)); }, progressFn);
progressFn);
success = visitor.parseAndBuildPages(); success = visitor.parseAndBuildPages();
SD.remove(tmpHtmlPath.c_str()); SD.remove(tmpHtmlPath.c_str());
if (!success) { if (!success) {
Serial.printf("[%lu] [SCT] Failed to parse XML and build pages\n", millis()); Serial.printf("[%lu] [SCT] Failed to parse XML and build pages\n", millis());
file.close();
SD.remove(filePath.c_str());
return false; return false;
} }
const auto lutOffset = file.position(); writeCacheMetadata(fontId, lineCompression, extraParagraphSpacing, viewportWidth, viewportHeight);
bool hasFailedLutRecords = false;
// Write LUT
for (const auto& pos : lut) {
if (pos == 0) {
hasFailedLutRecords = true;
break;
}
serialization::writePod(file, pos);
}
if (hasFailedLutRecords) {
Serial.printf("[%lu] [SCT] Failed to write LUT due to invalid page positions\n", millis());
file.close();
SD.remove(filePath.c_str());
return false;
}
// Go back and write LUT offset
file.seek(HEADER_SIZE - sizeof(size_t) - sizeof(pageCount));
serialization::writePod(file, pageCount);
serialization::writePod(file, lutOffset);
file.close();
return true; return true;
} }
std::unique_ptr<Page> Section::loadPageFromSectionFile() { std::unique_ptr<Page> Section::loadPageFromSD() const {
if (!FsHelpers::openFileForRead("SCT", filePath, file)) { const auto filePath = cachePath + "/page_" + std::to_string(currentPage) + ".bin";
File inputFile;
if (!FsHelpers::openFileForRead("SCT", filePath, inputFile)) {
return nullptr; return nullptr;
} }
auto page = Page::deserialize(inputFile);
file.seek(HEADER_SIZE - sizeof(size_t)); inputFile.close();
size_t lutOffset;
serialization::readPod(file, lutOffset);
file.seek(lutOffset + sizeof(size_t) * currentPage);
size_t pagePos;
serialization::readPod(file, pagePos);
file.seek(pagePos);
auto page = Page::deserialize(file);
file.close();
return page; return page;
} }

View File

@ -11,12 +11,11 @@ class Section {
std::shared_ptr<Epub> epub; std::shared_ptr<Epub> epub;
const int spineIndex; const int spineIndex;
GfxRenderer& renderer; GfxRenderer& renderer;
std::string filePath; std::string cachePath;
File file;
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, int viewportWidth, void writeCacheMetadata(int fontId, float lineCompression, bool extraParagraphSpacing, int viewportWidth,
int viewportHeight); int viewportHeight) const;
size_t onPageComplete(std::unique_ptr<Page> page); void onPageComplete(std::unique_ptr<Page> page);
public: public:
int pageCount = 0; int pageCount = 0;
@ -26,13 +25,14 @@ class Section {
: epub(epub), : epub(epub),
spineIndex(spineIndex), spineIndex(spineIndex),
renderer(renderer), renderer(renderer),
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {} cachePath(epub->getCachePath() + "/" + std::to_string(spineIndex)) {}
~Section() = default; ~Section() = default;
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, int viewportWidth, bool loadCacheMetadata(int fontId, float lineCompression, bool extraParagraphSpacing, int viewportWidth,
int viewportHeight); int viewportHeight);
void setupCacheDir() const;
bool clearCache() const; bool clearCache() const;
bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, int viewportWidth, bool persistPageDataToSD(int fontId, float lineCompression, bool extraParagraphSpacing, int viewportWidth,
int viewportHeight, const std::function<void()>& progressSetupFn = nullptr, int viewportHeight, const std::function<void()>& progressSetupFn = nullptr,
const std::function<void(int)>& progressFn = nullptr); const std::function<void(int)>& progressFn = nullptr);
std::unique_ptr<Page> loadPageFromSectionFile(); std::unique_ptr<Page> loadPageFromSD() const;
}; };

View File

@ -24,33 +24,34 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
} }
} }
bool TextBlock::serialize(File& file) const { void TextBlock::serialize(File& file) const {
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) { // words
Serial.printf("[%lu] [TXB] Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(), const uint32_t wc = words.size();
words.size(), wordXpos.size(), wordStyles.size()); serialization::writePod(file, wc);
return false;
}
// Word data
serialization::writePod(file, static_cast<uint32_t>(words.size()));
for (const auto& w : words) serialization::writeString(file, w); for (const auto& w : words) serialization::writeString(file, w);
// wordXpos
const uint32_t xc = wordXpos.size();
serialization::writePod(file, xc);
for (auto x : wordXpos) serialization::writePod(file, x); for (auto x : wordXpos) serialization::writePod(file, x);
// wordStyles
const uint32_t sc = wordStyles.size();
serialization::writePod(file, sc);
for (auto s : wordStyles) serialization::writePod(file, s); for (auto s : wordStyles) serialization::writePod(file, s);
// Block style // style
serialization::writePod(file, style); serialization::writePod(file, style);
return true;
} }
std::unique_ptr<TextBlock> TextBlock::deserialize(File& file) { std::unique_ptr<TextBlock> TextBlock::deserialize(File& file) {
uint32_t wc; uint32_t wc, xc, sc;
std::list<std::string> words; std::list<std::string> words;
std::list<uint16_t> wordXpos; std::list<uint16_t> wordXpos;
std::list<EpdFontStyle> wordStyles; std::list<EpdFontStyle> wordStyles;
BLOCK_STYLE style; BLOCK_STYLE style;
// Word count // words
serialization::readPod(file, wc); serialization::readPod(file, wc);
// Sanity check: prevent allocation of unreasonably large lists (max 10000 words per block) // Sanity check: prevent allocation of unreasonably large lists (max 10000 words per block)
@ -59,15 +60,27 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(File& file) {
return nullptr; return nullptr;
} }
// Word data
words.resize(wc); words.resize(wc);
wordXpos.resize(wc);
wordStyles.resize(wc);
for (auto& w : words) serialization::readString(file, w); for (auto& w : words) serialization::readString(file, w);
// wordXpos
serialization::readPod(file, xc);
wordXpos.resize(xc);
for (auto& x : wordXpos) serialization::readPod(file, x); for (auto& x : wordXpos) serialization::readPod(file, x);
// wordStyles
serialization::readPod(file, sc);
wordStyles.resize(sc);
for (auto& s : wordStyles) serialization::readPod(file, s); for (auto& s : wordStyles) serialization::readPod(file, s);
// Block style // Validate data consistency: all three lists must have the same size
if (wc != xc || wc != sc) {
Serial.printf("[%lu] [TXB] Deserialization failed: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(), wc,
xc, sc);
return nullptr;
}
// style
serialization::readPod(file, style); serialization::readPod(file, style);
return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), style)); return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), style));

View File

@ -36,6 +36,6 @@ class TextBlock final : public Block {
// given a renderer works out where to break the words into lines // given a renderer works out where to break the words into lines
void render(const GfxRenderer& renderer, int fontId, int x, int y) const; void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
BlockType getType() override { return TEXT_BLOCK; } BlockType getType() override { return TEXT_BLOCK; }
bool serialize(File& file) const; void serialize(File& file) const;
static std::unique_ptr<TextBlock> deserialize(File& file); static std::unique_ptr<TextBlock> deserialize(File& file);
}; };

View File

@ -11,7 +11,7 @@ CrossPointSettings CrossPointSettings::instance;
namespace { namespace {
constexpr uint8_t SETTINGS_FILE_VERSION = 1; constexpr uint8_t SETTINGS_FILE_VERSION = 1;
// Increment this when adding new persisted settings fields // Increment this when adding new persisted settings fields
constexpr uint8_t SETTINGS_COUNT = 6; constexpr uint8_t SETTINGS_COUNT = 5;
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
} // namespace } // namespace
@ -31,7 +31,6 @@ bool CrossPointSettings::saveToFile() const {
serialization::writePod(outputFile, shortPwrBtn); serialization::writePod(outputFile, shortPwrBtn);
serialization::writePod(outputFile, statusBar); serialization::writePod(outputFile, statusBar);
serialization::writePod(outputFile, orientation); serialization::writePod(outputFile, orientation);
serialization::writePod(outputFile, frontButtonLayout);
outputFile.close(); outputFile.close();
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
@ -68,8 +67,6 @@ bool CrossPointSettings::loadFromFile() {
if (++settingsRead >= fileSettingsCount) break; if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, orientation); serialization::readPod(inputFile, orientation);
if (++settingsRead >= fileSettingsCount) break; if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, frontButtonLayout);
if (++settingsRead >= fileSettingsCount) break;
} while (false); } while (false);
inputFile.close(); inputFile.close();

View File

@ -28,11 +28,6 @@ class CrossPointSettings {
LANDSCAPE_CCW = 3 // 800x480 logical coordinates, native panel orientation LANDSCAPE_CCW = 3 // 800x480 logical coordinates, native panel orientation
}; };
// Front button layout options
// Default: Back, Confirm, Left, Right
// Swapped: Left, Right, Back, Confirm
enum FRONT_BUTTON_LAYOUT { BACK_CONFIRM_LEFT_RIGHT = 0, LEFT_RIGHT_BACK_CONFIRM = 1 };
// Sleep screen settings // Sleep screen settings
uint8_t sleepScreen = DARK; uint8_t sleepScreen = DARK;
// Status bar settings // Status bar settings
@ -44,8 +39,6 @@ class CrossPointSettings {
// EPUB reading orientation settings // EPUB reading orientation settings
// 0 = portrait (default), 1 = landscape clockwise, 2 = inverted, 3 = landscape counter-clockwise // 0 = portrait (default), 1 = landscape clockwise, 2 = inverted, 3 = landscape counter-clockwise
uint8_t orientation = PORTRAIT; uint8_t orientation = PORTRAIT;
// Front button layout
uint8_t frontButtonLayout = BACK_CONFIRM_LEFT_RIGHT;
~CrossPointSettings() = default; ~CrossPointSettings() = default;

View File

@ -1,77 +0,0 @@
#include "MappedInputManager.h"
decltype(InputManager::BTN_BACK) MappedInputManager::mapButton(const Button button) const {
const auto layout = static_cast<CrossPointSettings::FRONT_BUTTON_LAYOUT>(SETTINGS.frontButtonLayout);
switch (button) {
case Button::Back:
switch (layout) {
case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM:
return InputManager::BTN_LEFT;
case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT:
default:
return InputManager::BTN_BACK;
}
case Button::Confirm:
switch (layout) {
case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM:
return InputManager::BTN_RIGHT;
case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT:
default:
return InputManager::BTN_CONFIRM;
}
case Button::Left:
switch (layout) {
case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM:
return InputManager::BTN_BACK;
case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT:
default:
return InputManager::BTN_LEFT;
}
case Button::Right:
switch (layout) {
case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM:
return InputManager::BTN_CONFIRM;
case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT:
default:
return InputManager::BTN_RIGHT;
}
case Button::Up:
return InputManager::BTN_UP;
case Button::Down:
return InputManager::BTN_DOWN;
case Button::Power:
return InputManager::BTN_POWER;
case Button::PageBack:
return InputManager::BTN_UP;
case Button::PageForward:
return InputManager::BTN_DOWN;
}
return InputManager::BTN_BACK;
}
bool MappedInputManager::wasPressed(const Button button) const { return inputManager.wasPressed(mapButton(button)); }
bool MappedInputManager::wasReleased(const Button button) const { return inputManager.wasReleased(mapButton(button)); }
bool MappedInputManager::isPressed(const Button button) const { return inputManager.isPressed(mapButton(button)); }
bool MappedInputManager::wasAnyPressed() const { return inputManager.wasAnyPressed(); }
bool MappedInputManager::wasAnyReleased() const { return inputManager.wasAnyReleased(); }
unsigned long MappedInputManager::getHeldTime() const { return inputManager.getHeldTime(); }
MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const char* confirm, const char* previous,
const char* next) const {
const auto layout = static_cast<CrossPointSettings::FRONT_BUTTON_LAYOUT>(SETTINGS.frontButtonLayout);
switch (layout) {
case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM:
return {previous, next, back, confirm};
case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT:
default:
return {back, confirm, previous, next};
}
}

View File

@ -1,31 +0,0 @@
#pragma once
#include <InputManager.h>
#include "CrossPointSettings.h"
class MappedInputManager {
public:
enum class Button { Back, Confirm, Left, Right, Up, Down, Power, PageBack, PageForward };
struct Labels {
const char* btn1;
const char* btn2;
const char* btn3;
const char* btn4;
};
explicit MappedInputManager(InputManager& inputManager) : inputManager(inputManager) {}
bool wasPressed(Button button) const;
bool wasReleased(Button button) const;
bool isPressed(Button button) const;
bool wasAnyPressed() const;
bool wasAnyReleased() const;
unsigned long getHeldTime() const;
Labels mapLabels(const char* back, const char* confirm, const char* previous, const char* next) const;
private:
InputManager& inputManager;
decltype(InputManager::BTN_BACK) mapButton(Button button) const;
};

View File

@ -5,19 +5,18 @@
#include <string> #include <string>
#include <utility> #include <utility>
#include "../MappedInputManager.h" class InputManager;
class GfxRenderer; class GfxRenderer;
class Activity { class Activity {
protected: protected:
std::string name; std::string name;
GfxRenderer& renderer; GfxRenderer& renderer;
MappedInputManager& mappedInput; InputManager& inputManager;
public: public:
explicit Activity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput) explicit Activity(std::string name, GfxRenderer& renderer, InputManager& inputManager)
: name(std::move(name)), renderer(renderer), mappedInput(mappedInput) {} : name(std::move(name)), renderer(renderer), inputManager(inputManager) {}
virtual ~Activity() = default; virtual ~Activity() = default;
virtual void onEnter() { Serial.printf("[%lu] [ACT] Entering activity: %s\n", millis(), name.c_str()); } virtual void onEnter() { Serial.printf("[%lu] [ACT] Entering activity: %s\n", millis(), name.c_str()); }
virtual void onExit() { Serial.printf("[%lu] [ACT] Exiting activity: %s\n", millis(), name.c_str()); } virtual void onExit() { Serial.printf("[%lu] [ACT] Exiting activity: %s\n", millis(), name.c_str()); }

View File

@ -10,8 +10,8 @@ class ActivityWithSubactivity : public Activity {
void enterNewActivity(Activity* activity); void enterNewActivity(Activity* activity);
public: public:
explicit ActivityWithSubactivity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput) explicit ActivityWithSubactivity(std::string name, GfxRenderer& renderer, InputManager& inputManager)
: Activity(std::move(name), renderer, mappedInput) {} : Activity(std::move(name), renderer, inputManager) {}
void loop() override; void loop() override;
void onExit() override; void onExit() override;
}; };

View File

@ -3,7 +3,6 @@
class BootActivity final : public Activity { class BootActivity final : public Activity {
public: public:
explicit BootActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) explicit BootActivity(GfxRenderer& renderer, InputManager& inputManager) : Activity("Boot", renderer, inputManager) {}
: Activity("Boot", renderer, mappedInput) {}
void onEnter() override; void onEnter() override;
}; };

View File

@ -5,8 +5,8 @@ class Bitmap;
class SleepActivity final : public Activity { class SleepActivity final : public Activity {
public: public:
explicit SleepActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) explicit SleepActivity(GfxRenderer& renderer, InputManager& inputManager)
: Activity("Sleep", renderer, mappedInput) {} : Activity("Sleep", renderer, inputManager) {}
void onEnter() override; void onEnter() override;
private: private:

View File

@ -49,14 +49,14 @@ void HomeActivity::onExit() {
} }
void HomeActivity::loop() { void HomeActivity::loop() {
const bool prevPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) || const bool prevPressed =
mappedInput.wasPressed(MappedInputManager::Button::Left); inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT);
const bool nextPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) || const bool nextPressed =
mappedInput.wasPressed(MappedInputManager::Button::Right); inputManager.wasPressed(InputManager::BTN_DOWN) || inputManager.wasPressed(InputManager::BTN_RIGHT);
const int menuCount = getMenuItemCount(); const int menuCount = getMenuItemCount();
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (inputManager.wasReleased(InputManager::BTN_CONFIRM)) {
if (hasContinueReading) { if (hasContinueReading) {
// Menu: Continue Reading, Browse, File transfer, Settings // Menu: Continue Reading, Browse, File transfer, Settings
if (selectorIndex == 0) { if (selectorIndex == 0) {
@ -143,8 +143,7 @@ void HomeActivity::render() const {
renderer.drawText(UI_FONT_ID, 20, menuY, "Settings", selectorIndex != menuIndex); renderer.drawText(UI_FONT_ID, 20, menuY, "Settings", selectorIndex != menuIndex);
const auto labels = mappedInput.mapLabels("Back", "Confirm", "Left", "Right"); renderer.drawButtonHints(UI_FONT_ID, "Back", "Confirm", "Left", "Right");
renderer.drawButtonHints(UI_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
} }

View File

@ -24,10 +24,10 @@ class HomeActivity final : public Activity {
int getMenuItemCount() const; int getMenuItemCount() const;
public: public:
explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, explicit HomeActivity(GfxRenderer& renderer, InputManager& inputManager,
const std::function<void()>& onContinueReading, const std::function<void()>& onReaderOpen, const std::function<void()>& onContinueReading, const std::function<void()>& onReaderOpen,
const std::function<void()>& onSettingsOpen, const std::function<void()>& onFileTransferOpen) const std::function<void()>& onSettingsOpen, const std::function<void()>& onFileTransferOpen)
: Activity("Home", renderer, mappedInput), : Activity("Home", renderer, inputManager),
onContinueReading(onContinueReading), onContinueReading(onContinueReading),
onReaderOpen(onReaderOpen), onReaderOpen(onReaderOpen),
onSettingsOpen(onSettingsOpen), onSettingsOpen(onSettingsOpen),

View File

@ -57,7 +57,7 @@ void CrossPointWebServerActivity::onEnter() {
// Launch network mode selection subactivity // Launch network mode selection subactivity
Serial.printf("[%lu] [WEBACT] Launching NetworkModeSelectionActivity...\n", millis()); Serial.printf("[%lu] [WEBACT] Launching NetworkModeSelectionActivity...\n", millis());
enterNewActivity(new NetworkModeSelectionActivity( enterNewActivity(new NetworkModeSelectionActivity(
renderer, mappedInput, [this](const NetworkMode mode) { onNetworkModeSelected(mode); }, renderer, inputManager, [this](const NetworkMode mode) { onNetworkModeSelected(mode); },
[this]() { onGoBack(); } // Cancel goes back to home [this]() { onGoBack(); } // Cancel goes back to home
)); ));
} }
@ -141,7 +141,7 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode)
state = WebServerActivityState::WIFI_SELECTION; state = WebServerActivityState::WIFI_SELECTION;
Serial.printf("[%lu] [WEBACT] Launching WifiSelectionActivity...\n", millis()); Serial.printf("[%lu] [WEBACT] Launching WifiSelectionActivity...\n", millis());
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, enterNewActivity(new WifiSelectionActivity(renderer, inputManager,
[this](const bool connected) { onWifiSelectionComplete(connected); })); [this](const bool connected) { onWifiSelectionComplete(connected); }));
} else { } else {
// AP mode - start access point // AP mode - start access point
@ -174,7 +174,7 @@ void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected)
exitActivity(); exitActivity();
state = WebServerActivityState::MODE_SELECTION; state = WebServerActivityState::MODE_SELECTION;
enterNewActivity(new NetworkModeSelectionActivity( enterNewActivity(new NetworkModeSelectionActivity(
renderer, mappedInput, [this](const NetworkMode mode) { onNetworkModeSelected(mode); }, renderer, inputManager, [this](const NetworkMode mode) { onNetworkModeSelected(mode); },
[this]() { onGoBack(); })); [this]() { onGoBack(); }));
} }
} }
@ -305,7 +305,7 @@ void CrossPointWebServerActivity::loop() {
} }
// Handle exit on Back button // Handle exit on Back button
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { if (inputManager.wasPressed(InputManager::BTN_BACK)) {
onGoBack(); onGoBack();
return; return;
} }
@ -428,6 +428,5 @@ void CrossPointWebServerActivity::renderServerRunning() const {
REGULAR); REGULAR);
} }
const auto labels = mappedInput.mapLabels("« Exit", "", "", ""); renderer.drawButtonHints(UI_FONT_ID, "« Exit", "", "", "");
renderer.drawButtonHints(UI_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
} }

View File

@ -63,9 +63,9 @@ class CrossPointWebServerActivity final : public ActivityWithSubactivity {
void stopWebServer(); void stopWebServer();
public: public:
explicit CrossPointWebServerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, explicit CrossPointWebServerActivity(GfxRenderer& renderer, InputManager& inputManager,
const std::function<void()>& onGoBack) const std::function<void()>& onGoBack)
: ActivityWithSubactivity("CrossPointWebServer", renderer, mappedInput), onGoBack(onGoBack) {} : ActivityWithSubactivity("CrossPointWebServer", renderer, inputManager), onGoBack(onGoBack) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;

View File

@ -51,23 +51,23 @@ void NetworkModeSelectionActivity::onExit() {
void NetworkModeSelectionActivity::loop() { void NetworkModeSelectionActivity::loop() {
// Handle back button - cancel // Handle back button - cancel
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { if (inputManager.wasPressed(InputManager::BTN_BACK)) {
onCancel(); onCancel();
return; return;
} }
// Handle confirm button - select current option // Handle confirm button - select current option
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
const NetworkMode mode = (selectedIndex == 0) ? NetworkMode::JOIN_NETWORK : NetworkMode::CREATE_HOTSPOT; const NetworkMode mode = (selectedIndex == 0) ? NetworkMode::JOIN_NETWORK : NetworkMode::CREATE_HOTSPOT;
onModeSelected(mode); onModeSelected(mode);
return; return;
} }
// Handle navigation // Handle navigation
const bool prevPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) || const bool prevPressed =
mappedInput.wasPressed(MappedInputManager::Button::Left); inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT);
const bool nextPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) || const bool nextPressed =
mappedInput.wasPressed(MappedInputManager::Button::Right); inputManager.wasPressed(InputManager::BTN_DOWN) || inputManager.wasPressed(InputManager::BTN_RIGHT);
if (prevPressed) { if (prevPressed) {
selectedIndex = (selectedIndex + MENU_ITEM_COUNT - 1) % MENU_ITEM_COUNT; selectedIndex = (selectedIndex + MENU_ITEM_COUNT - 1) % MENU_ITEM_COUNT;
@ -122,8 +122,7 @@ void NetworkModeSelectionActivity::render() const {
} }
// Draw help text at bottom // Draw help text at bottom
const auto labels = mappedInput.mapLabels("« Back", "Select", "", ""); renderer.drawButtonHints(UI_FONT_ID, "« Back", "Select", "", "");
renderer.drawButtonHints(UI_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
} }

View File

@ -31,10 +31,10 @@ class NetworkModeSelectionActivity final : public Activity {
void render() const; void render() const;
public: public:
explicit NetworkModeSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, explicit NetworkModeSelectionActivity(GfxRenderer& renderer, InputManager& inputManager,
const std::function<void(NetworkMode)>& onModeSelected, const std::function<void(NetworkMode)>& onModeSelected,
const std::function<void()>& onCancel) const std::function<void()>& onCancel)
: Activity("NetworkModeSelection", renderer, mappedInput), onModeSelected(onModeSelected), onCancel(onCancel) {} : Activity("NetworkModeSelection", renderer, inputManager), onModeSelected(onModeSelected), onCancel(onCancel) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;

View File

@ -190,7 +190,7 @@ void WifiSelectionActivity::selectNetwork(const int index) {
// Don't allow screen updates while changing activity // Don't allow screen updates while changing activity
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, "Enter WiFi Password", renderer, inputManager, "Enter WiFi Password",
"", // No initial text "", // No initial text
50, // Y position 50, // Y position
64, // Max password length 64, // Max password length
@ -302,19 +302,17 @@ void WifiSelectionActivity::loop() {
// Handle save prompt state // Handle save prompt state
if (state == WifiSelectionState::SAVE_PROMPT) { if (state == WifiSelectionState::SAVE_PROMPT) {
if (mappedInput.wasPressed(MappedInputManager::Button::Up) || if (inputManager.wasPressed(InputManager::BTN_LEFT) || inputManager.wasPressed(InputManager::BTN_UP)) {
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
if (savePromptSelection > 0) { if (savePromptSelection > 0) {
savePromptSelection--; savePromptSelection--;
updateRequired = true; updateRequired = true;
} }
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || } else if (inputManager.wasPressed(InputManager::BTN_RIGHT) || inputManager.wasPressed(InputManager::BTN_DOWN)) {
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
if (savePromptSelection < 1) { if (savePromptSelection < 1) {
savePromptSelection++; savePromptSelection++;
updateRequired = true; updateRequired = true;
} }
} else if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { } else if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
if (savePromptSelection == 0) { if (savePromptSelection == 0) {
// User chose "Yes" - save the password // User chose "Yes" - save the password
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
@ -323,7 +321,7 @@ void WifiSelectionActivity::loop() {
} }
// Complete - parent will start web server // Complete - parent will start web server
onComplete(true); onComplete(true);
} else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { } else if (inputManager.wasPressed(InputManager::BTN_BACK)) {
// Skip saving, complete anyway // Skip saving, complete anyway
onComplete(true); onComplete(true);
} }
@ -332,19 +330,17 @@ void WifiSelectionActivity::loop() {
// Handle forget prompt state (connection failed with saved credentials) // Handle forget prompt state (connection failed with saved credentials)
if (state == WifiSelectionState::FORGET_PROMPT) { if (state == WifiSelectionState::FORGET_PROMPT) {
if (mappedInput.wasPressed(MappedInputManager::Button::Up) || if (inputManager.wasPressed(InputManager::BTN_LEFT) || inputManager.wasPressed(InputManager::BTN_UP)) {
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
if (forgetPromptSelection > 0) { if (forgetPromptSelection > 0) {
forgetPromptSelection--; forgetPromptSelection--;
updateRequired = true; updateRequired = true;
} }
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || } else if (inputManager.wasPressed(InputManager::BTN_RIGHT) || inputManager.wasPressed(InputManager::BTN_DOWN)) {
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
if (forgetPromptSelection < 1) { if (forgetPromptSelection < 1) {
forgetPromptSelection++; forgetPromptSelection++;
updateRequired = true; updateRequired = true;
} }
} else if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { } else if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
if (forgetPromptSelection == 0) { if (forgetPromptSelection == 0) {
// User chose "Yes" - forget the network // User chose "Yes" - forget the network
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
@ -360,7 +356,7 @@ void WifiSelectionActivity::loop() {
// Go back to network list // Go back to network list
state = WifiSelectionState::NETWORK_LIST; state = WifiSelectionState::NETWORK_LIST;
updateRequired = true; updateRequired = true;
} else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { } else if (inputManager.wasPressed(InputManager::BTN_BACK)) {
// Skip forgetting, go back to network list // Skip forgetting, go back to network list
state = WifiSelectionState::NETWORK_LIST; state = WifiSelectionState::NETWORK_LIST;
updateRequired = true; updateRequired = true;
@ -377,8 +373,7 @@ void WifiSelectionActivity::loop() {
// Handle connection failed state // Handle connection failed state
if (state == WifiSelectionState::CONNECTION_FAILED) { if (state == WifiSelectionState::CONNECTION_FAILED) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back) || if (inputManager.wasPressed(InputManager::BTN_BACK) || inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
// If we used saved credentials, offer to forget the network // If we used saved credentials, offer to forget the network
if (usedSavedPassword) { if (usedSavedPassword) {
state = WifiSelectionState::FORGET_PROMPT; state = WifiSelectionState::FORGET_PROMPT;
@ -395,13 +390,13 @@ void WifiSelectionActivity::loop() {
// Handle network list state // Handle network list state
if (state == WifiSelectionState::NETWORK_LIST) { if (state == WifiSelectionState::NETWORK_LIST) {
// Check for Back button to exit (cancel) // Check for Back button to exit (cancel)
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { if (inputManager.wasPressed(InputManager::BTN_BACK)) {
onComplete(false); onComplete(false);
return; return;
} }
// Check for Confirm button to select network or rescan // Check for Confirm button to select network or rescan
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
if (!networks.empty()) { if (!networks.empty()) {
selectNetwork(selectedNetworkIndex); selectNetwork(selectedNetworkIndex);
} else { } else {
@ -411,14 +406,12 @@ void WifiSelectionActivity::loop() {
} }
// Handle UP/DOWN navigation // Handle UP/DOWN navigation
if (mappedInput.wasPressed(MappedInputManager::Button::Up) || if (inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT)) {
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
if (selectedNetworkIndex > 0) { if (selectedNetworkIndex > 0) {
selectedNetworkIndex--; selectedNetworkIndex--;
updateRequired = true; updateRequired = true;
} }
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || } else if (inputManager.wasPressed(InputManager::BTN_DOWN) || inputManager.wasPressed(InputManager::BTN_RIGHT)) {
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
if (!networks.empty() && selectedNetworkIndex < static_cast<int>(networks.size()) - 1) { if (!networks.empty() && selectedNetworkIndex < static_cast<int>(networks.size()) - 1) {
selectedNetworkIndex++; selectedNetworkIndex++;
updateRequired = true; updateRequired = true;
@ -564,8 +557,7 @@ void WifiSelectionActivity::renderNetworkList() const {
// Draw help text // Draw help text
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, "* = Encrypted | + = Saved"); renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, "* = Encrypted | + = Saved");
const auto labels = mappedInput.mapLabels("« Back", "Connect", "", ""); renderer.drawButtonHints(UI_FONT_ID, "« Back", "Connect", "", "");
renderer.drawButtonHints(UI_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
} }
void WifiSelectionActivity::renderConnecting() const { void WifiSelectionActivity::renderConnecting() const {

View File

@ -92,9 +92,9 @@ class WifiSelectionActivity final : public ActivityWithSubactivity {
std::string getSignalStrengthIndicator(int32_t rssi) const; std::string getSignalStrengthIndicator(int32_t rssi) const;
public: public:
explicit WifiSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, explicit WifiSelectionActivity(GfxRenderer& renderer, InputManager& inputManager,
const std::function<void(bool connected)>& onComplete) const std::function<void(bool connected)>& onComplete)
: ActivityWithSubactivity("WifiSelection", renderer, mappedInput), onComplete(onComplete) {} : ActivityWithSubactivity("WifiSelection", renderer, inputManager), onComplete(onComplete) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;

View File

@ -106,12 +106,12 @@ void EpubReaderActivity::loop() {
} }
// Enter chapter selection activity // Enter chapter selection activity
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (inputManager.wasReleased(InputManager::BTN_CONFIRM)) {
// Don't start activity transition while rendering // Don't start activity transition while rendering
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity(); exitActivity();
enterNewActivity(new EpubReaderChapterSelectionActivity( enterNewActivity(new EpubReaderChapterSelectionActivity(
this->renderer, this->mappedInput, epub, currentSpineIndex, this->renderer, this->inputManager, epub, currentSpineIndex,
[this] { [this] {
exitActivity(); exitActivity();
updateRequired = true; updateRequired = true;
@ -129,21 +129,21 @@ void EpubReaderActivity::loop() {
} }
// Long press BACK (1s+) goes directly to home // Long press BACK (1s+) goes directly to home
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) { if (inputManager.isPressed(InputManager::BTN_BACK) && inputManager.getHeldTime() >= goHomeMs) {
onGoHome(); onGoHome();
return; return;
} }
// Short press BACK goes to file selection // Short press BACK goes to file selection
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) { if (inputManager.wasReleased(InputManager::BTN_BACK) && inputManager.getHeldTime() < goHomeMs) {
onGoBack(); onGoBack();
return; return;
} }
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::PageBack) || const bool prevReleased =
mappedInput.wasReleased(MappedInputManager::Button::Left); inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT);
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::PageForward) || const bool nextReleased =
mappedInput.wasReleased(MappedInputManager::Button::Right); inputManager.wasReleased(InputManager::BTN_DOWN) || inputManager.wasReleased(InputManager::BTN_RIGHT);
if (!prevReleased && !nextReleased) { if (!prevReleased && !nextReleased) {
return; return;
@ -157,7 +157,7 @@ void EpubReaderActivity::loop() {
return; return;
} }
const bool skipChapter = mappedInput.getHeldTime() > skipChapterMs; const bool skipChapter = inputManager.getHeldTime() > skipChapterMs;
if (skipChapter) { if (skipChapter) {
// We don't want to delete the section mid-render, so grab the semaphore // We don't want to delete the section mid-render, so grab the semaphore
@ -254,8 +254,8 @@ void EpubReaderActivity::renderScreen() {
const auto viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight; const auto viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
const auto viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom; const auto viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom;
if (!section->loadSectionFile(READER_FONT_ID, lineCompression, SETTINGS.extraParagraphSpacing, viewportWidth, if (!section->loadCacheMetadata(READER_FONT_ID, lineCompression, SETTINGS.extraParagraphSpacing, viewportWidth,
viewportHeight)) { viewportHeight)) {
Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis()); Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis());
// Progress bar dimensions // Progress bar dimensions
@ -282,6 +282,8 @@ void EpubReaderActivity::renderScreen() {
pagesUntilFullRefresh = 0; pagesUntilFullRefresh = 0;
} }
section->setupCacheDir();
// Setup callback - only called for chapters >= 50KB, redraws with progress bar // Setup callback - only called for chapters >= 50KB, redraws with progress bar
auto progressSetup = [this, boxXWithBar, boxWidthWithBar, boxHeightWithBar, barX, barY] { auto progressSetup = [this, boxXWithBar, boxWidthWithBar, boxHeightWithBar, barX, barY] {
renderer.fillRect(boxXWithBar, boxY, boxWidthWithBar, boxHeightWithBar, false); renderer.fillRect(boxXWithBar, boxY, boxWidthWithBar, boxHeightWithBar, false);
@ -298,8 +300,8 @@ void EpubReaderActivity::renderScreen() {
renderer.displayBuffer(EInkDisplay::FAST_REFRESH); renderer.displayBuffer(EInkDisplay::FAST_REFRESH);
}; };
if (!section->createSectionFile(READER_FONT_ID, lineCompression, SETTINGS.extraParagraphSpacing, viewportWidth, if (!section->persistPageDataToSD(READER_FONT_ID, lineCompression, SETTINGS.extraParagraphSpacing, viewportWidth,
viewportHeight, progressSetup, progressCallback)) { viewportHeight, progressSetup, progressCallback)) {
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;
@ -334,7 +336,7 @@ void EpubReaderActivity::renderScreen() {
} }
{ {
auto p = section->loadPageFromSectionFile(); auto p = section->loadPageFromSD();
if (!p) { if (!p) {
Serial.printf("[%lu] [ERS] Failed to load page from SD - clearing section cache\n", millis()); Serial.printf("[%lu] [ERS] Failed to load page from SD - clearing section cache\n", millis());
section->clearCache(); section->clearCache();

View File

@ -27,9 +27,9 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const; void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const;
public: public:
explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Epub> epub, explicit EpubReaderActivity(GfxRenderer& renderer, InputManager& inputManager, std::unique_ptr<Epub> epub,
const std::function<void()>& onGoBack, const std::function<void()>& onGoHome) const std::function<void()>& onGoBack, const std::function<void()>& onGoHome)
: ActivityWithSubactivity("EpubReader", renderer, mappedInput), : ActivityWithSubactivity("EpubReader", renderer, inputManager),
epub(std::move(epub)), epub(std::move(epub)),
onGoBack(onGoBack), onGoBack(onGoBack),
onGoHome(onGoHome) {} onGoHome(onGoHome) {}

View File

@ -66,17 +66,17 @@ void EpubReaderChapterSelectionActivity::onExit() {
} }
void EpubReaderChapterSelectionActivity::loop() { void EpubReaderChapterSelectionActivity::loop() {
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) || const bool prevReleased =
mappedInput.wasReleased(MappedInputManager::Button::Left); inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT);
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) || const bool nextReleased =
mappedInput.wasReleased(MappedInputManager::Button::Right); inputManager.wasReleased(InputManager::BTN_DOWN) || inputManager.wasReleased(InputManager::BTN_RIGHT);
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; const bool skipPage = inputManager.getHeldTime() > SKIP_PAGE_MS;
const int pageItems = getPageItems(); const int pageItems = getPageItems();
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (inputManager.wasReleased(InputManager::BTN_CONFIRM)) {
onSelectSpineIndex(selectorIndex); onSelectSpineIndex(selectorIndex);
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { } else if (inputManager.wasReleased(InputManager::BTN_BACK)) {
onGoBack(); onGoBack();
} else if (prevReleased) { } else if (prevReleased) {
if (skipPage) { if (skipPage) {

View File

@ -27,11 +27,11 @@ class EpubReaderChapterSelectionActivity final : public Activity {
void renderScreen(); void renderScreen();
public: public:
explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, InputManager& inputManager,
const std::shared_ptr<Epub>& epub, const int currentSpineIndex, const std::shared_ptr<Epub>& epub, const int currentSpineIndex,
const std::function<void()>& onGoBack, const std::function<void()>& onGoBack,
const std::function<void(int newSpineIndex)>& onSelectSpineIndex) const std::function<void(int newSpineIndex)>& onSelectSpineIndex)
: Activity("EpubReaderChapterSelection", renderer, mappedInput), : Activity("EpubReaderChapterSelection", renderer, inputManager),
epub(epub), epub(epub),
currentSpineIndex(currentSpineIndex), currentSpineIndex(currentSpineIndex),
onGoBack(onGoBack), onGoBack(onGoBack),

View File

@ -89,7 +89,7 @@ void FileSelectionActivity::onExit() {
void FileSelectionActivity::loop() { void FileSelectionActivity::loop() {
// Long press BACK (1s+) goes to root folder // Long press BACK (1s+) goes to root folder
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS) { if (inputManager.isPressed(InputManager::BTN_BACK) && inputManager.getHeldTime() >= GO_HOME_MS) {
if (basepath != "/") { if (basepath != "/") {
basepath = "/"; basepath = "/";
loadFiles(); loadFiles();
@ -98,14 +98,14 @@ void FileSelectionActivity::loop() {
return; return;
} }
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) || const bool prevReleased =
mappedInput.wasReleased(MappedInputManager::Button::Left); inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT);
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) || const bool nextReleased =
mappedInput.wasReleased(MappedInputManager::Button::Right); inputManager.wasReleased(InputManager::BTN_DOWN) || inputManager.wasReleased(InputManager::BTN_RIGHT);
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; const bool skipPage = inputManager.getHeldTime() > SKIP_PAGE_MS;
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (inputManager.wasReleased(InputManager::BTN_CONFIRM)) {
if (files.empty()) { if (files.empty()) {
return; return;
} }
@ -118,9 +118,9 @@ void FileSelectionActivity::loop() {
} else { } else {
onSelect(basepath + files[selectorIndex]); onSelect(basepath + files[selectorIndex]);
} }
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { } else if (inputManager.wasReleased(InputManager::BTN_BACK)) {
// Short press: go up one directory, or go home if at root // Short press: go up one directory, or go home if at root
if (mappedInput.getHeldTime() < GO_HOME_MS) { if (inputManager.getHeldTime() < GO_HOME_MS) {
if (basepath != "/") { if (basepath != "/") {
basepath.replace(basepath.find_last_of('/'), std::string::npos, ""); basepath.replace(basepath.find_last_of('/'), std::string::npos, "");
if (basepath.empty()) basepath = "/"; if (basepath.empty()) basepath = "/";
@ -166,8 +166,7 @@ void FileSelectionActivity::render() const {
renderer.drawCenteredText(READER_FONT_ID, 10, "Books", true, BOLD); renderer.drawCenteredText(READER_FONT_ID, 10, "Books", true, BOLD);
// Help text // Help text
const auto labels = mappedInput.mapLabels("« Home", "Open", "", ""); renderer.drawButtonHints(UI_FONT_ID, "« Home", "Open", "", "");
renderer.drawButtonHints(UI_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
if (files.empty()) { if (files.empty()) {
renderer.drawText(UI_FONT_ID, 20, 60, "No books found"); renderer.drawText(UI_FONT_ID, 20, 60, "No books found");

View File

@ -25,10 +25,10 @@ class FileSelectionActivity final : public Activity {
void loadFiles(); void loadFiles();
public: public:
explicit FileSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, explicit FileSelectionActivity(GfxRenderer& renderer, InputManager& inputManager,
const std::function<void(const std::string&)>& onSelect, const std::function<void(const std::string&)>& onSelect,
const std::function<void()>& onGoHome, std::string initialPath = "/") const std::function<void()>& onGoHome, std::string initialPath = "/")
: Activity("FileSelection", renderer, mappedInput), : Activity("FileSelection", renderer, inputManager),
basepath(initialPath.empty() ? "/" : std::move(initialPath)), basepath(initialPath.empty() ? "/" : std::move(initialPath)),
onSelect(onSelect), onSelect(onSelect),
onGoHome(onGoHome) {} onGoHome(onGoHome) {}

View File

@ -61,7 +61,7 @@ std::unique_ptr<Xtc> ReaderActivity::loadXtc(const std::string& path) {
void ReaderActivity::onSelectBookFile(const std::string& path) { void ReaderActivity::onSelectBookFile(const std::string& path) {
currentBookPath = path; // Track current book path currentBookPath = path; // Track current book path
exitActivity(); exitActivity();
enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Loading...")); enterNewActivity(new FullScreenMessageActivity(renderer, inputManager, "Loading..."));
if (isXtcFile(path)) { if (isXtcFile(path)) {
// Load XTC file // Load XTC file
@ -70,7 +70,7 @@ void ReaderActivity::onSelectBookFile(const std::string& path) {
onGoToXtcReader(std::move(xtc)); onGoToXtcReader(std::move(xtc));
} else { } else {
exitActivity(); exitActivity();
enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Failed to load XTC", REGULAR, enterNewActivity(new FullScreenMessageActivity(renderer, inputManager, "Failed to load XTC", REGULAR,
EInkDisplay::HALF_REFRESH)); EInkDisplay::HALF_REFRESH));
delay(2000); delay(2000);
onGoToFileSelection(); onGoToFileSelection();
@ -82,7 +82,7 @@ void ReaderActivity::onSelectBookFile(const std::string& path) {
onGoToEpubReader(std::move(epub)); onGoToEpubReader(std::move(epub));
} else { } else {
exitActivity(); exitActivity();
enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Failed to load epub", REGULAR, enterNewActivity(new FullScreenMessageActivity(renderer, inputManager, "Failed to load epub", REGULAR,
EInkDisplay::HALF_REFRESH)); EInkDisplay::HALF_REFRESH));
delay(2000); delay(2000);
onGoToFileSelection(); onGoToFileSelection();
@ -95,7 +95,7 @@ void ReaderActivity::onGoToFileSelection(const std::string& fromBookPath) {
// If coming from a book, start in that book's folder; otherwise start from root // If coming from a book, start in that book's folder; otherwise start from root
const auto initialPath = fromBookPath.empty() ? "/" : extractFolderPath(fromBookPath); const auto initialPath = fromBookPath.empty() ? "/" : extractFolderPath(fromBookPath);
enterNewActivity(new FileSelectionActivity( enterNewActivity(new FileSelectionActivity(
renderer, mappedInput, [this](const std::string& path) { onSelectBookFile(path); }, onGoBack, initialPath)); renderer, inputManager, [this](const std::string& path) { onSelectBookFile(path); }, onGoBack, initialPath));
} }
void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) { void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) {
@ -103,7 +103,7 @@ void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) {
currentBookPath = epubPath; currentBookPath = epubPath;
exitActivity(); exitActivity();
enterNewActivity(new EpubReaderActivity( enterNewActivity(new EpubReaderActivity(
renderer, mappedInput, std::move(epub), [this, epubPath] { onGoToFileSelection(epubPath); }, renderer, inputManager, std::move(epub), [this, epubPath] { onGoToFileSelection(epubPath); },
[this] { onGoBack(); })); [this] { onGoBack(); }));
} }
@ -112,7 +112,7 @@ void ReaderActivity::onGoToXtcReader(std::unique_ptr<Xtc> xtc) {
currentBookPath = xtcPath; currentBookPath = xtcPath;
exitActivity(); exitActivity();
enterNewActivity(new XtcReaderActivity( enterNewActivity(new XtcReaderActivity(
renderer, mappedInput, std::move(xtc), [this, xtcPath] { onGoToFileSelection(xtcPath); }, renderer, inputManager, std::move(xtc), [this, xtcPath] { onGoToFileSelection(xtcPath); },
[this] { onGoBack(); })); [this] { onGoBack(); }));
} }

View File

@ -21,9 +21,9 @@ class ReaderActivity final : public ActivityWithSubactivity {
void onGoToXtcReader(std::unique_ptr<Xtc> xtc); void onGoToXtcReader(std::unique_ptr<Xtc> xtc);
public: public:
explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath, explicit ReaderActivity(GfxRenderer& renderer, InputManager& inputManager, std::string initialBookPath,
const std::function<void()>& onGoBack) const std::function<void()>& onGoBack)
: ActivityWithSubactivity("Reader", renderer, mappedInput), : ActivityWithSubactivity("Reader", renderer, inputManager),
initialBookPath(std::move(initialBookPath)), initialBookPath(std::move(initialBookPath)),
onGoBack(onGoBack) {} onGoBack(onGoBack) {}
void onEnter() override; void onEnter() override;

View File

@ -71,21 +71,21 @@ void XtcReaderActivity::onExit() {
void XtcReaderActivity::loop() { void XtcReaderActivity::loop() {
// Long press BACK (1s+) goes directly to home // Long press BACK (1s+) goes directly to home
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) { if (inputManager.isPressed(InputManager::BTN_BACK) && inputManager.getHeldTime() >= goHomeMs) {
onGoHome(); onGoHome();
return; return;
} }
// Short press BACK goes to file selection // Short press BACK goes to file selection
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) { if (inputManager.wasReleased(InputManager::BTN_BACK) && inputManager.getHeldTime() < goHomeMs) {
onGoBack(); onGoBack();
return; return;
} }
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::PageBack) || const bool prevReleased =
mappedInput.wasReleased(MappedInputManager::Button::Left); inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT);
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::PageForward) || const bool nextReleased =
mappedInput.wasReleased(MappedInputManager::Button::Right); inputManager.wasReleased(InputManager::BTN_DOWN) || inputManager.wasReleased(InputManager::BTN_RIGHT);
if (!prevReleased && !nextReleased) { if (!prevReleased && !nextReleased) {
return; return;
@ -98,7 +98,7 @@ void XtcReaderActivity::loop() {
return; return;
} }
const bool skipPages = mappedInput.getHeldTime() > skipPageMs; const bool skipPages = inputManager.getHeldTime() > skipPageMs;
const int skipAmount = skipPages ? 10 : 1; const int skipAmount = skipPages ? 10 : 1;
if (prevReleased) { if (prevReleased) {

View File

@ -32,9 +32,9 @@ class XtcReaderActivity final : public Activity {
void loadProgress(); void loadProgress();
public: public:
explicit XtcReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Xtc> xtc, explicit XtcReaderActivity(GfxRenderer& renderer, InputManager& inputManager, std::unique_ptr<Xtc> xtc,
const std::function<void()>& onGoBack, const std::function<void()>& onGoHome) const std::function<void()>& onGoBack, const std::function<void()>& onGoHome)
: Activity("XtcReader", renderer, mappedInput), xtc(std::move(xtc)), onGoBack(onGoBack), onGoHome(onGoHome) {} : Activity("XtcReader", renderer, inputManager), xtc(std::move(xtc)), onGoBack(onGoBack), onGoHome(onGoHome) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;

View File

@ -72,7 +72,7 @@ void OtaUpdateActivity::onEnter() {
// Launch WiFi selection subactivity // Launch WiFi selection subactivity
Serial.printf("[%lu] [OTA] Launching WifiSelectionActivity...\n", millis()); Serial.printf("[%lu] [OTA] Launching WifiSelectionActivity...\n", millis());
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, enterNewActivity(new WifiSelectionActivity(renderer, inputManager,
[this](const bool connected) { onWifiSelectionComplete(connected); })); [this](const bool connected) { onWifiSelectionComplete(connected); }));
} }
@ -191,7 +191,7 @@ void OtaUpdateActivity::loop() {
} }
if (state == WAITING_CONFIRMATION) { if (state == WAITING_CONFIRMATION) {
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
Serial.printf("[%lu] [OTA] New update available, starting download...\n", millis()); Serial.printf("[%lu] [OTA] New update available, starting download...\n", millis());
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
state = UPDATE_IN_PROGRESS; state = UPDATE_IN_PROGRESS;
@ -215,7 +215,7 @@ void OtaUpdateActivity::loop() {
updateRequired = true; updateRequired = true;
} }
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { if (inputManager.wasPressed(InputManager::BTN_BACK)) {
goBack(); goBack();
} }
@ -223,14 +223,14 @@ void OtaUpdateActivity::loop() {
} }
if (state == FAILED) { if (state == FAILED) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { if (inputManager.wasPressed(InputManager::BTN_BACK)) {
goBack(); goBack();
} }
return; return;
} }
if (state == NO_UPDATE) { if (state == NO_UPDATE) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { if (inputManager.wasPressed(InputManager::BTN_BACK)) {
goBack(); goBack();
} }
return; return;

View File

@ -35,9 +35,8 @@ class OtaUpdateActivity : public ActivityWithSubactivity {
void render(); void render();
public: public:
explicit OtaUpdateActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, explicit OtaUpdateActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& goBack)
const std::function<void()>& goBack) : ActivityWithSubactivity("OtaUpdate", renderer, inputManager), goBack(goBack), updater() {}
: ActivityWithSubactivity("OtaUpdate", renderer, mappedInput), goBack(goBack), updater() {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;

View File

@ -9,7 +9,7 @@
// Define the static settings list // Define the static settings list
namespace { namespace {
constexpr int settingsCount = 7; constexpr int settingsCount = 6;
const SettingInfo settingsList[settingsCount] = { const SettingInfo settingsList[settingsCount] = {
// Should match with SLEEP_SCREEN_MODE // Should match with SLEEP_SCREEN_MODE
{"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}}, {"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}},
@ -20,10 +20,6 @@ const SettingInfo settingsList[settingsCount] = {
SettingType::ENUM, SettingType::ENUM,
&CrossPointSettings::orientation, &CrossPointSettings::orientation,
{"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}}, {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}},
{"Front Button Layout",
SettingType::ENUM,
&CrossPointSettings::frontButtonLayout,
{"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm"}},
{"Check for updates", SettingType::ACTION, nullptr, {}}, {"Check for updates", SettingType::ACTION, nullptr, {}},
}; };
} // namespace } // namespace
@ -72,26 +68,24 @@ void SettingsActivity::loop() {
} }
// Handle actions with early return // Handle actions with early return
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
toggleCurrentSetting(); toggleCurrentSetting();
updateRequired = true; updateRequired = true;
return; return;
} }
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { if (inputManager.wasPressed(InputManager::BTN_BACK)) {
SETTINGS.saveToFile(); SETTINGS.saveToFile();
onGoHome(); onGoHome();
return; return;
} }
// Handle navigation // Handle navigation
if (mappedInput.wasPressed(MappedInputManager::Button::Up) || if (inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT)) {
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
// Move selection up (with wrap-around) // Move selection up (with wrap-around)
selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount - 1); selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount - 1);
updateRequired = true; updateRequired = true;
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || } else if (inputManager.wasPressed(InputManager::BTN_DOWN) || inputManager.wasPressed(InputManager::BTN_RIGHT)) {
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
// Move selection down // Move selection down
if (selectedSettingIndex < settingsCount - 1) { if (selectedSettingIndex < settingsCount - 1) {
selectedSettingIndex++; selectedSettingIndex++;
@ -119,7 +113,7 @@ void SettingsActivity::toggleCurrentSetting() {
if (std::string(setting.name) == "Check for updates") { if (std::string(setting.name) == "Check for updates") {
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity(); exitActivity();
enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] { enterNewActivity(new OtaUpdateActivity(renderer, inputManager, [this] {
exitActivity(); exitActivity();
updateRequired = true; updateRequired = true;
})); }));
@ -179,13 +173,10 @@ void SettingsActivity::render() const {
} }
} }
// Draw version text above button hints
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
pageHeight - 60, CROSSPOINT_VERSION);
// Draw help text // Draw help text
const auto labels = mappedInput.mapLabels("« Save", "Toggle", "", ""); renderer.drawButtonHints(UI_FONT_ID, "« Save", "Toggle", "", "");
renderer.drawButtonHints(UI_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
pageHeight - 30, CROSSPOINT_VERSION);
// Always use standard refresh for settings screen // Always use standard refresh for settings screen
renderer.displayBuffer(); renderer.displayBuffer();

View File

@ -34,9 +34,8 @@ class SettingsActivity final : public ActivityWithSubactivity {
void toggleCurrentSetting(); void toggleCurrentSetting();
public: public:
explicit SettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, explicit SettingsActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoHome)
const std::function<void()>& onGoHome) : ActivityWithSubactivity("Settings", renderer, inputManager), onGoHome(onGoHome) {}
: ActivityWithSubactivity("Settings", renderer, mappedInput), onGoHome(onGoHome) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;

View File

@ -13,10 +13,10 @@ class FullScreenMessageActivity final : public Activity {
EInkDisplay::RefreshMode refreshMode; EInkDisplay::RefreshMode refreshMode;
public: public:
explicit FullScreenMessageActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string text, explicit FullScreenMessageActivity(GfxRenderer& renderer, InputManager& inputManager, std::string text,
const EpdFontStyle style = REGULAR, const EpdFontStyle style = REGULAR,
const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH)
: Activity("FullScreenMessage", renderer, mappedInput), : Activity("FullScreenMessage", renderer, inputManager),
text(std::move(text)), text(std::move(text)),
style(style), style(style),
refreshMode(refreshMode) {} refreshMode(refreshMode) {}

View File

@ -138,7 +138,7 @@ void KeyboardEntryActivity::handleKeyPress() {
void KeyboardEntryActivity::loop() { void KeyboardEntryActivity::loop() {
// Navigation // Navigation
if (mappedInput.wasPressed(MappedInputManager::Button::Up)) { if (inputManager.wasPressed(InputManager::BTN_UP)) {
if (selectedRow > 0) { if (selectedRow > 0) {
selectedRow--; selectedRow--;
// Clamp column to valid range for new row // Clamp column to valid range for new row
@ -148,7 +148,7 @@ void KeyboardEntryActivity::loop() {
updateRequired = true; updateRequired = true;
} }
if (mappedInput.wasPressed(MappedInputManager::Button::Down)) { if (inputManager.wasPressed(InputManager::BTN_DOWN)) {
if (selectedRow < NUM_ROWS - 1) { if (selectedRow < NUM_ROWS - 1) {
selectedRow++; selectedRow++;
const int maxCol = getRowLength(selectedRow) - 1; const int maxCol = getRowLength(selectedRow) - 1;
@ -157,7 +157,7 @@ void KeyboardEntryActivity::loop() {
updateRequired = true; updateRequired = true;
} }
if (mappedInput.wasPressed(MappedInputManager::Button::Left)) { if (inputManager.wasPressed(InputManager::BTN_LEFT)) {
// Special bottom row case // Special bottom row case
if (selectedRow == SPECIAL_ROW) { if (selectedRow == SPECIAL_ROW) {
// Bottom row has special key widths // Bottom row has special key widths
@ -187,7 +187,7 @@ void KeyboardEntryActivity::loop() {
updateRequired = true; updateRequired = true;
} }
if (mappedInput.wasPressed(MappedInputManager::Button::Right)) { if (inputManager.wasPressed(InputManager::BTN_RIGHT)) {
const int maxCol = getRowLength(selectedRow) - 1; const int maxCol = getRowLength(selectedRow) - 1;
// Special bottom row case // Special bottom row case
@ -220,13 +220,13 @@ void KeyboardEntryActivity::loop() {
} }
// Selection // Selection
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
handleKeyPress(); handleKeyPress();
updateRequired = true; updateRequired = true;
} }
// Cancel // Cancel
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { if (inputManager.wasPressed(InputManager::BTN_BACK)) {
if (onCancel) { if (onCancel) {
onCancel(); onCancel();
} }

View File

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <InputManager.h>
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/semphr.h> #include <freertos/semphr.h>
#include <freertos/task.h> #include <freertos/task.h>
@ -30,7 +31,7 @@ class KeyboardEntryActivity : public Activity {
/** /**
* Constructor * Constructor
* @param renderer Reference to the GfxRenderer for drawing * @param renderer Reference to the GfxRenderer for drawing
* @param mappedInput Reference to MappedInputManager for handling input * @param inputManager Reference to InputManager for handling input
* @param title Title to display above the keyboard * @param title Title to display above the keyboard
* @param initialText Initial text to show in the input field * @param initialText Initial text to show in the input field
* @param startY Y position to start rendering the keyboard * @param startY Y position to start rendering the keyboard
@ -39,11 +40,11 @@ class KeyboardEntryActivity : public Activity {
* @param onComplete Callback invoked when input is complete * @param onComplete Callback invoked when input is complete
* @param onCancel Callback invoked when input is cancelled * @param onCancel Callback invoked when input is cancelled
*/ */
explicit KeyboardEntryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, explicit KeyboardEntryActivity(GfxRenderer& renderer, InputManager& inputManager, std::string title = "Enter Text",
std::string title = "Enter Text", std::string initialText = "", const int startY = 10, std::string initialText = "", const int startY = 10, const size_t maxLength = 0,
const size_t maxLength = 0, const bool isPassword = false, const bool isPassword = false, OnCompleteCallback onComplete = nullptr,
OnCompleteCallback onComplete = nullptr, OnCancelCallback onCancel = nullptr) OnCancelCallback onCancel = nullptr)
: Activity("KeyboardEntry", renderer, mappedInput), : Activity("KeyboardEntry", renderer, inputManager),
title(std::move(title)), title(std::move(title)),
text(std::move(initialText)), text(std::move(initialText)),
startY(startY), startY(startY),

View File

@ -16,7 +16,6 @@
#include "Battery.h" #include "Battery.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "CrossPointState.h" #include "CrossPointState.h"
#include "MappedInputManager.h"
#include "activities/boot_sleep/BootActivity.h" #include "activities/boot_sleep/BootActivity.h"
#include "activities/boot_sleep/SleepActivity.h" #include "activities/boot_sleep/SleepActivity.h"
#include "activities/home/HomeActivity.h" #include "activities/home/HomeActivity.h"
@ -42,7 +41,6 @@
EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY); EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY);
InputManager inputManager; InputManager inputManager;
MappedInputManager mappedInputManager(inputManager);
GfxRenderer renderer(einkDisplay); GfxRenderer renderer(einkDisplay);
Activity* currentActivity; Activity* currentActivity;
@ -126,7 +124,7 @@ void waitForPowerRelease() {
// Enter deep sleep mode // Enter deep sleep mode
void enterDeepSleep() { void enterDeepSleep() {
exitActivity(); exitActivity();
enterNewActivity(new SleepActivity(renderer, mappedInputManager)); enterNewActivity(new SleepActivity(renderer, inputManager));
einkDisplay.deepSleep(); einkDisplay.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);
@ -141,24 +139,24 @@ void enterDeepSleep() {
void onGoHome(); void onGoHome();
void onGoToReader(const std::string& initialEpubPath) { void onGoToReader(const std::string& initialEpubPath) {
exitActivity(); exitActivity();
enterNewActivity(new ReaderActivity(renderer, mappedInputManager, initialEpubPath, onGoHome)); enterNewActivity(new ReaderActivity(renderer, inputManager, initialEpubPath, onGoHome));
} }
void onGoToReaderHome() { onGoToReader(std::string()); } void onGoToReaderHome() { onGoToReader(std::string()); }
void onContinueReading() { onGoToReader(APP_STATE.openEpubPath); } void onContinueReading() { onGoToReader(APP_STATE.openEpubPath); }
void onGoToFileTransfer() { void onGoToFileTransfer() {
exitActivity(); exitActivity();
enterNewActivity(new CrossPointWebServerActivity(renderer, mappedInputManager, onGoHome)); enterNewActivity(new CrossPointWebServerActivity(renderer, inputManager, onGoHome));
} }
void onGoToSettings() { void onGoToSettings() {
exitActivity(); exitActivity();
enterNewActivity(new SettingsActivity(renderer, mappedInputManager, onGoHome)); enterNewActivity(new SettingsActivity(renderer, inputManager, onGoHome));
} }
void onGoHome() { void onGoHome() {
exitActivity(); exitActivity();
enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToReaderHome, onGoToSettings, enterNewActivity(new HomeActivity(renderer, inputManager, onContinueReading, onGoToReaderHome, onGoToSettings,
onGoToFileTransfer)); onGoToFileTransfer));
} }
@ -195,7 +193,7 @@ void setup() {
Serial.printf("[%lu] [ ] SD card initialization failed\n", millis()); Serial.printf("[%lu] [ ] SD card initialization failed\n", millis());
setupDisplayAndFonts(); setupDisplayAndFonts();
exitActivity(); exitActivity();
enterNewActivity(new FullScreenMessageActivity(renderer, mappedInputManager, "SD card error", BOLD)); enterNewActivity(new FullScreenMessageActivity(renderer, inputManager, "SD card error", BOLD));
return; return;
} }
@ -207,7 +205,7 @@ void setup() {
setupDisplayAndFonts(); setupDisplayAndFonts();
exitActivity(); exitActivity();
enterNewActivity(new BootActivity(renderer, mappedInputManager)); enterNewActivity(new BootActivity(renderer, inputManager));
APP_STATE.loadFromFile(); APP_STATE.loadFromFile();
if (APP_STATE.openEpubPath.empty()) { if (APP_STATE.openEpubPath.empty()) {