Compare commits

..

No commits in common. "0d32d21d756c5b45b5a7b3b3d0a29429763387ff" and "cfe838e03b2e8ef02fcb960093eeffed48d528e2" have entirely different histories.

27 changed files with 266 additions and 273 deletions

View File

@ -34,7 +34,7 @@ jobs:
sudo apt-get install -y clang-format-21 sudo apt-get install -y clang-format-21
- name: Run cppcheck - name: Run cppcheck
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high run: pio check --fail-on-defect medium --fail-on-defect high
- name: Run clang-format - name: Run clang-format
run: PATH="/usr/lib/llvm-21/bin:$PATH" ./bin/clang-format-fix && git diff --exit-code || (echo "Please run 'bin/clang-format-fix' to fix formatting issues" && exit 1) run: PATH="/usr/lib/llvm-21/bin:$PATH" ./bin/clang-format-fix && git diff --exit-code || (echo "Please run 'bin/clang-format-fix' to fix formatting issues" && exit 1)

View File

@ -30,22 +30,24 @@ bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
// Stream read (reusing your existing stream logic) // Stream read (reusing your existing stream logic)
if (!readItemContentsToStream(containerPath, containerParser, 512)) { if (!readItemContentsToStream(containerPath, containerParser, 512)) {
Serial.printf("[%lu] [EBP] Could not read META-INF/container.xml\n", millis()); Serial.printf("[%lu] [EBP] Could not read META-INF/container.xml\n", millis());
containerParser.teardown();
return false; return false;
} }
// Extract the result // Extract the result
if (containerParser.fullPath.empty()) { if (containerParser.fullPath.empty()) {
Serial.printf("[%lu] [EBP] Could not find valid rootfile in container.xml\n", millis()); Serial.printf("[%lu] [EBP] Could not find valid rootfile in container.xml\n", millis());
containerParser.teardown();
return false; return false;
} }
*contentOpfFile = std::move(containerParser.fullPath); *contentOpfFile = std::move(containerParser.fullPath);
containerParser.teardown();
return true; return true;
} }
bool Epub::parseContentOpf(const std::string& contentOpfFilePath) { bool Epub::parseContentOpf(const std::string& contentOpfFilePath) {
Serial.printf("[%lu] [EBP] Parsing content.opf: %s\n", millis(), contentOpfFilePath.c_str());
size_t contentOpfSize; size_t contentOpfSize;
if (!getItemSize(contentOpfFilePath, &contentOpfSize)) { if (!getItemSize(contentOpfFilePath, &contentOpfSize)) {
Serial.printf("[%lu] [EBP] Could not get size of content.opf\n", millis()); Serial.printf("[%lu] [EBP] Could not get size of content.opf\n", millis());
@ -61,6 +63,7 @@ bool Epub::parseContentOpf(const std::string& contentOpfFilePath) {
if (!readItemContentsToStream(contentOpfFilePath, opfParser, 1024)) { if (!readItemContentsToStream(contentOpfFilePath, opfParser, 1024)) {
Serial.printf("[%lu] [EBP] Could not read content.opf\n", millis()); Serial.printf("[%lu] [EBP] Could not read content.opf\n", millis());
opfParser.teardown();
return false; return false;
} }
@ -81,6 +84,8 @@ bool Epub::parseContentOpf(const std::string& contentOpfFilePath) {
} }
Serial.printf("[%lu] [EBP] Successfully parsed content.opf\n", millis()); Serial.printf("[%lu] [EBP] Successfully parsed content.opf\n", millis());
opfParser.teardown();
return true; return true;
} }
@ -91,8 +96,6 @@ bool Epub::parseTocNcxFile() {
return false; return false;
} }
Serial.printf("[%lu] [EBP] Parsing toc ncx file: %s\n", millis(), tocNcxItem.c_str());
size_t tocSize; size_t tocSize;
if (!getItemSize(tocNcxItem, &tocSize)) { if (!getItemSize(tocNcxItem, &tocSize)) {
Serial.printf("[%lu] [EBP] Could not get size of toc ncx\n", millis()); Serial.printf("[%lu] [EBP] Could not get size of toc ncx\n", millis());
@ -108,18 +111,22 @@ bool Epub::parseTocNcxFile() {
if (!readItemContentsToStream(tocNcxItem, ncxParser, 1024)) { if (!readItemContentsToStream(tocNcxItem, ncxParser, 1024)) {
Serial.printf("[%lu] [EBP] Could not read toc ncx stream\n", millis()); Serial.printf("[%lu] [EBP] Could not read toc ncx stream\n", millis());
ncxParser.teardown();
return false; return false;
} }
this->toc = std::move(ncxParser.toc); this->toc = std::move(ncxParser.toc);
Serial.printf("[%lu] [EBP] Parsed %d TOC items\n", millis(), this->toc.size()); Serial.printf("[%lu] [EBP] Parsed %d TOC items\n", millis(), this->toc.size());
ncxParser.teardown();
return true; return true;
} }
// load in the meta data for the epub file // load in the meta data for the epub file
bool Epub::load() { bool Epub::load() {
Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str()); Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str());
ZipFile zip("/sd" + filepath);
std::string contentOpfFilePath; std::string contentOpfFilePath;
if (!findContentOpfFile(&contentOpfFilePath)) { if (!findContentOpfFile(&contentOpfFilePath)) {
@ -148,20 +155,44 @@ bool Epub::load() {
} }
void Epub::initializeSpineItemSizes() { void Epub::initializeSpineItemSizes() {
Serial.printf("[%lu] [EBP] Calculating book size\n", millis()); setupCacheDir();
const size_t spineItemsCount = getSpineItemsCount(); size_t spineItemsCount = getSpineItemsCount();
size_t cumSpineItemSize = 0; size_t cumSpineItemSize = 0;
const ZipFile zip("/sd" + filepath); if (SD.exists((getCachePath() + "/spine_size.bin").c_str())) {
File f = SD.open((getCachePath() + "/spine_size.bin").c_str());
uint8_t data[4];
for (size_t i = 0; i < spineItemsCount; i++) {
f.read(data, 4);
cumSpineItemSize = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
cumulativeSpineItemSize.emplace_back(cumSpineItemSize);
// Serial.printf("[%lu] [EBP] Loading item %d size %u to %u %u\n", millis(),
// i, cumSpineItemSize, data[1], data[0]);
}
f.close();
} else {
File f = SD.open((getCachePath() + "/spine_size.bin").c_str(), FILE_WRITE);
uint8_t data[4];
// determine size of spine items
for (size_t i = 0; i < spineItemsCount; i++) {
std::string spineItem = getSpineItem(i);
size_t s = 0;
getItemSize(spineItem, &s);
cumSpineItemSize += s;
cumulativeSpineItemSize.emplace_back(cumSpineItemSize);
for (size_t i = 0; i < spineItemsCount; i++) { // and persist to cache
std::string spineItem = getSpineItem(i); data[0] = cumSpineItemSize & 0xFF;
size_t s = 0; data[1] = (cumSpineItemSize >> 8) & 0xFF;
getItemSize(zip, spineItem, &s); data[2] = (cumSpineItemSize >> 16) & 0xFF;
cumSpineItemSize += s; data[3] = (cumSpineItemSize >> 24) & 0xFF;
cumulativeSpineItemSize.emplace_back(cumSpineItemSize); // Serial.printf("[%lu] [EBP] Persisting item %d size %u to %u %u\n", millis(),
// i, cumSpineItemSize, data[1], data[0]);
f.write(data, 4);
}
f.close();
} }
Serial.printf("[%lu] [EBP] Book size: %lu\n", millis(), cumSpineItemSize); Serial.printf("[%lu] [EBP] Book size: %lu\n", millis(), cumSpineItemSize);
} }
@ -260,31 +291,17 @@ bool Epub::readItemContentsToStream(const std::string& itemHref, Print& out, con
bool Epub::getItemSize(const std::string& itemHref, size_t* size) const { bool Epub::getItemSize(const std::string& itemHref, size_t* size) const {
const ZipFile zip("/sd" + filepath); const ZipFile zip("/sd" + filepath);
return getItemSize(zip, itemHref, size);
}
bool Epub::getItemSize(const ZipFile& zip, const std::string& itemHref, size_t* size) {
const std::string path = normalisePath(itemHref); const std::string path = normalisePath(itemHref);
return zip.getInflatedFileSize(path.c_str(), size); return zip.getInflatedFileSize(path.c_str(), size);
} }
int Epub::getSpineItemsCount() const { return spine.size(); } int Epub::getSpineItemsCount() const { return spine.size(); }
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return cumulativeSpineItemSize.at(spineIndex); }
if (spineIndex < 0 || spineIndex >= static_cast<int>(cumulativeSpineItemSize.size())) {
Serial.printf("[%lu] [EBP] getCumulativeSpineItemSize index:%d is out of range\n", millis(), spineIndex);
return 0;
}
return cumulativeSpineItemSize.at(spineIndex);
}
std::string& Epub::getSpineItem(const int spineIndex) { std::string& Epub::getSpineItem(const int spineIndex) {
static std::string emptyString; if (spineIndex < 0 || spineIndex >= spine.size()) {
if (spine.empty()) {
Serial.printf("[%lu] [EBP] getSpineItem called but spine is empty\n", millis());
return emptyString;
}
if (spineIndex < 0 || spineIndex >= static_cast<int>(spine.size())) {
Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex); Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex);
return spine.at(0).second; return spine.at(0).second;
} }
@ -293,12 +310,7 @@ std::string& Epub::getSpineItem(const int spineIndex) {
} }
EpubTocEntry& Epub::getTocItem(const int tocTndex) { EpubTocEntry& Epub::getTocItem(const int tocTndex) {
static EpubTocEntry emptyEntry("", "", "", 0); if (tocTndex < 0 || tocTndex >= toc.size()) {
if (toc.empty()) {
Serial.printf("[%lu] [EBP] getTocItem called but toc is empty\n", millis());
return emptyEntry;
}
if (tocTndex < 0 || tocTndex >= static_cast<int>(toc.size())) {
Serial.printf("[%lu] [EBP] getTocItem index:%d is out of range\n", millis(), tocTndex); Serial.printf("[%lu] [EBP] getTocItem index:%d is out of range\n", millis(), tocTndex);
return toc.at(0); return toc.at(0);
} }

View File

@ -33,7 +33,6 @@ class Epub {
bool parseContentOpf(const std::string& contentOpfFilePath); bool parseContentOpf(const std::string& contentOpfFilePath);
bool parseTocNcxFile(); bool parseTocNcxFile();
void initializeSpineItemSizes(); void initializeSpineItemSizes();
static bool getItemSize(const ZipFile& zip, const std::string& itemHref, size_t* size);
public: public:
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) { explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {

View File

@ -19,25 +19,14 @@ void ParsedText::addWord(std::string word, const EpdFontStyle fontStyle) {
// Consumes data to minimize memory usage // Consumes data to minimize memory usage
void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fontId, const int horizontalMargin, void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fontId, const int horizontalMargin,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine, const std::function<void(std::shared_ptr<TextBlock>)>& processLine) {
const bool includeLastLine) {
if (words.empty()) { if (words.empty()) {
return; return;
} }
const size_t totalWordCount = words.size();
const int pageWidth = renderer.getScreenWidth() - horizontalMargin; const int pageWidth = renderer.getScreenWidth() - horizontalMargin;
const int spaceWidth = renderer.getSpaceWidth(fontId); const int spaceWidth = renderer.getSpaceWidth(fontId);
const auto wordWidths = calculateWordWidths(renderer, fontId);
const auto lineBreakIndices = computeLineBreaks(pageWidth, spaceWidth, wordWidths);
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
for (size_t i = 0; i < lineCount; ++i) {
extractLine(i, pageWidth, spaceWidth, wordWidths, lineBreakIndices, processLine);
}
}
std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& renderer, const int fontId) {
const size_t totalWordCount = words.size();
std::vector<uint16_t> wordWidths; std::vector<uint16_t> wordWidths;
wordWidths.reserve(totalWordCount); wordWidths.reserve(totalWordCount);
@ -58,13 +47,6 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
std::advance(wordStylesIt, 1); std::advance(wordStylesIt, 1);
} }
return wordWidths;
}
std::vector<size_t> ParsedText::computeLineBreaks(const int pageWidth, const int spaceWidth,
const std::vector<uint16_t>& wordWidths) const {
const size_t totalWordCount = words.size();
// DP table to store the minimum badness (cost) of lines starting at index i // DP table to store the minimum badness (cost) of lines starting at index i
std::vector<int> dp(totalWordCount); std::vector<int> dp(totalWordCount);
// 'ans[i]' stores the index 'j' of the *last word* in the optimal line starting at 'i' // 'ans[i]' stores the index 'j' of the *last word* in the optimal line starting at 'i'
@ -124,59 +106,66 @@ std::vector<size_t> ParsedText::computeLineBreaks(const int pageWidth, const int
currentWordIndex = nextBreakIndex; currentWordIndex = nextBreakIndex;
} }
return lineBreakIndices; // Initialize iterators for consumption
} auto wordStartIt = words.begin();
auto wordStyleStartIt = wordStyles.begin();
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const int spaceWidth, size_t wordWidthIndex = 0;
const std::vector<uint16_t>& wordWidths, const std::vector<size_t>& lineBreakIndices,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine) { size_t lastBreakAt = 0;
const size_t lineBreak = lineBreakIndices[breakIndex]; for (const size_t lineBreak : lineBreakIndices) {
const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0; const size_t lineWordCount = lineBreak - lastBreakAt;
const size_t lineWordCount = lineBreak - lastBreakAt;
// Calculate end iterators for the range to splice
// Calculate total word width for this line auto wordEndIt = wordStartIt;
int lineWordWidthSum = 0; auto wordStyleEndIt = wordStyleStartIt;
for (size_t i = lastBreakAt; i < lineBreak; i++) { std::advance(wordEndIt, lineWordCount);
lineWordWidthSum += wordWidths[i]; std::advance(wordStyleEndIt, lineWordCount);
}
// Calculate total word width for this line
// Calculate spacing int lineWordWidthSum = 0;
const int spareSpace = pageWidth - lineWordWidthSum; for (size_t i = 0; i < lineWordCount; ++i) {
lineWordWidthSum += wordWidths[wordWidthIndex + i];
int spacing = spaceWidth; }
const bool isLastLine = lineBreak == words.size();
// Calculate spacing
if (style == TextBlock::JUSTIFIED && !isLastLine && lineWordCount >= 2) { int spareSpace = pageWidth - lineWordWidthSum;
spacing = spareSpace / (lineWordCount - 1);
} int spacing = spaceWidth;
const bool isLastLine = lineBreak == totalWordCount;
// Calculate initial x position
uint16_t xpos = 0; if (style == TextBlock::JUSTIFIED && !isLastLine && lineWordCount >= 2) {
if (style == TextBlock::RIGHT_ALIGN) { spacing = spareSpace / (lineWordCount - 1);
xpos = spareSpace - (lineWordCount - 1) * spaceWidth; }
} else if (style == TextBlock::CENTER_ALIGN) {
xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2; // Calculate initial x position
} uint16_t xpos = 0;
if (style == TextBlock::RIGHT_ALIGN) {
// Pre-calculate X positions for words xpos = spareSpace - (lineWordCount - 1) * spaceWidth;
std::list<uint16_t> lineXPos; } else if (style == TextBlock::CENTER_ALIGN) {
for (size_t i = lastBreakAt; i < lineBreak; i++) { xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
const uint16_t currentWordWidth = wordWidths[i]; }
lineXPos.push_back(xpos);
xpos += currentWordWidth + spacing; // Pre-calculate X positions for words
} std::list<uint16_t> lineXPos;
for (size_t i = 0; i < lineWordCount; ++i) {
// Iterators always start at the beginning as we are moving content with splice below const uint16_t currentWordWidth = wordWidths[wordWidthIndex + i];
auto wordEndIt = words.begin(); lineXPos.push_back(xpos);
auto wordStyleEndIt = wordStyles.begin(); xpos += currentWordWidth + spacing;
std::advance(wordEndIt, lineWordCount); }
std::advance(wordStyleEndIt, lineWordCount);
// *** CRITICAL STEP: CONSUME DATA USING SPLICE ***
// *** CRITICAL STEP: CONSUME DATA USING SPLICE *** std::list<std::string> lineWords;
std::list<std::string> lineWords; lineWords.splice(lineWords.begin(), words, wordStartIt, wordEndIt);
lineWords.splice(lineWords.begin(), words, words.begin(), wordEndIt); std::list<EpdFontStyle> lineWordStyles;
std::list<EpdFontStyle> lineWordStyles; lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyleStartIt, wordStyleEndIt);
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyles.begin(), wordStyleEndIt);
processLine(
processLine(std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style)); std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style));
// Update pointers/indices for the next line
wordStartIt = wordEndIt;
wordStyleStartIt = wordStyleEndIt;
wordWidthIndex += lineWordCount;
lastBreakAt = lineBreak;
}
} }

View File

@ -2,11 +2,11 @@
#include <EpdFontFamily.h> #include <EpdFontFamily.h>
#include <cstdint>
#include <functional> #include <functional>
#include <list> #include <list>
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector>
#include "blocks/TextBlock.h" #include "blocks/TextBlock.h"
@ -18,12 +18,6 @@ class ParsedText {
TextBlock::BLOCK_STYLE style; TextBlock::BLOCK_STYLE style;
bool extraParagraphSpacing; bool extraParagraphSpacing;
std::vector<size_t> computeLineBreaks(int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths) const;
void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths,
const std::vector<size_t>& lineBreakIndices,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine);
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
public: public:
explicit ParsedText(const TextBlock::BLOCK_STYLE style, const bool extraParagraphSpacing) explicit ParsedText(const TextBlock::BLOCK_STYLE style, const bool extraParagraphSpacing)
: style(style), extraParagraphSpacing(extraParagraphSpacing) {} : style(style), extraParagraphSpacing(extraParagraphSpacing) {}
@ -32,9 +26,7 @@ class ParsedText {
void addWord(std::string word, EpdFontStyle fontStyle); void addWord(std::string word, EpdFontStyle fontStyle);
void setStyle(const TextBlock::BLOCK_STYLE style) { this->style = style; } void setStyle(const TextBlock::BLOCK_STYLE style) { this->style = style; }
TextBlock::BLOCK_STYLE getStyle() const { return style; } TextBlock::BLOCK_STYLE getStyle() const { return style; }
size_t size() const { return words.size(); }
bool isEmpty() const { return words.empty(); } bool isEmpty() const { return words.empty(); }
void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, int horizontalMargin, void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, int horizontalMargin,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine, const std::function<void(std::shared_ptr<TextBlock>)>& processLine);
bool includeLastLine = true);
}; };

View File

@ -21,10 +21,9 @@ class Section {
int currentPage = 0; int currentPage = 0;
explicit Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer) explicit Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
: epub(epub), : epub(epub), spineIndex(spineIndex), renderer(renderer) {
spineIndex(spineIndex), cachePath = epub->getCachePath() + "/" + std::to_string(spineIndex);
renderer(renderer), }
cachePath(epub->getCachePath() + "/" + std::to_string(spineIndex)) {}
~Section() = default; ~Section() = default;
bool loadCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom, bool loadCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
int marginLeft, bool extraParagraphSpacing); int marginLeft, bool extraParagraphSpacing);

View File

@ -143,17 +143,6 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
self->partWordBuffer[self->partWordBufferIndex++] = s[i]; self->partWordBuffer[self->partWordBufferIndex++] = s[i];
} }
// If we have > 750 words buffered up, perform the layout and consume out all but the last line
// There should be enough here to build out 1-2 full pages and doing this will free up a lot of
// memory.
// Spotted when reading Intermezzo, there are some really long text blocks in there.
if (self->currentTextBlock->size() > 750) {
Serial.printf("[%lu] [EHP] Text block too long, splitting into multiple pages\n", millis());
self->currentTextBlock->layoutAndExtractLines(
self->renderer, self->fontId, self->marginLeft + self->marginRight,
[self](const std::shared_ptr<TextBlock>& textBlock) { self->addLineToPage(textBlock); }, false);
}
} }
void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* name) { void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* name) {

View File

@ -14,11 +14,12 @@ bool ContainerParser::setup() {
return true; return true;
} }
ContainerParser::~ContainerParser() { bool ContainerParser::teardown() {
if (parser) { if (parser) {
XML_ParserFree(parser); XML_ParserFree(parser);
parser = nullptr; parser = nullptr;
} }
return true;
} }
size_t ContainerParser::write(const uint8_t data) { return write(&data, 1); } size_t ContainerParser::write(const uint8_t data) { return write(&data, 1); }

View File

@ -23,9 +23,9 @@ class ContainerParser final : public Print {
std::string fullPath; std::string fullPath;
explicit ContainerParser(const size_t xmlSize) : remainingSize(xmlSize) {} explicit ContainerParser(const size_t xmlSize) : remainingSize(xmlSize) {}
~ContainerParser() override;
bool setup(); bool setup();
bool teardown();
size_t write(uint8_t) override; size_t write(uint8_t) override;
size_t write(const uint8_t* buffer, size_t size) override; size_t write(const uint8_t* buffer, size_t size) override;

View File

@ -4,7 +4,7 @@
#include <ZipFile.h> #include <ZipFile.h>
namespace { namespace {
constexpr char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml"; constexpr const char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml";
} }
bool ContentOpfParser::setup() { bool ContentOpfParser::setup() {
@ -20,11 +20,12 @@ bool ContentOpfParser::setup() {
return true; return true;
} }
ContentOpfParser::~ContentOpfParser() { bool ContentOpfParser::teardown() {
if (parser) { if (parser) {
XML_ParserFree(parser); XML_ParserFree(parser);
parser = nullptr; parser = nullptr;
} }
return true;
} }
size_t ContentOpfParser::write(const uint8_t data) { return write(&data, 1); } size_t ContentOpfParser::write(const uint8_t data) { return write(&data, 1); }

View File

@ -34,9 +34,9 @@ class ContentOpfParser final : public Print {
explicit ContentOpfParser(const std::string& baseContentPath, const size_t xmlSize) explicit ContentOpfParser(const std::string& baseContentPath, const size_t xmlSize)
: baseContentPath(baseContentPath), remainingSize(xmlSize) {} : baseContentPath(baseContentPath), remainingSize(xmlSize) {}
~ContentOpfParser() override;
bool setup(); bool setup();
bool teardown();
size_t write(uint8_t) override; size_t write(uint8_t) override;
size_t write(const uint8_t* buffer, size_t size) override; size_t write(const uint8_t* buffer, size_t size) override;

View File

@ -1,6 +1,5 @@
#include "TocNcxParser.h" #include "TocNcxParser.h"
#include <Esp.h>
#include <HardwareSerial.h> #include <HardwareSerial.h>
bool TocNcxParser::setup() { bool TocNcxParser::setup() {
@ -16,11 +15,12 @@ bool TocNcxParser::setup() {
return true; return true;
} }
TocNcxParser::~TocNcxParser() { bool TocNcxParser::teardown() {
if (parser) { if (parser) {
XML_ParserFree(parser); XML_ParserFree(parser);
parser = nullptr; parser = nullptr;
} }
return true;
} }
size_t TocNcxParser::write(const uint8_t data) { return write(&data, 1); } size_t TocNcxParser::write(const uint8_t data) { return write(&data, 1); }

View File

@ -28,9 +28,9 @@ class TocNcxParser final : public Print {
explicit TocNcxParser(const std::string& baseContentPath, const size_t xmlSize) explicit TocNcxParser(const std::string& baseContentPath, const size_t xmlSize)
: baseContentPath(baseContentPath), remainingSize(xmlSize) {} : baseContentPath(baseContentPath), remainingSize(xmlSize) {}
~TocNcxParser() override;
bool setup(); bool setup();
bool teardown();
size_t write(uint8_t) override; size_t write(uint8_t) override;
size_t write(const uint8_t* buffer, size_t size) override; size_t write(const uint8_t* buffer, size_t size) override;

View File

@ -136,13 +136,6 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
auto* outputRow = static_cast<uint8_t*>(malloc(outputRowSize)); auto* outputRow = static_cast<uint8_t*>(malloc(outputRowSize));
auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes())); auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes()));
if (!outputRow || !rowBytes) {
Serial.printf("[%lu] [GFX] !! Failed to allocate BMP row buffers\n", millis());
free(outputRow);
free(rowBytes);
return;
}
for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) { for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) {
// The BMP's (0, 0) is the bottom-left corner (if the height is positive, top-left if negative). // The BMP's (0, 0) is the bottom-left corner (if the height is positive, top-left if negative).
// Screen's (0, 0) is the top-left corner. // Screen's (0, 0) is the top-left corner.
@ -190,10 +183,6 @@ void GfxRenderer::clearScreen(const uint8_t color) const { einkDisplay.clearScre
void GfxRenderer::invertScreen() const { void GfxRenderer::invertScreen() const {
uint8_t* buffer = einkDisplay.getFrameBuffer(); uint8_t* buffer = einkDisplay.getFrameBuffer();
if (!buffer) {
Serial.printf("[%lu] [GFX] !! No framebuffer in invertScreen\n", millis());
return;
}
for (int i = 0; i < EInkDisplay::BUFFER_SIZE; i++) { for (int i = 0; i < EInkDisplay::BUFFER_SIZE; i++) {
buffer[i] = ~buffer[i]; buffer[i] = ~buffer[i];
} }
@ -267,10 +256,6 @@ void GfxRenderer::freeBwBufferChunks() {
*/ */
void GfxRenderer::storeBwBuffer() { void GfxRenderer::storeBwBuffer() {
const uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); const uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
if (!frameBuffer) {
Serial.printf("[%lu] [GFX] !! No framebuffer in storeBwBuffer\n", millis());
return;
}
// Allocate and copy each chunk // Allocate and copy each chunk
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) { for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
@ -321,12 +306,6 @@ void GfxRenderer::restoreBwBuffer() {
} }
uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
if (!frameBuffer) {
Serial.printf("[%lu] [GFX] !! No framebuffer in restoreBwBuffer\n", millis());
freeBwBufferChunks();
return;
}
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) { for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
// Check if chunk is missing // Check if chunk is missing
if (!bwBufferChunks[i]) { if (!bwBufferChunks[i]) {

View File

@ -27,28 +27,31 @@ bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t*
return true; return true;
} }
ZipFile::ZipFile(std::string filePath) : filePath(std::move(filePath)) { bool ZipFile::loadFileStat(const char* filename, mz_zip_archive_file_stat* fileStat) const {
const bool status = mz_zip_reader_init_file(&zipArchive, this->filePath.c_str(), 0); mz_zip_archive zipArchive = {};
const bool status = mz_zip_reader_init_file(&zipArchive, filePath.c_str(), 0);
if (!status) { if (!status) {
Serial.printf("[%lu] [ZIP] mz_zip_reader_init_file() failed for %s! Error: %s\n", millis(), this->filePath.c_str(), Serial.printf("[%lu] [ZIP] mz_zip_reader_init_file() failed! Error: %s\n", millis(),
mz_zip_get_error_string(zipArchive.m_last_error)); mz_zip_get_error_string(zipArchive.m_last_error));
return false;
} }
}
bool ZipFile::loadFileStat(const char* filename, mz_zip_archive_file_stat* fileStat) const {
// find the file // find the file
mz_uint32 fileIndex = 0; mz_uint32 fileIndex = 0;
if (!mz_zip_reader_locate_file_v2(&zipArchive, filename, nullptr, 0, &fileIndex)) { if (!mz_zip_reader_locate_file_v2(&zipArchive, filename, nullptr, 0, &fileIndex)) {
Serial.printf("[%lu] [ZIP] Could not find file %s\n", millis(), filename); Serial.printf("[%lu] [ZIP] Could not find file %s\n", millis(), filename);
mz_zip_reader_end(&zipArchive);
return false; return false;
} }
if (!mz_zip_reader_file_stat(&zipArchive, fileIndex, fileStat)) { if (!mz_zip_reader_file_stat(&zipArchive, fileIndex, fileStat)) {
Serial.printf("[%lu] [ZIP] mz_zip_reader_file_stat() failed! Error: %s\n", millis(), Serial.printf("[%lu] [ZIP] mz_zip_reader_file_stat() failed! Error: %s\n", millis(),
mz_zip_get_error_string(zipArchive.m_last_error)); mz_zip_get_error_string(zipArchive.m_last_error));
mz_zip_reader_end(&zipArchive);
return false; return false;
} }
mz_zip_reader_end(&zipArchive);
return true; return true;
} }
@ -115,11 +118,6 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
const auto inflatedDataSize = static_cast<size_t>(fileStat.m_uncomp_size); const auto inflatedDataSize = static_cast<size_t>(fileStat.m_uncomp_size);
const auto dataSize = trailingNullByte ? inflatedDataSize + 1 : inflatedDataSize; const auto dataSize = trailingNullByte ? inflatedDataSize + 1 : inflatedDataSize;
const auto data = static_cast<uint8_t*>(malloc(dataSize)); const auto data = static_cast<uint8_t*>(malloc(dataSize));
if (data == nullptr) {
Serial.printf("[%lu] [ZIP] Failed to allocate memory for output buffer (%zu bytes)\n", millis(), dataSize);
fclose(file);
return nullptr;
}
if (fileStat.m_method == MZ_NO_COMPRESSION) { if (fileStat.m_method == MZ_NO_COMPRESSION) {
// no deflation, just read content // no deflation, just read content

View File

@ -1,19 +1,19 @@
#pragma once #pragma once
#include <Print.h> #include <Print.h>
#include <functional>
#include <string> #include <string>
#include "miniz.h" #include "miniz.h"
class ZipFile { class ZipFile {
std::string filePath; std::string filePath;
mutable mz_zip_archive zipArchive = {};
bool loadFileStat(const char* filename, mz_zip_archive_file_stat* fileStat) const; bool loadFileStat(const char* filename, mz_zip_archive_file_stat* fileStat) const;
long getDataOffset(const mz_zip_archive_file_stat& fileStat) const; long getDataOffset(const mz_zip_archive_file_stat& fileStat) const;
public: public:
explicit ZipFile(std::string filePath); explicit ZipFile(std::string filePath) : filePath(std::move(filePath)) {}
~ZipFile() { mz_zip_reader_end(&zipArchive); } ~ZipFile() = default;
bool getInflatedFileSize(const char* filename, size_t* size) const; bool getInflatedFileSize(const char* filename, size_t* size) const;
uint8_t* readFileToMemory(const char* filename, size_t* size = nullptr, bool trailingNullByte = false) const; uint8_t* readFileToMemory(const char* filename, size_t* size = nullptr, bool trailingNullByte = false) const;
bool readFileToStream(const char* filename, Print& out, size_t chunkSize) const; bool readFileToStream(const char* filename, Print& out, size_t chunkSize) const;

View File

@ -9,8 +9,8 @@ framework = arduino
monitor_speed = 115200 monitor_speed = 115200
upload_speed = 921600 upload_speed = 921600
check_tool = cppcheck check_tool = cppcheck
check_flags = --enable=all --suppress=missingIncludeSystem --suppress=unusedFunction --suppress=unmatchedSuppression --inline-suppr
check_skip_packages = yes check_skip_packages = yes
check_severity = medium, high
board_upload.flash_size = 16MB board_upload.flash_size = 16MB
board_upload.maximum_size = 16777216 board_upload.maximum_size = 16777216

View File

@ -111,12 +111,12 @@ bool WifiCredentialStore::loadFromFile() {
bool WifiCredentialStore::addCredential(const std::string& ssid, const std::string& password) { bool WifiCredentialStore::addCredential(const std::string& ssid, const std::string& password) {
// Check if this SSID already exists and update it // Check if this SSID already exists and update it
const auto cred = find_if(credentials.begin(), credentials.end(), for (auto& cred : credentials) {
[&ssid](const WifiCredential& cred) { return cred.ssid == ssid; }); if (cred.ssid == ssid) {
if (cred != credentials.end()) { cred.password = password;
cred->password = password; Serial.printf("[%lu] [WCS] Updated credentials for: %s\n", millis(), ssid.c_str());
Serial.printf("[%lu] [WCS] Updated credentials for: %s\n", millis(), ssid.c_str()); return saveToFile();
return saveToFile(); }
} }
// Check if we've reached the limit // Check if we've reached the limit
@ -132,24 +132,22 @@ bool WifiCredentialStore::addCredential(const std::string& ssid, const std::stri
} }
bool WifiCredentialStore::removeCredential(const std::string& ssid) { bool WifiCredentialStore::removeCredential(const std::string& ssid) {
const auto cred = find_if(credentials.begin(), credentials.end(), for (auto it = credentials.begin(); it != credentials.end(); ++it) {
[&ssid](const WifiCredential& cred) { return cred.ssid == ssid; }); if (it->ssid == ssid) {
if (cred != credentials.end()) { credentials.erase(it);
credentials.erase(cred); Serial.printf("[%lu] [WCS] Removed credentials for: %s\n", millis(), ssid.c_str());
Serial.printf("[%lu] [WCS] Removed credentials for: %s\n", millis(), ssid.c_str()); return saveToFile();
return saveToFile(); }
} }
return false; // Not found return false; // Not found
} }
const WifiCredential* WifiCredentialStore::findCredential(const std::string& ssid) const { const WifiCredential* WifiCredentialStore::findCredential(const std::string& ssid) const {
const auto cred = find_if(credentials.begin(), credentials.end(), for (const auto& cred : credentials) {
[&ssid](const WifiCredential& cred) { return cred.ssid == ssid; }); if (cred.ssid == ssid) {
return &cred;
if (cred != credentials.end()) { }
return &*cred;
} }
return nullptr; return nullptr;
} }

View File

@ -1,11 +1,11 @@
#include "SleepActivity.h" #include "SleepActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <SD.h>
#include <vector> #include <vector>
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "SD.h"
#include "config.h" #include "config.h"
#include "images/CrossLarge.h" #include "images/CrossLarge.h"

View File

@ -217,7 +217,8 @@ void CrossPointWebServerActivity::render() const {
} }
void CrossPointWebServerActivity::renderServerRunning() const { void CrossPointWebServerActivity::renderServerRunning() const {
const auto pageHeight = renderer.getScreenHeight(); const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID); const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (pageHeight - height * 5) / 2; const auto top = (pageHeight - height * 5) / 2;
@ -225,7 +226,7 @@ void CrossPointWebServerActivity::renderServerRunning() const {
std::string ssidInfo = "Network: " + connectedSSID; std::string ssidInfo = "Network: " + connectedSSID;
if (ssidInfo.length() > 28) { if (ssidInfo.length() > 28) {
ssidInfo.replace(25, ssidInfo.length() - 25, "..."); ssidInfo = ssidInfo.substr(0, 25) + "...";
} }
renderer.drawCenteredText(UI_FONT_ID, top + 10, ssidInfo.c_str(), true, REGULAR); renderer.drawCenteredText(UI_FONT_ID, top + 10, ssidInfo.c_str(), true, REGULAR);

View File

@ -138,7 +138,6 @@ void WifiSelectionActivity::processWifiScanResults() {
// Convert map to vector // Convert map to vector
networks.clear(); networks.clear();
for (const auto& pair : uniqueNetworks) { for (const auto& pair : uniqueNetworks) {
// cppcheck-suppress useStlAlgorithm
networks.push_back(pair.second); networks.push_back(pair.second);
} }
@ -335,10 +334,11 @@ void WifiSelectionActivity::loop() {
// User chose "Yes" - forget the network // User chose "Yes" - forget the network
WIFI_STORE.removeCredential(selectedSSID); WIFI_STORE.removeCredential(selectedSSID);
// Update the network list to reflect the change // Update the network list to reflect the change
const auto network = find_if(networks.begin(), networks.end(), for (auto& network : networks) {
[this](const WifiNetworkInfo& net) { return net.ssid == selectedSSID; }); if (network.ssid == selectedSSID) {
if (network != networks.end()) { network.hasSavedPassword = false;
network->hasSavedPassword = false; break;
}
} }
} }
// Go back to network list // Go back to network list
@ -468,8 +468,8 @@ void WifiSelectionActivity::render() const {
} }
void WifiSelectionActivity::renderNetworkList() const { void WifiSelectionActivity::renderNetworkList() const {
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = GfxRenderer::getScreenHeight();
// Draw header // Draw header
renderer.drawCenteredText(READER_FONT_ID, 10, "WiFi Networks", true, BOLD); renderer.drawCenteredText(READER_FONT_ID, 10, "WiFi Networks", true, BOLD);
@ -506,7 +506,7 @@ void WifiSelectionActivity::renderNetworkList() const {
// Draw network name (truncate if too long) // Draw network name (truncate if too long)
std::string displayName = network.ssid; std::string displayName = network.ssid;
if (displayName.length() > 16) { if (displayName.length() > 16) {
displayName.replace(13, displayName.length() - 13, "..."); displayName = displayName.substr(0, 13) + "...";
} }
renderer.drawText(UI_FONT_ID, 20, networkY, displayName.c_str()); renderer.drawText(UI_FONT_ID, 20, networkY, displayName.c_str());
@ -544,13 +544,15 @@ void WifiSelectionActivity::renderNetworkList() const {
} }
void WifiSelectionActivity::renderPasswordEntry() const { void WifiSelectionActivity::renderPasswordEntry() const {
const auto pageHeight = GfxRenderer::getScreenHeight();
// Draw header // Draw header
renderer.drawCenteredText(READER_FONT_ID, 5, "WiFi Password", true, BOLD); renderer.drawCenteredText(READER_FONT_ID, 5, "WiFi Password", true, BOLD);
// Draw network name with good spacing from header // Draw network name with good spacing from header
std::string networkInfo = "Network: " + selectedSSID; std::string networkInfo = "Network: " + selectedSSID;
if (networkInfo.length() > 30) { if (networkInfo.length() > 30) {
networkInfo.replace(27, networkInfo.length() - 27, "..."); networkInfo = networkInfo.substr(0, 27) + "...";
} }
renderer.drawCenteredText(UI_FONT_ID, 38, networkInfo.c_str(), true, REGULAR); renderer.drawCenteredText(UI_FONT_ID, 38, networkInfo.c_str(), true, REGULAR);
@ -561,7 +563,7 @@ void WifiSelectionActivity::renderPasswordEntry() const {
} }
void WifiSelectionActivity::renderConnecting() const { void WifiSelectionActivity::renderConnecting() const {
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = GfxRenderer::getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID); const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (pageHeight - height) / 2; const auto top = (pageHeight - height) / 2;
@ -572,14 +574,15 @@ void WifiSelectionActivity::renderConnecting() const {
std::string ssidInfo = "to " + selectedSSID; std::string ssidInfo = "to " + selectedSSID;
if (ssidInfo.length() > 25) { if (ssidInfo.length() > 25) {
ssidInfo.replace(22, ssidInfo.length() - 22, "..."); ssidInfo = ssidInfo.substr(0, 22) + "...";
} }
renderer.drawCenteredText(UI_FONT_ID, top, ssidInfo.c_str(), true, REGULAR); renderer.drawCenteredText(UI_FONT_ID, top, ssidInfo.c_str(), true, REGULAR);
} }
} }
void WifiSelectionActivity::renderConnected() const { void WifiSelectionActivity::renderConnected() const {
const auto pageHeight = renderer.getScreenHeight(); const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID); const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (pageHeight - height * 4) / 2; const auto top = (pageHeight - height * 4) / 2;
@ -587,7 +590,7 @@ void WifiSelectionActivity::renderConnected() const {
std::string ssidInfo = "Network: " + selectedSSID; std::string ssidInfo = "Network: " + selectedSSID;
if (ssidInfo.length() > 28) { if (ssidInfo.length() > 28) {
ssidInfo.replace(25, ssidInfo.length() - 25, "..."); ssidInfo = ssidInfo.substr(0, 25) + "...";
} }
renderer.drawCenteredText(UI_FONT_ID, top + 10, ssidInfo.c_str(), true, REGULAR); renderer.drawCenteredText(UI_FONT_ID, top + 10, ssidInfo.c_str(), true, REGULAR);
@ -598,8 +601,8 @@ void WifiSelectionActivity::renderConnected() const {
} }
void WifiSelectionActivity::renderSavePrompt() const { void WifiSelectionActivity::renderSavePrompt() const {
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = GfxRenderer::getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID); const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (pageHeight - height * 3) / 2; const auto top = (pageHeight - height * 3) / 2;
@ -607,7 +610,7 @@ void WifiSelectionActivity::renderSavePrompt() const {
std::string ssidInfo = "Network: " + selectedSSID; std::string ssidInfo = "Network: " + selectedSSID;
if (ssidInfo.length() > 28) { if (ssidInfo.length() > 28) {
ssidInfo.replace(25, ssidInfo.length() - 25, "..."); ssidInfo = ssidInfo.substr(0, 25) + "...";
} }
renderer.drawCenteredText(UI_FONT_ID, top, ssidInfo.c_str(), true, REGULAR); renderer.drawCenteredText(UI_FONT_ID, top, ssidInfo.c_str(), true, REGULAR);
@ -638,7 +641,7 @@ void WifiSelectionActivity::renderSavePrompt() const {
} }
void WifiSelectionActivity::renderConnectionFailed() const { void WifiSelectionActivity::renderConnectionFailed() const {
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = GfxRenderer::getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID); const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (pageHeight - height * 2) / 2; const auto top = (pageHeight - height * 2) / 2;
@ -648,8 +651,8 @@ void WifiSelectionActivity::renderConnectionFailed() const {
} }
void WifiSelectionActivity::renderForgetPrompt() const { void WifiSelectionActivity::renderForgetPrompt() const {
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = GfxRenderer::getScreenHeight();
const auto height = renderer.getLineHeight(UI_FONT_ID); const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (pageHeight - height * 3) / 2; const auto top = (pageHeight - height * 3) / 2;
@ -657,7 +660,7 @@ void WifiSelectionActivity::renderForgetPrompt() const {
std::string ssidInfo = "Network: " + selectedSSID; std::string ssidInfo = "Network: " + selectedSSID;
if (ssidInfo.length() > 28) { if (ssidInfo.length() > 28) {
ssidInfo.replace(25, ssidInfo.length() - 25, "..."); ssidInfo = ssidInfo.substr(0, 25) + "...";
} }
renderer.drawCenteredText(UI_FONT_ID, top, ssidInfo.c_str(), true, REGULAR); renderer.drawCenteredText(UI_FONT_ID, top, ssidInfo.c_str(), true, REGULAR);

View File

@ -383,7 +383,9 @@ void CrossPointWebServer::handleFileList() {
// Folders come first // Folders come first
if (a.isDirectory != b.isDirectory) return a.isDirectory > b.isDirectory; if (a.isDirectory != b.isDirectory) return a.isDirectory > b.isDirectory;
// Then sort by epub status (epubs first among files) // Then sort by epub status (epubs first among files)
if (a.isEpub != b.isEpub) return a.isEpub > b.isEpub; if (!a.isDirectory && !b.isDirectory) {
if (a.isEpub != b.isEpub) return a.isEpub > b.isEpub;
}
// Then alphabetically // Then alphabetically
return a.name < b.name; return a.name < b.name;
}); });
@ -511,6 +513,13 @@ void CrossPointWebServer::handleUpload() {
Serial.printf("[%lu] [WEB] [UPLOAD] START: %s to path: %s\n", millis(), uploadFileName.c_str(), uploadPath.c_str()); Serial.printf("[%lu] [WEB] [UPLOAD] START: %s to path: %s\n", millis(), uploadFileName.c_str(), uploadPath.c_str());
Serial.printf("[%lu] [WEB] [UPLOAD] Free heap: %d bytes\n", millis(), ESP.getFreeHeap()); Serial.printf("[%lu] [WEB] [UPLOAD] Free heap: %d bytes\n", millis(), ESP.getFreeHeap());
// Validate file extension
if (!isEpubFile(uploadFileName)) {
uploadError = "Only .epub files are allowed";
Serial.printf("[%lu] [WEB] [UPLOAD] REJECTED - not an epub file\n", millis());
return;
}
// Create file path // Create file path
String filePath = uploadPath; String filePath = uploadPath;
if (!filePath.endsWith("/")) filePath += "/"; if (!filePath.endsWith("/")) filePath += "/";

View File

@ -383,7 +383,7 @@ void EpubReaderActivity::renderStatusBar() const {
title = tocItem.title; title = tocItem.title;
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str()); titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
while (titleWidth > availableTextWidth && title.length() > 11) { while (titleWidth > availableTextWidth && title.length() > 11) {
title.replace(title.length() - 8, 8, "..."); title = title.substr(0, title.length() - 8) + "...";
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str()); titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
} }
} }

View File

@ -93,7 +93,7 @@ void FileSelectionActivity::loop() {
} }
} else if (inputManager.wasPressed(InputManager::BTN_BACK)) { } else if (inputManager.wasPressed(InputManager::BTN_BACK)) {
if (basepath != "/") { if (basepath != "/") {
basepath.replace(basepath.find_last_of('/'), std::string::npos, ""); basepath = basepath.substr(0, basepath.rfind('/'));
if (basepath.empty()) basepath = "/"; if (basepath.empty()) basepath = "/";
loadFiles(); loadFiles();
updateRequired = true; updateRequired = true;

View File

@ -20,7 +20,7 @@ KeyboardEntryActivity::KeyboardEntryActivity(GfxRenderer& renderer, InputManager
void KeyboardEntryActivity::setText(const std::string& newText) { void KeyboardEntryActivity::setText(const std::string& newText) {
text = newText; text = newText;
if (maxLength > 0 && text.length() > maxLength) { if (maxLength > 0 && text.length() > maxLength) {
text.resize(maxLength); text = text.substr(0, maxLength);
} }
} }

View File

@ -8,10 +8,10 @@
<div class="modal-overlay" id="uploadModal"> <div class="modal-overlay" id="uploadModal">
<div class="modal"> <div class="modal">
<button class="modal-close" onclick="closeUploadModal()">&times;</button> <button class="modal-close" onclick="closeUploadModal()">&times;</button>
<h3>📤 Upload file</h3> <h3>📤 Upload eBook</h3>
<div class="upload-form"> <div class="upload-form">
<p class="file-info">Select a file to upload to <strong id="uploadPathDisplay"></strong></p> <p class="file-info">Select an .epub file to upload to <strong id="uploadPathDisplay"></strong></p>
<input type="file" id="fileInput" onchange="validateFile()"> <input type="file" id="fileInput" accept=".epub" onchange="validateFile()">
<button id="uploadBtn" class="upload-btn" onclick="uploadFile()" disabled>Upload</button> <button id="uploadBtn" class="upload-btn" onclick="uploadFile()" disabled>Upload</button>
<div id="progress-container"> <div id="progress-container">
<div id="progress-bar"><div id="progress-fill"></div></div> <div id="progress-bar"><div id="progress-fill"></div></div>
@ -92,7 +92,19 @@
const fileInput = document.getElementById('fileInput'); const fileInput = document.getElementById('fileInput');
const uploadBtn = document.getElementById('uploadBtn'); const uploadBtn = document.getElementById('uploadBtn');
const file = fileInput.files[0]; const file = fileInput.files[0];
uploadBtn.disabled = !file;
if (file) {
const fileName = file.name.toLowerCase();
if (!fileName.endsWith('.epub')) {
alert('Only .epub files are allowed!');
fileInput.value = '';
uploadBtn.disabled = true;
return;
}
uploadBtn.disabled = false;
} else {
uploadBtn.disabled = true;
}
} }
function uploadFile() { function uploadFile() {
@ -105,6 +117,12 @@
return; return;
} }
const fileName = file.name.toLowerCase();
if (!fileName.endsWith('.epub')) {
alert('Only .epub files are allowed!');
return;
}
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);

View File

@ -5,6 +5,7 @@
#include <InputManager.h> #include <InputManager.h>
#include <SD.h> #include <SD.h>
#include <SPI.h> #include <SPI.h>
#include <WiFi.h>
#include <builtinFonts/bookerly_2b.h> #include <builtinFonts/bookerly_2b.h>
#include <builtinFonts/bookerly_bold_2b.h> #include <builtinFonts/bookerly_bold_2b.h>
#include <builtinFonts/bookerly_bold_italic_2b.h> #include <builtinFonts/bookerly_bold_italic_2b.h>
@ -202,18 +203,20 @@ void setup() {
} }
void loop() { void loop() {
static unsigned long lastLoopTime = 0;
static unsigned long maxLoopDuration = 0; static unsigned long maxLoopDuration = 0;
const unsigned long loopStartTime = millis();
unsigned long loopStartTime = millis();
static unsigned long lastMemPrint = 0; static unsigned long lastMemPrint = 0;
inputManager.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(),
ESP.getHeapSize(), ESP.getMinFreeHeap()); ESP.getHeapSize(), ESP.getMinFreeHeap());
lastMemPrint = millis(); lastMemPrint = millis();
} }
inputManager.update();
// Check for any user activity (button press or release) // Check for any user activity (button press or release)
static unsigned long lastActivityTime = millis(); static unsigned long lastActivityTime = millis();
if (inputManager.wasAnyPressed() || inputManager.wasAnyReleased()) { if (inputManager.wasAnyPressed() || inputManager.wasAnyReleased()) {
@ -234,13 +237,13 @@ void loop() {
return; return;
} }
const unsigned long activityStartTime = millis(); unsigned long activityStartTime = millis();
if (currentActivity) { if (currentActivity) {
currentActivity->loop(); currentActivity->loop();
} }
const unsigned long activityDuration = millis() - activityStartTime; unsigned long activityDuration = millis() - activityStartTime;
const unsigned long loopDuration = millis() - loopStartTime; unsigned long loopDuration = millis() - loopStartTime;
if (loopDuration > maxLoopDuration) { if (loopDuration > maxLoopDuration) {
maxLoopDuration = loopDuration; maxLoopDuration = loopDuration;
if (maxLoopDuration > 50) { if (maxLoopDuration > 50) {
@ -249,6 +252,8 @@ void loop() {
} }
} }
lastLoopTime = loopStartTime;
// Add delay at the end of the loop to prevent tight spinning // Add delay at the end of the loop to prevent tight spinning
// When an activity requests skip loop delay (e.g., webserver running), use yield() for faster response // When an activity requests skip loop delay (e.g., webserver running), use yield() for faster response
// Otherwise, use longer delay to save power // Otherwise, use longer delay to save power