Compare commits

...

6 Commits

Author SHA1 Message Date
Jonas Diemer
b01eb50325
Shorten continueLabel to actual screen width. (#151)
Some checks are pending
CI / build (push) Waiting to run
2025-12-29 23:18:23 +11:00
Jonas Diemer
1bfe694807
Improvement/settings selection by inversion (#152)
Style in settings like everywhere else. Also improved the alignment of
the settings value.
2025-12-29 23:18:16 +11:00
Jonas Diemer
7b32a87596
Recalibrated power button duration, decreased long setting slightly. (#149)
Slight tuning, as I noticed sometimes inconsistent behavior (reported
>200ms of calibration value, I assume related to the Serial output).
2025-12-29 23:18:12 +11:00
Dave Allie
071ccb9d1b
Custom zip parsing (#140)
## Summary

* Use custom zip central directory parsing to lower memory usage when
loading zipped epub content
2025-12-29 21:17:29 +11:00
Yona
d7f4bd54f5
Add side button layout configuration while on reader (#147)
## Summary

Allow swapping the side button layout between *next page - prev page*
and *prev page - next page* while reading
2025-12-29 21:17:10 +11:00
Dave Allie
2437943c94
Remove usused module 2025-12-29 21:07:26 +11:00
15 changed files with 416 additions and 233 deletions

View File

@ -60,9 +60,6 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
}
ContentOpfParser opfParser(getCachePath(), getBasePath(), contentOpfSize, bookMetadataCache.get());
Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(),
ESP.getHeapSize(), ESP.getMinFreeHeap());
if (!opfParser.setup()) {
Serial.printf("[%lu] [EBP] Could not setup content.opf parser\n", millis());
return false;
@ -321,10 +318,9 @@ bool Epub::generateCoverBmp() const {
}
uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, const bool trailingNullByte) const {
const ZipFile zip("/sd" + filepath);
const std::string path = FsHelpers::normalisePath(itemHref);
const auto content = zip.readFileToMemory(path.c_str(), size, trailingNullByte);
const auto content = ZipFile(filepath).readFileToMemory(path.c_str(), size, trailingNullByte);
if (!content) {
Serial.printf("[%lu] [EBP] Failed to read item %s\n", millis(), path.c_str());
return nullptr;
@ -334,20 +330,13 @@ uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size
}
bool Epub::readItemContentsToStream(const std::string& itemHref, Print& out, const size_t chunkSize) const {
const ZipFile zip("/sd" + filepath);
const std::string path = FsHelpers::normalisePath(itemHref);
return zip.readFileToStream(path.c_str(), out, chunkSize);
return ZipFile(filepath).readFileToStream(path.c_str(), out, chunkSize);
}
bool Epub::getItemSize(const std::string& itemHref, size_t* size) const {
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 = FsHelpers::normalisePath(itemHref);
return zip.getInflatedFileSize(path.c_str(), size);
return ZipFile(filepath).getInflatedFileSize(path.c_str(), size);
}
int Epub::getSpineItemsCount() const {

View File

@ -24,7 +24,6 @@ class Epub {
bool findContentOpfFile(std::string* contentOpfFile) const;
bool parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata);
bool parseTocNcxFile() const;
static bool getItemSize(const ZipFile& zip, const std::string& itemHref, size_t* size);
public:
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
@ -54,5 +53,5 @@ class Epub {
size_t getCumulativeSpineItemSize(int spineIndex) const;
size_t getBookSize() const;
uint8_t calculateProgress(const int currentSpineIndex, const float currentSpineRead) const;
uint8_t calculateProgress(int currentSpineIndex, float currentSpineRead) const;
};

View File

@ -122,7 +122,26 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
// LUTs complete
// Loop through spines from spine file matching up TOC indexes, calculating cumulative size and writing to book.bin
const ZipFile zip("/sd" + epubPath);
ZipFile zip(epubPath);
// Pre-open zip file to speed up size calculations
if (!zip.open()) {
Serial.printf("[%lu] [BMC] Could not open EPUB zip for size calculations\n", millis());
bookFile.close();
spineFile.close();
tocFile.close();
return false;
}
// TODO: For large ZIPs loading the all localHeaderOffsets will crash.
// However not having them loaded is extremely slow. Need a better solution here.
// Perhaps only a cache of spine items or a better way to speedup lookups?
if (!zip.loadAllFileStatSlims()) {
Serial.printf("[%lu] [BMC] Could not load zip local header offsets for size calculations\n", millis());
bookFile.close();
spineFile.close();
tocFile.close();
zip.close();
return false;
}
size_t cumSize = 0;
spineFile.seek(0);
for (int i = 0; i < spineCount; i++) {
@ -157,6 +176,8 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
// Write out spine data to book.bin
writeSpineEntry(bookFile, spineEntry);
}
// Close opened zip file
zip.close();
// Loop through toc entries from toc file writing to book.bin
tocFile.seek(0);
@ -223,6 +244,8 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
int spineIndex = -1;
// find spine index
// TODO: This lookup is slow as need to scan through all items each time. We can't hold it all in memory due to size.
// But perhaps we can load just the hrefs in a vector/list to do an index lookup?
spineFile.seek(0);
for (int i = 0; i < spineCount; i++) {
auto spineEntry = readSpineEntry(spineFile);

View File

@ -1,92 +0,0 @@
#include "FsHelpers.h"
#include <SD.h>
#include <vector>
bool FsHelpers::openFileForRead(const char* moduleName, const std::string& path, File& file) {
file = SD.open(path.c_str(), FILE_READ);
if (!file) {
Serial.printf("[%lu] [%s] Failed to open file for reading: %s\n", millis(), moduleName, path.c_str());
return false;
}
return true;
}
bool FsHelpers::openFileForWrite(const char* moduleName, const std::string& path, File& file) {
file = SD.open(path.c_str(), FILE_WRITE, true);
if (!file) {
Serial.printf("[%lu] [%s] Failed to open file for writing: %s\n", millis(), moduleName, path.c_str());
return false;
}
return true;
}
bool FsHelpers::removeDir(const char* path) {
// 1. Open the directory
File dir = SD.open(path);
if (!dir) {
return false;
}
if (!dir.isDirectory()) {
return false;
}
File file = dir.openNextFile();
while (file) {
String filePath = path;
if (!filePath.endsWith("/")) {
filePath += "/";
}
filePath += file.name();
if (file.isDirectory()) {
if (!removeDir(filePath.c_str())) {
return false;
}
} else {
if (!SD.remove(filePath.c_str())) {
return false;
}
}
file = dir.openNextFile();
}
return SD.rmdir(path);
}
std::string FsHelpers::normalisePath(const std::string& path) {
std::vector<std::string> components;
std::string component;
for (const auto c : path) {
if (c == '/') {
if (!component.empty()) {
if (component == "..") {
if (!components.empty()) {
components.pop_back();
}
} else {
components.push_back(component);
}
component.clear();
}
} else {
component += c;
}
}
if (!component.empty()) {
components.push_back(component);
}
std::string result;
for (const auto& c : components) {
if (!result.empty()) {
result += "/";
}
result += c;
}
return result;
}

View File

@ -1,12 +0,0 @@
#pragma once
#include <FS.h>
#include <string>
class FsHelpers {
public:
static bool openFileForRead(const char* moduleName, const std::string& path, File& file);
static bool openFileForWrite(const char* moduleName, const std::string& path, File& file);
static bool removeDir(const char* path);
static std::string normalisePath(const std::string& path);
};

View File

@ -3,7 +3,6 @@
#include <FsHelpers.h>
#include <HardwareSerial.h>
#include <Serialization.h>
#include <ZipFile.h>
#include "../BookMetadataCache.h"
@ -183,6 +182,8 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
if (strcmp(atts[i], "idref") == 0) {
const std::string idref = atts[i + 1];
// Resolve the idref to href using items map
// TODO: This lookup is slow as need to scan through all items each time.
// It can take up to 200ms per item when getting to 1500 items.
self->tempItemStore.seek(0);
std::string itemId;
std::string href;

View File

@ -6,6 +6,7 @@
bool FsHelpers::openFileForRead(const char* moduleName, const char* path, File& file) {
if (!SD.exists(path)) {
Serial.printf("[%lu] [%s] File does not exist: %s\n", millis(), moduleName, path);
return false;
}

View File

@ -1,5 +1,6 @@
#include "ZipFile.h"
#include <FsHelpers.h>
#include <HardwareSerial.h>
#include <miniz.h>
@ -27,45 +28,135 @@ bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t*
return true;
}
ZipFile::ZipFile(std::string filePath) : filePath(std::move(filePath)) {
const bool status = mz_zip_reader_init_file(&zipArchive, this->filePath.c_str(), 0);
if (!status) {
Serial.printf("[%lu] [ZIP] mz_zip_reader_init_file() failed for %s! Error: %s\n", millis(), this->filePath.c_str(),
mz_zip_get_error_string(zipArchive.m_last_error));
}
}
bool ZipFile::loadFileStat(const char* filename, mz_zip_archive_file_stat* fileStat) const {
// find the file
mz_uint32 fileIndex = 0;
if (!mz_zip_reader_locate_file_v2(&zipArchive, filename, nullptr, 0, &fileIndex)) {
Serial.printf("[%lu] [ZIP] Could not find file %s\n", millis(), filename);
bool ZipFile::loadAllFileStatSlims() {
const bool wasOpen = isOpen();
if (!wasOpen && !open()) {
return false;
}
if (!mz_zip_reader_file_stat(&zipArchive, fileIndex, fileStat)) {
Serial.printf("[%lu] [ZIP] mz_zip_reader_file_stat() failed! Error: %s\n", millis(),
mz_zip_get_error_string(zipArchive.m_last_error));
if (!loadZipDetails()) {
if (!wasOpen) {
close();
}
return false;
}
file.seek(zipDetails.centralDirOffset);
uint32_t sig;
char itemName[256];
fileStatSlimCache.clear();
fileStatSlimCache.reserve(zipDetails.totalEntries);
while (file.available()) {
file.read(reinterpret_cast<uint8_t*>(&sig), 4);
if (sig != 0x02014b50) break; // End of list
FileStatSlim fileStat = {};
file.seek(6, SeekCur);
file.read(reinterpret_cast<uint8_t*>(&fileStat.method), 2);
file.seek(8, SeekCur);
file.read(reinterpret_cast<uint8_t*>(&fileStat.compressedSize), 4);
file.read(reinterpret_cast<uint8_t*>(&fileStat.uncompressedSize), 4);
uint16_t nameLen, m, k;
file.read(reinterpret_cast<uint8_t*>(&nameLen), 2);
file.read(reinterpret_cast<uint8_t*>(&m), 2);
file.read(reinterpret_cast<uint8_t*>(&k), 2);
file.seek(8, SeekCur);
file.read(reinterpret_cast<uint8_t*>(&fileStat.localHeaderOffset), 4);
file.read(reinterpret_cast<uint8_t*>(itemName), nameLen);
itemName[nameLen] = '\0';
fileStatSlimCache.emplace(itemName, fileStat);
// Skip the rest of this entry (extra field + comment)
file.seek(m + k, SeekCur);
}
if (!wasOpen) {
close();
}
return true;
}
long ZipFile::getDataOffset(const mz_zip_archive_file_stat& fileStat) const {
bool ZipFile::loadFileStatSlim(const char* filename, FileStatSlim* fileStat) {
if (!fileStatSlimCache.empty()) {
const auto it = fileStatSlimCache.find(filename);
if (it != fileStatSlimCache.end()) {
*fileStat = it->second;
return true;
}
return false;
}
const bool wasOpen = isOpen();
if (!wasOpen && !open()) {
return false;
}
if (!loadZipDetails()) {
if (!wasOpen) {
close();
}
return false;
}
file.seek(zipDetails.centralDirOffset);
uint32_t sig;
char itemName[256];
bool found = false;
while (file.available()) {
file.read(reinterpret_cast<uint8_t*>(&sig), 4);
if (sig != 0x02014b50) break; // End of list
file.seek(6, SeekCur);
file.read(reinterpret_cast<uint8_t*>(&fileStat->method), 2);
file.seek(8, SeekCur);
file.read(reinterpret_cast<uint8_t*>(&fileStat->compressedSize), 4);
file.read(reinterpret_cast<uint8_t*>(&fileStat->uncompressedSize), 4);
uint16_t nameLen, m, k;
file.read(reinterpret_cast<uint8_t*>(&nameLen), 2);
file.read(reinterpret_cast<uint8_t*>(&m), 2);
file.read(reinterpret_cast<uint8_t*>(&k), 2);
file.seek(8, SeekCur);
file.read(reinterpret_cast<uint8_t*>(&fileStat->localHeaderOffset), 4);
file.read(reinterpret_cast<uint8_t*>(itemName), nameLen);
itemName[nameLen] = '\0';
if (strcmp(itemName, filename) == 0) {
found = true;
break;
}
// Skip the rest of this entry (extra field + comment)
file.seek(m + k, SeekCur);
}
if (!wasOpen) {
close();
}
return found;
}
long ZipFile::getDataOffset(const FileStatSlim& fileStat) {
const bool wasOpen = isOpen();
if (!wasOpen && !open()) {
return -1;
}
constexpr auto localHeaderSize = 30;
uint8_t pLocalHeader[localHeaderSize];
const uint64_t fileOffset = fileStat.m_local_header_ofs;
const uint64_t fileOffset = fileStat.localHeaderOffset;
FILE* file = fopen(filePath.c_str(), "r");
if (!file) {
Serial.printf("[%lu] [ZIP] Failed to open file for reading local header\n", millis());
return -1;
file.seek(fileOffset);
const size_t read = file.read(pLocalHeader, localHeaderSize);
if (!wasOpen) {
close();
}
fseek(file, fileOffset, SEEK_SET);
const size_t read = fread(pLocalHeader, 1, localHeaderSize, file);
fclose(file);
if (read != localHeaderSize) {
Serial.printf("[%lu] [ZIP] Something went wrong reading the local header\n", millis());
@ -83,48 +174,140 @@ long ZipFile::getDataOffset(const mz_zip_archive_file_stat& fileStat) const {
return fileOffset + localHeaderSize + filenameLength + extraOffset;
}
bool ZipFile::getInflatedFileSize(const char* filename, size_t* size) const {
mz_zip_archive_file_stat fileStat;
if (!loadFileStat(filename, &fileStat)) {
bool ZipFile::loadZipDetails() {
if (zipDetails.isSet) {
return true;
}
const bool wasOpen = isOpen();
if (!wasOpen && !open()) {
return false;
}
*size = static_cast<size_t>(fileStat.m_uncomp_size);
const size_t fileSize = file.size();
if (fileSize < 22) {
Serial.printf("[%lu] [ZIP] File too small to be a valid zip\n", millis());
if (!wasOpen) {
close();
}
return false; // Minimum EOCD size is 22 bytes
}
// We scan the last 1KB (or the whole file if smaller) for the EOCD signature
// 0x06054b50 is stored as 0x50, 0x4b, 0x05, 0x06 in little-endian
const int scanRange = fileSize > 1024 ? 1024 : fileSize;
const auto buffer = static_cast<uint8_t*>(malloc(scanRange));
if (!buffer) {
Serial.printf("[%lu] [ZIP] Failed to allocate memory for EOCD scan buffer\n", millis());
if (!wasOpen) {
close();
}
return false;
}
file.seek(fileSize - scanRange);
file.read(buffer, scanRange);
// Scan backwards for the signature
int foundOffset = -1;
for (int i = scanRange - 22; i >= 0; i--) {
constexpr uint32_t signature = 0x06054b50;
if (*reinterpret_cast<uint32_t*>(&buffer[i]) == signature) {
foundOffset = i;
break;
}
}
if (foundOffset == -1) {
Serial.printf("[%lu] [ZIP] EOCD signature not found in zip file\n", millis());
free(buffer);
if (!wasOpen) {
close();
}
return false;
}
// Now extract the values we need from the EOCD record
// Relative positions within EOCD:
// Offset 10: Total number of entries (2 bytes)
// Offset 16: Offset of start of central directory with respect to the starting disk number (4 bytes)
zipDetails.totalEntries = *reinterpret_cast<uint16_t*>(&buffer[foundOffset + 10]);
zipDetails.centralDirOffset = *reinterpret_cast<uint32_t*>(&buffer[foundOffset + 16]);
zipDetails.isSet = true;
free(buffer);
if (!wasOpen) {
close();
}
return true;
}
uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const bool trailingNullByte) const {
mz_zip_archive_file_stat fileStat;
if (!loadFileStat(filename, &fileStat)) {
bool ZipFile::open() {
if (!FsHelpers::openFileForRead("ZIP", filePath, file)) {
return false;
}
return true;
}
bool ZipFile::close() {
if (file) {
file.close();
}
return true;
}
bool ZipFile::getInflatedFileSize(const char* filename, size_t* size) {
FileStatSlim fileStat = {};
if (!loadFileStatSlim(filename, &fileStat)) {
return false;
}
*size = static_cast<size_t>(fileStat.uncompressedSize);
return true;
}
uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const bool trailingNullByte) {
const bool wasOpen = isOpen();
if (!wasOpen && !open()) {
return nullptr;
}
FileStatSlim fileStat = {};
if (!loadFileStatSlim(filename, &fileStat)) {
if (!wasOpen) {
close();
}
return nullptr;
}
const long fileOffset = getDataOffset(fileStat);
if (fileOffset < 0) {
if (!wasOpen) {
close();
}
return nullptr;
}
FILE* file = fopen(filePath.c_str(), "rb");
if (!file) {
Serial.printf("[%lu] [ZIP] Failed to open file for reading\n", millis());
return nullptr;
}
fseek(file, fileOffset, SEEK_SET);
file.seek(fileOffset);
const auto deflatedDataSize = static_cast<size_t>(fileStat.m_comp_size);
const auto inflatedDataSize = static_cast<size_t>(fileStat.m_uncomp_size);
const auto deflatedDataSize = fileStat.compressedSize;
const auto inflatedDataSize = fileStat.uncompressedSize;
const auto dataSize = trailingNullByte ? inflatedDataSize + 1 : inflatedDataSize;
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);
if (!wasOpen) {
close();
}
return nullptr;
}
if (fileStat.m_method == MZ_NO_COMPRESSION) {
if (fileStat.method == MZ_NO_COMPRESSION) {
// no deflation, just read content
const size_t dataRead = fread(data, 1, inflatedDataSize, file);
fclose(file);
const size_t dataRead = file.read(data, inflatedDataSize);
if (!wasOpen) {
close();
}
if (dataRead != inflatedDataSize) {
Serial.printf("[%lu] [ZIP] Failed to read data\n", millis());
@ -133,17 +316,21 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
}
// Continue out of block with data set
} else if (fileStat.m_method == MZ_DEFLATED) {
} else if (fileStat.method == MZ_DEFLATED) {
// Read out deflated content from file
const auto deflatedData = static_cast<uint8_t*>(malloc(deflatedDataSize));
if (deflatedData == nullptr) {
Serial.printf("[%lu] [ZIP] Failed to allocate memory for decompression buffer\n", millis());
fclose(file);
if (!wasOpen) {
close();
}
return nullptr;
}
const size_t dataRead = fread(deflatedData, 1, deflatedDataSize, file);
fclose(file);
const size_t dataRead = file.read(deflatedData, deflatedDataSize);
if (!wasOpen) {
close();
}
if (dataRead != deflatedDataSize) {
Serial.printf("[%lu] [ZIP] Failed to read data, expected %d got %d\n", millis(), deflatedDataSize, dataRead);
@ -152,7 +339,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
return nullptr;
}
bool success = inflateOneShot(deflatedData, deflatedDataSize, data, inflatedDataSize);
const bool success = inflateOneShot(deflatedData, deflatedDataSize, data, inflatedDataSize);
free(deflatedData);
if (!success) {
@ -164,7 +351,9 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
// Continue out of block with data set
} else {
Serial.printf("[%lu] [ZIP] Unsupported compression method\n", millis());
fclose(file);
if (!wasOpen) {
close();
}
return nullptr;
}
@ -173,9 +362,14 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
return data;
}
bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t chunkSize) const {
mz_zip_archive_file_stat fileStat;
if (!loadFileStat(filename, &fileStat)) {
bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t chunkSize) {
const bool wasOpen = isOpen();
if (!wasOpen && !open()) {
return false;
}
FileStatSlim fileStat = {};
if (!loadFileStatSlim(filename, &fileStat)) {
return false;
}
@ -184,32 +378,30 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
return false;
}
FILE* file = fopen(filePath.c_str(), "rb");
if (!file) {
Serial.printf("[%lu] [ZIP] Failed to open file for streaming\n", millis());
return false;
}
fseek(file, fileOffset, SEEK_SET);
file.seek(fileOffset);
const auto deflatedDataSize = fileStat.compressedSize;
const auto inflatedDataSize = fileStat.uncompressedSize;
const auto deflatedDataSize = static_cast<size_t>(fileStat.m_comp_size);
const auto inflatedDataSize = static_cast<size_t>(fileStat.m_uncomp_size);
if (fileStat.m_method == MZ_NO_COMPRESSION) {
if (fileStat.method == MZ_NO_COMPRESSION) {
// no deflation, just read content
const auto buffer = static_cast<uint8_t*>(malloc(chunkSize));
if (!buffer) {
Serial.printf("[%lu] [ZIP] Failed to allocate memory for buffer\n", millis());
fclose(file);
if (!wasOpen) {
close();
}
return false;
}
size_t remaining = inflatedDataSize;
while (remaining > 0) {
const size_t dataRead = fread(buffer, 1, remaining < chunkSize ? remaining : chunkSize, file);
const size_t dataRead = file.read(buffer, remaining < chunkSize ? remaining : chunkSize);
if (dataRead == 0) {
Serial.printf("[%lu] [ZIP] Could not read more bytes\n", millis());
free(buffer);
fclose(file);
if (!wasOpen) {
close();
}
return false;
}
@ -217,17 +409,21 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
remaining -= dataRead;
}
fclose(file);
if (!wasOpen) {
close();
}
free(buffer);
return true;
}
if (fileStat.m_method == MZ_DEFLATED) {
if (fileStat.method == MZ_DEFLATED) {
// Setup inflator
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
if (!inflator) {
Serial.printf("[%lu] [ZIP] Failed to allocate memory for inflator\n", millis());
fclose(file);
if (!wasOpen) {
close();
}
return false;
}
memset(inflator, 0, sizeof(tinfl_decompressor));
@ -238,7 +434,9 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
if (!fileReadBuffer) {
Serial.printf("[%lu] [ZIP] Failed to allocate memory for zip file read buffer\n", millis());
free(inflator);
fclose(file);
if (!wasOpen) {
close();
}
return false;
}
@ -247,7 +445,9 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
Serial.printf("[%lu] [ZIP] Failed to allocate memory for dictionary\n", millis());
free(inflator);
free(fileReadBuffer);
fclose(file);
if (!wasOpen) {
close();
}
return false;
}
memset(outputBuffer, 0, TINFL_LZ_DICT_SIZE);
@ -267,7 +467,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
}
fileReadBufferFilledBytes =
fread(fileReadBuffer, 1, fileRemainingBytes < chunkSize ? fileRemainingBytes : chunkSize, file);
file.read(fileReadBuffer, fileRemainingBytes < chunkSize ? fileRemainingBytes : chunkSize);
fileRemainingBytes -= fileReadBufferFilledBytes;
fileReadBufferCursor = 0;
@ -294,7 +494,9 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
processedOutputBytes += outBytes;
if (out.write(outputBuffer + outputCursor, outBytes) != outBytes) {
Serial.printf("[%lu] [ZIP] Failed to write all output bytes to stream\n", millis());
fclose(file);
if (!wasOpen) {
close();
}
free(outputBuffer);
free(fileReadBuffer);
free(inflator);
@ -306,7 +508,9 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
if (status < 0) {
Serial.printf("[%lu] [ZIP] tinfl_decompress() failed with status %d\n", millis(), status);
fclose(file);
if (!wasOpen) {
close();
}
free(outputBuffer);
free(fileReadBuffer);
free(inflator);
@ -316,7 +520,9 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
if (status == TINFL_STATUS_DONE) {
Serial.printf("[%lu] [ZIP] Decompressed %d bytes into %d bytes\n", millis(), deflatedDataSize,
inflatedDataSize);
fclose(file);
if (!wasOpen) {
close();
}
free(inflator);
free(fileReadBuffer);
free(outputBuffer);
@ -326,13 +532,19 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
// If we get here, EOF reached without TINFL_STATUS_DONE
Serial.printf("[%lu] [ZIP] Unexpected EOF\n", millis());
fclose(file);
if (!wasOpen) {
close();
}
free(outputBuffer);
free(fileReadBuffer);
free(inflator);
return false;
}
if (!wasOpen) {
close();
}
Serial.printf("[%lu] [ZIP] Unsupported compression method\n", millis());
return false;
}

View File

@ -1,20 +1,46 @@
#pragma once
#include <Print.h>
#include <FS.h>
#include <string>
#include "miniz.h"
#include <unordered_map>
class ZipFile {
std::string filePath;
mutable mz_zip_archive zipArchive = {};
bool loadFileStat(const char* filename, mz_zip_archive_file_stat* fileStat) const;
long getDataOffset(const mz_zip_archive_file_stat& fileStat) const;
public:
struct FileStatSlim {
uint16_t method; // Compression method
uint32_t compressedSize; // Compressed size
uint32_t uncompressedSize; // Uncompressed size
uint32_t localHeaderOffset; // Offset of local file header
};
struct ZipDetails {
uint32_t centralDirOffset;
uint16_t totalEntries;
bool isSet;
};
private:
const std::string& filePath;
File file;
ZipDetails zipDetails = {0, 0, false};
std::unordered_map<std::string, FileStatSlim> fileStatSlimCache;
bool loadFileStatSlim(const char* filename, FileStatSlim* fileStat);
long getDataOffset(const FileStatSlim& fileStat);
bool loadZipDetails();
public:
explicit ZipFile(std::string filePath);
~ZipFile() { mz_zip_reader_end(&zipArchive); }
bool getInflatedFileSize(const char* filename, size_t* size) 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;
explicit ZipFile(const std::string& filePath) : filePath(filePath) {}
~ZipFile() = default;
// Zip file can be opened and closed by hand in order to allow for quick calculation of inflated file size
// It is NOT recommended to pre-open it for any kind of inflation due to memory constraints
bool isOpen() const { return !!file; }
bool open();
bool close();
bool loadAllFileStatSlims();
bool getInflatedFileSize(const char* filename, size_t* size);
// Due to the memory required to run each of these, it is recommended to not preopen the zip file for multiple
// These functions will open and close the zip as needed
uint8_t* readFileToMemory(const char* filename, size_t* size = nullptr, bool trailingNullByte = false);
bool readFileToStream(const char* filename, Print& out, size_t chunkSize);
};

View File

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

View File

@ -33,6 +33,11 @@ class CrossPointSettings {
// Swapped: Left, Right, Back, Confirm
enum FRONT_BUTTON_LAYOUT { BACK_CONFIRM_LEFT_RIGHT = 0, LEFT_RIGHT_BACK_CONFIRM = 1 };
// Side button layout options
// Default: Previous, Next
// Swapped: Next, Previous
enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1 };
// Sleep screen settings
uint8_t sleepScreen = DARK;
// Status bar settings
@ -46,13 +51,15 @@ class CrossPointSettings {
uint8_t orientation = PORTRAIT;
// Front button layout
uint8_t frontButtonLayout = BACK_CONFIRM_LEFT_RIGHT;
// Side button layout
uint8_t sideButtonLayout = PREV_NEXT;
~CrossPointSettings() = default;
// Get singleton instance
static CrossPointSettings& getInstance() { return instance; }
uint16_t getPowerButtonDuration() const { return shortPwrBtn ? 10 : 500; }
uint16_t getPowerButtonDuration() const { return shortPwrBtn ? 10 : 400; }
bool saveToFile() const;
bool loadFromFile();

View File

@ -1,11 +1,12 @@
#include "MappedInputManager.h"
decltype(InputManager::BTN_BACK) MappedInputManager::mapButton(const Button button) const {
const auto layout = static_cast<CrossPointSettings::FRONT_BUTTON_LAYOUT>(SETTINGS.frontButtonLayout);
const auto frontLayout = static_cast<CrossPointSettings::FRONT_BUTTON_LAYOUT>(SETTINGS.frontButtonLayout);
const auto sideLayout = static_cast<CrossPointSettings::SIDE_BUTTON_LAYOUT>(SETTINGS.sideButtonLayout);
switch (button) {
case Button::Back:
switch (layout) {
switch (frontLayout) {
case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM:
return InputManager::BTN_LEFT;
case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT:
@ -13,7 +14,7 @@ decltype(InputManager::BTN_BACK) MappedInputManager::mapButton(const Button butt
return InputManager::BTN_BACK;
}
case Button::Confirm:
switch (layout) {
switch (frontLayout) {
case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM:
return InputManager::BTN_RIGHT;
case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT:
@ -21,7 +22,7 @@ decltype(InputManager::BTN_BACK) MappedInputManager::mapButton(const Button butt
return InputManager::BTN_CONFIRM;
}
case Button::Left:
switch (layout) {
switch (frontLayout) {
case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM:
return InputManager::BTN_BACK;
case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT:
@ -29,7 +30,7 @@ decltype(InputManager::BTN_BACK) MappedInputManager::mapButton(const Button butt
return InputManager::BTN_LEFT;
}
case Button::Right:
switch (layout) {
switch (frontLayout) {
case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM:
return InputManager::BTN_CONFIRM;
case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT:
@ -43,9 +44,21 @@ decltype(InputManager::BTN_BACK) MappedInputManager::mapButton(const Button butt
case Button::Power:
return InputManager::BTN_POWER;
case Button::PageBack:
return InputManager::BTN_UP;
case Button::PageForward:
switch (sideLayout) {
case CrossPointSettings::NEXT_PREV:
return InputManager::BTN_DOWN;
case CrossPointSettings::PREV_NEXT:
default:
return InputManager::BTN_UP;
}
case Button::PageForward:
switch (sideLayout) {
case CrossPointSettings::NEXT_PREV:
return InputManager::BTN_UP;
case CrossPointSettings::PREV_NEXT:
default:
return InputManager::BTN_DOWN;
}
}
return InputManager::BTN_BACK;

View File

@ -122,12 +122,16 @@ void HomeActivity::render() const {
if (bookName.length() > 5 && bookName.substr(bookName.length() - 5) == ".epub") {
bookName.resize(bookName.length() - 5);
}
// Truncate if too long
if (bookName.length() > 25) {
bookName.resize(22);
bookName += "...";
}
std::string continueLabel = "Continue: " + bookName;
int itemWidth = renderer.getTextWidth(UI_FONT_ID, continueLabel.c_str());
while (itemWidth > renderer.getScreenWidth() - 40 && continueLabel.length() > 8) {
continueLabel.replace(continueLabel.length() - 5, 5, "...");
itemWidth = renderer.getTextWidth(UI_FONT_ID, continueLabel.c_str());
Serial.printf("[%lu] [HOM] width: %lu, pageWidth: %lu\n", millis(), itemWidth, pageWidth);
}
renderer.drawText(UI_FONT_ID, 20, menuY, continueLabel.c_str(), selectorIndex != menuIndex);
menuY += 30;
menuIndex++;

View File

@ -9,7 +9,7 @@
// Define the static settings list
namespace {
constexpr int settingsCount = 7;
constexpr int settingsCount = 8;
const SettingInfo settingsList[settingsCount] = {
// Should match with SLEEP_SCREEN_MODE
{"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}},
@ -24,6 +24,10 @@ const SettingInfo settingsList[settingsCount] = {
SettingType::ENUM,
&CrossPointSettings::frontButtonLayout,
{"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm"}},
{"Side Button Layout (reader)",
SettingType::ENUM,
&CrossPointSettings::sideButtonLayout,
{"Prev, Next", "Next, Prev"}},
{"Check for updates", SettingType::ACTION, nullptr, {}},
};
} // namespace
@ -155,6 +159,9 @@ void SettingsActivity::render() const {
// Draw header
renderer.drawCenteredText(READER_FONT_ID, 10, "Settings", true, BOLD);
// Draw selection
renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30);
// Draw all settings
for (int i = 0; i < settingsCount; i++) {
const int settingY = 60 + i * 30; // 30 pixels between settings
@ -165,18 +172,19 @@ void SettingsActivity::render() const {
}
// Draw setting name
renderer.drawText(UI_FONT_ID, 20, settingY, settingsList[i].name);
renderer.drawText(UI_FONT_ID, 20, settingY, settingsList[i].name, i != selectedSettingIndex);
// Draw value based on setting type
std::string valueText = "";
if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) {
const bool value = SETTINGS.*(settingsList[i].valuePtr);
renderer.drawText(UI_FONT_ID, pageWidth - 80, settingY, value ? "ON" : "OFF");
valueText = value ? "ON" : "OFF";
} else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) {
const uint8_t value = SETTINGS.*(settingsList[i].valuePtr);
auto valueText = settingsList[i].enumValues[value];
const auto width = renderer.getTextWidth(UI_FONT_ID, valueText.c_str());
renderer.drawText(UI_FONT_ID, pageWidth - 50 - width, settingY, valueText.c_str());
valueText = settingsList[i].enumValues[value];
}
const auto width = renderer.getTextWidth(UI_FONT_ID, valueText.c_str());
renderer.drawText(UI_FONT_ID, pageWidth - 20 - width, settingY, valueText.c_str(), i != selectedSettingIndex);
}
// Draw version text above button hints

View File

@ -85,7 +85,7 @@ void verifyWakeupLongPress() {
const auto start = millis();
bool abort = false;
// It takes us some time to wake up from deep sleep, so we need to subtract that from the duration
uint16_t calibration = 25;
uint16_t calibration = 29;
uint16_t calibratedPressDuration =
(calibration < SETTINGS.getPowerButtonDuration()) ? SETTINGS.getPowerButtonDuration() - calibration : 1;
@ -180,8 +180,6 @@ void setup() {
Serial.begin(115200);
}
Serial.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION "\n", millis());
inputManager.begin();
// Initialize pins
pinMode(BAT_GPIO0, INPUT);
@ -204,6 +202,9 @@ void setup() {
// verify power button press duration after we've read settings.
verifyWakeupLongPress();
// First serial output only here to avoid timing inconsistencies for power button press duration verification
Serial.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION "\n", millis());
setupDisplayAndFonts();
exitActivity();