Compare commits
38 Commits
1.0.0
...
5816ab2a47
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5816ab2a47 | ||
|
|
2c0a105550 | ||
|
|
6e51afb977 | ||
|
|
cb24947477 | ||
|
|
7a385d78a4 | ||
|
|
0991782fb4 | ||
|
|
3ae1007cbe | ||
|
|
efb9b72e64 | ||
|
|
4a210823a8 | ||
|
|
f5b85f5ca1 | ||
|
|
7e93411f46 | ||
|
|
44452a42e9 | ||
|
|
0c2df24f5c | ||
|
|
3a12ca2725 | ||
|
|
98e6789626 | ||
|
|
b5d28a3a9c | ||
|
|
14ef625679 | ||
|
|
64d161e88b | ||
|
|
e73bb3213f | ||
|
|
6202bfd651 | ||
|
|
9b04c2ec76 | ||
|
|
ffddc2472b | ||
|
|
5765bbe821 | ||
|
|
b4b028be3a | ||
|
|
f34d7d2aac | ||
|
|
71769490fb | ||
|
|
cda0a3f898 | ||
|
|
7f40c3f477 | ||
|
|
a87eacc6ab | ||
|
|
1caad578fc | ||
|
|
5b90b68e99 | ||
|
|
67ddd60fce | ||
|
|
76908d38e1 | ||
|
|
e6f5fa43e6 | ||
|
|
e7e31ac487 | ||
|
|
9f78fd33e8 | ||
|
|
bd8132a260 | ||
|
|
f89ce514c8 |
@@ -51,7 +51,7 @@ For more details about the scope of the project, see the [SCOPE.md](SCOPE.md) do
|
|||||||
|
|
||||||
### Web (latest firmware)
|
### Web (latest firmware)
|
||||||
|
|
||||||
1. Connect your Xteink X4 to your computer via USB-C
|
1. Connect your Xteink X4 to your computer via USB-C and wake/unlock the device
|
||||||
2. Go to https://xteink.dve.al/ and click "Flash CrossPoint firmware"
|
2. Go to https://xteink.dve.al/ and click "Flash CrossPoint firmware"
|
||||||
|
|
||||||
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
|
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
|
||||||
|
|||||||
@@ -230,6 +230,7 @@ Accessible by pressing **Confirm** while inside a book.
|
|||||||
Please note that this firmware is currently in active development. The following features are **not yet supported** but are planned for future updates:
|
Please note that this firmware is currently in active development. The following features are **not yet supported** but are planned for future updates:
|
||||||
|
|
||||||
* **Images:** Embedded images in e-books will not render.
|
* **Images:** Embedded images in e-books will not render.
|
||||||
|
* **Cover Images:** Large cover images embedded into EPUB require several seconds (~10s for ~2000 pixel tall image) to convert for sleep screen and home screen thumbnail. Consider optimizing the EPUB with e.g. https://github.com/bigbag/epub-to-xtc-converter to speed this up.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -242,3 +243,5 @@ pio device monitor
|
|||||||
```
|
```
|
||||||
|
|
||||||
If the device is stuck in a bootloop, press and release the Reset button. Then, press and hold on to the configured Back button and the Power Button to boot to the Home Screen.
|
If the device is stuck in a bootloop, press and release the Reset button. Then, press and hold on to the configured Back button and the Power Button to boot to the Home Screen.
|
||||||
|
|
||||||
|
There can be issues with broken cache or config. In this case, delete the `.crosspoint` directory on your SD card (or consider deleting only `settings.bin`, `state.bin`, or `epub_*` cache directories in the `.crosspoint/` folder).
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ fi
|
|||||||
# --modified: files tracked by git that have been modified (staged or unstaged)
|
# --modified: files tracked by git that have been modified (staged or unstaged)
|
||||||
# --exclude-standard: ignores files in .gitignore
|
# --exclude-standard: ignores files in .gitignore
|
||||||
# Additionally exclude files in 'lib/EpdFont/builtinFonts/' as they are script-generated.
|
# Additionally exclude files in 'lib/EpdFont/builtinFonts/' as they are script-generated.
|
||||||
|
# Also exclude files in 'lib/Epub/Epub/hyphenation/generated/' as they are script-generated.
|
||||||
git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \
|
git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \
|
||||||
| grep -E '\.(c|cpp|h|hpp)$' \
|
| grep -E '\.(c|cpp|h|hpp)$' \
|
||||||
| grep -v -E '^lib/EpdFont/builtinFonts/' \
|
| grep -v -E '^lib/EpdFont/builtinFonts/' \
|
||||||
|
| grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \
|
||||||
| xargs -r clang-format -style=file -i
|
| xargs -r clang-format -style=file -i
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#include "Epub.h"
|
#include "Epub.h"
|
||||||
|
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HalStorage.h>
|
||||||
#include <JpegToBmpConverter.h>
|
#include <JpegToBmpConverter.h>
|
||||||
#include <SDCardManager.h>
|
#include <Logging.h>
|
||||||
#include <ZipFile.h>
|
#include <ZipFile.h>
|
||||||
|
|
||||||
#include "Epub/parsers/ContainerParser.h"
|
#include "Epub/parsers/ContainerParser.h"
|
||||||
@@ -17,7 +17,7 @@ bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
|
|||||||
|
|
||||||
// Get file size without loading it all into heap
|
// Get file size without loading it all into heap
|
||||||
if (!getItemSize(containerPath, &containerSize)) {
|
if (!getItemSize(containerPath, &containerSize)) {
|
||||||
Serial.printf("[%lu] [EBP] Could not find or size META-INF/container.xml\n", millis());
|
LOG_ERR("EBP", "Could not find or size META-INF/container.xml");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,13 +29,13 @@ 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());
|
LOG_ERR("EBP", "Could not read META-INF/container.xml");
|
||||||
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());
|
LOG_ERR("EBP", "Could not find valid rootfile in container.xml");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,28 +46,28 @@ bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
|
|||||||
bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
|
bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
|
||||||
std::string contentOpfFilePath;
|
std::string contentOpfFilePath;
|
||||||
if (!findContentOpfFile(&contentOpfFilePath)) {
|
if (!findContentOpfFile(&contentOpfFilePath)) {
|
||||||
Serial.printf("[%lu] [EBP] Could not find content.opf in zip\n", millis());
|
LOG_ERR("EBP", "Could not find content.opf in zip");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
contentBasePath = contentOpfFilePath.substr(0, contentOpfFilePath.find_last_of('/') + 1);
|
contentBasePath = contentOpfFilePath.substr(0, contentOpfFilePath.find_last_of('/') + 1);
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Parsing content.opf: %s\n", millis(), contentOpfFilePath.c_str());
|
LOG_DBG("EBP", "Parsing content.opf: %s", 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());
|
LOG_ERR("EBP", "Could not get size of content.opf");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentOpfParser opfParser(getCachePath(), getBasePath(), contentOpfSize, bookMetadataCache.get());
|
ContentOpfParser opfParser(getCachePath(), getBasePath(), contentOpfSize, bookMetadataCache.get());
|
||||||
if (!opfParser.setup()) {
|
if (!opfParser.setup()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not setup content.opf parser\n", millis());
|
LOG_ERR("EBP", "Could not setup content.opf parser");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!readItemContentsToStream(contentOpfFilePath, opfParser, 1024)) {
|
if (!readItemContentsToStream(contentOpfFilePath, opfParser, 1024)) {
|
||||||
Serial.printf("[%lu] [EBP] Could not read content.opf\n", millis());
|
LOG_ERR("EBP", "Could not read content.opf");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,27 +90,27 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
|
|||||||
cssFiles = opfParser.cssFiles;
|
cssFiles = opfParser.cssFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Successfully parsed content.opf\n", millis());
|
LOG_DBG("EBP", "Successfully parsed content.opf");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Epub::parseTocNcxFile() const {
|
bool Epub::parseTocNcxFile() const {
|
||||||
// the ncx file should have been specified in the content.opf file
|
// the ncx file should have been specified in the content.opf file
|
||||||
if (tocNcxItem.empty()) {
|
if (tocNcxItem.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] No ncx file specified\n", millis());
|
LOG_DBG("EBP", "No ncx file specified");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Parsing toc ncx file: %s\n", millis(), tocNcxItem.c_str());
|
LOG_DBG("EBP", "Parsing toc ncx file: %s", tocNcxItem.c_str());
|
||||||
|
|
||||||
const auto tmpNcxPath = getCachePath() + "/toc.ncx";
|
const auto tmpNcxPath = getCachePath() + "/toc.ncx";
|
||||||
FsFile tempNcxFile;
|
FsFile tempNcxFile;
|
||||||
if (!SdMan.openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) {
|
if (!Storage.openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
readItemContentsToStream(tocNcxItem, tempNcxFile, 1024);
|
readItemContentsToStream(tocNcxItem, tempNcxFile, 1024);
|
||||||
tempNcxFile.close();
|
tempNcxFile.close();
|
||||||
if (!SdMan.openFileForRead("EBP", tmpNcxPath, tempNcxFile)) {
|
if (!Storage.openFileForRead("EBP", tmpNcxPath, tempNcxFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const auto ncxSize = tempNcxFile.size();
|
const auto ncxSize = tempNcxFile.size();
|
||||||
@@ -118,14 +118,14 @@ bool Epub::parseTocNcxFile() const {
|
|||||||
TocNcxParser ncxParser(contentBasePath, ncxSize, bookMetadataCache.get());
|
TocNcxParser ncxParser(contentBasePath, ncxSize, bookMetadataCache.get());
|
||||||
|
|
||||||
if (!ncxParser.setup()) {
|
if (!ncxParser.setup()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not setup toc ncx parser\n", millis());
|
LOG_ERR("EBP", "Could not setup toc ncx parser");
|
||||||
tempNcxFile.close();
|
tempNcxFile.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto ncxBuffer = static_cast<uint8_t*>(malloc(1024));
|
const auto ncxBuffer = static_cast<uint8_t*>(malloc(1024));
|
||||||
if (!ncxBuffer) {
|
if (!ncxBuffer) {
|
||||||
Serial.printf("[%lu] [EBP] Could not allocate memory for toc ncx parser\n", millis());
|
LOG_ERR("EBP", "Could not allocate memory for toc ncx parser");
|
||||||
tempNcxFile.close();
|
tempNcxFile.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -136,7 +136,7 @@ bool Epub::parseTocNcxFile() const {
|
|||||||
const auto processedSize = ncxParser.write(ncxBuffer, readSize);
|
const auto processedSize = ncxParser.write(ncxBuffer, readSize);
|
||||||
|
|
||||||
if (processedSize != readSize) {
|
if (processedSize != readSize) {
|
||||||
Serial.printf("[%lu] [EBP] Could not process all toc ncx data\n", millis());
|
LOG_ERR("EBP", "Could not process all toc ncx data");
|
||||||
free(ncxBuffer);
|
free(ncxBuffer);
|
||||||
tempNcxFile.close();
|
tempNcxFile.close();
|
||||||
return false;
|
return false;
|
||||||
@@ -145,29 +145,29 @@ bool Epub::parseTocNcxFile() const {
|
|||||||
|
|
||||||
free(ncxBuffer);
|
free(ncxBuffer);
|
||||||
tempNcxFile.close();
|
tempNcxFile.close();
|
||||||
SdMan.remove(tmpNcxPath.c_str());
|
Storage.remove(tmpNcxPath.c_str());
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Parsed TOC items\n", millis());
|
LOG_DBG("EBP", "Parsed TOC items");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Epub::parseTocNavFile() const {
|
bool Epub::parseTocNavFile() const {
|
||||||
// the nav file should have been specified in the content.opf file (EPUB 3)
|
// the nav file should have been specified in the content.opf file (EPUB 3)
|
||||||
if (tocNavItem.empty()) {
|
if (tocNavItem.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] No nav file specified\n", millis());
|
LOG_DBG("EBP", "No nav file specified");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Parsing toc nav file: %s\n", millis(), tocNavItem.c_str());
|
LOG_DBG("EBP", "Parsing toc nav file: %s", tocNavItem.c_str());
|
||||||
|
|
||||||
const auto tmpNavPath = getCachePath() + "/toc.nav";
|
const auto tmpNavPath = getCachePath() + "/toc.nav";
|
||||||
FsFile tempNavFile;
|
FsFile tempNavFile;
|
||||||
if (!SdMan.openFileForWrite("EBP", tmpNavPath, tempNavFile)) {
|
if (!Storage.openFileForWrite("EBP", tmpNavPath, tempNavFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
readItemContentsToStream(tocNavItem, tempNavFile, 1024);
|
readItemContentsToStream(tocNavItem, tempNavFile, 1024);
|
||||||
tempNavFile.close();
|
tempNavFile.close();
|
||||||
if (!SdMan.openFileForRead("EBP", tmpNavPath, tempNavFile)) {
|
if (!Storage.openFileForRead("EBP", tmpNavPath, tempNavFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const auto navSize = tempNavFile.size();
|
const auto navSize = tempNavFile.size();
|
||||||
@@ -178,13 +178,13 @@ bool Epub::parseTocNavFile() const {
|
|||||||
TocNavParser navParser(navContentBasePath, navSize, bookMetadataCache.get());
|
TocNavParser navParser(navContentBasePath, navSize, bookMetadataCache.get());
|
||||||
|
|
||||||
if (!navParser.setup()) {
|
if (!navParser.setup()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not setup toc nav parser\n", millis());
|
LOG_ERR("EBP", "Could not setup toc nav parser");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto navBuffer = static_cast<uint8_t*>(malloc(1024));
|
const auto navBuffer = static_cast<uint8_t*>(malloc(1024));
|
||||||
if (!navBuffer) {
|
if (!navBuffer) {
|
||||||
Serial.printf("[%lu] [EBP] Could not allocate memory for toc nav parser\n", millis());
|
LOG_ERR("EBP", "Could not allocate memory for toc nav parser");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ bool Epub::parseTocNavFile() const {
|
|||||||
const auto processedSize = navParser.write(navBuffer, readSize);
|
const auto processedSize = navParser.write(navBuffer, readSize);
|
||||||
|
|
||||||
if (processedSize != readSize) {
|
if (processedSize != readSize) {
|
||||||
Serial.printf("[%lu] [EBP] Could not process all toc nav data\n", millis());
|
LOG_ERR("EBP", "Could not process all toc nav data");
|
||||||
free(navBuffer);
|
free(navBuffer);
|
||||||
tempNavFile.close();
|
tempNavFile.close();
|
||||||
return false;
|
return false;
|
||||||
@@ -202,9 +202,9 @@ bool Epub::parseTocNavFile() const {
|
|||||||
|
|
||||||
free(navBuffer);
|
free(navBuffer);
|
||||||
tempNavFile.close();
|
tempNavFile.close();
|
||||||
SdMan.remove(tmpNavPath.c_str());
|
Storage.remove(tmpNavPath.c_str());
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Parsed TOC nav items\n", millis());
|
LOG_DBG("EBP", "Parsed TOC nav items");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,70 +212,69 @@ std::string Epub::getCssRulesCache() const { return cachePath + "/css_rules.cach
|
|||||||
|
|
||||||
bool Epub::loadCssRulesFromCache() const {
|
bool Epub::loadCssRulesFromCache() const {
|
||||||
FsFile cssCacheFile;
|
FsFile cssCacheFile;
|
||||||
if (SdMan.openFileForRead("EBP", getCssRulesCache(), cssCacheFile)) {
|
if (Storage.openFileForRead("EBP", getCssRulesCache(), cssCacheFile)) {
|
||||||
if (cssParser->loadFromCache(cssCacheFile)) {
|
if (cssParser->loadFromCache(cssCacheFile)) {
|
||||||
cssCacheFile.close();
|
cssCacheFile.close();
|
||||||
Serial.printf("[%lu] [EBP] Loaded CSS rules from cache\n", millis());
|
LOG_DBG("EBP", "Loaded CSS rules from cache");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
cssCacheFile.close();
|
cssCacheFile.close();
|
||||||
Serial.printf("[%lu] [EBP] CSS cache invalid, reparsing\n", millis());
|
LOG_DBG("EBP", "CSS cache invalid, reparsing");
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Epub::parseCssFiles() const {
|
void Epub::parseCssFiles() const {
|
||||||
if (cssFiles.empty()) {
|
if (cssFiles.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] No CSS files to parse, but CssParser created for inline styles\n", millis());
|
LOG_DBG("EBP", "No CSS files to parse, but CssParser created for inline styles");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to load from CSS cache first
|
// Try to load from CSS cache first
|
||||||
if (!loadCssRulesFromCache()) {
|
if (!loadCssRulesFromCache()) {
|
||||||
// Cache miss - parse CSS files
|
// Cache miss - parse CSS files
|
||||||
for (const auto& cssPath : cssFiles) {
|
for (const auto& cssPath : cssFiles) {
|
||||||
Serial.printf("[%lu] [EBP] Parsing CSS file: %s\n", millis(), cssPath.c_str());
|
LOG_DBG("EBP", "Parsing CSS file: %s", cssPath.c_str());
|
||||||
|
|
||||||
// Extract CSS file to temp location
|
// Extract CSS file to temp location
|
||||||
const auto tmpCssPath = getCachePath() + "/.tmp.css";
|
const auto tmpCssPath = getCachePath() + "/.tmp.css";
|
||||||
FsFile tempCssFile;
|
FsFile tempCssFile;
|
||||||
if (!SdMan.openFileForWrite("EBP", tmpCssPath, tempCssFile)) {
|
if (!Storage.openFileForWrite("EBP", tmpCssPath, tempCssFile)) {
|
||||||
Serial.printf("[%lu] [EBP] Could not create temp CSS file\n", millis());
|
LOG_ERR("EBP", "Could not create temp CSS file");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!readItemContentsToStream(cssPath, tempCssFile, 1024)) {
|
if (!readItemContentsToStream(cssPath, tempCssFile, 1024)) {
|
||||||
Serial.printf("[%lu] [EBP] Could not read CSS file: %s\n", millis(), cssPath.c_str());
|
LOG_ERR("EBP", "Could not read CSS file: %s", cssPath.c_str());
|
||||||
tempCssFile.close();
|
tempCssFile.close();
|
||||||
SdMan.remove(tmpCssPath.c_str());
|
Storage.remove(tmpCssPath.c_str());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
tempCssFile.close();
|
tempCssFile.close();
|
||||||
|
|
||||||
// Parse the CSS file
|
// Parse the CSS file
|
||||||
if (!SdMan.openFileForRead("EBP", tmpCssPath, tempCssFile)) {
|
if (!Storage.openFileForRead("EBP", tmpCssPath, tempCssFile)) {
|
||||||
Serial.printf("[%lu] [EBP] Could not open temp CSS file for reading\n", millis());
|
LOG_ERR("EBP", "Could not open temp CSS file for reading");
|
||||||
SdMan.remove(tmpCssPath.c_str());
|
Storage.remove(tmpCssPath.c_str());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
cssParser->loadFromStream(tempCssFile);
|
cssParser->loadFromStream(tempCssFile);
|
||||||
tempCssFile.close();
|
tempCssFile.close();
|
||||||
SdMan.remove(tmpCssPath.c_str());
|
Storage.remove(tmpCssPath.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to cache for next time
|
// Save to cache for next time
|
||||||
FsFile cssCacheFile;
|
FsFile cssCacheFile;
|
||||||
if (SdMan.openFileForWrite("EBP", getCssRulesCache(), cssCacheFile)) {
|
if (Storage.openFileForWrite("EBP", getCssRulesCache(), cssCacheFile)) {
|
||||||
cssParser->saveToCache(cssCacheFile);
|
cssParser->saveToCache(cssCacheFile);
|
||||||
cssCacheFile.close();
|
cssCacheFile.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Loaded %zu CSS style rules from %zu files\n", millis(), cssParser->ruleCount(),
|
LOG_DBG("EBP", "Loaded %zu CSS style rules from %zu files", cssParser->ruleCount(), cssFiles.size());
|
||||||
cssFiles.size());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// load in the meta data for the epub file
|
// load in the meta data for the epub file
|
||||||
bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
|
bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
|
||||||
Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str());
|
LOG_DBG("EBP", "Loading ePub: %s", filepath.c_str());
|
||||||
|
|
||||||
// Initialize spine/TOC cache
|
// Initialize spine/TOC cache
|
||||||
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
||||||
@@ -285,15 +284,15 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
|
|||||||
// Try to load existing cache first
|
// Try to load existing cache first
|
||||||
if (bookMetadataCache->load()) {
|
if (bookMetadataCache->load()) {
|
||||||
if (!skipLoadingCss && !loadCssRulesFromCache()) {
|
if (!skipLoadingCss && !loadCssRulesFromCache()) {
|
||||||
Serial.printf("[%lu] [EBP] Warning: CSS rules cache not found, attempting to parse CSS files\n", millis());
|
LOG_DBG("EBP", "Warning: CSS rules cache not found, attempting to parse CSS files");
|
||||||
// to get CSS file list
|
// to get CSS file list
|
||||||
if (!parseContentOpf(bookMetadataCache->coreMetadata)) {
|
if (!parseContentOpf(bookMetadataCache->coreMetadata)) {
|
||||||
Serial.printf("[%lu] [EBP] Could not parse content.opf from cached bookMetadata for CSS files\n", millis());
|
LOG_ERR("EBP", "Could not parse content.opf from cached bookMetadata for CSS files");
|
||||||
// continue anyway - book will work without CSS and we'll still load any inline style CSS
|
// continue anyway - book will work without CSS and we'll still load any inline style CSS
|
||||||
}
|
}
|
||||||
parseCssFiles();
|
parseCssFiles();
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,14 +302,14 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cache doesn't exist or is invalid, build it
|
// Cache doesn't exist or is invalid, build it
|
||||||
Serial.printf("[%lu] [EBP] Cache not found, building spine/TOC cache\n", millis());
|
LOG_DBG("EBP", "Cache not found, building spine/TOC cache");
|
||||||
setupCacheDir();
|
setupCacheDir();
|
||||||
|
|
||||||
const uint32_t indexingStart = millis();
|
const uint32_t indexingStart = millis();
|
||||||
|
|
||||||
// Begin building cache - stream entries to disk immediately
|
// Begin building cache - stream entries to disk immediately
|
||||||
if (!bookMetadataCache->beginWrite()) {
|
if (!bookMetadataCache->beginWrite()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not begin writing cache\n", millis());
|
LOG_ERR("EBP", "Could not begin writing cache");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,23 +317,23 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
|
|||||||
const uint32_t opfStart = millis();
|
const uint32_t opfStart = millis();
|
||||||
BookMetadataCache::BookMetadata bookMetadata;
|
BookMetadataCache::BookMetadata bookMetadata;
|
||||||
if (!bookMetadataCache->beginContentOpfPass()) {
|
if (!bookMetadataCache->beginContentOpfPass()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not begin writing content.opf pass\n", millis());
|
LOG_ERR("EBP", "Could not begin writing content.opf pass");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!parseContentOpf(bookMetadata)) {
|
if (!parseContentOpf(bookMetadata)) {
|
||||||
Serial.printf("[%lu] [EBP] Could not parse content.opf\n", millis());
|
LOG_ERR("EBP", "Could not parse content.opf");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!bookMetadataCache->endContentOpfPass()) {
|
if (!bookMetadataCache->endContentOpfPass()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not end writing content.opf pass\n", millis());
|
LOG_ERR("EBP", "Could not end writing content.opf pass");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [EBP] OPF pass completed in %lu ms\n", millis(), millis() - opfStart);
|
LOG_DBG("EBP", "OPF pass completed in %lu ms", millis() - opfStart);
|
||||||
|
|
||||||
// TOC Pass - try EPUB 3 nav first, fall back to NCX
|
// TOC Pass - try EPUB 3 nav first, fall back to NCX
|
||||||
const uint32_t tocStart = millis();
|
const uint32_t tocStart = millis();
|
||||||
if (!bookMetadataCache->beginTocPass()) {
|
if (!bookMetadataCache->beginTocPass()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not begin writing toc pass\n", millis());
|
LOG_ERR("EBP", "Could not begin writing toc pass");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,50 +341,50 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
|
|||||||
|
|
||||||
// Try EPUB 3 nav document first (preferred)
|
// Try EPUB 3 nav document first (preferred)
|
||||||
if (!tocNavItem.empty()) {
|
if (!tocNavItem.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] Attempting to parse EPUB 3 nav document\n", millis());
|
LOG_DBG("EBP", "Attempting to parse EPUB 3 nav document");
|
||||||
tocParsed = parseTocNavFile();
|
tocParsed = parseTocNavFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to NCX if nav parsing failed or wasn't available
|
// Fall back to NCX if nav parsing failed or wasn't available
|
||||||
if (!tocParsed && !tocNcxItem.empty()) {
|
if (!tocParsed && !tocNcxItem.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] Falling back to NCX TOC\n", millis());
|
LOG_DBG("EBP", "Falling back to NCX TOC");
|
||||||
tocParsed = parseTocNcxFile();
|
tocParsed = parseTocNcxFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tocParsed) {
|
if (!tocParsed) {
|
||||||
Serial.printf("[%lu] [EBP] Warning: Could not parse any TOC format\n", millis());
|
LOG_ERR("EBP", "Warning: Could not parse any TOC format");
|
||||||
// Continue anyway - book will work without TOC
|
// Continue anyway - book will work without TOC
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bookMetadataCache->endTocPass()) {
|
if (!bookMetadataCache->endTocPass()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not end writing toc pass\n", millis());
|
LOG_ERR("EBP", "Could not end writing toc pass");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [EBP] TOC pass completed in %lu ms\n", millis(), millis() - tocStart);
|
LOG_DBG("EBP", "TOC pass completed in %lu ms", millis() - tocStart);
|
||||||
|
|
||||||
// Close the cache files
|
// Close the cache files
|
||||||
if (!bookMetadataCache->endWrite()) {
|
if (!bookMetadataCache->endWrite()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not end writing cache\n", millis());
|
LOG_ERR("EBP", "Could not end writing cache");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build final book.bin
|
// Build final book.bin
|
||||||
const uint32_t buildStart = millis();
|
const uint32_t buildStart = millis();
|
||||||
if (!bookMetadataCache->buildBookBin(filepath, bookMetadata)) {
|
if (!bookMetadataCache->buildBookBin(filepath, bookMetadata)) {
|
||||||
Serial.printf("[%lu] [EBP] Could not update mappings and sizes\n", millis());
|
LOG_ERR("EBP", "Could not update mappings and sizes");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [EBP] buildBookBin completed in %lu ms\n", millis(), millis() - buildStart);
|
LOG_DBG("EBP", "buildBookBin completed in %lu ms", millis() - buildStart);
|
||||||
Serial.printf("[%lu] [EBP] Total indexing completed in %lu ms\n", millis(), millis() - indexingStart);
|
LOG_DBG("EBP", "Total indexing completed in %lu ms", millis() - indexingStart);
|
||||||
|
|
||||||
if (!bookMetadataCache->cleanupTmpFiles()) {
|
if (!bookMetadataCache->cleanupTmpFiles()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not cleanup tmp files - ignoring\n", millis());
|
LOG_DBG("EBP", "Could not cleanup tmp files - ignoring");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload the cache from disk so it's in the correct state
|
// Reload the cache from disk so it's in the correct state
|
||||||
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
||||||
if (!bookMetadataCache->load()) {
|
if (!bookMetadataCache->load()) {
|
||||||
Serial.printf("[%lu] [EBP] Failed to reload cache after writing\n", millis());
|
LOG_ERR("EBP", "Failed to reload cache after writing");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,31 +393,31 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
|
|||||||
parseCssFiles();
|
parseCssFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Epub::clearCache() const {
|
bool Epub::clearCache() const {
|
||||||
if (!SdMan.exists(cachePath.c_str())) {
|
if (!Storage.exists(cachePath.c_str())) {
|
||||||
Serial.printf("[%lu] [EPB] Cache does not exist, no action needed\n", millis());
|
LOG_DBG("EPB", "Cache does not exist, no action needed");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SdMan.removeDir(cachePath.c_str())) {
|
if (!Storage.removeDir(cachePath.c_str())) {
|
||||||
Serial.printf("[%lu] [EPB] Failed to clear cache\n", millis());
|
LOG_ERR("EPB", "Failed to clear cache");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [EPB] Cache cleared successfully\n", millis());
|
LOG_DBG("EPB", "Cache cleared successfully");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Epub::setupCacheDir() const {
|
void Epub::setupCacheDir() const {
|
||||||
if (SdMan.exists(cachePath.c_str())) {
|
if (Storage.exists(cachePath.c_str())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SdMan.mkdir(cachePath.c_str());
|
Storage.mkdir(cachePath.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string& Epub::getCachePath() const { return cachePath; }
|
const std::string& Epub::getCachePath() const { return cachePath; }
|
||||||
@@ -459,55 +458,55 @@ std::string Epub::getCoverBmpPath(bool cropped) const {
|
|||||||
|
|
||||||
bool Epub::generateCoverBmp(bool cropped) const {
|
bool Epub::generateCoverBmp(bool cropped) const {
|
||||||
// Already generated, return true
|
// Already generated, return true
|
||||||
if (SdMan.exists(getCoverBmpPath(cropped).c_str())) {
|
if (Storage.exists(getCoverBmpPath(cropped).c_str())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
Serial.printf("[%lu] [EBP] Cannot generate cover BMP, cache not loaded\n", millis());
|
LOG_ERR("EBP", "Cannot generate cover BMP, cache not loaded");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||||
if (coverImageHref.empty()) {
|
if (coverImageHref.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] No known cover image\n", millis());
|
LOG_ERR("EBP", "No known cover image");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
||||||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
||||||
Serial.printf("[%lu] [EBP] Generating BMP from JPG cover image (%s mode)\n", millis(), cropped ? "cropped" : "fit");
|
LOG_DBG("EBP", "Generating BMP from JPG cover image (%s mode)", cropped ? "cropped" : "fit");
|
||||||
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||||
|
|
||||||
FsFile coverJpg;
|
FsFile coverJpg;
|
||||||
if (!SdMan.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
if (!Storage.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
|
|
||||||
if (!SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
if (!Storage.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
FsFile coverBmp;
|
FsFile coverBmp;
|
||||||
if (!SdMan.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
|
if (!Storage.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp, cropped);
|
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp, cropped);
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
coverBmp.close();
|
coverBmp.close();
|
||||||
SdMan.remove(coverJpgTempPath.c_str());
|
Storage.remove(coverJpgTempPath.c_str());
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Serial.printf("[%lu] [EBP] Failed to generate BMP from JPG cover image\n", millis());
|
LOG_ERR("EBP", "Failed to generate BMP from cover image");
|
||||||
SdMan.remove(getCoverBmpPath(cropped).c_str());
|
Storage.remove(getCoverBmpPath(cropped).c_str());
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [EBP] Generated BMP from JPG cover image, success: %s\n", millis(), success ? "yes" : "no");
|
LOG_DBG("EBP", "Generated BMP from cover image, success: %s", success ? "yes" : "no");
|
||||||
return success;
|
return success;
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [EBP] Cover image is not a JPG, skipping\n", millis());
|
LOG_ERR("EBP", "Cover image is not a supported format, skipping");
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -518,36 +517,36 @@ std::string Epub::getThumbBmpPath(int height) const { return cachePath + "/thumb
|
|||||||
|
|
||||||
bool Epub::generateThumbBmp(int height) const {
|
bool Epub::generateThumbBmp(int height) const {
|
||||||
// Already generated, return true
|
// Already generated, return true
|
||||||
if (SdMan.exists(getThumbBmpPath(height).c_str())) {
|
if (Storage.exists(getThumbBmpPath(height).c_str())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
Serial.printf("[%lu] [EBP] Cannot generate thumb BMP, cache not loaded\n", millis());
|
LOG_ERR("EBP", "Cannot generate thumb BMP, cache not loaded");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||||
if (coverImageHref.empty()) {
|
if (coverImageHref.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] No known cover image for thumbnail\n", millis());
|
LOG_DBG("EBP", "No known cover image for thumbnail");
|
||||||
} else if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
} else if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
||||||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
||||||
Serial.printf("[%lu] [EBP] Generating thumb BMP from JPG cover image\n", millis());
|
LOG_DBG("EBP", "Generating thumb BMP from JPG cover image");
|
||||||
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||||
|
|
||||||
FsFile coverJpg;
|
FsFile coverJpg;
|
||||||
if (!SdMan.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
if (!Storage.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
|
|
||||||
if (!SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
if (!Storage.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
FsFile thumbBmp;
|
FsFile thumbBmp;
|
||||||
if (!SdMan.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) {
|
if (!Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) {
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -559,29 +558,28 @@ bool Epub::generateThumbBmp(int height) const {
|
|||||||
THUMB_TARGET_HEIGHT);
|
THUMB_TARGET_HEIGHT);
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
thumbBmp.close();
|
thumbBmp.close();
|
||||||
SdMan.remove(coverJpgTempPath.c_str());
|
Storage.remove(coverJpgTempPath.c_str());
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Serial.printf("[%lu] [EBP] Failed to generate thumb BMP from JPG cover image\n", millis());
|
LOG_ERR("EBP", "Failed to generate thumb BMP from JPG cover image");
|
||||||
SdMan.remove(getThumbBmpPath(height).c_str());
|
Storage.remove(getThumbBmpPath(height).c_str());
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [EBP] Generated thumb BMP from JPG cover image, success: %s\n", millis(),
|
LOG_DBG("EBP", "Generated thumb BMP from JPG cover image, success: %s", success ? "yes" : "no");
|
||||||
success ? "yes" : "no");
|
|
||||||
return success;
|
return success;
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [EBP] Cover image is not a JPG, skipping thumbnail\n", millis());
|
LOG_ERR("EBP", "Cover image is not a supported format, skipping thumbnail");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write an empty bmp file to avoid generation attempts in the future
|
// Write an empty bmp file to avoid generation attempts in the future
|
||||||
FsFile thumbBmp;
|
FsFile thumbBmp;
|
||||||
SdMan.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp);
|
Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp);
|
||||||
thumbBmp.close();
|
thumbBmp.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, const bool trailingNullByte) const {
|
uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, const bool trailingNullByte) const {
|
||||||
if (itemHref.empty()) {
|
if (itemHref.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] Failed to read item, empty href\n", millis());
|
LOG_DBG("EBP", "Failed to read item, empty href");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -589,7 +587,7 @@ uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size
|
|||||||
|
|
||||||
const auto content = ZipFile(filepath).readFileToMemory(path.c_str(), size, trailingNullByte);
|
const auto content = ZipFile(filepath).readFileToMemory(path.c_str(), size, trailingNullByte);
|
||||||
if (!content) {
|
if (!content) {
|
||||||
Serial.printf("[%lu] [EBP] Failed to read item %s\n", millis(), path.c_str());
|
LOG_DBG("EBP", "Failed to read item %s", path.c_str());
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -598,7 +596,7 @@ 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 {
|
bool Epub::readItemContentsToStream(const std::string& itemHref, Print& out, const size_t chunkSize) const {
|
||||||
if (itemHref.empty()) {
|
if (itemHref.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] Failed to read item, empty href\n", millis());
|
LOG_DBG("EBP", "Failed to read item, empty href");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -622,12 +620,12 @@ size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return get
|
|||||||
|
|
||||||
BookMetadataCache::SpineEntry Epub::getSpineItem(const int spineIndex) const {
|
BookMetadataCache::SpineEntry Epub::getSpineItem(const int spineIndex) const {
|
||||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
Serial.printf("[%lu] [EBP] getSpineItem called but cache not loaded\n", millis());
|
LOG_ERR("EBP", "getSpineItem called but cache not loaded");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (spineIndex < 0 || spineIndex >= bookMetadataCache->getSpineCount()) {
|
if (spineIndex < 0 || spineIndex >= bookMetadataCache->getSpineCount()) {
|
||||||
Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex);
|
LOG_ERR("EBP", "getSpineItem index:%d is out of range", spineIndex);
|
||||||
return bookMetadataCache->getSpineEntry(0);
|
return bookMetadataCache->getSpineEntry(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,12 +634,12 @@ BookMetadataCache::SpineEntry Epub::getSpineItem(const int spineIndex) const {
|
|||||||
|
|
||||||
BookMetadataCache::TocEntry Epub::getTocItem(const int tocIndex) const {
|
BookMetadataCache::TocEntry Epub::getTocItem(const int tocIndex) const {
|
||||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
Serial.printf("[%lu] [EBP] getTocItem called but cache not loaded\n", millis());
|
LOG_DBG("EBP", "getTocItem called but cache not loaded");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
|
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
|
||||||
Serial.printf("[%lu] [EBP] getTocItem index:%d is out of range\n", millis(), tocIndex);
|
LOG_DBG("EBP", "getTocItem index:%d is out of range", tocIndex);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,18 +657,18 @@ int Epub::getTocItemsCount() const {
|
|||||||
// work out the section index for a toc index
|
// work out the section index for a toc index
|
||||||
int Epub::getSpineIndexForTocIndex(const int tocIndex) const {
|
int Epub::getSpineIndexForTocIndex(const int tocIndex) const {
|
||||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex called but cache not loaded\n", millis());
|
LOG_ERR("EBP", "getSpineIndexForTocIndex called but cache not loaded");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
|
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
|
||||||
Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex: tocIndex %d out of range\n", millis(), tocIndex);
|
LOG_ERR("EBP", "getSpineIndexForTocIndex: tocIndex %d out of range", tocIndex);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int spineIndex = bookMetadataCache->getTocEntry(tocIndex).spineIndex;
|
const int spineIndex = bookMetadataCache->getTocEntry(tocIndex).spineIndex;
|
||||||
if (spineIndex < 0) {
|
if (spineIndex < 0) {
|
||||||
Serial.printf("[%lu] [EBP] Section not found for TOC index %d\n", millis(), tocIndex);
|
LOG_DBG("EBP", "Section not found for TOC index %d", tocIndex);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -688,12 +686,11 @@ size_t Epub::getBookSize() const {
|
|||||||
|
|
||||||
int Epub::getSpineIndexForTextReference() const {
|
int Epub::getSpineIndexForTextReference() const {
|
||||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
Serial.printf("[%lu] [EBP] getSpineIndexForTextReference called but cache not loaded\n", millis());
|
LOG_ERR("EBP", "getSpineIndexForTextReference called but cache not loaded");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [ERS] Core Metadata: cover(%d)=%s, textReference(%d)=%s\n", millis(),
|
LOG_DBG("EBP", "Core Metadata: cover(%d)=%s, textReference(%d)=%s",
|
||||||
bookMetadataCache->coreMetadata.coverItemHref.size(),
|
bookMetadataCache->coreMetadata.coverItemHref.size(), bookMetadataCache->coreMetadata.coverItemHref.c_str(),
|
||||||
bookMetadataCache->coreMetadata.coverItemHref.c_str(),
|
|
||||||
bookMetadataCache->coreMetadata.textReferenceHref.size(),
|
bookMetadataCache->coreMetadata.textReferenceHref.size(),
|
||||||
bookMetadataCache->coreMetadata.textReferenceHref.c_str());
|
bookMetadataCache->coreMetadata.textReferenceHref.c_str());
|
||||||
|
|
||||||
@@ -705,13 +702,13 @@ int Epub::getSpineIndexForTextReference() const {
|
|||||||
// loop through spine items to get the correct index matching the text href
|
// loop through spine items to get the correct index matching the text href
|
||||||
for (size_t i = 0; i < getSpineItemsCount(); i++) {
|
for (size_t i = 0; i < getSpineItemsCount(); i++) {
|
||||||
if (getSpineItem(i).href == bookMetadataCache->coreMetadata.textReferenceHref) {
|
if (getSpineItem(i).href == bookMetadataCache->coreMetadata.textReferenceHref) {
|
||||||
Serial.printf("[%lu] [ERS] Text reference %s found at index %d\n", millis(),
|
LOG_DBG("EBP", "Text reference %s found at index %d", bookMetadataCache->coreMetadata.textReferenceHref.c_str(),
|
||||||
bookMetadataCache->coreMetadata.textReferenceHref.c_str(), i);
|
i);
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// This should not happen, as we checked for empty textReferenceHref earlier
|
// This should not happen, as we checked for empty textReferenceHref earlier
|
||||||
Serial.printf("[%lu] [EBP] Section not found for text reference\n", millis());
|
LOG_DBG("EBP", "Section not found for text reference");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "BookMetadataCache.h"
|
#include "BookMetadataCache.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <Logging.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
#include <ZipFile.h>
|
#include <ZipFile.h>
|
||||||
|
|
||||||
@@ -21,15 +21,15 @@ bool BookMetadataCache::beginWrite() {
|
|||||||
buildMode = true;
|
buildMode = true;
|
||||||
spineCount = 0;
|
spineCount = 0;
|
||||||
tocCount = 0;
|
tocCount = 0;
|
||||||
Serial.printf("[%lu] [BMC] Entering write mode\n", millis());
|
LOG_DBG("BMC", "Entering write mode");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BookMetadataCache::beginContentOpfPass() {
|
bool BookMetadataCache::beginContentOpfPass() {
|
||||||
Serial.printf("[%lu] [BMC] Beginning content opf pass\n", millis());
|
LOG_DBG("BMC", "Beginning content opf pass");
|
||||||
|
|
||||||
// Open spine file for writing
|
// Open spine file for writing
|
||||||
return SdMan.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile);
|
return Storage.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BookMetadataCache::endContentOpfPass() {
|
bool BookMetadataCache::endContentOpfPass() {
|
||||||
@@ -38,12 +38,12 @@ bool BookMetadataCache::endContentOpfPass() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool BookMetadataCache::beginTocPass() {
|
bool BookMetadataCache::beginTocPass() {
|
||||||
Serial.printf("[%lu] [BMC] Beginning toc pass\n", millis());
|
LOG_DBG("BMC", "Beginning toc pass");
|
||||||
|
|
||||||
if (!SdMan.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
if (!Storage.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!SdMan.openFileForWrite("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
if (!Storage.openFileForWrite("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
||||||
spineFile.close();
|
spineFile.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ bool BookMetadataCache::beginTocPass() {
|
|||||||
});
|
});
|
||||||
spineFile.seek(0);
|
spineFile.seek(0);
|
||||||
useSpineHrefIndex = true;
|
useSpineHrefIndex = true;
|
||||||
Serial.printf("[%lu] [BMC] Using fast index for %d spine items\n", millis(), spineCount);
|
LOG_DBG("BMC", "Using fast index for %d spine items", spineCount);
|
||||||
} else {
|
} else {
|
||||||
useSpineHrefIndex = false;
|
useSpineHrefIndex = false;
|
||||||
}
|
}
|
||||||
@@ -87,27 +87,27 @@ bool BookMetadataCache::endTocPass() {
|
|||||||
|
|
||||||
bool BookMetadataCache::endWrite() {
|
bool BookMetadataCache::endWrite() {
|
||||||
if (!buildMode) {
|
if (!buildMode) {
|
||||||
Serial.printf("[%lu] [BMC] endWrite called but not in build mode\n", millis());
|
LOG_DBG("BMC", "endWrite called but not in build mode");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildMode = false;
|
buildMode = false;
|
||||||
Serial.printf("[%lu] [BMC] Wrote %d spine, %d TOC entries\n", millis(), spineCount, tocCount);
|
LOG_DBG("BMC", "Wrote %d spine, %d TOC entries", spineCount, tocCount);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMetadata& metadata) {
|
bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMetadata& metadata) {
|
||||||
// Open all three files, writing to meta, reading from spine and toc
|
// Open all three files, writing to meta, reading from spine and toc
|
||||||
if (!SdMan.openFileForWrite("BMC", cachePath + bookBinFile, bookFile)) {
|
if (!Storage.openFileForWrite("BMC", cachePath + bookBinFile, bookFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SdMan.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
if (!Storage.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||||
bookFile.close();
|
bookFile.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SdMan.openFileForRead("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
if (!Storage.openFileForRead("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
||||||
bookFile.close();
|
bookFile.close();
|
||||||
spineFile.close();
|
spineFile.close();
|
||||||
return false;
|
return false;
|
||||||
@@ -167,7 +167,7 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
ZipFile zip(epubPath);
|
ZipFile zip(epubPath);
|
||||||
// Pre-open zip file to speed up size calculations
|
// Pre-open zip file to speed up size calculations
|
||||||
if (!zip.open()) {
|
if (!zip.open()) {
|
||||||
Serial.printf("[%lu] [BMC] Could not open EPUB zip for size calculations\n", millis());
|
LOG_ERR("BMC", "Could not open EPUB zip for size calculations");
|
||||||
bookFile.close();
|
bookFile.close();
|
||||||
spineFile.close();
|
spineFile.close();
|
||||||
tocFile.close();
|
tocFile.close();
|
||||||
@@ -185,7 +185,7 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
bool useBatchSizes = false;
|
bool useBatchSizes = false;
|
||||||
|
|
||||||
if (spineCount >= LARGE_SPINE_THRESHOLD) {
|
if (spineCount >= LARGE_SPINE_THRESHOLD) {
|
||||||
Serial.printf("[%lu] [BMC] Using batch size lookup for %d spine items\n", millis(), spineCount);
|
LOG_DBG("BMC", "Using batch size lookup for %d spine items", spineCount);
|
||||||
|
|
||||||
std::vector<ZipFile::SizeTarget> targets;
|
std::vector<ZipFile::SizeTarget> targets;
|
||||||
targets.reserve(spineCount);
|
targets.reserve(spineCount);
|
||||||
@@ -208,7 +208,7 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
|
|
||||||
spineSizes.resize(spineCount, 0);
|
spineSizes.resize(spineCount, 0);
|
||||||
int matched = zip.fillUncompressedSizes(targets, spineSizes);
|
int matched = zip.fillUncompressedSizes(targets, spineSizes);
|
||||||
Serial.printf("[%lu] [BMC] Batch lookup matched %d/%d spine items\n", millis(), matched, spineCount);
|
LOG_DBG("BMC", "Batch lookup matched %d/%d spine items", matched, spineCount);
|
||||||
|
|
||||||
targets.clear();
|
targets.clear();
|
||||||
targets.shrink_to_fit();
|
targets.shrink_to_fit();
|
||||||
@@ -227,9 +227,8 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
// Not a huge deal if we don't fine a TOC entry for the spine entry, this is expected behaviour for EPUBs
|
// Not a huge deal if we don't fine a TOC entry for the spine entry, this is expected behaviour for EPUBs
|
||||||
// Logging here is for debugging
|
// Logging here is for debugging
|
||||||
if (spineEntry.tocIndex == -1) {
|
if (spineEntry.tocIndex == -1) {
|
||||||
Serial.printf(
|
LOG_DBG("BMC", "Warning: Could not find TOC entry for spine item %d: %s, using title from last section", i,
|
||||||
"[%lu] [BMC] Warning: Could not find TOC entry for spine item %d: %s, using title from last section\n",
|
spineEntry.href.c_str());
|
||||||
millis(), i, spineEntry.href.c_str());
|
|
||||||
spineEntry.tocIndex = lastSpineTocIndex;
|
spineEntry.tocIndex = lastSpineTocIndex;
|
||||||
}
|
}
|
||||||
lastSpineTocIndex = spineEntry.tocIndex;
|
lastSpineTocIndex = spineEntry.tocIndex;
|
||||||
@@ -240,13 +239,13 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
if (itemSize == 0) {
|
if (itemSize == 0) {
|
||||||
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
||||||
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||||
Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str());
|
LOG_ERR("BMC", "Warning: Could not get size for spine item: %s", path.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
||||||
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||||
Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str());
|
LOG_ERR("BMC", "Warning: Could not get size for spine item: %s", path.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,16 +269,16 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
spineFile.close();
|
spineFile.close();
|
||||||
tocFile.close();
|
tocFile.close();
|
||||||
|
|
||||||
Serial.printf("[%lu] [BMC] Successfully built book.bin\n", millis());
|
LOG_DBG("BMC", "Successfully built book.bin");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BookMetadataCache::cleanupTmpFiles() const {
|
bool BookMetadataCache::cleanupTmpFiles() const {
|
||||||
if (SdMan.exists((cachePath + tmpSpineBinFile).c_str())) {
|
if (Storage.exists((cachePath + tmpSpineBinFile).c_str())) {
|
||||||
SdMan.remove((cachePath + tmpSpineBinFile).c_str());
|
Storage.remove((cachePath + tmpSpineBinFile).c_str());
|
||||||
}
|
}
|
||||||
if (SdMan.exists((cachePath + tmpTocBinFile).c_str())) {
|
if (Storage.exists((cachePath + tmpTocBinFile).c_str())) {
|
||||||
SdMan.remove((cachePath + tmpTocBinFile).c_str());
|
Storage.remove((cachePath + tmpTocBinFile).c_str());
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -306,7 +305,7 @@ uint32_t BookMetadataCache::writeTocEntry(FsFile& file, const TocEntry& entry) c
|
|||||||
// this is because in this function we're marking positions of the items
|
// this is because in this function we're marking positions of the items
|
||||||
void BookMetadataCache::createSpineEntry(const std::string& href) {
|
void BookMetadataCache::createSpineEntry(const std::string& href) {
|
||||||
if (!buildMode || !spineFile) {
|
if (!buildMode || !spineFile) {
|
||||||
Serial.printf("[%lu] [BMC] createSpineEntry called but not in build mode\n", millis());
|
LOG_DBG("BMC", "createSpineEntry called but not in build mode");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,7 +317,7 @@ void BookMetadataCache::createSpineEntry(const std::string& href) {
|
|||||||
void BookMetadataCache::createTocEntry(const std::string& title, const std::string& href, const std::string& anchor,
|
void BookMetadataCache::createTocEntry(const std::string& title, const std::string& href, const std::string& anchor,
|
||||||
const uint8_t level) {
|
const uint8_t level) {
|
||||||
if (!buildMode || !tocFile || !spineFile) {
|
if (!buildMode || !tocFile || !spineFile) {
|
||||||
Serial.printf("[%lu] [BMC] createTocEntry called but not in build mode\n", millis());
|
LOG_DBG("BMC", "createTocEntry called but not in build mode");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,7 +339,7 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (spineIndex == -1) {
|
if (spineIndex == -1) {
|
||||||
Serial.printf("[%lu] [BMC] createTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str());
|
LOG_DBG("BMC", "createTocEntry: Could not find spine item for TOC href %s", href.c_str());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
spineFile.seek(0);
|
spineFile.seek(0);
|
||||||
@@ -352,7 +351,7 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (spineIndex == -1) {
|
if (spineIndex == -1) {
|
||||||
Serial.printf("[%lu] [BMC] createTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str());
|
LOG_DBG("BMC", "createTocEntry: Could not find spine item for TOC href %s", href.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,14 +363,14 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
|
|||||||
/* ============= READING / LOADING FUNCTIONS ================ */
|
/* ============= READING / LOADING FUNCTIONS ================ */
|
||||||
|
|
||||||
bool BookMetadataCache::load() {
|
bool BookMetadataCache::load() {
|
||||||
if (!SdMan.openFileForRead("BMC", cachePath + bookBinFile, bookFile)) {
|
if (!Storage.openFileForRead("BMC", cachePath + bookBinFile, bookFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t version;
|
uint8_t version;
|
||||||
serialization::readPod(bookFile, version);
|
serialization::readPod(bookFile, version);
|
||||||
if (version != BOOK_CACHE_VERSION) {
|
if (version != BOOK_CACHE_VERSION) {
|
||||||
Serial.printf("[%lu] [BMC] Cache version mismatch: expected %d, got %d\n", millis(), BOOK_CACHE_VERSION, version);
|
LOG_DBG("BMC", "Cache version mismatch: expected %d, got %d", BOOK_CACHE_VERSION, version);
|
||||||
bookFile.close();
|
bookFile.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -387,18 +386,18 @@ bool BookMetadataCache::load() {
|
|||||||
serialization::readString(bookFile, coreMetadata.textReferenceHref);
|
serialization::readString(bookFile, coreMetadata.textReferenceHref);
|
||||||
|
|
||||||
loaded = true;
|
loaded = true;
|
||||||
Serial.printf("[%lu] [BMC] Loaded cache data: %d spine, %d TOC entries\n", millis(), spineCount, tocCount);
|
LOG_DBG("BMC", "Loaded cache data: %d spine, %d TOC entries", spineCount, tocCount);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index) {
|
BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index) {
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
Serial.printf("[%lu] [BMC] getSpineEntry called but cache not loaded\n", millis());
|
LOG_ERR("BMC", "getSpineEntry called but cache not loaded");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index < 0 || index >= static_cast<int>(spineCount)) {
|
if (index < 0 || index >= static_cast<int>(spineCount)) {
|
||||||
Serial.printf("[%lu] [BMC] getSpineEntry index %d out of range\n", millis(), index);
|
LOG_ERR("BMC", "getSpineEntry index %d out of range", index);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,12 +411,12 @@ BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index)
|
|||||||
|
|
||||||
BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
|
BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
Serial.printf("[%lu] [BMC] getTocEntry called but cache not loaded\n", millis());
|
LOG_ERR("BMC", "getTocEntry called but cache not loaded");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index < 0 || index >= static_cast<int>(tocCount)) {
|
if (index < 0 || index >= static_cast<int>(tocCount)) {
|
||||||
Serial.printf("[%lu] [BMC] getTocEntry index %d out of range\n", millis(), index);
|
LOG_ERR("BMC", "getTocEntry index %d out of range", index);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "Page.h"
|
#include "Page.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <Logging.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
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) {
|
||||||
@@ -60,7 +60,7 @@ std::unique_ptr<Page> Page::deserialize(FsFile& file) {
|
|||||||
auto pl = PageLine::deserialize(file);
|
auto pl = PageLine::deserialize(file);
|
||||||
page->elements.push_back(std::move(pl));
|
page->elements.push_back(std::move(pl));
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown tag %u\n", millis(), tag);
|
LOG_ERR("PGE", "Deserialization failed: Unknown tag %u", tag);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <SdFat.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ void stripSoftHyphensInPlace(std::string& word) {
|
|||||||
// Returns the rendered width for a word while ignoring soft hyphen glyphs and optionally appending a visible hyphen.
|
// Returns the rendered width for a word while ignoring soft hyphen glyphs and optionally appending a visible hyphen.
|
||||||
uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const std::string& word,
|
uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const std::string& word,
|
||||||
const EpdFontFamily::Style style, const bool appendHyphen = false) {
|
const EpdFontFamily::Style style, const bool appendHyphen = false) {
|
||||||
|
if (word.size() == 1 && word[0] == ' ' && !appendHyphen) {
|
||||||
|
return renderer.getSpaceWidth(fontId);
|
||||||
|
}
|
||||||
const bool hasSoftHyphen = containsSoftHyphen(word);
|
const bool hasSoftHyphen = containsSoftHyphen(word);
|
||||||
if (!hasSoftHyphen && !appendHyphen) {
|
if (!hasSoftHyphen && !appendHyphen) {
|
||||||
return renderer.getTextWidth(fontId, word.c_str(), style);
|
return renderer.getTextWidth(fontId, word.c_str(), style);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "Section.h"
|
#include "Section.h"
|
||||||
|
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
#include <Logging.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
#include "Page.h"
|
#include "Page.h"
|
||||||
@@ -16,16 +17,16 @@ constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) +
|
|||||||
|
|
||||||
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
Serial.printf("[%lu] [SCT] File not open for writing page %d\n", millis(), pageCount);
|
LOG_ERR("SCT", "File not open for writing page %d", pageCount);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint32_t position = file.position();
|
const uint32_t position = file.position();
|
||||||
if (!page->serialize(file)) {
|
if (!page->serialize(file)) {
|
||||||
Serial.printf("[%lu] [SCT] Failed to serialize page %d\n", millis(), pageCount);
|
LOG_ERR("SCT", "Failed to serialize page %d", pageCount);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [SCT] Page %d processed\n", millis(), pageCount);
|
LOG_DBG("SCT", "Page %d processed", pageCount);
|
||||||
|
|
||||||
pageCount++;
|
pageCount++;
|
||||||
return position;
|
return position;
|
||||||
@@ -36,7 +37,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
|
|||||||
const uint16_t viewportHeight, const bool hyphenationEnabled,
|
const uint16_t viewportHeight, const bool hyphenationEnabled,
|
||||||
const bool embeddedStyle) {
|
const bool embeddedStyle) {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
Serial.printf("[%lu] [SCT] File not open for writing header\n", millis());
|
LOG_DBG("SCT", "File not open for writing header");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
|
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
|
||||||
@@ -60,7 +61,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
|
|||||||
bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||||
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle) {
|
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle) {
|
||||||
if (!SdMan.openFileForRead("SCT", filePath, file)) {
|
if (!Storage.openFileForRead("SCT", filePath, file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
|||||||
serialization::readPod(file, version);
|
serialization::readPod(file, version);
|
||||||
if (version != SECTION_FILE_VERSION) {
|
if (version != SECTION_FILE_VERSION) {
|
||||||
file.close();
|
file.close();
|
||||||
Serial.printf("[%lu] [SCT] Deserialization failed: Unknown version %u\n", millis(), version);
|
LOG_ERR("SCT", "Deserialization failed: Unknown version %u", version);
|
||||||
clearCache();
|
clearCache();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -96,7 +97,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
|||||||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
|
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
|
||||||
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle) {
|
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle) {
|
||||||
file.close();
|
file.close();
|
||||||
Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis());
|
LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
|
||||||
clearCache();
|
clearCache();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -104,23 +105,23 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
|||||||
|
|
||||||
serialization::readPod(file, pageCount);
|
serialization::readPod(file, pageCount);
|
||||||
file.close();
|
file.close();
|
||||||
Serial.printf("[%lu] [SCT] Deserialization succeeded: %d pages\n", millis(), pageCount);
|
LOG_DBG("SCT", "Deserialization succeeded: %d pages", pageCount);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 (!SdMan.exists(filePath.c_str())) {
|
if (!Storage.exists(filePath.c_str())) {
|
||||||
Serial.printf("[%lu] [SCT] Cache does not exist, no action needed\n", millis());
|
LOG_DBG("SCT", "Cache does not exist, no action needed");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SdMan.remove(filePath.c_str())) {
|
if (!Storage.remove(filePath.c_str())) {
|
||||||
Serial.printf("[%lu] [SCT] Failed to clear cache\n", millis());
|
LOG_ERR("SCT", "Failed to clear cache");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [SCT] Cache cleared successfully\n", millis());
|
LOG_DBG("SCT", "Cache cleared successfully");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +135,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
// Create cache directory if it doesn't exist
|
// Create cache directory if it doesn't exist
|
||||||
{
|
{
|
||||||
const auto sectionsDir = epub->getCachePath() + "/sections";
|
const auto sectionsDir = epub->getCachePath() + "/sections";
|
||||||
SdMan.mkdir(sectionsDir.c_str());
|
Storage.mkdir(sectionsDir.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retry logic for SD card timing issues
|
// Retry logic for SD card timing issues
|
||||||
@@ -142,17 +143,17 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
uint32_t fileSize = 0;
|
uint32_t fileSize = 0;
|
||||||
for (int attempt = 0; attempt < 3 && !success; attempt++) {
|
for (int attempt = 0; attempt < 3 && !success; attempt++) {
|
||||||
if (attempt > 0) {
|
if (attempt > 0) {
|
||||||
Serial.printf("[%lu] [SCT] Retrying stream (attempt %d)...\n", millis(), attempt + 1);
|
LOG_DBG("SCT", "Retrying stream (attempt %d)...", attempt + 1);
|
||||||
delay(50); // Brief delay before retry
|
delay(50); // Brief delay before retry
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any incomplete file from previous attempt before retrying
|
// Remove any incomplete file from previous attempt before retrying
|
||||||
if (SdMan.exists(tmpHtmlPath.c_str())) {
|
if (Storage.exists(tmpHtmlPath.c_str())) {
|
||||||
SdMan.remove(tmpHtmlPath.c_str());
|
Storage.remove(tmpHtmlPath.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
FsFile tmpHtml;
|
FsFile tmpHtml;
|
||||||
if (!SdMan.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
|
if (!Storage.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
success = epub->readItemContentsToStream(localPath, tmpHtml, 1024);
|
success = epub->readItemContentsToStream(localPath, tmpHtml, 1024);
|
||||||
@@ -160,20 +161,20 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
tmpHtml.close();
|
tmpHtml.close();
|
||||||
|
|
||||||
// If streaming failed, remove the incomplete file immediately
|
// If streaming failed, remove the incomplete file immediately
|
||||||
if (!success && SdMan.exists(tmpHtmlPath.c_str())) {
|
if (!success && Storage.exists(tmpHtmlPath.c_str())) {
|
||||||
SdMan.remove(tmpHtmlPath.c_str());
|
Storage.remove(tmpHtmlPath.c_str());
|
||||||
Serial.printf("[%lu] [SCT] Removed incomplete temp file after failed attempt\n", millis());
|
LOG_DBG("SCT", "Removed incomplete temp file after failed attempt");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Serial.printf("[%lu] [SCT] Failed to stream item contents to temp file after retries\n", millis());
|
LOG_ERR("SCT", "Failed to stream item contents to temp file after retries");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [SCT] Streamed temp HTML to %s (%d bytes)\n", millis(), tmpHtmlPath.c_str(), fileSize);
|
LOG_DBG("SCT", "Streamed temp HTML to %s (%d bytes)", tmpHtmlPath.c_str(), fileSize);
|
||||||
|
|
||||||
if (!SdMan.openFileForWrite("SCT", filePath, file)) {
|
if (!Storage.openFileForWrite("SCT", filePath, file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||||
@@ -188,11 +189,11 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
Hyphenator::setPreferredLanguage(epub->getLanguage());
|
Hyphenator::setPreferredLanguage(epub->getLanguage());
|
||||||
success = visitor.parseAndBuildPages();
|
success = visitor.parseAndBuildPages();
|
||||||
|
|
||||||
SdMan.remove(tmpHtmlPath.c_str());
|
Storage.remove(tmpHtmlPath.c_str());
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Serial.printf("[%lu] [SCT] Failed to parse XML and build pages\n", millis());
|
LOG_ERR("SCT", "Failed to parse XML and build pages");
|
||||||
file.close();
|
file.close();
|
||||||
SdMan.remove(filePath.c_str());
|
Storage.remove(filePath.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,9 +209,9 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasFailedLutRecords) {
|
if (hasFailedLutRecords) {
|
||||||
Serial.printf("[%lu] [SCT] Failed to write LUT due to invalid page positions\n", millis());
|
LOG_ERR("SCT", "Failed to write LUT due to invalid page positions");
|
||||||
file.close();
|
file.close();
|
||||||
SdMan.remove(filePath.c_str());
|
Storage.remove(filePath.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +224,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Page> Section::loadPageFromSectionFile() {
|
std::unique_ptr<Page> Section::loadPageFromSectionFile() {
|
||||||
if (!SdMan.openFileForRead("SCT", filePath, file)) {
|
if (!Storage.openFileForRead("SCT", filePath, file)) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
#include "TextBlock.h"
|
#include "TextBlock.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
#include <Logging.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
|
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
|
||||||
// Validate iterator bounds before rendering
|
// Validate iterator bounds before rendering
|
||||||
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
|
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
|
||||||
Serial.printf("[%lu] [TXB] Render skipped: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(),
|
LOG_ERR("TXB", "Render skipped: size mismatch (words=%u, xpos=%u, styles=%u)\n", (uint32_t)words.size(),
|
||||||
(uint32_t)words.size(), (uint32_t)wordXpos.size(), (uint32_t)wordStyles.size());
|
(uint32_t)wordXpos.size(), (uint32_t)wordStyles.size());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,8 +50,8 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
|||||||
|
|
||||||
bool TextBlock::serialize(FsFile& file) const {
|
bool TextBlock::serialize(FsFile& file) const {
|
||||||
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
|
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
|
||||||
Serial.printf("[%lu] [TXB] Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(),
|
LOG_ERR("TXB", "Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u)\n", words.size(),
|
||||||
words.size(), wordXpos.size(), wordStyles.size());
|
wordXpos.size(), wordStyles.size());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +90,7 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
|||||||
|
|
||||||
// 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)
|
||||||
if (wc > 10000) {
|
if (wc > 10000) {
|
||||||
Serial.printf("[%lu] [TXB] Deserialization failed: word count %u exceeds maximum\n", millis(), wc);
|
LOG_ERR("TXB", "Deserialization failed: word count %u exceeds maximum", wc);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <EpdFontFamily.h>
|
#include <EpdFontFamily.h>
|
||||||
#include <SdFat.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <list>
|
#include <list>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "CssParser.h"
|
#include "CssParser.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
@@ -449,7 +449,7 @@ void CssParser::processRuleBlock(const std::string& selectorGroup, const std::st
|
|||||||
|
|
||||||
bool CssParser::loadFromStream(FsFile& source) {
|
bool CssParser::loadFromStream(FsFile& source) {
|
||||||
if (!source) {
|
if (!source) {
|
||||||
Serial.printf("[%lu] [CSS] Cannot read from invalid file\n", millis());
|
LOG_ERR("CSS", "Cannot read from invalid file");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,7 +470,7 @@ bool CssParser::loadFromStream(FsFile& source) {
|
|||||||
processRuleBlock(selector, body);
|
processRuleBlock(selector, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [CSS] Parsed %zu rules\n", millis(), rulesBySelector_.size());
|
LOG_DBG("CSS", "Parsed %zu rules", rulesBySelector_.size());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,7 +582,7 @@ bool CssParser::saveToCache(FsFile& file) const {
|
|||||||
file.write(reinterpret_cast<const uint8_t*>(&definedBits), sizeof(definedBits));
|
file.write(reinterpret_cast<const uint8_t*>(&definedBits), sizeof(definedBits));
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [CSS] Saved %u rules to cache\n", millis(), ruleCount);
|
LOG_DBG("CSS", "Saved %u rules to cache", ruleCount);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,7 +597,7 @@ bool CssParser::loadFromCache(FsFile& file) {
|
|||||||
// Read and verify version
|
// Read and verify version
|
||||||
uint8_t version = 0;
|
uint8_t version = 0;
|
||||||
if (file.read(&version, 1) != 1 || version != CSS_CACHE_VERSION) {
|
if (file.read(&version, 1) != 1 || version != CSS_CACHE_VERSION) {
|
||||||
Serial.printf("[%lu] [CSS] Cache version mismatch (got %u, expected %u)\n", millis(), version, CSS_CACHE_VERSION);
|
LOG_DBG("CSS", "Cache version mismatch (got %u, expected %u)", version, CSS_CACHE_VERSION);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -694,6 +694,6 @@ bool CssParser::loadFromCache(FsFile& file) {
|
|||||||
rulesBySelector_[selector] = style;
|
rulesBySelector_[selector] = style;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [CSS] Loaded %u rules from cache\n", millis(), ruleCount);
|
LOG_DBG("CSS", "Loaded %u rules from cache", ruleCount);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <SdFat.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|||||||
76
lib/Epub/Epub/htmlEntities.cpp
Normal file
76
lib/Epub/Epub/htmlEntities.cpp
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// from
|
||||||
|
// https://github.com/atomic14/diy-esp32-epub-reader/blob/2c2f57fdd7e2a788d14a0bcb26b9e845a47aac42/lib/Epub/RubbishHtmlParser/htmlEntities.cpp
|
||||||
|
|
||||||
|
#include "htmlEntities.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
struct EntityPair {
|
||||||
|
const char* key;
|
||||||
|
const char* value;
|
||||||
|
};
|
||||||
|
|
||||||
|
static const EntityPair ENTITY_LOOKUP[] = {
|
||||||
|
{""", "\""}, {"⁄", "⁄"}, {"&", "&"}, {"<", "<"}, {">", ">"},
|
||||||
|
{"À", "À"}, {"Á", "Á"}, {"Â", "Â"}, {"Ã", "Ã"}, {"Ä", "Ä"},
|
||||||
|
{"Å", "Å"}, {"Æ", "Æ"}, {"Ç", "Ç"}, {"È", "È"}, {"É", "É"},
|
||||||
|
{"Ê", "Ê"}, {"Ë", "Ë"}, {"Ì", "Ì"}, {"Í", "Í"}, {"Î", "Î"},
|
||||||
|
{"Ï", "Ï"}, {"Ð", "Ð"}, {"Ñ", "Ñ"}, {"Ò", "Ò"}, {"Ó", "Ó"},
|
||||||
|
{"Ô", "Ô"}, {"Õ", "Õ"}, {"Ö", "Ö"}, {"Ø", "Ø"}, {"Ù", "Ù"},
|
||||||
|
{"Ú", "Ú"}, {"Û", "Û"}, {"Ü", "Ü"}, {"Ý", "Ý"}, {"Þ", "Þ"},
|
||||||
|
{"ß", "ß"}, {"à", "à"}, {"á", "á"}, {"â", "â"}, {"ã", "ã"},
|
||||||
|
{"ä", "ä"}, {"å", "å"}, {"æ", "æ"}, {"ç", "ç"}, {"è", "è"},
|
||||||
|
{"é", "é"}, {"ê", "ê"}, {"ë", "ë"}, {"ì", "ì"}, {"í", "í"},
|
||||||
|
{"î", "î"}, {"ï", "ï"}, {"ð", "ð"}, {"ñ", "ñ"}, {"ò", "ò"},
|
||||||
|
{"ó", "ó"}, {"ô", "ô"}, {"õ", "õ"}, {"ö", "ö"}, {"ø", "ø"},
|
||||||
|
{"ù", "ù"}, {"ú", "ú"}, {"û", "û"}, {"ü", "ü"}, {"ý", "ý"},
|
||||||
|
{"þ", "þ"}, {"ÿ", "ÿ"}, {" ", "\xC2\xA0"}, {"¡", "¡"}, {"¢", "¢"},
|
||||||
|
{"£", "£"}, {"¤", "¤"}, {"¥", "¥"}, {"¦", "¦"}, {"§", "§"},
|
||||||
|
{"¨", "¨"}, {"©", "©"}, {"ª", "ª"}, {"«", "«"}, {"¬", "¬"},
|
||||||
|
{"­", ""}, {"®", "®"}, {"¯", "¯"}, {"°", "°"}, {"±", "±"},
|
||||||
|
{"²", "²"}, {"³", "³"}, {"´", "´"}, {"µ", "µ"}, {"¶", "¶"},
|
||||||
|
{"¸", "¸"}, {"¹", "¹"}, {"º", "º"}, {"»", "»"}, {"¼", "¼"},
|
||||||
|
{"½", "½"}, {"¾", "¾"}, {"¿", "¿"}, {"×", "×"}, {"÷", "÷"},
|
||||||
|
{"∀", "∀"}, {"∂", "∂"}, {"∃", "∃"}, {"∅", "∅"}, {"∇", "∇"},
|
||||||
|
{"∈", "∈"}, {"∉", "∉"}, {"∋", "∋"}, {"∏", "∏"}, {"∑", "∑"},
|
||||||
|
{"−", "−"}, {"∗", "∗"}, {"√", "√"}, {"∝", "∝"}, {"∞", "∞"},
|
||||||
|
{"∠", "∠"}, {"∧", "∧"}, {"∨", "∨"}, {"∩", "∩"}, {"∪", "∪"},
|
||||||
|
{"∫", "∫"}, {"∴", "∴"}, {"∼", "∼"}, {"≅", "≅"}, {"≈", "≈"},
|
||||||
|
{"≠", "≠"}, {"≡", "≡"}, {"≤", "≤"}, {"≥", "≥"}, {"⊂", "⊂"},
|
||||||
|
{"⊃", "⊃"}, {"⊄", "⊄"}, {"⊆", "⊆"}, {"⊇", "⊇"}, {"⊕", "⊕"},
|
||||||
|
{"⊗", "⊗"}, {"⊥", "⊥"}, {"⋅", "⋅"}, {"Α", "Α"}, {"Β", "Β"},
|
||||||
|
{"Γ", "Γ"}, {"Δ", "Δ"}, {"Ε", "Ε"}, {"Ζ", "Ζ"}, {"Η", "Η"},
|
||||||
|
{"Θ", "Θ"}, {"Ι", "Ι"}, {"Κ", "Κ"}, {"Λ", "Λ"}, {"Μ", "Μ"},
|
||||||
|
{"Ν", "Ν"}, {"Ξ", "Ξ"}, {"Ο", "Ο"}, {"Π", "Π"}, {"Ρ", "Ρ"},
|
||||||
|
{"Σ", "Σ"}, {"Τ", "Τ"}, {"Υ", "Υ"}, {"Φ", "Φ"}, {"Χ", "Χ"},
|
||||||
|
{"Ψ", "Ψ"}, {"Ω", "Ω"}, {"α", "α"}, {"β", "β"}, {"γ", "γ"},
|
||||||
|
{"δ", "δ"}, {"ε", "ε"}, {"ζ", "ζ"}, {"η", "η"}, {"θ", "θ"},
|
||||||
|
{"ι", "ι"}, {"κ", "κ"}, {"λ", "λ"}, {"μ", "μ"}, {"ν", "ν"},
|
||||||
|
{"ξ", "ξ"}, {"ο", "ο"}, {"π", "π"}, {"ρ", "ρ"}, {"ς", "ς"},
|
||||||
|
{"σ", "σ"}, {"τ", "τ"}, {"υ", "υ"}, {"φ", "φ"}, {"χ", "χ"},
|
||||||
|
{"ψ", "ψ"}, {"ω", "ω"}, {"ϑ", "ϑ"}, {"ϒ", "ϒ"}, {"ϖ", "ϖ"},
|
||||||
|
{"Œ", "Œ"}, {"œ", "œ"}, {"Š", "Š"}, {"š", "š"}, {"Ÿ", "Ÿ"},
|
||||||
|
{"ƒ", "ƒ"}, {"ˆ", "ˆ"}, {"˜", "˜"}, {" ", " "}, {" ", " "},
|
||||||
|
{" ", " "}, {"‌", ""}, {"‍", ""}, {"‎", ""}, {"‏", ""},
|
||||||
|
{"–", "–"}, {"—", "—"}, {"‘", "‘"}, {"’", "’"}, {"‚", "‚"},
|
||||||
|
{"“", "“"}, {"”", "”"}, {"„", "„"}, {"†", "†"}, {"‡", "‡"},
|
||||||
|
{"•", "•"}, {"…", "…"}, {"‰", "‰"}, {"′", "′"}, {"″", "″"},
|
||||||
|
{"‹", "‹"}, {"›", "›"}, {"‾", "‾"}, {"€", "€"}, {"™", "™"},
|
||||||
|
{"←", "←"}, {"↑", "↑"}, {"→", "→"}, {"↓", "↓"}, {"↔", "↔"},
|
||||||
|
{"↵", "↵"}, {"⌈", "⌈"}, {"⌉", "⌉"}, {"⌊", "⌊"}, {"⌋", "⌋"},
|
||||||
|
{"◊", "◊"}, {"♠", "♠"}, {"♣", "♣"}, {"♥", "♥"}, {"♦", "♦"}};
|
||||||
|
|
||||||
|
static const size_t ENTITY_LOOKUP_COUNT = sizeof(ENTITY_LOOKUP) / sizeof(ENTITY_LOOKUP[0]);
|
||||||
|
|
||||||
|
// Lookup a single HTML entity and return its UTF-8 value
|
||||||
|
const char* lookupHtmlEntity(const char* entity, int len) {
|
||||||
|
for (size_t i = 0; i < ENTITY_LOOKUP_COUNT; i++) {
|
||||||
|
const char* key = ENTITY_LOOKUP[i].key;
|
||||||
|
const size_t keyLen = strlen(key);
|
||||||
|
if (static_cast<size_t>(len) == keyLen && memcmp(entity, key, keyLen) == 0) {
|
||||||
|
return ENTITY_LOOKUP[i].value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr; // Entity not found
|
||||||
|
}
|
||||||
9
lib/Epub/Epub/htmlEntities.h
Normal file
9
lib/Epub/Epub/htmlEntities.h
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// from
|
||||||
|
// https://github.com/atomic14/diy-esp32-epub-reader/blob/2c2f57fdd7e2a788d14a0bcb26b9e845a47aac42/lib/Epub/RubbishHtmlParser/htmlEntities.cpp
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// Lookup a single HTML entity (including & and ;) and return its UTF-8 value
|
||||||
|
// Returns nullptr if entity is not found
|
||||||
|
const char* lookupHtmlEntity(const char* entity, int len);
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "generated/hyph-en.trie.h"
|
#include "generated/hyph-en.trie.h"
|
||||||
#include "generated/hyph-es.trie.h"
|
#include "generated/hyph-es.trie.h"
|
||||||
#include "generated/hyph-fr.trie.h"
|
#include "generated/hyph-fr.trie.h"
|
||||||
|
#include "generated/hyph-it.trie.h"
|
||||||
#include "generated/hyph-ru.trie.h"
|
#include "generated/hyph-ru.trie.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -18,15 +19,17 @@ LanguageHyphenator frenchHyphenator(fr_patterns, isLatinLetter, toLowerLatin);
|
|||||||
LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin);
|
LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin);
|
||||||
LanguageHyphenator russianHyphenator(ru_ru_patterns, isCyrillicLetter, toLowerCyrillic);
|
LanguageHyphenator russianHyphenator(ru_ru_patterns, isCyrillicLetter, toLowerCyrillic);
|
||||||
LanguageHyphenator spanishHyphenator(es_patterns, isLatinLetter, toLowerLatin);
|
LanguageHyphenator spanishHyphenator(es_patterns, isLatinLetter, toLowerLatin);
|
||||||
|
LanguageHyphenator italianHyphenator(it_patterns, isLatinLetter, toLowerLatin);
|
||||||
|
|
||||||
using EntryArray = std::array<LanguageEntry, 5>;
|
using EntryArray = std::array<LanguageEntry, 6>;
|
||||||
|
|
||||||
const EntryArray& entries() {
|
const EntryArray& entries() {
|
||||||
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
|
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
|
||||||
{"french", "fr", &frenchHyphenator},
|
{"french", "fr", &frenchHyphenator},
|
||||||
{"german", "de", &germanHyphenator},
|
{"german", "de", &germanHyphenator},
|
||||||
{"russian", "ru", &russianHyphenator},
|
{"russian", "ru", &russianHyphenator},
|
||||||
{"spanish", "es", &spanishHyphenator}}};
|
{"spanish", "es", &spanishHyphenator},
|
||||||
|
{"italian", "it", &italianHyphenator}}};
|
||||||
return kEntries;
|
return kEntries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
113
lib/Epub/Epub/hyphenation/generated/hyph-it.trie.h
Normal file
113
lib/Epub/Epub/hyphenation/generated/hyph-it.trie.h
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "../SerializedHyphenationTrie.h"
|
||||||
|
|
||||||
|
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
||||||
|
alignas(4) constexpr uint8_t it_trie_data[] = {
|
||||||
|
0x00, 0x00, 0x05, 0xC4, 0x17, 0x0C, 0x33, 0x35, 0x0C, 0x29, 0x22, 0x0D, 0x3E, 0x0B, 0x47, 0x20,
|
||||||
|
0x0D, 0x16, 0x0B, 0x34, 0x0D, 0x21, 0x0C, 0x3D, 0x1F, 0x0C, 0x2A, 0x17, 0x2A, 0x0B, 0x02, 0x0C,
|
||||||
|
0x01, 0x02, 0x16, 0x02, 0x0D, 0x0C, 0x0C, 0x0D, 0x03, 0x0C, 0x01, 0x0C, 0x0E, 0x0D, 0x04, 0x02,
|
||||||
|
0x0B, 0xA0, 0x00, 0x42, 0x21, 0x6E, 0xFD, 0xA0, 0x00, 0x72, 0x21, 0x6E, 0xFD, 0xA1, 0x00, 0x61,
|
||||||
|
0x6D, 0xFD, 0x21, 0x69, 0xFB, 0x21, 0x74, 0xFD, 0x22, 0x70, 0x6E, 0xEC, 0xFD, 0xA0, 0x00, 0x91,
|
||||||
|
0x21, 0x6F, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x00, 0xA2, 0x21, 0x73, 0xFD, 0x21, 0x70, 0xFD, 0xA0,
|
||||||
|
0x00, 0xC2, 0x21, 0x6D, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x72, 0xFD, 0xA0, 0x00,
|
||||||
|
0xE1, 0x21, 0x6F, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0xA3, 0x01, 0x11,
|
||||||
|
0x61, 0x69, 0x6F, 0xDF, 0xEE, 0xFD, 0xA0, 0x00, 0xF2, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21,
|
||||||
|
0x69, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0xA1, 0x01, 0x11, 0x69, 0xFD, 0xA0, 0x01, 0x12,
|
||||||
|
0x21, 0x75, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x78, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x6B, 0xFD, 0x21,
|
||||||
|
0x6E, 0xFD, 0xA0, 0x00, 0x71, 0x21, 0x65, 0xFD, 0x22, 0x61, 0x65, 0xF7, 0xFD, 0x21, 0x72, 0xFB,
|
||||||
|
0xA0, 0x01, 0x52, 0x21, 0x61, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x70, 0xFD, 0x21, 0x69, 0xFD, 0xA0,
|
||||||
|
0x01, 0x71, 0x21, 0x6F, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD, 0xA0, 0x00,
|
||||||
|
0x61, 0x21, 0x6F, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x70, 0xFF, 0x50, 0x21, 0x6F, 0xFC, 0x21, 0x74,
|
||||||
|
0xFD, 0x22, 0x70, 0x72, 0xF3, 0xFD, 0x21, 0x61, 0xE8, 0x21, 0x72, 0xFD, 0xA0, 0x00, 0xF1, 0x22,
|
||||||
|
0x6C, 0x72, 0xFD, 0xFD, 0x21, 0x69, 0xE3, 0x21, 0x6C, 0xFD, 0x41, 0x65, 0xFF, 0x43, 0xA0, 0x01,
|
||||||
|
0x11, 0x25, 0x61, 0x68, 0x6F, 0x72, 0x73, 0xE8, 0xEE, 0xF6, 0xF9, 0xFD, 0xA0, 0x01, 0x82, 0x21,
|
||||||
|
0x72, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x65, 0xFD, 0xA0, 0x01,
|
||||||
|
0xA2, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD, 0x41, 0x75, 0xFF, 0x4C, 0x42, 0x6C,
|
||||||
|
0x72, 0xFF, 0xFC, 0xFF, 0x48, 0x21, 0x62, 0xF9, 0x22, 0x68, 0x75, 0xEF, 0xFD, 0x47, 0x63, 0x64,
|
||||||
|
0x6C, 0x6E, 0x70, 0x72, 0x74, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF,
|
||||||
|
0x5C, 0xFF, 0x5C, 0x21, 0x73, 0xEA, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0xA1, 0x01, 0x11, 0x72,
|
||||||
|
0xFD, 0x41, 0x6E, 0xFF, 0x15, 0x21, 0x67, 0xFC, 0xA0, 0x01, 0xC2, 0x21, 0x74, 0xFD, 0x21, 0x6C,
|
||||||
|
0xFD, 0x22, 0x61, 0x65, 0xF4, 0xFD, 0x52, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x6C, 0x6E, 0x6F,
|
||||||
|
0x70, 0x72, 0x73, 0x74, 0x77, 0x68, 0x6A, 0x6B, 0x7A, 0xFE, 0xC2, 0xFE, 0xCD, 0xFE, 0xF7, 0xFF,
|
||||||
|
0x12, 0xFF, 0x20, 0xFF, 0x37, 0xFF, 0x46, 0xFF, 0x55, 0xFF, 0x6B, 0xFF, 0x8B, 0xFF, 0xA5, 0xFF,
|
||||||
|
0xC2, 0xFF, 0xE6, 0xFF, 0xFB, 0xFF, 0x88, 0xFF, 0x88, 0xFF, 0x88, 0xFF, 0x88, 0xA0, 0x01, 0xE2,
|
||||||
|
0xA0, 0x00, 0xD1, 0x24, 0x61, 0x65, 0x6F, 0x75, 0xFD, 0xFD, 0xFD, 0xFD, 0x21, 0x6F, 0xF4, 0x21,
|
||||||
|
0x61, 0xF1, 0xA0, 0x01, 0xE1, 0x21, 0x2E, 0xFD, 0x24, 0x69, 0x75, 0x79, 0x74, 0xEB, 0xF4, 0xF7,
|
||||||
|
0xFD, 0x21, 0x75, 0xDF, 0xA0, 0x00, 0x51, 0x22, 0x69, 0x77, 0xFA, 0xFD, 0x21, 0x69, 0xD7, 0xAE,
|
||||||
|
0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x76, 0x6C, 0x72, 0x2E, 0x27,
|
||||||
|
0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xF5, 0xF5, 0xE3, 0xE3, 0x22, 0x2E,
|
||||||
|
0x27, 0xC4, 0xC7, 0xC6, 0x00, 0x51, 0x68, 0x2E, 0x27, 0x62, 0x72, 0x6E, 0xFF, 0xBF, 0xFF, 0xBF,
|
||||||
|
0xFF, 0xFB, 0xFF, 0xBF, 0xFE, 0xFB, 0xFF, 0xBF, 0xD0, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x6B,
|
||||||
|
0x6D, 0x6E, 0x71, 0x73, 0x74, 0x7A, 0x68, 0x6C, 0x72, 0x2E, 0x27, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF,
|
||||||
|
0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF,
|
||||||
|
0xAA, 0xFF, 0xEB, 0xFF, 0xBC, 0xFF, 0xBC, 0xFF, 0xAA, 0xFF, 0xAA, 0xCE, 0x02, 0x01, 0x62, 0x64,
|
||||||
|
0x67, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x2E, 0x27, 0xFF, 0x77, 0xFF, 0x77,
|
||||||
|
0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x89, 0xFF, 0x77, 0xFF, 0x77,
|
||||||
|
0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xCA, 0x02, 0x01, 0x62, 0x67, 0x66, 0x6E, 0x6C,
|
||||||
|
0x72, 0x73, 0x74, 0x2E, 0x27, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x5C, 0xFF,
|
||||||
|
0x5C, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xA0, 0x02, 0x12, 0xA1, 0x00, 0x51, 0x74,
|
||||||
|
0xFD, 0xD1, 0x02, 0x01, 0x62, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74,
|
||||||
|
0x76, 0x77, 0x7A, 0x2E, 0x27, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0xFB, 0xFF,
|
||||||
|
0x33, 0xFF, 0x21, 0xFF, 0x33, 0xFF, 0x21, 0xFF, 0x33, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF,
|
||||||
|
0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0x41, 0x70, 0xFD, 0x4D, 0xCB, 0x02, 0x01, 0x62, 0x64,
|
||||||
|
0x68, 0x69, 0x6C, 0x6D, 0x6E, 0x72, 0x76, 0x2E, 0x27, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFF,
|
||||||
|
0xFC, 0xFE, 0xF9, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xC2,
|
||||||
|
0x02, 0x01, 0x2E, 0x27, 0xFE, 0xC3, 0xFE, 0xC3, 0xCB, 0x02, 0x01, 0x67, 0x66, 0x68, 0x6B, 0x6C,
|
||||||
|
0x6D, 0x72, 0x73, 0x74, 0x2E, 0x27, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xCC, 0xFE, 0xBA, 0xFE, 0xCC,
|
||||||
|
0xFE, 0xBA, 0xFE, 0xCC, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xBA, 0xA0, 0x02, 0x33, 0x42,
|
||||||
|
0x2E, 0x27, 0xFE, 0x93, 0xFE, 0x93, 0xD5, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A,
|
||||||
|
0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x7A, 0x2E, 0x27, 0xFE, 0x8C,
|
||||||
|
0xFE, 0x8C, 0xFE, 0x8C, 0xFF, 0xF6, 0xFE, 0x8C, 0xFE, 0x9E, 0xFE, 0x9E, 0xFE, 0x8C, 0xFE, 0x8C,
|
||||||
|
0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C,
|
||||||
|
0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFF, 0xF9, 0xCF, 0x02, 0x01, 0x62, 0x63, 0x66, 0x6C, 0x6D,
|
||||||
|
0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x2E, 0x27, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A,
|
||||||
|
0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A,
|
||||||
|
0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xA0, 0x02, 0x62, 0xA1, 0x01, 0xE1, 0x6E, 0xFD,
|
||||||
|
0x21, 0x72, 0xF8, 0x21, 0x65, 0xFD, 0xA1, 0x01, 0xE1, 0x66, 0xFD, 0x41, 0x74, 0xFE, 0x07, 0x21,
|
||||||
|
0x69, 0xFC, 0x21, 0x65, 0xFD, 0xD3, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6B, 0x6C, 0x6D,
|
||||||
|
0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x7A, 0x68, 0x2E, 0x27, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD,
|
||||||
|
0xFD, 0xFD, 0xFD, 0xFF, 0xE6, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD,
|
||||||
|
0xFD, 0xFD, 0xFD, 0xFF, 0xF1, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFF, 0xFD, 0xFD, 0xFD, 0xFD,
|
||||||
|
0xFD, 0xA0, 0x02, 0x82, 0xA1, 0x01, 0xE1, 0x65, 0xFD, 0x21, 0x63, 0xF8, 0xA1, 0x01, 0xE1, 0x69,
|
||||||
|
0xFD, 0xCB, 0x02, 0x01, 0x64, 0x68, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x7A, 0x2E, 0x27, 0xFD,
|
||||||
|
0xB1, 0xFD, 0xC3, 0xFD, 0xC3, 0xFF, 0xF3, 0xFD, 0xB1, 0xFD, 0xC3, 0xFF, 0xFB, 0xFD, 0xB1, 0xFD,
|
||||||
|
0xB1, 0xFD, 0xB1, 0xFD, 0xB1, 0xC3, 0x02, 0x01, 0x71, 0x2E, 0x27, 0xFD, 0x8D, 0xFD, 0x8D, 0xFD,
|
||||||
|
0x8D, 0xA0, 0x02, 0x53, 0xA1, 0x01, 0xE1, 0x73, 0xFD, 0xD5, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66,
|
||||||
|
0x68, 0x67, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x78, 0x77, 0x7A, 0x2E,
|
||||||
|
0x27, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x8B, 0xFD, 0x79, 0xFD, 0x79, 0xFD,
|
||||||
|
0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFF, 0xFB, 0xFD,
|
||||||
|
0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0x43, 0x6D, 0x2E, 0x27, 0xFD,
|
||||||
|
0x37, 0xFD, 0x37, 0xFD, 0x37, 0xA0, 0x02, 0xC2, 0xA1, 0x02, 0x32, 0x6D, 0xFD, 0x41, 0x6E, 0xFE,
|
||||||
|
0x8F, 0x4B, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x76, 0xFD, 0x21, 0xFD,
|
||||||
|
0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD,
|
||||||
|
0x21, 0xFD, 0x21, 0xA0, 0x02, 0xE1, 0x22, 0x2E, 0x27, 0xFD, 0xFD, 0xC7, 0x02, 0xA2, 0x68, 0x73,
|
||||||
|
0x70, 0x74, 0x7A, 0x2E, 0x27, 0xFF, 0xC0, 0xFF, 0xCD, 0xFF, 0xD2, 0xFF, 0xD6, 0xFC, 0xF7, 0xFF,
|
||||||
|
0xF8, 0xFF, 0xFB, 0xC1, 0x00, 0x51, 0x2E, 0xFC, 0xDF, 0x41, 0x68, 0xFF, 0x18, 0xA1, 0x00, 0x51,
|
||||||
|
0x63, 0xFC, 0xC1, 0x01, 0xE1, 0x73, 0xFE, 0xB6, 0xC2, 0x00, 0x51, 0x6B, 0x73, 0xFC, 0xCA, 0xFC,
|
||||||
|
0x06, 0xD2, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73,
|
||||||
|
0x74, 0x76, 0x77, 0x7A, 0x2E, 0x27, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1,
|
||||||
|
0xFF, 0xE2, 0xFC, 0xD3, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xD3, 0xFF, 0xEC, 0xFF, 0xF1,
|
||||||
|
0xFC, 0xC1, 0xFC, 0xC1, 0xFF, 0xF7, 0xFC, 0xC1, 0xFE, 0x2E, 0xC6, 0x02, 0x01, 0x63, 0x6C, 0x72,
|
||||||
|
0x76, 0x2E, 0x27, 0xFC, 0x88, 0xFC, 0x9A, 0xFC, 0x9A, 0xFC, 0x88, 0xFC, 0x88, 0xFD, 0xF5, 0x41,
|
||||||
|
0x72, 0xFB, 0xAF, 0xA0, 0x02, 0xF2, 0xC5, 0x02, 0x01, 0x68, 0x61, 0x79, 0x2E, 0x27, 0xFC, 0x7E,
|
||||||
|
0xFF, 0xF9, 0xFF, 0xFD, 0xFC, 0x6C, 0xFC, 0x6C, 0xCA, 0x02, 0x01, 0x62, 0x63, 0x66, 0x68, 0x6D,
|
||||||
|
0x70, 0x74, 0x77, 0x2E, 0x27, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC,
|
||||||
|
0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0x42, 0x6F, 0x69, 0xFC, 0x48, 0xFC, 0x27,
|
||||||
|
0xCB, 0x02, 0x01, 0x62, 0x64, 0x6C, 0x6E, 0x70, 0x74, 0x73, 0x76, 0x7A, 0x2E, 0x27, 0xFC, 0x32,
|
||||||
|
0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32,
|
||||||
|
0xFC, 0x32, 0xFD, 0x9F, 0x5A, 0x2E, 0x27, 0x61, 0x65, 0x6F, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68,
|
||||||
|
0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xFB,
|
||||||
|
0xC2, 0xFB, 0xF9, 0xFC, 0x14, 0xFC, 0x23, 0xFC, 0x28, 0xFC, 0x2B, 0xFC, 0x64, 0xFC, 0x97, 0xFC,
|
||||||
|
0xC4, 0xFC, 0xED, 0xFD, 0x27, 0xFD, 0x4B, 0xFD, 0x54, 0xFD, 0x82, 0xFD, 0xC4, 0xFE, 0x11, 0xFE,
|
||||||
|
0x5D, 0xFE, 0x81, 0xFE, 0x95, 0xFF, 0x17, 0xFF, 0x4D, 0xFF, 0x86, 0xFF, 0xA2, 0xFF, 0xB4, 0xFF,
|
||||||
|
0xD5, 0xFF, 0xDC,
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr SerializedHyphenationPatterns it_patterns = {
|
||||||
|
it_trie_data,
|
||||||
|
sizeof(it_trie_data),
|
||||||
|
};
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
#include "ChapterHtmlSlimParser.h"
|
#include "ChapterHtmlSlimParser.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HalStorage.h>
|
||||||
#include <SDCardManager.h>
|
#include <Logging.h>
|
||||||
#include <expat.h>
|
#include <expat.h>
|
||||||
|
|
||||||
#include "../Page.h"
|
#include "../Page.h"
|
||||||
|
#include "../htmlEntities.h"
|
||||||
|
|
||||||
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
|
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
|
||||||
constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
|
constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
|
||||||
|
|
||||||
// Minimum file size (in bytes) to show indexing popup - smaller chapters don't benefit from it
|
// Minimum file size (in bytes) to show indexing popup - smaller chapters don't benefit from it
|
||||||
constexpr size_t MIN_SIZE_FOR_POPUP = 50 * 1024; // 50KB
|
constexpr size_t MIN_SIZE_FOR_POPUP = 10 * 1024; // 10KB
|
||||||
|
|
||||||
const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
|
const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
|
||||||
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
|
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
|
||||||
@@ -168,7 +169,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [EHP] Image alt: %s\n", millis(), alt.c_str());
|
LOG_DBG("EHP", "Image alt: %s", alt.c_str());
|
||||||
|
|
||||||
self->startNewTextBlock(centeredBlockStyle);
|
self->startNewTextBlock(centeredBlockStyle);
|
||||||
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
|
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
|
||||||
@@ -359,6 +360,28 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect U+00A0 (non-breaking space): UTF-8 encoding is 0xC2 0xA0
|
||||||
|
// Render a visible space without allowing a line break around it.
|
||||||
|
if (static_cast<uint8_t>(s[i]) == 0xC2 && i + 1 < len && static_cast<uint8_t>(s[i + 1]) == 0xA0) {
|
||||||
|
// Flush any pending text so style is applied correctly.
|
||||||
|
if (self->partWordBufferIndex > 0) {
|
||||||
|
self->flushPartWordBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a standalone space that attaches to the previous word.
|
||||||
|
self->partWordBuffer[0] = ' ';
|
||||||
|
self->partWordBuffer[1] = '\0';
|
||||||
|
self->partWordBufferIndex = 1;
|
||||||
|
self->nextWordContinues = true; // Attach space to previous word (no break).
|
||||||
|
self->flushPartWordBuffer();
|
||||||
|
|
||||||
|
// Ensure the next real word attaches to this space (no break).
|
||||||
|
self->nextWordContinues = true;
|
||||||
|
|
||||||
|
i++; // Skip the second byte (0xA0)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip Zero Width No-Break Space / BOM (U+FEFF) = 0xEF 0xBB 0xBF
|
// Skip Zero Width No-Break Space / BOM (U+FEFF) = 0xEF 0xBB 0xBF
|
||||||
const XML_Char FEFF_BYTE_1 = static_cast<XML_Char>(0xEF);
|
const XML_Char FEFF_BYTE_1 = static_cast<XML_Char>(0xEF);
|
||||||
const XML_Char FEFF_BYTE_2 = static_cast<XML_Char>(0xBB);
|
const XML_Char FEFF_BYTE_2 = static_cast<XML_Char>(0xBB);
|
||||||
@@ -386,13 +409,29 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
|||||||
// memory.
|
// memory.
|
||||||
// Spotted when reading Intermezzo, there are some really long text blocks in there.
|
// Spotted when reading Intermezzo, there are some really long text blocks in there.
|
||||||
if (self->currentTextBlock->size() > 750) {
|
if (self->currentTextBlock->size() > 750) {
|
||||||
Serial.printf("[%lu] [EHP] Text block too long, splitting into multiple pages\n", millis());
|
LOG_DBG("EHP", "Text block too long, splitting into multiple pages");
|
||||||
self->currentTextBlock->layoutAndExtractLines(
|
self->currentTextBlock->layoutAndExtractLines(
|
||||||
self->renderer, self->fontId, self->viewportWidth,
|
self->renderer, self->fontId, self->viewportWidth,
|
||||||
[self](const std::shared_ptr<TextBlock>& textBlock) { self->addLineToPage(textBlock); }, false);
|
[self](const std::shared_ptr<TextBlock>& textBlock) { self->addLineToPage(textBlock); }, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void XMLCALL ChapterHtmlSlimParser::defaultHandlerExpand(void* userData, const XML_Char* s, const int len) {
|
||||||
|
// Check if this looks like an entity reference (&...;)
|
||||||
|
if (len >= 3 && s[0] == '&' && s[len - 1] == ';') {
|
||||||
|
const char* utf8Value = lookupHtmlEntity(s, len);
|
||||||
|
if (utf8Value != nullptr) {
|
||||||
|
// Known entity: expand to its UTF-8 value
|
||||||
|
characterData(userData, utf8Value, strlen(utf8Value));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Unknown entity: preserve original &...; sequence
|
||||||
|
characterData(userData, s, len);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Not an entity we recognize - skip it
|
||||||
|
}
|
||||||
|
|
||||||
void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* name) {
|
void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* name) {
|
||||||
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
||||||
|
|
||||||
@@ -477,12 +516,16 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
int done;
|
int done;
|
||||||
|
|
||||||
if (!parser) {
|
if (!parser) {
|
||||||
Serial.printf("[%lu] [EHP] Couldn't allocate memory for parser\n", millis());
|
LOG_ERR("EHP", "Couldn't allocate memory for parser");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle HTML entities (like ) that aren't in XML spec or DTD
|
||||||
|
// Using DefaultHandlerExpand preserves normal entity expansion from DOCTYPE
|
||||||
|
XML_SetDefaultHandlerExpand(parser, defaultHandlerExpand);
|
||||||
|
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (!SdMan.openFileForRead("EHP", filepath, file)) {
|
if (!Storage.openFileForRead("EHP", filepath, file)) {
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -499,7 +542,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
do {
|
do {
|
||||||
void* const buf = XML_GetBuffer(parser, 1024);
|
void* const buf = XML_GetBuffer(parser, 1024);
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
Serial.printf("[%lu] [EHP] Couldn't allocate memory for buffer\n", millis());
|
LOG_ERR("EHP", "Couldn't allocate memory for buffer");
|
||||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
XML_SetCharacterDataHandler(parser, nullptr);
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
@@ -511,7 +554,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
const size_t len = file.read(buf, 1024);
|
const size_t len = file.read(buf, 1024);
|
||||||
|
|
||||||
if (len == 0 && file.available() > 0) {
|
if (len == 0 && file.available() > 0) {
|
||||||
Serial.printf("[%lu] [EHP] File read error\n", millis());
|
LOG_ERR("EHP", "File read error");
|
||||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
XML_SetCharacterDataHandler(parser, nullptr);
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
@@ -523,7 +566,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
done = file.available() == 0;
|
done = file.available() == 0;
|
||||||
|
|
||||||
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
||||||
Serial.printf("[%lu] [EHP] Parse error at line %lu:\n%s\n", millis(), XML_GetCurrentLineNumber(parser),
|
LOG_ERR("EHP", "Parse error at line %lu:\n%s", XML_GetCurrentLineNumber(parser),
|
||||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
@@ -568,7 +611,7 @@ void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr<TextBlock> line) {
|
|||||||
|
|
||||||
void ChapterHtmlSlimParser::makePages() {
|
void ChapterHtmlSlimParser::makePages() {
|
||||||
if (!currentTextBlock) {
|
if (!currentTextBlock) {
|
||||||
Serial.printf("[%lu] [EHP] !! No text block to make pages for !!\n", millis());
|
LOG_ERR("EHP", "!! No text block to make pages for !!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ class ChapterHtmlSlimParser {
|
|||||||
// XML callbacks
|
// XML callbacks
|
||||||
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||||
static void XMLCALL characterData(void* userData, const XML_Char* s, int len);
|
static void XMLCALL characterData(void* userData, const XML_Char* s, int len);
|
||||||
|
static void XMLCALL defaultHandlerExpand(void* userData, const XML_Char* s, int len);
|
||||||
static void XMLCALL endElement(void* userData, const XML_Char* name);
|
static void XMLCALL endElement(void* userData, const XML_Char* name);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
#include "ContainerParser.h"
|
#include "ContainerParser.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
bool ContainerParser::setup() {
|
bool ContainerParser::setup() {
|
||||||
parser = XML_ParserCreate(nullptr);
|
parser = XML_ParserCreate(nullptr);
|
||||||
if (!parser) {
|
if (!parser) {
|
||||||
Serial.printf("[%lu] [CTR] Couldn't allocate memory for parser\n", millis());
|
LOG_ERR("CTR", "Couldn't allocate memory for parser");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ size_t ContainerParser::write(const uint8_t* buffer, const size_t size) {
|
|||||||
while (remainingInBuffer > 0) {
|
while (remainingInBuffer > 0) {
|
||||||
void* const buf = XML_GetBuffer(parser, 1024);
|
void* const buf = XML_GetBuffer(parser, 1024);
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
Serial.printf("[%lu] [CTR] Couldn't allocate buffer\n", millis());
|
LOG_DBG("CTR", "Couldn't allocate buffer");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ size_t ContainerParser::write(const uint8_t* buffer, const size_t size) {
|
|||||||
memcpy(buf, currentBufferPos, toRead);
|
memcpy(buf, currentBufferPos, toRead);
|
||||||
|
|
||||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||||
Serial.printf("[%lu] [CTR] Parse error: %s\n", millis(), XML_ErrorString(XML_GetErrorCode(parser)));
|
LOG_ERR("CTR", "Parse error: %s", XML_ErrorString(XML_GetErrorCode(parser)));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#include "ContentOpfParser.h"
|
#include "ContentOpfParser.h"
|
||||||
|
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <HardwareSerial.h>
|
#include <Logging.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
#include "../BookMetadataCache.h"
|
#include "../BookMetadataCache.h"
|
||||||
@@ -15,7 +15,7 @@ constexpr char itemCacheFile[] = "/.items.bin";
|
|||||||
bool ContentOpfParser::setup() {
|
bool ContentOpfParser::setup() {
|
||||||
parser = XML_ParserCreate(nullptr);
|
parser = XML_ParserCreate(nullptr);
|
||||||
if (!parser) {
|
if (!parser) {
|
||||||
Serial.printf("[%lu] [COF] Couldn't allocate memory for parser\n", millis());
|
LOG_DBG("COF", "Couldn't allocate memory for parser");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,8 +36,8 @@ ContentOpfParser::~ContentOpfParser() {
|
|||||||
if (tempItemStore) {
|
if (tempItemStore) {
|
||||||
tempItemStore.close();
|
tempItemStore.close();
|
||||||
}
|
}
|
||||||
if (SdMan.exists((cachePath + itemCacheFile).c_str())) {
|
if (Storage.exists((cachePath + itemCacheFile).c_str())) {
|
||||||
SdMan.remove((cachePath + itemCacheFile).c_str());
|
Storage.remove((cachePath + itemCacheFile).c_str());
|
||||||
}
|
}
|
||||||
itemIndex.clear();
|
itemIndex.clear();
|
||||||
itemIndex.shrink_to_fit();
|
itemIndex.shrink_to_fit();
|
||||||
@@ -56,7 +56,7 @@ size_t ContentOpfParser::write(const uint8_t* buffer, const size_t size) {
|
|||||||
void* const buf = XML_GetBuffer(parser, 1024);
|
void* const buf = XML_GetBuffer(parser, 1024);
|
||||||
|
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
Serial.printf("[%lu] [COF] Couldn't allocate memory for buffer\n", millis());
|
LOG_ERR("COF", "Couldn't allocate memory for buffer");
|
||||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
XML_SetCharacterDataHandler(parser, nullptr);
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
@@ -69,7 +69,7 @@ size_t ContentOpfParser::write(const uint8_t* buffer, const size_t size) {
|
|||||||
memcpy(buf, currentBufferPos, toRead);
|
memcpy(buf, currentBufferPos, toRead);
|
||||||
|
|
||||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||||
Serial.printf("[%lu] [COF] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
|
LOG_DBG("COF", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
|
||||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
@@ -118,20 +118,16 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
|
|
||||||
if (self->state == IN_PACKAGE && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
|
if (self->state == IN_PACKAGE && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
|
||||||
self->state = IN_MANIFEST;
|
self->state = IN_MANIFEST;
|
||||||
if (!SdMan.openFileForWrite("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
if (!Storage.openFileForWrite("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||||
Serial.printf(
|
LOG_ERR("COF", "Couldn't open temp items file for writing. This is probably going to be a fatal error.");
|
||||||
"[%lu] [COF] Couldn't open temp items file for writing. This is probably going to be a fatal error.\n",
|
|
||||||
millis());
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self->state == IN_PACKAGE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) {
|
if (self->state == IN_PACKAGE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) {
|
||||||
self->state = IN_SPINE;
|
self->state = IN_SPINE;
|
||||||
if (!SdMan.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
if (!Storage.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||||
Serial.printf(
|
LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error.");
|
||||||
"[%lu] [COF] Couldn't open temp items file for reading. This is probably going to be a fatal error.\n",
|
|
||||||
millis());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort item index for binary search if we have enough items
|
// Sort item index for binary search if we have enough items
|
||||||
@@ -140,7 +136,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
return a.idHash < b.idHash || (a.idHash == b.idHash && a.idLen < b.idLen);
|
return a.idHash < b.idHash || (a.idHash == b.idHash && a.idLen < b.idLen);
|
||||||
});
|
});
|
||||||
self->useItemIndex = true;
|
self->useItemIndex = true;
|
||||||
Serial.printf("[%lu] [COF] Using fast index for %zu manifest items\n", millis(), self->itemIndex.size());
|
LOG_DBG("COF", "Using fast index for %zu manifest items", self->itemIndex.size());
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -148,11 +144,9 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
if (self->state == IN_PACKAGE && (strcmp(name, "guide") == 0 || strcmp(name, "opf:guide") == 0)) {
|
if (self->state == IN_PACKAGE && (strcmp(name, "guide") == 0 || strcmp(name, "opf:guide") == 0)) {
|
||||||
self->state = IN_GUIDE;
|
self->state = IN_GUIDE;
|
||||||
// TODO Remove print
|
// TODO Remove print
|
||||||
Serial.printf("[%lu] [COF] Entering guide state.\n", millis());
|
LOG_DBG("COF", "Entering guide state.");
|
||||||
if (!SdMan.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
if (!Storage.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||||
Serial.printf(
|
LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error.");
|
||||||
"[%lu] [COF] Couldn't open temp items file for reading. This is probably going to be a fatal error.\n",
|
|
||||||
millis());
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -214,8 +208,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
if (self->tocNcxPath.empty()) {
|
if (self->tocNcxPath.empty()) {
|
||||||
self->tocNcxPath = href;
|
self->tocNcxPath = href;
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [COF] Warning: Multiple NCX files found in manifest. Ignoring duplicate: %s\n", millis(),
|
LOG_DBG("COF", "Warning: Multiple NCX files found in manifest. Ignoring duplicate: %s", href.c_str());
|
||||||
href.c_str());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +222,15 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
// Properties is space-separated, check if "nav" is present as a word
|
// Properties is space-separated, check if "nav" is present as a word
|
||||||
if (properties == "nav" || properties.find("nav ") == 0 || properties.find(" nav") != std::string::npos) {
|
if (properties == "nav" || properties.find("nav ") == 0 || properties.find(" nav") != std::string::npos) {
|
||||||
self->tocNavPath = href;
|
self->tocNavPath = href;
|
||||||
Serial.printf("[%lu] [COF] Found EPUB 3 nav document: %s\n", millis(), href.c_str());
|
LOG_DBG("COF", "Found EPUB 3 nav document: %s", href.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EPUB 3: Check for cover image (properties contains "cover-image")
|
||||||
|
if (!properties.empty() && self->coverItemHref.empty()) {
|
||||||
|
if (properties == "cover-image" || properties.find("cover-image ") == 0 ||
|
||||||
|
properties.find(" cover-image") != std::string::npos) {
|
||||||
|
self->coverItemHref = href;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -302,7 +303,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
if (type == "text" || type == "start") {
|
if (type == "text" || type == "start") {
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [COF] Skipping non-text reference in guide: %s\n", millis(), type.c_str());
|
LOG_DBG("COF", "Skipping non-text reference in guide: %s", type.c_str());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else if (strcmp(atts[i], "href") == 0) {
|
} else if (strcmp(atts[i], "href") == 0) {
|
||||||
@@ -310,7 +311,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((type == "text" || (type == "start" && !self->textReferenceHref.empty())) && (textHref.length() > 0)) {
|
if ((type == "text" || (type == "start" && !self->textReferenceHref.empty())) && (textHref.length() > 0)) {
|
||||||
Serial.printf("[%lu] [COF] Found %s reference in guide: %s.\n", millis(), type.c_str(), textHref.c_str());
|
LOG_DBG("COF", "Found %s reference in guide: %s.", type.c_str(), textHref.c_str());
|
||||||
self->textReferenceHref = textHref;
|
self->textReferenceHref = textHref;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
#include "TocNavParser.h"
|
#include "TocNavParser.h"
|
||||||
|
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <HardwareSerial.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
#include "../BookMetadataCache.h"
|
#include "../BookMetadataCache.h"
|
||||||
|
|
||||||
bool TocNavParser::setup() {
|
bool TocNavParser::setup() {
|
||||||
parser = XML_ParserCreate(nullptr);
|
parser = XML_ParserCreate(nullptr);
|
||||||
if (!parser) {
|
if (!parser) {
|
||||||
Serial.printf("[%lu] [NAV] Couldn't allocate memory for parser\n", millis());
|
LOG_DBG("NAV", "Couldn't allocate memory for parser");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ size_t TocNavParser::write(const uint8_t* buffer, const size_t size) {
|
|||||||
while (remainingInBuffer > 0) {
|
while (remainingInBuffer > 0) {
|
||||||
void* const buf = XML_GetBuffer(parser, 1024);
|
void* const buf = XML_GetBuffer(parser, 1024);
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
Serial.printf("[%lu] [NAV] Couldn't allocate memory for buffer\n", millis());
|
LOG_DBG("NAV", "Couldn't allocate memory for buffer");
|
||||||
XML_StopParser(parser, XML_FALSE);
|
XML_StopParser(parser, XML_FALSE);
|
||||||
XML_SetElementHandler(parser, nullptr, nullptr);
|
XML_SetElementHandler(parser, nullptr, nullptr);
|
||||||
XML_SetCharacterDataHandler(parser, nullptr);
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
@@ -52,7 +52,7 @@ size_t TocNavParser::write(const uint8_t* buffer, const size_t size) {
|
|||||||
memcpy(buf, currentBufferPos, toRead);
|
memcpy(buf, currentBufferPos, toRead);
|
||||||
|
|
||||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||||
Serial.printf("[%lu] [NAV] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
|
LOG_DBG("NAV", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
|
||||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||||
XML_StopParser(parser, XML_FALSE);
|
XML_StopParser(parser, XML_FALSE);
|
||||||
XML_SetElementHandler(parser, nullptr, nullptr);
|
XML_SetElementHandler(parser, nullptr, nullptr);
|
||||||
@@ -88,7 +88,7 @@ void XMLCALL TocNavParser::startElement(void* userData, const XML_Char* name, co
|
|||||||
for (int i = 0; atts[i]; i += 2) {
|
for (int i = 0; atts[i]; i += 2) {
|
||||||
if ((strcmp(atts[i], "epub:type") == 0 || strcmp(atts[i], "type") == 0) && strcmp(atts[i + 1], "toc") == 0) {
|
if ((strcmp(atts[i], "epub:type") == 0 || strcmp(atts[i], "type") == 0) && strcmp(atts[i + 1], "toc") == 0) {
|
||||||
self->state = IN_NAV_TOC;
|
self->state = IN_NAV_TOC;
|
||||||
Serial.printf("[%lu] [NAV] Found nav toc element\n", millis());
|
LOG_DBG("NAV", "Found nav toc element");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,7 +179,7 @@ void XMLCALL TocNavParser::endElement(void* userData, const XML_Char* name) {
|
|||||||
|
|
||||||
if (strcmp(name, "nav") == 0 && self->state >= IN_NAV_TOC) {
|
if (strcmp(name, "nav") == 0 && self->state >= IN_NAV_TOC) {
|
||||||
self->state = IN_BODY;
|
self->state = IN_BODY;
|
||||||
Serial.printf("[%lu] [NAV] Finished parsing nav toc\n", millis());
|
LOG_DBG("NAV", "Finished parsing nav toc");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
#include "TocNcxParser.h"
|
#include "TocNcxParser.h"
|
||||||
|
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <HardwareSerial.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
#include "../BookMetadataCache.h"
|
#include "../BookMetadataCache.h"
|
||||||
|
|
||||||
bool TocNcxParser::setup() {
|
bool TocNcxParser::setup() {
|
||||||
parser = XML_ParserCreate(nullptr);
|
parser = XML_ParserCreate(nullptr);
|
||||||
if (!parser) {
|
if (!parser) {
|
||||||
Serial.printf("[%lu] [TOC] Couldn't allocate memory for parser\n", millis());
|
LOG_DBG("TOC", "Couldn't allocate memory for parser");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ size_t TocNcxParser::write(const uint8_t* buffer, const size_t size) {
|
|||||||
while (remainingInBuffer > 0) {
|
while (remainingInBuffer > 0) {
|
||||||
void* const buf = XML_GetBuffer(parser, 1024);
|
void* const buf = XML_GetBuffer(parser, 1024);
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
Serial.printf("[%lu] [TOC] Couldn't allocate memory for buffer\n", millis());
|
LOG_DBG("TOC", "Couldn't allocate memory for buffer");
|
||||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
XML_SetCharacterDataHandler(parser, nullptr);
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
@@ -52,7 +52,7 @@ size_t TocNcxParser::write(const uint8_t* buffer, const size_t size) {
|
|||||||
memcpy(buf, currentBufferPos, toRead);
|
memcpy(buf, currentBufferPos, toRead);
|
||||||
|
|
||||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||||
Serial.printf("[%lu] [TOC] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
|
LOG_DBG("TOC", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
|
||||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <SdFat.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
#include "GfxRenderer.h"
|
#include "GfxRenderer.h"
|
||||||
|
|
||||||
|
#include <Logging.h>
|
||||||
#include <Utf8.h>
|
#include <Utf8.h>
|
||||||
|
|
||||||
void GfxRenderer::begin() {
|
void GfxRenderer::begin() {
|
||||||
frameBuffer = display.getFrameBuffer();
|
frameBuffer = display.getFrameBuffer();
|
||||||
if (!frameBuffer) {
|
if (!frameBuffer) {
|
||||||
Serial.printf("[%lu] [GFX] !! No framebuffer\n", millis());
|
LOG_ERR("GFX", "!! No framebuffer");
|
||||||
assert(false);
|
assert(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,7 +58,7 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
|||||||
|
|
||||||
// Bounds checking against physical panel dimensions
|
// Bounds checking against physical panel dimensions
|
||||||
if (phyX < 0 || phyX >= HalDisplay::DISPLAY_WIDTH || phyY < 0 || phyY >= HalDisplay::DISPLAY_HEIGHT) {
|
if (phyX < 0 || phyX >= HalDisplay::DISPLAY_WIDTH || phyY < 0 || phyY >= HalDisplay::DISPLAY_HEIGHT) {
|
||||||
Serial.printf("[%lu] [GFX] !! Outside range (%d, %d) -> (%d, %d)\n", millis(), x, y, phyX, phyY);
|
LOG_ERR("GFX", "!! Outside range (%d, %d) -> (%d, %d)", x, y, phyX, phyY);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
|||||||
|
|
||||||
int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontFamily::Style style) const {
|
int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontFamily::Style style) const {
|
||||||
if (fontMap.count(fontId) == 0) {
|
if (fontMap.count(fontId) == 0) {
|
||||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +101,7 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fontMap.count(fontId) == 0) {
|
if (fontMap.count(fontId) == 0) {
|
||||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto font = fontMap.at(fontId);
|
const auto font = fontMap.at(fontId);
|
||||||
@@ -133,7 +134,7 @@ void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) con
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// TODO: Implement
|
// TODO: Implement
|
||||||
Serial.printf("[%lu] [GFX] Line drawing not supported\n", millis());
|
LOG_ERR("GFX", "Line drawing not supported");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,8 +420,8 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
|||||||
bool isScaled = false;
|
bool isScaled = false;
|
||||||
int cropPixX = std::floor(bitmap.getWidth() * cropX / 2.0f);
|
int cropPixX = std::floor(bitmap.getWidth() * cropX / 2.0f);
|
||||||
int cropPixY = std::floor(bitmap.getHeight() * cropY / 2.0f);
|
int cropPixY = std::floor(bitmap.getHeight() * cropY / 2.0f);
|
||||||
Serial.printf("[%lu] [GFX] Cropping %dx%d by %dx%d pix, is %s\n", millis(), bitmap.getWidth(), bitmap.getHeight(),
|
LOG_DBG("GFX", "Cropping %dx%d by %dx%d pix, is %s", bitmap.getWidth(), bitmap.getHeight(), cropPixX, cropPixY,
|
||||||
cropPixX, cropPixY, bitmap.isTopDown() ? "top-down" : "bottom-up");
|
bitmap.isTopDown() ? "top-down" : "bottom-up");
|
||||||
|
|
||||||
if (maxWidth > 0 && (1.0f - cropX) * bitmap.getWidth() > maxWidth) {
|
if (maxWidth > 0 && (1.0f - cropX) * bitmap.getWidth() > maxWidth) {
|
||||||
scale = static_cast<float>(maxWidth) / static_cast<float>((1.0f - cropX) * bitmap.getWidth());
|
scale = static_cast<float>(maxWidth) / static_cast<float>((1.0f - cropX) * bitmap.getWidth());
|
||||||
@@ -430,7 +431,7 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
|||||||
scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>((1.0f - cropY) * bitmap.getHeight()));
|
scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>((1.0f - cropY) * bitmap.getHeight()));
|
||||||
isScaled = true;
|
isScaled = true;
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [GFX] Scaling by %f - %s\n", millis(), scale, isScaled ? "scaled" : "not scaled");
|
LOG_DBG("GFX", "Scaling by %f - %s", scale, isScaled ? "scaled" : "not scaled");
|
||||||
|
|
||||||
// Calculate output row size (2 bits per pixel, packed into bytes)
|
// Calculate output row size (2 bits per pixel, packed into bytes)
|
||||||
// IMPORTANT: Use int, not uint8_t, to avoid overflow for images > 1020 pixels wide
|
// IMPORTANT: Use int, not uint8_t, to avoid overflow for images > 1020 pixels wide
|
||||||
@@ -439,7 +440,7 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
|||||||
auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes()));
|
auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes()));
|
||||||
|
|
||||||
if (!outputRow || !rowBytes) {
|
if (!outputRow || !rowBytes) {
|
||||||
Serial.printf("[%lu] [GFX] !! Failed to allocate BMP row buffers\n", millis());
|
LOG_ERR("GFX", "!! Failed to allocate BMP row buffers");
|
||||||
free(outputRow);
|
free(outputRow);
|
||||||
free(rowBytes);
|
free(rowBytes);
|
||||||
return;
|
return;
|
||||||
@@ -458,7 +459,7 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) {
|
if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) {
|
||||||
Serial.printf("[%lu] [GFX] Failed to read row %d from bitmap\n", millis(), bmpY);
|
LOG_ERR("GFX", "Failed to read row %d from bitmap", bmpY);
|
||||||
free(outputRow);
|
free(outputRow);
|
||||||
free(rowBytes);
|
free(rowBytes);
|
||||||
return;
|
return;
|
||||||
@@ -521,7 +522,7 @@ void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y,
|
|||||||
auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes()));
|
auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes()));
|
||||||
|
|
||||||
if (!outputRow || !rowBytes) {
|
if (!outputRow || !rowBytes) {
|
||||||
Serial.printf("[%lu] [GFX] !! Failed to allocate 1-bit BMP row buffers\n", millis());
|
LOG_ERR("GFX", "!! Failed to allocate 1-bit BMP row buffers");
|
||||||
free(outputRow);
|
free(outputRow);
|
||||||
free(rowBytes);
|
free(rowBytes);
|
||||||
return;
|
return;
|
||||||
@@ -530,7 +531,7 @@ void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y,
|
|||||||
for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) {
|
for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) {
|
||||||
// Read rows sequentially using readNextRow
|
// Read rows sequentially using readNextRow
|
||||||
if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) {
|
if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) {
|
||||||
Serial.printf("[%lu] [GFX] Failed to read row %d from 1-bit bitmap\n", millis(), bmpY);
|
LOG_ERR("GFX", "Failed to read row %d from 1-bit bitmap", bmpY);
|
||||||
free(outputRow);
|
free(outputRow);
|
||||||
free(rowBytes);
|
free(rowBytes);
|
||||||
return;
|
return;
|
||||||
@@ -588,7 +589,7 @@ void GfxRenderer::fillPolygon(const int* xPoints, const int* yPoints, int numPoi
|
|||||||
// Allocate node buffer for scanline algorithm
|
// Allocate node buffer for scanline algorithm
|
||||||
auto* nodeX = static_cast<int*>(malloc(numPoints * sizeof(int)));
|
auto* nodeX = static_cast<int*>(malloc(numPoints * sizeof(int)));
|
||||||
if (!nodeX) {
|
if (!nodeX) {
|
||||||
Serial.printf("[%lu] [GFX] !! Failed to allocate polygon node buffer\n", millis());
|
LOG_ERR("GFX", "!! Failed to allocate polygon node buffer");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -655,7 +656,7 @@ void GfxRenderer::invertScreen() const {
|
|||||||
|
|
||||||
void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const {
|
void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const {
|
||||||
auto elapsed = millis() - start_ms;
|
auto elapsed = millis() - start_ms;
|
||||||
Serial.printf("[%lu] [GFX] Time = %lu ms from clearScreen to displayBuffer\n", millis(), elapsed);
|
LOG_DBG("GFX", "Time = %lu ms from clearScreen to displayBuffer", elapsed);
|
||||||
display.displayBuffer(refreshMode, fadingFix);
|
display.displayBuffer(refreshMode, fadingFix);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,7 +710,7 @@ int GfxRenderer::getScreenHeight() const {
|
|||||||
|
|
||||||
int GfxRenderer::getSpaceWidth(const int fontId) const {
|
int GfxRenderer::getSpaceWidth(const int fontId) const {
|
||||||
if (fontMap.count(fontId) == 0) {
|
if (fontMap.count(fontId) == 0) {
|
||||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -718,7 +719,7 @@ int GfxRenderer::getSpaceWidth(const int fontId) const {
|
|||||||
|
|
||||||
int GfxRenderer::getTextAdvanceX(const int fontId, const char* text) const {
|
int GfxRenderer::getTextAdvanceX(const int fontId, const char* text) const {
|
||||||
if (fontMap.count(fontId) == 0) {
|
if (fontMap.count(fontId) == 0) {
|
||||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -732,7 +733,7 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text) const {
|
|||||||
|
|
||||||
int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
||||||
if (fontMap.count(fontId) == 0) {
|
if (fontMap.count(fontId) == 0) {
|
||||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -741,7 +742,7 @@ int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
|||||||
|
|
||||||
int GfxRenderer::getLineHeight(const int fontId) const {
|
int GfxRenderer::getLineHeight(const int fontId) const {
|
||||||
if (fontMap.count(fontId) == 0) {
|
if (fontMap.count(fontId) == 0) {
|
||||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -750,7 +751,7 @@ int GfxRenderer::getLineHeight(const int fontId) const {
|
|||||||
|
|
||||||
int GfxRenderer::getTextHeight(const int fontId) const {
|
int GfxRenderer::getTextHeight(const int fontId) const {
|
||||||
if (fontMap.count(fontId) == 0) {
|
if (fontMap.count(fontId) == 0) {
|
||||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->ascender;
|
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->ascender;
|
||||||
@@ -764,7 +765,7 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fontMap.count(fontId) == 0) {
|
if (fontMap.count(fontId) == 0) {
|
||||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto font = fontMap.at(fontId);
|
const auto font = fontMap.at(fontId);
|
||||||
@@ -872,8 +873,7 @@ bool GfxRenderer::storeBwBuffer() {
|
|||||||
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
|
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
|
||||||
// Check if any chunks are already allocated
|
// Check if any chunks are already allocated
|
||||||
if (bwBufferChunks[i]) {
|
if (bwBufferChunks[i]) {
|
||||||
Serial.printf("[%lu] [GFX] !! BW buffer chunk %zu already stored - this is likely a bug, freeing chunk\n",
|
LOG_ERR("GFX", "!! BW buffer chunk %zu already stored - this is likely a bug, freeing chunk", i);
|
||||||
millis(), i);
|
|
||||||
free(bwBufferChunks[i]);
|
free(bwBufferChunks[i]);
|
||||||
bwBufferChunks[i] = nullptr;
|
bwBufferChunks[i] = nullptr;
|
||||||
}
|
}
|
||||||
@@ -882,8 +882,7 @@ bool GfxRenderer::storeBwBuffer() {
|
|||||||
bwBufferChunks[i] = static_cast<uint8_t*>(malloc(BW_BUFFER_CHUNK_SIZE));
|
bwBufferChunks[i] = static_cast<uint8_t*>(malloc(BW_BUFFER_CHUNK_SIZE));
|
||||||
|
|
||||||
if (!bwBufferChunks[i]) {
|
if (!bwBufferChunks[i]) {
|
||||||
Serial.printf("[%lu] [GFX] !! Failed to allocate BW buffer chunk %zu (%zu bytes)\n", millis(), i,
|
LOG_ERR("GFX", "!! Failed to allocate BW buffer chunk %zu (%zu bytes)", i, BW_BUFFER_CHUNK_SIZE);
|
||||||
BW_BUFFER_CHUNK_SIZE);
|
|
||||||
// Free previously allocated chunks
|
// Free previously allocated chunks
|
||||||
freeBwBufferChunks();
|
freeBwBufferChunks();
|
||||||
return false;
|
return false;
|
||||||
@@ -892,8 +891,7 @@ bool GfxRenderer::storeBwBuffer() {
|
|||||||
memcpy(bwBufferChunks[i], frameBuffer + offset, BW_BUFFER_CHUNK_SIZE);
|
memcpy(bwBufferChunks[i], frameBuffer + offset, BW_BUFFER_CHUNK_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [GFX] Stored BW buffer in %zu chunks (%zu bytes each)\n", millis(), BW_BUFFER_NUM_CHUNKS,
|
LOG_DBG("GFX", "Stored BW buffer in %zu chunks (%zu bytes each)", BW_BUFFER_NUM_CHUNKS, BW_BUFFER_CHUNK_SIZE);
|
||||||
BW_BUFFER_CHUNK_SIZE);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -920,7 +918,7 @@ void GfxRenderer::restoreBwBuffer() {
|
|||||||
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]) {
|
||||||
Serial.printf("[%lu] [GFX] !! BW buffer chunks not stored - this is likely a bug\n", millis());
|
LOG_ERR("GFX", "!! BW buffer chunks not stored - this is likely a bug");
|
||||||
freeBwBufferChunks();
|
freeBwBufferChunks();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -932,7 +930,7 @@ void GfxRenderer::restoreBwBuffer() {
|
|||||||
display.cleanupGrayscaleBuffers(frameBuffer);
|
display.cleanupGrayscaleBuffers(frameBuffer);
|
||||||
|
|
||||||
freeBwBufferChunks();
|
freeBwBufferChunks();
|
||||||
Serial.printf("[%lu] [GFX] Restored and freed BW buffer chunks\n", millis());
|
LOG_DBG("GFX", "Restored and freed BW buffer chunks");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -954,7 +952,7 @@ void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp,
|
|||||||
|
|
||||||
// no glyph?
|
// no glyph?
|
||||||
if (!glyph) {
|
if (!glyph) {
|
||||||
Serial.printf("[%lu] [GFX] No glyph for codepoint %d\n", millis(), cp);
|
LOG_ERR("GFX", "No glyph for codepoint %d", cp);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#include "JpegToBmpConverter.h"
|
#include "JpegToBmpConverter.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <HalStorage.h>
|
||||||
#include <SdFat.h>
|
#include <Logging.h>
|
||||||
#include <picojpeg.h>
|
#include <picojpeg.h>
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
@@ -201,8 +201,7 @@ unsigned char JpegToBmpConverter::jpegReadCallback(unsigned char* pBuf, const un
|
|||||||
// Internal implementation with configurable target size and bit depth
|
// Internal implementation with configurable target size and bit depth
|
||||||
bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight,
|
bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight,
|
||||||
bool oneBit, bool crop) {
|
bool oneBit, bool crop) {
|
||||||
Serial.printf("[%lu] [JPG] Converting JPEG to %s BMP (target: %dx%d)\n", millis(), oneBit ? "1-bit" : "2-bit",
|
LOG_DBG("JPG", "Converting JPEG to %s BMP (target: %dx%d)", oneBit ? "1-bit" : "2-bit", targetWidth, targetHeight);
|
||||||
targetWidth, targetHeight);
|
|
||||||
|
|
||||||
// Setup context for picojpeg callback
|
// Setup context for picojpeg callback
|
||||||
JpegReadContext context = {.file = jpegFile, .bufferPos = 0, .bufferFilled = 0};
|
JpegReadContext context = {.file = jpegFile, .bufferPos = 0, .bufferFilled = 0};
|
||||||
@@ -211,12 +210,12 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
pjpeg_image_info_t imageInfo;
|
pjpeg_image_info_t imageInfo;
|
||||||
const unsigned char status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
|
const unsigned char status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
|
||||||
if (status != 0) {
|
if (status != 0) {
|
||||||
Serial.printf("[%lu] [JPG] JPEG decode init failed with error code: %d\n", millis(), status);
|
LOG_ERR("JPG", "JPEG decode init failed with error code: %d", status);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [JPG] JPEG dimensions: %dx%d, components: %d, MCUs: %dx%d\n", millis(), imageInfo.m_width,
|
LOG_DBG("JPG", "JPEG dimensions: %dx%d, components: %d, MCUs: %dx%d", imageInfo.m_width, imageInfo.m_height,
|
||||||
imageInfo.m_height, imageInfo.m_comps, imageInfo.m_MCUSPerRow, imageInfo.m_MCUSPerCol);
|
imageInfo.m_comps, imageInfo.m_MCUSPerRow, imageInfo.m_MCUSPerCol);
|
||||||
|
|
||||||
// Safety limits to prevent memory issues on ESP32
|
// Safety limits to prevent memory issues on ESP32
|
||||||
constexpr int MAX_IMAGE_WIDTH = 2048;
|
constexpr int MAX_IMAGE_WIDTH = 2048;
|
||||||
@@ -224,8 +223,8 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
constexpr int MAX_MCU_ROW_BYTES = 65536;
|
constexpr int MAX_MCU_ROW_BYTES = 65536;
|
||||||
|
|
||||||
if (imageInfo.m_width > MAX_IMAGE_WIDTH || imageInfo.m_height > MAX_IMAGE_HEIGHT) {
|
if (imageInfo.m_width > MAX_IMAGE_WIDTH || imageInfo.m_height > MAX_IMAGE_HEIGHT) {
|
||||||
Serial.printf("[%lu] [JPG] Image too large (%dx%d), max supported: %dx%d\n", millis(), imageInfo.m_width,
|
LOG_DBG("JPG", "Image too large (%dx%d), max supported: %dx%d", imageInfo.m_width, imageInfo.m_height,
|
||||||
imageInfo.m_height, MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT);
|
MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,8 +261,8 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
scaleY_fp = (static_cast<uint32_t>(imageInfo.m_height) << 16) / outHeight;
|
scaleY_fp = (static_cast<uint32_t>(imageInfo.m_height) << 16) / outHeight;
|
||||||
needsScaling = true;
|
needsScaling = true;
|
||||||
|
|
||||||
Serial.printf("[%lu] [JPG] Pre-scaling %dx%d -> %dx%d (fit to %dx%d)\n", millis(), imageInfo.m_width,
|
LOG_DBG("JPG", "Pre-scaling %dx%d -> %dx%d (fit to %dx%d)", imageInfo.m_width, imageInfo.m_height, outWidth,
|
||||||
imageInfo.m_height, outWidth, outHeight, targetWidth, targetHeight);
|
outHeight, targetWidth, targetHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write BMP header with output dimensions
|
// Write BMP header with output dimensions
|
||||||
@@ -282,7 +281,7 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
// Allocate row buffer
|
// Allocate row buffer
|
||||||
auto* rowBuffer = static_cast<uint8_t*>(malloc(bytesPerRow));
|
auto* rowBuffer = static_cast<uint8_t*>(malloc(bytesPerRow));
|
||||||
if (!rowBuffer) {
|
if (!rowBuffer) {
|
||||||
Serial.printf("[%lu] [JPG] Failed to allocate row buffer\n", millis());
|
LOG_ERR("JPG", "Failed to allocate row buffer");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,15 +292,14 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
|
|
||||||
// Validate MCU row buffer size before allocation
|
// Validate MCU row buffer size before allocation
|
||||||
if (mcuRowPixels > MAX_MCU_ROW_BYTES) {
|
if (mcuRowPixels > MAX_MCU_ROW_BYTES) {
|
||||||
Serial.printf("[%lu] [JPG] MCU row buffer too large (%d bytes), max: %d\n", millis(), mcuRowPixels,
|
LOG_DBG("JPG", "MCU row buffer too large (%d bytes), max: %d", mcuRowPixels, MAX_MCU_ROW_BYTES);
|
||||||
MAX_MCU_ROW_BYTES);
|
|
||||||
free(rowBuffer);
|
free(rowBuffer);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto* mcuRowBuffer = static_cast<uint8_t*>(malloc(mcuRowPixels));
|
auto* mcuRowBuffer = static_cast<uint8_t*>(malloc(mcuRowPixels));
|
||||||
if (!mcuRowBuffer) {
|
if (!mcuRowBuffer) {
|
||||||
Serial.printf("[%lu] [JPG] Failed to allocate MCU row buffer (%d bytes)\n", millis(), mcuRowPixels);
|
LOG_ERR("JPG", "Failed to allocate MCU row buffer (%d bytes)", mcuRowPixels);
|
||||||
free(rowBuffer);
|
free(rowBuffer);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -349,10 +347,9 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
const unsigned char mcuStatus = pjpeg_decode_mcu();
|
const unsigned char mcuStatus = pjpeg_decode_mcu();
|
||||||
if (mcuStatus != 0) {
|
if (mcuStatus != 0) {
|
||||||
if (mcuStatus == PJPG_NO_MORE_BLOCKS) {
|
if (mcuStatus == PJPG_NO_MORE_BLOCKS) {
|
||||||
Serial.printf("[%lu] [JPG] Unexpected end of blocks at MCU (%d, %d)\n", millis(), mcuX, mcuY);
|
LOG_ERR("JPG", "Unexpected end of blocks at MCU (%d, %d)", mcuX, mcuY);
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [JPG] JPEG decode MCU failed at (%d, %d) with error code: %d\n", millis(), mcuX, mcuY,
|
LOG_ERR("JPG", "JPEG decode MCU failed at (%d, %d) with error code: %d", mcuX, mcuY, mcuStatus);
|
||||||
mcuStatus);
|
|
||||||
}
|
}
|
||||||
free(mcuRowBuffer);
|
free(mcuRowBuffer);
|
||||||
free(rowBuffer);
|
free(rowBuffer);
|
||||||
@@ -549,7 +546,7 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
free(mcuRowBuffer);
|
free(mcuRowBuffer);
|
||||||
free(rowBuffer);
|
free(rowBuffer);
|
||||||
|
|
||||||
Serial.printf("[%lu] [JPG] Successfully converted JPEG to BMP\n", millis());
|
LOG_DBG("JPG", "Successfully converted JPEG to BMP");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#include "KOReaderCredentialStore.h"
|
#include "KOReaderCredentialStore.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <HalStorage.h>
|
||||||
|
#include <Logging.h>
|
||||||
#include <MD5Builder.h>
|
#include <MD5Builder.h>
|
||||||
#include <SDCardManager.h>
|
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
// Initialize the static instance
|
// Initialize the static instance
|
||||||
@@ -32,10 +32,10 @@ void KOReaderCredentialStore::obfuscate(std::string& data) const {
|
|||||||
|
|
||||||
bool KOReaderCredentialStore::saveToFile() const {
|
bool KOReaderCredentialStore::saveToFile() const {
|
||||||
// Make sure the directory exists
|
// Make sure the directory exists
|
||||||
SdMan.mkdir("/.crosspoint");
|
Storage.mkdir("/.crosspoint");
|
||||||
|
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (!SdMan.openFileForWrite("KRS", KOREADER_FILE, file)) {
|
if (!Storage.openFileForWrite("KRS", KOREADER_FILE, file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ bool KOReaderCredentialStore::saveToFile() const {
|
|||||||
|
|
||||||
// Write username (plaintext - not particularly sensitive)
|
// Write username (plaintext - not particularly sensitive)
|
||||||
serialization::writeString(file, username);
|
serialization::writeString(file, username);
|
||||||
Serial.printf("[%lu] [KRS] Saving username: %s\n", millis(), username.c_str());
|
LOG_DBG("KRS", "Saving username: %s", username.c_str());
|
||||||
|
|
||||||
// Write password (obfuscated)
|
// Write password (obfuscated)
|
||||||
std::string obfuscatedPwd = password;
|
std::string obfuscatedPwd = password;
|
||||||
@@ -58,14 +58,14 @@ bool KOReaderCredentialStore::saveToFile() const {
|
|||||||
serialization::writePod(file, static_cast<uint8_t>(matchMethod));
|
serialization::writePod(file, static_cast<uint8_t>(matchMethod));
|
||||||
|
|
||||||
file.close();
|
file.close();
|
||||||
Serial.printf("[%lu] [KRS] Saved KOReader credentials to file\n", millis());
|
LOG_DBG("KRS", "Saved KOReader credentials to file");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool KOReaderCredentialStore::loadFromFile() {
|
bool KOReaderCredentialStore::loadFromFile() {
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (!SdMan.openFileForRead("KRS", KOREADER_FILE, file)) {
|
if (!Storage.openFileForRead("KRS", KOREADER_FILE, file)) {
|
||||||
Serial.printf("[%lu] [KRS] No credentials file found\n", millis());
|
LOG_DBG("KRS", "No credentials file found");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ bool KOReaderCredentialStore::loadFromFile() {
|
|||||||
uint8_t version;
|
uint8_t version;
|
||||||
serialization::readPod(file, version);
|
serialization::readPod(file, version);
|
||||||
if (version != KOREADER_FILE_VERSION) {
|
if (version != KOREADER_FILE_VERSION) {
|
||||||
Serial.printf("[%lu] [KRS] Unknown file version: %u\n", millis(), version);
|
LOG_DBG("KRS", "Unknown file version: %u", version);
|
||||||
file.close();
|
file.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -110,14 +110,14 @@ bool KOReaderCredentialStore::loadFromFile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
file.close();
|
file.close();
|
||||||
Serial.printf("[%lu] [KRS] Loaded KOReader credentials for user: %s\n", millis(), username.c_str());
|
LOG_DBG("KRS", "Loaded KOReader credentials for user: %s", username.c_str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void KOReaderCredentialStore::setCredentials(const std::string& user, const std::string& pass) {
|
void KOReaderCredentialStore::setCredentials(const std::string& user, const std::string& pass) {
|
||||||
username = user;
|
username = user;
|
||||||
password = pass;
|
password = pass;
|
||||||
Serial.printf("[%lu] [KRS] Set credentials for user: %s\n", millis(), user.c_str());
|
LOG_DBG("KRS", "Set credentials for user: %s", user.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string KOReaderCredentialStore::getMd5Password() const {
|
std::string KOReaderCredentialStore::getMd5Password() const {
|
||||||
@@ -140,12 +140,12 @@ void KOReaderCredentialStore::clearCredentials() {
|
|||||||
username.clear();
|
username.clear();
|
||||||
password.clear();
|
password.clear();
|
||||||
saveToFile();
|
saveToFile();
|
||||||
Serial.printf("[%lu] [KRS] Cleared KOReader credentials\n", millis());
|
LOG_DBG("KRS", "Cleared KOReader credentials");
|
||||||
}
|
}
|
||||||
|
|
||||||
void KOReaderCredentialStore::setServerUrl(const std::string& url) {
|
void KOReaderCredentialStore::setServerUrl(const std::string& url) {
|
||||||
serverUrl = url;
|
serverUrl = url;
|
||||||
Serial.printf("[%lu] [KRS] Set server URL: %s\n", millis(), url.empty() ? "(default)" : url.c_str());
|
LOG_DBG("KRS", "Set server URL: %s", url.empty() ? "(default)" : url.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string KOReaderCredentialStore::getBaseUrl() const {
|
std::string KOReaderCredentialStore::getBaseUrl() const {
|
||||||
@@ -163,6 +163,5 @@ std::string KOReaderCredentialStore::getBaseUrl() const {
|
|||||||
|
|
||||||
void KOReaderCredentialStore::setMatchMethod(DocumentMatchMethod method) {
|
void KOReaderCredentialStore::setMatchMethod(DocumentMatchMethod method) {
|
||||||
matchMethod = method;
|
matchMethod = method;
|
||||||
Serial.printf("[%lu] [KRS] Set match method: %s\n", millis(),
|
LOG_DBG("KRS", "Set match method: %s", method == DocumentMatchMethod::FILENAME ? "Filename" : "Binary");
|
||||||
method == DocumentMatchMethod::FILENAME ? "Filename" : "Binary");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#include "KOReaderDocumentId.h"
|
#include "KOReaderDocumentId.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <HalStorage.h>
|
||||||
|
#include <Logging.h>
|
||||||
#include <MD5Builder.h>
|
#include <MD5Builder.h>
|
||||||
#include <SDCardManager.h>
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// Extract filename from path (everything after last '/')
|
// Extract filename from path (everything after last '/')
|
||||||
@@ -27,7 +27,7 @@ std::string KOReaderDocumentId::calculateFromFilename(const std::string& filePat
|
|||||||
md5.calculate();
|
md5.calculate();
|
||||||
|
|
||||||
std::string result = md5.toString().c_str();
|
std::string result = md5.toString().c_str();
|
||||||
Serial.printf("[%lu] [KODoc] Filename hash: %s (from '%s')\n", millis(), result.c_str(), filename.c_str());
|
LOG_DBG("KODoc", "Filename hash: %s (from '%s')", result.c_str(), filename.c_str());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,13 +43,13 @@ size_t KOReaderDocumentId::getOffset(int i) {
|
|||||||
|
|
||||||
std::string KOReaderDocumentId::calculate(const std::string& filePath) {
|
std::string KOReaderDocumentId::calculate(const std::string& filePath) {
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (!SdMan.openFileForRead("KODoc", filePath, file)) {
|
if (!Storage.openFileForRead("KODoc", filePath, file)) {
|
||||||
Serial.printf("[%lu] [KODoc] Failed to open file: %s\n", millis(), filePath.c_str());
|
LOG_DBG("KODoc", "Failed to open file: %s", filePath.c_str());
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const size_t fileSize = file.fileSize();
|
const size_t fileSize = file.fileSize();
|
||||||
Serial.printf("[%lu] [KODoc] Calculating hash for file: %s (size: %zu)\n", millis(), filePath.c_str(), fileSize);
|
LOG_DBG("KODoc", "Calculating hash for file: %s (size: %zu)", filePath.c_str(), fileSize);
|
||||||
|
|
||||||
// Initialize MD5 builder
|
// Initialize MD5 builder
|
||||||
MD5Builder md5;
|
MD5Builder md5;
|
||||||
@@ -70,7 +70,7 @@ std::string KOReaderDocumentId::calculate(const std::string& filePath) {
|
|||||||
|
|
||||||
// Seek to offset
|
// Seek to offset
|
||||||
if (!file.seekSet(offset)) {
|
if (!file.seekSet(offset)) {
|
||||||
Serial.printf("[%lu] [KODoc] Failed to seek to offset %zu\n", millis(), offset);
|
LOG_DBG("KODoc", "Failed to seek to offset %zu", offset);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ std::string KOReaderDocumentId::calculate(const std::string& filePath) {
|
|||||||
md5.calculate();
|
md5.calculate();
|
||||||
std::string result = md5.toString().c_str();
|
std::string result = md5.toString().c_str();
|
||||||
|
|
||||||
Serial.printf("[%lu] [KODoc] Hash calculated: %s (from %zu bytes)\n", millis(), result.c_str(), totalBytesRead);
|
LOG_DBG("KODoc", "Hash calculated: %s (from %zu bytes)", result.c_str(), totalBytesRead);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include <HTTPClient.h>
|
#include <HTTPClient.h>
|
||||||
#include <HardwareSerial.h>
|
#include <Logging.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <WiFiClientSecure.h>
|
#include <WiFiClientSecure.h>
|
||||||
|
|
||||||
@@ -30,12 +30,12 @@ bool isHttpsUrl(const std::string& url) { return url.rfind("https://", 0) == 0;
|
|||||||
|
|
||||||
KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
|
KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
|
||||||
if (!KOREADER_STORE.hasCredentials()) {
|
if (!KOREADER_STORE.hasCredentials()) {
|
||||||
Serial.printf("[%lu] [KOSync] No credentials configured\n", millis());
|
LOG_DBG("KOSync", "No credentials configured");
|
||||||
return NO_CREDENTIALS;
|
return NO_CREDENTIALS;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string url = KOREADER_STORE.getBaseUrl() + "/users/auth";
|
std::string url = KOREADER_STORE.getBaseUrl() + "/users/auth";
|
||||||
Serial.printf("[%lu] [KOSync] Authenticating: %s\n", millis(), url.c_str());
|
LOG_DBG("KOSync", "Authenticating: %s", url.c_str());
|
||||||
|
|
||||||
HTTPClient http;
|
HTTPClient http;
|
||||||
std::unique_ptr<WiFiClientSecure> secureClient;
|
std::unique_ptr<WiFiClientSecure> secureClient;
|
||||||
@@ -53,7 +53,7 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
|
|||||||
const int httpCode = http.GET();
|
const int httpCode = http.GET();
|
||||||
http.end();
|
http.end();
|
||||||
|
|
||||||
Serial.printf("[%lu] [KOSync] Auth response: %d\n", millis(), httpCode);
|
LOG_DBG("KOSync", "Auth response: %d", httpCode);
|
||||||
|
|
||||||
if (httpCode == 200) {
|
if (httpCode == 200) {
|
||||||
return OK;
|
return OK;
|
||||||
@@ -68,12 +68,12 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
|
|||||||
KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& documentHash,
|
KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& documentHash,
|
||||||
KOReaderProgress& outProgress) {
|
KOReaderProgress& outProgress) {
|
||||||
if (!KOREADER_STORE.hasCredentials()) {
|
if (!KOREADER_STORE.hasCredentials()) {
|
||||||
Serial.printf("[%lu] [KOSync] No credentials configured\n", millis());
|
LOG_DBG("KOSync", "No credentials configured");
|
||||||
return NO_CREDENTIALS;
|
return NO_CREDENTIALS;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress/" + documentHash;
|
std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress/" + documentHash;
|
||||||
Serial.printf("[%lu] [KOSync] Getting progress: %s\n", millis(), url.c_str());
|
LOG_DBG("KOSync", "Getting progress: %s", url.c_str());
|
||||||
|
|
||||||
HTTPClient http;
|
HTTPClient http;
|
||||||
std::unique_ptr<WiFiClientSecure> secureClient;
|
std::unique_ptr<WiFiClientSecure> secureClient;
|
||||||
@@ -99,7 +99,7 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc
|
|||||||
const DeserializationError error = deserializeJson(doc, responseBody);
|
const DeserializationError error = deserializeJson(doc, responseBody);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
Serial.printf("[%lu] [KOSync] JSON parse failed: %s\n", millis(), error.c_str());
|
LOG_ERR("KOSync", "JSON parse failed: %s", error.c_str());
|
||||||
return JSON_ERROR;
|
return JSON_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,14 +110,13 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc
|
|||||||
outProgress.deviceId = doc["device_id"].as<std::string>();
|
outProgress.deviceId = doc["device_id"].as<std::string>();
|
||||||
outProgress.timestamp = doc["timestamp"].as<int64_t>();
|
outProgress.timestamp = doc["timestamp"].as<int64_t>();
|
||||||
|
|
||||||
Serial.printf("[%lu] [KOSync] Got progress: %.2f%% at %s\n", millis(), outProgress.percentage * 100,
|
LOG_DBG("KOSync", "Got progress: %.2f%% at %s", outProgress.percentage * 100, outProgress.progress.c_str());
|
||||||
outProgress.progress.c_str());
|
|
||||||
return OK;
|
return OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
http.end();
|
http.end();
|
||||||
|
|
||||||
Serial.printf("[%lu] [KOSync] Get progress response: %d\n", millis(), httpCode);
|
LOG_DBG("KOSync", "Get progress response: %d", httpCode);
|
||||||
|
|
||||||
if (httpCode == 401) {
|
if (httpCode == 401) {
|
||||||
return AUTH_FAILED;
|
return AUTH_FAILED;
|
||||||
@@ -131,12 +130,12 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc
|
|||||||
|
|
||||||
KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgress& progress) {
|
KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgress& progress) {
|
||||||
if (!KOREADER_STORE.hasCredentials()) {
|
if (!KOREADER_STORE.hasCredentials()) {
|
||||||
Serial.printf("[%lu] [KOSync] No credentials configured\n", millis());
|
LOG_DBG("KOSync", "No credentials configured");
|
||||||
return NO_CREDENTIALS;
|
return NO_CREDENTIALS;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress";
|
std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress";
|
||||||
Serial.printf("[%lu] [KOSync] Updating progress: %s\n", millis(), url.c_str());
|
LOG_DBG("KOSync", "Updating progress: %s", url.c_str());
|
||||||
|
|
||||||
HTTPClient http;
|
HTTPClient http;
|
||||||
std::unique_ptr<WiFiClientSecure> secureClient;
|
std::unique_ptr<WiFiClientSecure> secureClient;
|
||||||
@@ -163,12 +162,12 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr
|
|||||||
std::string body;
|
std::string body;
|
||||||
serializeJson(doc, body);
|
serializeJson(doc, body);
|
||||||
|
|
||||||
Serial.printf("[%lu] [KOSync] Request body: %s\n", millis(), body.c_str());
|
LOG_DBG("KOSync", "Request body: %s", body.c_str());
|
||||||
|
|
||||||
const int httpCode = http.PUT(body.c_str());
|
const int httpCode = http.PUT(body.c_str());
|
||||||
http.end();
|
http.end();
|
||||||
|
|
||||||
Serial.printf("[%lu] [KOSync] Update progress response: %d\n", millis(), httpCode);
|
LOG_DBG("KOSync", "Update progress response: %d", httpCode);
|
||||||
|
|
||||||
if (httpCode == 200 || httpCode == 202) {
|
if (httpCode == 200 || httpCode == 202) {
|
||||||
return OK;
|
return OK;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "ProgressMapper.h"
|
#include "ProgressMapper.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
@@ -23,8 +23,8 @@ KOReaderPosition ProgressMapper::toKOReader(const std::shared_ptr<Epub>& epub, c
|
|||||||
const int tocIndex = epub->getTocIndexForSpineIndex(pos.spineIndex);
|
const int tocIndex = epub->getTocIndexForSpineIndex(pos.spineIndex);
|
||||||
const std::string chapterName = (tocIndex >= 0) ? epub->getTocItem(tocIndex).title : "unknown";
|
const std::string chapterName = (tocIndex >= 0) ? epub->getTocItem(tocIndex).title : "unknown";
|
||||||
|
|
||||||
Serial.printf("[%lu] [ProgressMapper] CrossPoint -> KOReader: chapter='%s', page=%d/%d -> %.2f%% at %s\n", millis(),
|
LOG_DBG("ProgressMapper", "CrossPoint -> KOReader: chapter='%s', page=%d/%d -> %.2f%% at %s", chapterName.c_str(),
|
||||||
chapterName.c_str(), pos.pageNumber, pos.totalPages, result.percentage * 100, result.xpath.c_str());
|
pos.pageNumber, pos.totalPages, result.percentage * 100, result.xpath.c_str());
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -76,8 +76,8 @@ CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr<Epub>& epu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [ProgressMapper] KOReader -> CrossPoint: %.2f%% at %s -> spine=%d, page=%d\n", millis(),
|
LOG_DBG("ProgressMapper", "KOReader -> CrossPoint: %.2f%% at %s -> spine=%d, page=%d", koPos.percentage * 100,
|
||||||
koPos.percentage * 100, koPos.xpath.c_str(), result.spineIndex, result.pageNumber);
|
koPos.xpath.c_str(), result.spineIndex, result.pageNumber);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
47
lib/Logging/Logging.cpp
Normal file
47
lib/Logging/Logging.cpp
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#include "Logging.h"
|
||||||
|
|
||||||
|
// Since logging can take a large amount of flash, we want to make the format string as short as possible.
|
||||||
|
// This logPrintf prepend the timestamp, level and origin to the user-provided message, so that the user only needs to
|
||||||
|
// provide the format string for the message itself.
|
||||||
|
void logPrintf(const char* level, const char* origin, const char* format, ...) {
|
||||||
|
if (!logSerial) {
|
||||||
|
return; // Serial not initialized, skip logging
|
||||||
|
}
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
char buf[256];
|
||||||
|
char* c = buf;
|
||||||
|
// add the timestamp
|
||||||
|
{
|
||||||
|
unsigned long ms = millis();
|
||||||
|
int len = snprintf(c, sizeof(buf), "[%lu] ", ms);
|
||||||
|
if (len < 0) {
|
||||||
|
return; // encoding error, skip logging
|
||||||
|
}
|
||||||
|
c += len;
|
||||||
|
}
|
||||||
|
// add the level
|
||||||
|
{
|
||||||
|
const char* p = level;
|
||||||
|
size_t remaining = sizeof(buf) - (c - buf);
|
||||||
|
while (*p && remaining > 1) {
|
||||||
|
*c++ = *p++;
|
||||||
|
remaining--;
|
||||||
|
}
|
||||||
|
if (remaining > 1) {
|
||||||
|
*c++ = ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// add the origin
|
||||||
|
{
|
||||||
|
int len = snprintf(c, sizeof(buf) - (c - buf), "[%s] ", origin);
|
||||||
|
if (len < 0) {
|
||||||
|
return; // encoding error, skip logging
|
||||||
|
}
|
||||||
|
c += len;
|
||||||
|
}
|
||||||
|
// add the user message
|
||||||
|
vsnprintf(c, sizeof(buf) - (c - buf), format, args);
|
||||||
|
va_end(args);
|
||||||
|
logSerial.print(buf);
|
||||||
|
}
|
||||||
71
lib/Logging/Logging.h
Normal file
71
lib/Logging/Logging.h
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <HardwareSerial.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
Define ENABLE_SERIAL_LOG to enable logging
|
||||||
|
Can be set in platformio.ini build_flags or as a compile definition
|
||||||
|
|
||||||
|
Define LOG_LEVEL to control log verbosity:
|
||||||
|
0 = ERR only
|
||||||
|
1 = ERR + INF
|
||||||
|
2 = ERR + INF + DBG
|
||||||
|
If not defined, defaults to 0
|
||||||
|
|
||||||
|
If you have a legitimate need for raw Serial access (e.g., binary data,
|
||||||
|
special formatting), use the underlying logSerial object directly:
|
||||||
|
logSerial.printf("Special case: %d\n", value);
|
||||||
|
logSerial.write(binaryData, length);
|
||||||
|
|
||||||
|
The logSerial reference (defined below) points to the real Serial object and
|
||||||
|
won't trigger deprecation warnings.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef LOG_LEVEL
|
||||||
|
#define LOG_LEVEL 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static HWCDC& logSerial = Serial;
|
||||||
|
|
||||||
|
void logPrintf(const char* level, const char* origin, const char* format, ...);
|
||||||
|
|
||||||
|
#ifdef ENABLE_SERIAL_LOG
|
||||||
|
#if LOG_LEVEL >= 0
|
||||||
|
#define LOG_ERR(origin, format, ...) logPrintf("[ERR]", origin, format "\n", ##__VA_ARGS__)
|
||||||
|
#else
|
||||||
|
#define LOG_ERR(origin, format, ...)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if LOG_LEVEL >= 1
|
||||||
|
#define LOG_INF(origin, format, ...) logPrintf("[INF]", origin, format "\n", ##__VA_ARGS__)
|
||||||
|
#else
|
||||||
|
#define LOG_INF(origin, format, ...)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if LOG_LEVEL >= 2
|
||||||
|
#define LOG_DBG(origin, format, ...) logPrintf("[DBG]", origin, format "\n", ##__VA_ARGS__)
|
||||||
|
#else
|
||||||
|
#define LOG_DBG(origin, format, ...)
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
#define LOG_DBG(origin, format, ...)
|
||||||
|
#define LOG_ERR(origin, format, ...)
|
||||||
|
#define LOG_INF(origin, format, ...)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class MySerialImpl : public Print {
|
||||||
|
public:
|
||||||
|
void begin(unsigned long baud) { logSerial.begin(baud); }
|
||||||
|
|
||||||
|
// Support boolean conversion for compatibility with code like:
|
||||||
|
// if (Serial) or while (!Serial)
|
||||||
|
operator bool() const { return logSerial; }
|
||||||
|
|
||||||
|
__attribute__((deprecated("Use LOG_* macro instead"))) size_t printf(const char* format, ...);
|
||||||
|
size_t write(uint8_t b) override;
|
||||||
|
size_t write(const uint8_t* buffer, size_t size) override;
|
||||||
|
void flush() override;
|
||||||
|
static MySerialImpl instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
#define Serial MySerialImpl::instance
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "OpdsParser.h"
|
#include "OpdsParser.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ OpdsParser::OpdsParser() {
|
|||||||
parser = XML_ParserCreate(nullptr);
|
parser = XML_ParserCreate(nullptr);
|
||||||
if (!parser) {
|
if (!parser) {
|
||||||
errorOccured = true;
|
errorOccured = true;
|
||||||
Serial.printf("[%lu] [OPDS] Couldn't allocate memory for parser\n", millis());
|
LOG_DBG("OPDS", "Couldn't allocate memory for parser");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) {
|
|||||||
void* const buf = XML_GetBuffer(parser, chunkSize);
|
void* const buf = XML_GetBuffer(parser, chunkSize);
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
errorOccured = true;
|
errorOccured = true;
|
||||||
Serial.printf("[%lu] [OPDS] Couldn't allocate memory for buffer\n", millis());
|
LOG_DBG("OPDS", "Couldn't allocate memory for buffer");
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
parser = nullptr;
|
parser = nullptr;
|
||||||
return length;
|
return length;
|
||||||
@@ -53,7 +53,7 @@ size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) {
|
|||||||
|
|
||||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), 0) == XML_STATUS_ERROR) {
|
if (XML_ParseBuffer(parser, static_cast<int>(toRead), 0) == XML_STATUS_ERROR) {
|
||||||
errorOccured = true;
|
errorOccured = true;
|
||||||
Serial.printf("[%lu] [OPDS] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
|
LOG_DBG("OPDS", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
|
||||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
parser = nullptr;
|
parser = nullptr;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <SdFat.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <JpegToBmpConverter.h>
|
#include <JpegToBmpConverter.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
|
||||||
Txt::Txt(std::string path, std::string cacheBasePath)
|
Txt::Txt(std::string path, std::string cacheBasePath)
|
||||||
: filepath(std::move(path)), cacheBasePath(std::move(cacheBasePath)) {
|
: filepath(std::move(path)), cacheBasePath(std::move(cacheBasePath)) {
|
||||||
@@ -15,14 +16,14 @@ bool Txt::load() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SdMan.exists(filepath.c_str())) {
|
if (!Storage.exists(filepath.c_str())) {
|
||||||
Serial.printf("[%lu] [TXT] File does not exist: %s\n", millis(), filepath.c_str());
|
LOG_ERR("TXT", "File does not exist: %s", filepath.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (!SdMan.openFileForRead("TXT", filepath, file)) {
|
if (!Storage.openFileForRead("TXT", filepath, file)) {
|
||||||
Serial.printf("[%lu] [TXT] Failed to open file: %s\n", millis(), filepath.c_str());
|
LOG_ERR("TXT", "Failed to open file: %s", filepath.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ bool Txt::load() {
|
|||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
loaded = true;
|
loaded = true;
|
||||||
Serial.printf("[%lu] [TXT] Loaded TXT file: %s (%zu bytes)\n", millis(), filepath.c_str(), fileSize);
|
LOG_DBG("TXT", "Loaded TXT file: %s (%zu bytes)", filepath.c_str(), fileSize);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,11 +49,11 @@ std::string Txt::getTitle() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Txt::setupCacheDir() const {
|
void Txt::setupCacheDir() const {
|
||||||
if (!SdMan.exists(cacheBasePath.c_str())) {
|
if (!Storage.exists(cacheBasePath.c_str())) {
|
||||||
SdMan.mkdir(cacheBasePath.c_str());
|
Storage.mkdir(cacheBasePath.c_str());
|
||||||
}
|
}
|
||||||
if (!SdMan.exists(cachePath.c_str())) {
|
if (!Storage.exists(cachePath.c_str())) {
|
||||||
SdMan.mkdir(cachePath.c_str());
|
Storage.mkdir(cachePath.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,8 +74,8 @@ std::string Txt::findCoverImage() const {
|
|||||||
// First priority: look for image with same name as txt file (e.g., mybook.jpg)
|
// First priority: look for image with same name as txt file (e.g., mybook.jpg)
|
||||||
for (const auto& ext : extensions) {
|
for (const auto& ext : extensions) {
|
||||||
std::string coverPath = folder + "/" + baseName + ext;
|
std::string coverPath = folder + "/" + baseName + ext;
|
||||||
if (SdMan.exists(coverPath.c_str())) {
|
if (Storage.exists(coverPath.c_str())) {
|
||||||
Serial.printf("[%lu] [TXT] Found matching cover image: %s\n", millis(), coverPath.c_str());
|
LOG_DBG("TXT", "Found matching cover image: %s", coverPath.c_str());
|
||||||
return coverPath;
|
return coverPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,8 +85,8 @@ std::string Txt::findCoverImage() const {
|
|||||||
for (const auto& name : coverNames) {
|
for (const auto& name : coverNames) {
|
||||||
for (const auto& ext : extensions) {
|
for (const auto& ext : extensions) {
|
||||||
std::string coverPath = folder + "/" + std::string(name) + ext;
|
std::string coverPath = folder + "/" + std::string(name) + ext;
|
||||||
if (SdMan.exists(coverPath.c_str())) {
|
if (Storage.exists(coverPath.c_str())) {
|
||||||
Serial.printf("[%lu] [TXT] Found fallback cover image: %s\n", millis(), coverPath.c_str());
|
LOG_DBG("TXT", "Found fallback cover image: %s", coverPath.c_str());
|
||||||
return coverPath;
|
return coverPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,13 +99,13 @@ std::string Txt::getCoverBmpPath() const { return cachePath + "/cover.bmp"; }
|
|||||||
|
|
||||||
bool Txt::generateCoverBmp() const {
|
bool Txt::generateCoverBmp() const {
|
||||||
// Already generated, return true
|
// Already generated, return true
|
||||||
if (SdMan.exists(getCoverBmpPath().c_str())) {
|
if (Storage.exists(getCoverBmpPath().c_str())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string coverImagePath = findCoverImage();
|
std::string coverImagePath = findCoverImage();
|
||||||
if (coverImagePath.empty()) {
|
if (coverImagePath.empty()) {
|
||||||
Serial.printf("[%lu] [TXT] No cover image found for TXT file\n", millis());
|
LOG_DBG("TXT", "No cover image found for TXT file");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,12 +121,12 @@ bool Txt::generateCoverBmp() const {
|
|||||||
|
|
||||||
if (isBmp) {
|
if (isBmp) {
|
||||||
// Copy BMP file to cache
|
// Copy BMP file to cache
|
||||||
Serial.printf("[%lu] [TXT] Copying BMP cover image to cache\n", millis());
|
LOG_DBG("TXT", "Copying BMP cover image to cache");
|
||||||
FsFile src, dst;
|
FsFile src, dst;
|
||||||
if (!SdMan.openFileForRead("TXT", coverImagePath, src)) {
|
if (!Storage.openFileForRead("TXT", coverImagePath, src)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!SdMan.openFileForWrite("TXT", getCoverBmpPath(), dst)) {
|
if (!Storage.openFileForWrite("TXT", getCoverBmpPath(), dst)) {
|
||||||
src.close();
|
src.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -136,18 +137,18 @@ bool Txt::generateCoverBmp() const {
|
|||||||
}
|
}
|
||||||
src.close();
|
src.close();
|
||||||
dst.close();
|
dst.close();
|
||||||
Serial.printf("[%lu] [TXT] Copied BMP cover to cache\n", millis());
|
LOG_DBG("TXT", "Copied BMP cover to cache");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isJpg) {
|
if (isJpg) {
|
||||||
// Convert JPG/JPEG to BMP (same approach as Epub)
|
// Convert JPG/JPEG to BMP (same approach as Epub)
|
||||||
Serial.printf("[%lu] [TXT] Generating BMP from JPG cover image\n", millis());
|
LOG_DBG("TXT", "Generating BMP from JPG cover image");
|
||||||
FsFile coverJpg, coverBmp;
|
FsFile coverJpg, coverBmp;
|
||||||
if (!SdMan.openFileForRead("TXT", coverImagePath, coverJpg)) {
|
if (!Storage.openFileForRead("TXT", coverImagePath, coverJpg)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!SdMan.openFileForWrite("TXT", getCoverBmpPath(), coverBmp)) {
|
if (!Storage.openFileForWrite("TXT", getCoverBmpPath(), coverBmp)) {
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -156,16 +157,16 @@ bool Txt::generateCoverBmp() const {
|
|||||||
coverBmp.close();
|
coverBmp.close();
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Serial.printf("[%lu] [TXT] Failed to generate BMP from JPG cover image\n", millis());
|
LOG_ERR("TXT", "Failed to generate BMP from JPG cover image");
|
||||||
SdMan.remove(getCoverBmpPath().c_str());
|
Storage.remove(getCoverBmpPath().c_str());
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [TXT] Generated BMP from JPG cover image\n", millis());
|
LOG_DBG("TXT", "Generated BMP from JPG cover image");
|
||||||
}
|
}
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PNG files are not supported (would need a PNG decoder)
|
// PNG files are not supported (would need a PNG decoder)
|
||||||
Serial.printf("[%lu] [TXT] Cover image format not supported (only BMP/JPG/JPEG)\n", millis());
|
LOG_ERR("TXT", "Cover image format not supported (only BMP/JPG/JPEG)");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +176,7 @@ bool Txt::readContent(uint8_t* buffer, size_t offset, size_t length) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (!SdMan.openFileForRead("TXT", filepath, file)) {
|
if (!Storage.openFileForRead("TXT", filepath, file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
|
|
||||||
#include "Xtc.h"
|
#include "Xtc.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <HalStorage.h>
|
||||||
#include <SDCardManager.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
bool Xtc::load() {
|
bool Xtc::load() {
|
||||||
Serial.printf("[%lu] [XTC] Loading XTC: %s\n", millis(), filepath.c_str());
|
LOG_DBG("XTC", "Loading XTC: %s", filepath.c_str());
|
||||||
|
|
||||||
// Initialize parser
|
// Initialize parser
|
||||||
parser.reset(new xtc::XtcParser());
|
parser.reset(new xtc::XtcParser());
|
||||||
@@ -19,43 +19,43 @@ bool Xtc::load() {
|
|||||||
// Open XTC file
|
// Open XTC file
|
||||||
xtc::XtcError err = parser->open(filepath.c_str());
|
xtc::XtcError err = parser->open(filepath.c_str());
|
||||||
if (err != xtc::XtcError::OK) {
|
if (err != xtc::XtcError::OK) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to load: %s\n", millis(), xtc::errorToString(err));
|
LOG_ERR("XTC", "Failed to load: %s", xtc::errorToString(err));
|
||||||
parser.reset();
|
parser.reset();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
loaded = true;
|
loaded = true;
|
||||||
Serial.printf("[%lu] [XTC] Loaded XTC: %s (%lu pages)\n", millis(), filepath.c_str(), parser->getPageCount());
|
LOG_DBG("XTC", "Loaded XTC: %s (%lu pages)", filepath.c_str(), parser->getPageCount());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Xtc::clearCache() const {
|
bool Xtc::clearCache() const {
|
||||||
if (!SdMan.exists(cachePath.c_str())) {
|
if (!Storage.exists(cachePath.c_str())) {
|
||||||
Serial.printf("[%lu] [XTC] Cache does not exist, no action needed\n", millis());
|
LOG_DBG("XTC", "Cache does not exist, no action needed");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SdMan.removeDir(cachePath.c_str())) {
|
if (!Storage.removeDir(cachePath.c_str())) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to clear cache\n", millis());
|
LOG_ERR("XTC", "Failed to clear cache");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTC] Cache cleared successfully\n", millis());
|
LOG_DBG("XTC", "Cache cleared successfully");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Xtc::setupCacheDir() const {
|
void Xtc::setupCacheDir() const {
|
||||||
if (SdMan.exists(cachePath.c_str())) {
|
if (Storage.exists(cachePath.c_str())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create directories recursively
|
// Create directories recursively
|
||||||
for (size_t i = 1; i < cachePath.length(); i++) {
|
for (size_t i = 1; i < cachePath.length(); i++) {
|
||||||
if (cachePath[i] == '/') {
|
if (cachePath[i] == '/') {
|
||||||
SdMan.mkdir(cachePath.substr(0, i).c_str());
|
Storage.mkdir(cachePath.substr(0, i).c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SdMan.mkdir(cachePath.c_str());
|
Storage.mkdir(cachePath.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Xtc::getTitle() const {
|
std::string Xtc::getTitle() const {
|
||||||
@@ -114,17 +114,17 @@ std::string Xtc::getCoverBmpPath() const { return cachePath + "/cover.bmp"; }
|
|||||||
|
|
||||||
bool Xtc::generateCoverBmp() const {
|
bool Xtc::generateCoverBmp() const {
|
||||||
// Already generated
|
// Already generated
|
||||||
if (SdMan.exists(getCoverBmpPath().c_str())) {
|
if (Storage.exists(getCoverBmpPath().c_str())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!loaded || !parser) {
|
if (!loaded || !parser) {
|
||||||
Serial.printf("[%lu] [XTC] Cannot generate cover BMP, file not loaded\n", millis());
|
LOG_ERR("XTC", "Cannot generate cover BMP, file not loaded");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parser->getPageCount() == 0) {
|
if (parser->getPageCount() == 0) {
|
||||||
Serial.printf("[%lu] [XTC] No pages in XTC file\n", millis());
|
LOG_ERR("XTC", "No pages in XTC file");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ bool Xtc::generateCoverBmp() const {
|
|||||||
// Get first page info for cover
|
// Get first page info for cover
|
||||||
xtc::PageInfo pageInfo;
|
xtc::PageInfo pageInfo;
|
||||||
if (!parser->getPageInfo(0, pageInfo)) {
|
if (!parser->getPageInfo(0, pageInfo)) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to get first page info\n", millis());
|
LOG_DBG("XTC", "Failed to get first page info");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,22 +152,22 @@ bool Xtc::generateCoverBmp() const {
|
|||||||
}
|
}
|
||||||
uint8_t* pageBuffer = static_cast<uint8_t*>(malloc(bitmapSize));
|
uint8_t* pageBuffer = static_cast<uint8_t*>(malloc(bitmapSize));
|
||||||
if (!pageBuffer) {
|
if (!pageBuffer) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to allocate page buffer (%lu bytes)\n", millis(), bitmapSize);
|
LOG_ERR("XTC", "Failed to allocate page buffer (%lu bytes)", bitmapSize);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load first page (cover)
|
// Load first page (cover)
|
||||||
size_t bytesRead = const_cast<xtc::XtcParser*>(parser.get())->loadPage(0, pageBuffer, bitmapSize);
|
size_t bytesRead = const_cast<xtc::XtcParser*>(parser.get())->loadPage(0, pageBuffer, bitmapSize);
|
||||||
if (bytesRead == 0) {
|
if (bytesRead == 0) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to load cover page\n", millis());
|
LOG_ERR("XTC", "Failed to load cover page");
|
||||||
free(pageBuffer);
|
free(pageBuffer);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create BMP file
|
// Create BMP file
|
||||||
FsFile coverBmp;
|
FsFile coverBmp;
|
||||||
if (!SdMan.openFileForWrite("XTC", getCoverBmpPath(), coverBmp)) {
|
if (!Storage.openFileForWrite("XTC", getCoverBmpPath(), coverBmp)) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to create cover BMP file\n", millis());
|
LOG_DBG("XTC", "Failed to create cover BMP file");
|
||||||
free(pageBuffer);
|
free(pageBuffer);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -297,7 +297,7 @@ bool Xtc::generateCoverBmp() const {
|
|||||||
coverBmp.close();
|
coverBmp.close();
|
||||||
free(pageBuffer);
|
free(pageBuffer);
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTC] Generated cover BMP: %s\n", millis(), getCoverBmpPath().c_str());
|
LOG_DBG("XTC", "Generated cover BMP: %s", getCoverBmpPath().c_str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,17 +306,17 @@ std::string Xtc::getThumbBmpPath(int height) const { return cachePath + "/thumb_
|
|||||||
|
|
||||||
bool Xtc::generateThumbBmp(int height) const {
|
bool Xtc::generateThumbBmp(int height) const {
|
||||||
// Already generated
|
// Already generated
|
||||||
if (SdMan.exists(getThumbBmpPath(height).c_str())) {
|
if (Storage.exists(getThumbBmpPath(height).c_str())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!loaded || !parser) {
|
if (!loaded || !parser) {
|
||||||
Serial.printf("[%lu] [XTC] Cannot generate thumb BMP, file not loaded\n", millis());
|
LOG_ERR("XTC", "Cannot generate thumb BMP, file not loaded");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parser->getPageCount() == 0) {
|
if (parser->getPageCount() == 0) {
|
||||||
Serial.printf("[%lu] [XTC] No pages in XTC file\n", millis());
|
LOG_ERR("XTC", "No pages in XTC file");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +326,7 @@ bool Xtc::generateThumbBmp(int height) const {
|
|||||||
// Get first page info for cover
|
// Get first page info for cover
|
||||||
xtc::PageInfo pageInfo;
|
xtc::PageInfo pageInfo;
|
||||||
if (!parser->getPageInfo(0, pageInfo)) {
|
if (!parser->getPageInfo(0, pageInfo)) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to get first page info\n", millis());
|
LOG_DBG("XTC", "Failed to get first page info");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,8 +348,8 @@ bool Xtc::generateThumbBmp(int height) const {
|
|||||||
// Copy cover.bmp to thumb.bmp
|
// Copy cover.bmp to thumb.bmp
|
||||||
if (generateCoverBmp()) {
|
if (generateCoverBmp()) {
|
||||||
FsFile src, dst;
|
FsFile src, dst;
|
||||||
if (SdMan.openFileForRead("XTC", getCoverBmpPath(), src)) {
|
if (Storage.openFileForRead("XTC", getCoverBmpPath(), src)) {
|
||||||
if (SdMan.openFileForWrite("XTC", getThumbBmpPath(height), dst)) {
|
if (Storage.openFileForWrite("XTC", getThumbBmpPath(height), dst)) {
|
||||||
uint8_t buffer[512];
|
uint8_t buffer[512];
|
||||||
while (src.available()) {
|
while (src.available()) {
|
||||||
size_t bytesRead = src.read(buffer, sizeof(buffer));
|
size_t bytesRead = src.read(buffer, sizeof(buffer));
|
||||||
@@ -359,8 +359,8 @@ bool Xtc::generateThumbBmp(int height) const {
|
|||||||
}
|
}
|
||||||
src.close();
|
src.close();
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [XTC] Copied cover to thumb (no scaling needed)\n", millis());
|
LOG_DBG("XTC", "Copied cover to thumb (no scaling needed)");
|
||||||
return SdMan.exists(getThumbBmpPath(height).c_str());
|
return Storage.exists(getThumbBmpPath(height).c_str());
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -368,8 +368,8 @@ bool Xtc::generateThumbBmp(int height) const {
|
|||||||
uint16_t thumbWidth = static_cast<uint16_t>(pageInfo.width * scale);
|
uint16_t thumbWidth = static_cast<uint16_t>(pageInfo.width * scale);
|
||||||
uint16_t thumbHeight = static_cast<uint16_t>(pageInfo.height * scale);
|
uint16_t thumbHeight = static_cast<uint16_t>(pageInfo.height * scale);
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTC] Generating thumb BMP: %dx%d -> %dx%d (scale: %.3f)\n", millis(), pageInfo.width,
|
LOG_DBG("XTC", "Generating thumb BMP: %dx%d -> %dx%d (scale: %.3f)", pageInfo.width, pageInfo.height, thumbWidth,
|
||||||
pageInfo.height, thumbWidth, thumbHeight, scale);
|
thumbHeight, scale);
|
||||||
|
|
||||||
// Allocate buffer for page data
|
// Allocate buffer for page data
|
||||||
size_t bitmapSize;
|
size_t bitmapSize;
|
||||||
@@ -380,22 +380,22 @@ bool Xtc::generateThumbBmp(int height) const {
|
|||||||
}
|
}
|
||||||
uint8_t* pageBuffer = static_cast<uint8_t*>(malloc(bitmapSize));
|
uint8_t* pageBuffer = static_cast<uint8_t*>(malloc(bitmapSize));
|
||||||
if (!pageBuffer) {
|
if (!pageBuffer) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to allocate page buffer (%lu bytes)\n", millis(), bitmapSize);
|
LOG_ERR("XTC", "Failed to allocate page buffer (%lu bytes)", bitmapSize);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load first page (cover)
|
// Load first page (cover)
|
||||||
size_t bytesRead = const_cast<xtc::XtcParser*>(parser.get())->loadPage(0, pageBuffer, bitmapSize);
|
size_t bytesRead = const_cast<xtc::XtcParser*>(parser.get())->loadPage(0, pageBuffer, bitmapSize);
|
||||||
if (bytesRead == 0) {
|
if (bytesRead == 0) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to load cover page for thumb\n", millis());
|
LOG_ERR("XTC", "Failed to load cover page for thumb");
|
||||||
free(pageBuffer);
|
free(pageBuffer);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create thumbnail BMP file - use 1-bit format for fast home screen rendering (no gray passes)
|
// Create thumbnail BMP file - use 1-bit format for fast home screen rendering (no gray passes)
|
||||||
FsFile thumbBmp;
|
FsFile thumbBmp;
|
||||||
if (!SdMan.openFileForWrite("XTC", getThumbBmpPath(height), thumbBmp)) {
|
if (!Storage.openFileForWrite("XTC", getThumbBmpPath(height), thumbBmp)) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to create thumb BMP file\n", millis());
|
LOG_DBG("XTC", "Failed to create thumb BMP file");
|
||||||
free(pageBuffer);
|
free(pageBuffer);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -558,8 +558,7 @@ bool Xtc::generateThumbBmp(int height) const {
|
|||||||
thumbBmp.close();
|
thumbBmp.close();
|
||||||
free(pageBuffer);
|
free(pageBuffer);
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTC] Generated thumb BMP (%dx%d): %s\n", millis(), thumbWidth, thumbHeight,
|
LOG_DBG("XTC", "Generated thumb BMP (%dx%d): %s", thumbWidth, thumbHeight, getThumbBmpPath(height).c_str());
|
||||||
getThumbBmpPath(height).c_str());
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
#include "XtcParser.h"
|
#include "XtcParser.h"
|
||||||
|
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HalStorage.h>
|
||||||
#include <SDCardManager.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ XtcError XtcParser::open(const char* filepath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Open file
|
// Open file
|
||||||
if (!SdMan.openFileForRead("XTC", filepath, m_file)) {
|
if (!Storage.openFileForRead("XTC", filepath, m_file)) {
|
||||||
m_lastError = XtcError::FILE_NOT_FOUND;
|
m_lastError = XtcError::FILE_NOT_FOUND;
|
||||||
return m_lastError;
|
return m_lastError;
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ XtcError XtcParser::open(const char* filepath) {
|
|||||||
// Read header
|
// Read header
|
||||||
m_lastError = readHeader();
|
m_lastError = readHeader();
|
||||||
if (m_lastError != XtcError::OK) {
|
if (m_lastError != XtcError::OK) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to read header: %s\n", millis(), errorToString(m_lastError));
|
LOG_DBG("XTC", "Failed to read header: %s", errorToString(m_lastError));
|
||||||
m_file.close();
|
m_file.close();
|
||||||
return m_lastError;
|
return m_lastError;
|
||||||
}
|
}
|
||||||
@@ -51,13 +51,13 @@ XtcError XtcParser::open(const char* filepath) {
|
|||||||
if (m_header.hasMetadata) {
|
if (m_header.hasMetadata) {
|
||||||
m_lastError = readTitle();
|
m_lastError = readTitle();
|
||||||
if (m_lastError != XtcError::OK) {
|
if (m_lastError != XtcError::OK) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to read title: %s\n", millis(), errorToString(m_lastError));
|
LOG_DBG("XTC", "Failed to read title: %s", errorToString(m_lastError));
|
||||||
m_file.close();
|
m_file.close();
|
||||||
return m_lastError;
|
return m_lastError;
|
||||||
}
|
}
|
||||||
m_lastError = readAuthor();
|
m_lastError = readAuthor();
|
||||||
if (m_lastError != XtcError::OK) {
|
if (m_lastError != XtcError::OK) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to read author: %s\n", millis(), errorToString(m_lastError));
|
LOG_DBG("XTC", "Failed to read author: %s", errorToString(m_lastError));
|
||||||
m_file.close();
|
m_file.close();
|
||||||
return m_lastError;
|
return m_lastError;
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ XtcError XtcParser::open(const char* filepath) {
|
|||||||
// Read page table
|
// Read page table
|
||||||
m_lastError = readPageTable();
|
m_lastError = readPageTable();
|
||||||
if (m_lastError != XtcError::OK) {
|
if (m_lastError != XtcError::OK) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to read page table: %s\n", millis(), errorToString(m_lastError));
|
LOG_DBG("XTC", "Failed to read page table: %s", errorToString(m_lastError));
|
||||||
m_file.close();
|
m_file.close();
|
||||||
return m_lastError;
|
return m_lastError;
|
||||||
}
|
}
|
||||||
@@ -74,14 +74,13 @@ XtcError XtcParser::open(const char* filepath) {
|
|||||||
// Read chapters if present
|
// Read chapters if present
|
||||||
m_lastError = readChapters();
|
m_lastError = readChapters();
|
||||||
if (m_lastError != XtcError::OK) {
|
if (m_lastError != XtcError::OK) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to read chapters: %s\n", millis(), errorToString(m_lastError));
|
LOG_DBG("XTC", "Failed to read chapters: %s", errorToString(m_lastError));
|
||||||
m_file.close();
|
m_file.close();
|
||||||
return m_lastError;
|
return m_lastError;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_isOpen = true;
|
m_isOpen = true;
|
||||||
Serial.printf("[%lu] [XTC] Opened file: %s (%u pages, %dx%d)\n", millis(), filepath, m_header.pageCount,
|
LOG_DBG("XTC", "Opened file: %s (%u pages, %dx%d)", filepath, m_header.pageCount, m_defaultWidth, m_defaultHeight);
|
||||||
m_defaultWidth, m_defaultHeight);
|
|
||||||
return XtcError::OK;
|
return XtcError::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,8 +105,7 @@ XtcError XtcParser::readHeader() {
|
|||||||
|
|
||||||
// Verify magic number (accept both XTC and XTCH)
|
// Verify magic number (accept both XTC and XTCH)
|
||||||
if (m_header.magic != XTC_MAGIC && m_header.magic != XTCH_MAGIC) {
|
if (m_header.magic != XTC_MAGIC && m_header.magic != XTCH_MAGIC) {
|
||||||
Serial.printf("[%lu] [XTC] Invalid magic: 0x%08X (expected 0x%08X or 0x%08X)\n", millis(), m_header.magic,
|
LOG_DBG("XTC", "Invalid magic: 0x%08X (expected 0x%08X or 0x%08X)", m_header.magic, XTC_MAGIC, XTCH_MAGIC);
|
||||||
XTC_MAGIC, XTCH_MAGIC);
|
|
||||||
return XtcError::INVALID_MAGIC;
|
return XtcError::INVALID_MAGIC;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +118,7 @@ XtcError XtcParser::readHeader() {
|
|||||||
const bool validVersion = m_header.versionMajor == 1 && m_header.versionMinor == 0 ||
|
const bool validVersion = m_header.versionMajor == 1 && m_header.versionMinor == 0 ||
|
||||||
m_header.versionMajor == 0 && m_header.versionMinor == 1;
|
m_header.versionMajor == 0 && m_header.versionMinor == 1;
|
||||||
if (!validVersion) {
|
if (!validVersion) {
|
||||||
Serial.printf("[%lu] [XTC] Unsupported version: %u.%u\n", millis(), m_header.versionMajor, m_header.versionMinor);
|
LOG_DBG("XTC", "Unsupported version: %u.%u", m_header.versionMajor, m_header.versionMinor);
|
||||||
return XtcError::INVALID_VERSION;
|
return XtcError::INVALID_VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +127,7 @@ XtcError XtcParser::readHeader() {
|
|||||||
return XtcError::CORRUPTED_HEADER;
|
return XtcError::CORRUPTED_HEADER;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTC] Header: magic=0x%08X (%s), ver=%u.%u, pages=%u, bitDepth=%u\n", millis(), m_header.magic,
|
LOG_DBG("XTC", "Header: magic=0x%08X (%s), ver=%u.%u, pages=%u, bitDepth=%u", m_header.magic,
|
||||||
(m_header.magic == XTCH_MAGIC) ? "XTCH" : "XTC", m_header.versionMajor, m_header.versionMinor,
|
(m_header.magic == XTCH_MAGIC) ? "XTCH" : "XTC", m_header.versionMajor, m_header.versionMinor,
|
||||||
m_header.pageCount, m_bitDepth);
|
m_header.pageCount, m_bitDepth);
|
||||||
|
|
||||||
@@ -146,7 +144,7 @@ XtcError XtcParser::readTitle() {
|
|||||||
m_file.read(titleBuf, sizeof(titleBuf) - 1);
|
m_file.read(titleBuf, sizeof(titleBuf) - 1);
|
||||||
m_title = titleBuf;
|
m_title = titleBuf;
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTC] Title: %s\n", millis(), m_title.c_str());
|
LOG_DBG("XTC", "Title: %s", m_title.c_str());
|
||||||
return XtcError::OK;
|
return XtcError::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,19 +159,19 @@ XtcError XtcParser::readAuthor() {
|
|||||||
m_file.read(authorBuf, sizeof(authorBuf) - 1);
|
m_file.read(authorBuf, sizeof(authorBuf) - 1);
|
||||||
m_author = authorBuf;
|
m_author = authorBuf;
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTC] Author: %s\n", millis(), m_author.c_str());
|
LOG_DBG("XTC", "Author: %s", m_author.c_str());
|
||||||
return XtcError::OK;
|
return XtcError::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
XtcError XtcParser::readPageTable() {
|
XtcError XtcParser::readPageTable() {
|
||||||
if (m_header.pageTableOffset == 0) {
|
if (m_header.pageTableOffset == 0) {
|
||||||
Serial.printf("[%lu] [XTC] Page table offset is 0, cannot read\n", millis());
|
LOG_DBG("XTC", "Page table offset is 0, cannot read");
|
||||||
return XtcError::CORRUPTED_HEADER;
|
return XtcError::CORRUPTED_HEADER;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seek to page table
|
// Seek to page table
|
||||||
if (!m_file.seek(m_header.pageTableOffset)) {
|
if (!m_file.seek(m_header.pageTableOffset)) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to seek to page table at %llu\n", millis(), m_header.pageTableOffset);
|
LOG_DBG("XTC", "Failed to seek to page table at %llu", m_header.pageTableOffset);
|
||||||
return XtcError::READ_ERROR;
|
return XtcError::READ_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +182,7 @@ XtcError XtcParser::readPageTable() {
|
|||||||
PageTableEntry entry;
|
PageTableEntry entry;
|
||||||
size_t bytesRead = m_file.read(reinterpret_cast<uint8_t*>(&entry), sizeof(PageTableEntry));
|
size_t bytesRead = m_file.read(reinterpret_cast<uint8_t*>(&entry), sizeof(PageTableEntry));
|
||||||
if (bytesRead != sizeof(PageTableEntry)) {
|
if (bytesRead != sizeof(PageTableEntry)) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to read page table entry %u\n", millis(), i);
|
LOG_DBG("XTC", "Failed to read page table entry %u", i);
|
||||||
return XtcError::READ_ERROR;
|
return XtcError::READ_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +199,7 @@ XtcError XtcParser::readPageTable() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTC] Read %u page table entries\n", millis(), m_header.pageCount);
|
LOG_DBG("XTC", "Read %u page table entries", m_header.pageCount);
|
||||||
return XtcError::OK;
|
return XtcError::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,7 +305,7 @@ XtcError XtcParser::readChapters() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_hasChapters = !m_chapters.empty();
|
m_hasChapters = !m_chapters.empty();
|
||||||
Serial.printf("[%lu] [XTC] Chapters: %u\n", millis(), static_cast<unsigned int>(m_chapters.size()));
|
LOG_DBG("XTC", "Chapters: %u", static_cast<unsigned int>(m_chapters.size()));
|
||||||
return XtcError::OK;
|
return XtcError::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,7 +332,7 @@ size_t XtcParser::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSiz
|
|||||||
|
|
||||||
// Seek to page data
|
// Seek to page data
|
||||||
if (!m_file.seek(page.offset)) {
|
if (!m_file.seek(page.offset)) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to seek to page %u at offset %lu\n", millis(), pageIndex, page.offset);
|
LOG_DBG("XTC", "Failed to seek to page %u at offset %lu", pageIndex, page.offset);
|
||||||
m_lastError = XtcError::READ_ERROR;
|
m_lastError = XtcError::READ_ERROR;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -343,7 +341,7 @@ size_t XtcParser::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSiz
|
|||||||
XtgPageHeader pageHeader;
|
XtgPageHeader pageHeader;
|
||||||
size_t headerRead = m_file.read(reinterpret_cast<uint8_t*>(&pageHeader), sizeof(XtgPageHeader));
|
size_t headerRead = m_file.read(reinterpret_cast<uint8_t*>(&pageHeader), sizeof(XtgPageHeader));
|
||||||
if (headerRead != sizeof(XtgPageHeader)) {
|
if (headerRead != sizeof(XtgPageHeader)) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to read page header for page %u\n", millis(), pageIndex);
|
LOG_DBG("XTC", "Failed to read page header for page %u", pageIndex);
|
||||||
m_lastError = XtcError::READ_ERROR;
|
m_lastError = XtcError::READ_ERROR;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -351,8 +349,8 @@ size_t XtcParser::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSiz
|
|||||||
// Verify page magic (XTG for 1-bit, XTH for 2-bit)
|
// Verify page magic (XTG for 1-bit, XTH for 2-bit)
|
||||||
const uint32_t expectedMagic = (m_bitDepth == 2) ? XTH_MAGIC : XTG_MAGIC;
|
const uint32_t expectedMagic = (m_bitDepth == 2) ? XTH_MAGIC : XTG_MAGIC;
|
||||||
if (pageHeader.magic != expectedMagic) {
|
if (pageHeader.magic != expectedMagic) {
|
||||||
Serial.printf("[%lu] [XTC] Invalid page magic for page %u: 0x%08X (expected 0x%08X)\n", millis(), pageIndex,
|
LOG_DBG("XTC", "Invalid page magic for page %u: 0x%08X (expected 0x%08X)", pageIndex, pageHeader.magic,
|
||||||
pageHeader.magic, expectedMagic);
|
expectedMagic);
|
||||||
m_lastError = XtcError::INVALID_MAGIC;
|
m_lastError = XtcError::INVALID_MAGIC;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -370,7 +368,7 @@ size_t XtcParser::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSiz
|
|||||||
|
|
||||||
// Check buffer size
|
// Check buffer size
|
||||||
if (bufferSize < bitmapSize) {
|
if (bufferSize < bitmapSize) {
|
||||||
Serial.printf("[%lu] [XTC] Buffer too small: need %u, have %u\n", millis(), bitmapSize, bufferSize);
|
LOG_DBG("XTC", "Buffer too small: need %u, have %u", bitmapSize, bufferSize);
|
||||||
m_lastError = XtcError::MEMORY_ERROR;
|
m_lastError = XtcError::MEMORY_ERROR;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -378,7 +376,7 @@ size_t XtcParser::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSiz
|
|||||||
// Read bitmap data
|
// Read bitmap data
|
||||||
size_t bytesRead = m_file.read(buffer, bitmapSize);
|
size_t bytesRead = m_file.read(buffer, bitmapSize);
|
||||||
if (bytesRead != bitmapSize) {
|
if (bytesRead != bitmapSize) {
|
||||||
Serial.printf("[%lu] [XTC] Page read error: expected %u, got %u\n", millis(), bitmapSize, bytesRead);
|
LOG_DBG("XTC", "Page read error: expected %u, got %u", bitmapSize, bytesRead);
|
||||||
m_lastError = XtcError::READ_ERROR;
|
m_lastError = XtcError::READ_ERROR;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -444,7 +442,7 @@ XtcError XtcParser::loadPageStreaming(uint32_t pageIndex,
|
|||||||
|
|
||||||
bool XtcParser::isValidXtcFile(const char* filepath) {
|
bool XtcParser::isValidXtcFile(const char* filepath) {
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (!SdMan.openFileForRead("XTC", filepath, file)) {
|
if (!Storage.openFileForRead("XTC", filepath, file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <SdFat.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#include "ZipFile.h"
|
#include "ZipFile.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <HalStorage.h>
|
||||||
#include <SDCardManager.h>
|
#include <Logging.h>
|
||||||
#include <miniz.h>
|
#include <miniz.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -10,7 +10,7 @@ bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t*
|
|||||||
// Setup inflator
|
// Setup inflator
|
||||||
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
|
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
|
||||||
if (!inflator) {
|
if (!inflator) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for inflator\n", millis());
|
LOG_ERR("ZIP", "Failed to allocate memory for inflator");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
memset(inflator, 0, sizeof(tinfl_decompressor));
|
memset(inflator, 0, sizeof(tinfl_decompressor));
|
||||||
@@ -23,7 +23,7 @@ bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t*
|
|||||||
free(inflator);
|
free(inflator);
|
||||||
|
|
||||||
if (status != TINFL_STATUS_DONE) {
|
if (status != TINFL_STATUS_DONE) {
|
||||||
Serial.printf("[%lu] [ZIP] tinfl_decompress() failed with status %d\n", millis(), status);
|
LOG_ERR("ZIP", "tinfl_decompress() failed with status %d", status);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,13 +195,13 @@ long ZipFile::getDataOffset(const FileStatSlim& fileStat) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (read != localHeaderSize) {
|
if (read != localHeaderSize) {
|
||||||
Serial.printf("[%lu] [ZIP] Something went wrong reading the local header\n", millis());
|
LOG_ERR("ZIP", "Something went wrong reading the local header");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pLocalHeader[0] + (pLocalHeader[1] << 8) + (pLocalHeader[2] << 16) + (pLocalHeader[3] << 24) !=
|
if (pLocalHeader[0] + (pLocalHeader[1] << 8) + (pLocalHeader[2] << 16) + (pLocalHeader[3] << 24) !=
|
||||||
0x04034b50 /* MZ_ZIP_LOCAL_DIR_HEADER_SIG */) {
|
0x04034b50 /* MZ_ZIP_LOCAL_DIR_HEADER_SIG */) {
|
||||||
Serial.printf("[%lu] [ZIP] Not a valid zip file header\n", millis());
|
LOG_ERR("ZIP", "Not a valid zip file header");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +222,7 @@ bool ZipFile::loadZipDetails() {
|
|||||||
|
|
||||||
const size_t fileSize = file.size();
|
const size_t fileSize = file.size();
|
||||||
if (fileSize < 22) {
|
if (fileSize < 22) {
|
||||||
Serial.printf("[%lu] [ZIP] File too small to be a valid zip\n", millis());
|
LOG_ERR("ZIP", "File too small to be a valid zip");
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@@ -234,7 +234,7 @@ bool ZipFile::loadZipDetails() {
|
|||||||
const int scanRange = fileSize > 1024 ? 1024 : fileSize;
|
const int scanRange = fileSize > 1024 ? 1024 : fileSize;
|
||||||
const auto buffer = static_cast<uint8_t*>(malloc(scanRange));
|
const auto buffer = static_cast<uint8_t*>(malloc(scanRange));
|
||||||
if (!buffer) {
|
if (!buffer) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for EOCD scan buffer\n", millis());
|
LOG_ERR("ZIP", "Failed to allocate memory for EOCD scan buffer");
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@@ -255,7 +255,7 @@ bool ZipFile::loadZipDetails() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (foundOffset == -1) {
|
if (foundOffset == -1) {
|
||||||
Serial.printf("[%lu] [ZIP] EOCD signature not found in zip file\n", millis());
|
LOG_ERR("ZIP", "EOCD signature not found in zip file");
|
||||||
free(buffer);
|
free(buffer);
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
@@ -279,7 +279,7 @@ bool ZipFile::loadZipDetails() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool ZipFile::open() {
|
bool ZipFile::open() {
|
||||||
if (!SdMan.openFileForRead("ZIP", filePath, file)) {
|
if (!Storage.openFileForRead("ZIP", filePath, file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -407,7 +407,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
|||||||
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) {
|
if (data == nullptr) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for output buffer (%zu bytes)\n", millis(), dataSize);
|
LOG_ERR("ZIP", "Failed to allocate memory for output buffer (%zu bytes)", dataSize);
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@@ -422,7 +422,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dataRead != inflatedDataSize) {
|
if (dataRead != inflatedDataSize) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to read data\n", millis());
|
LOG_ERR("ZIP", "Failed to read data");
|
||||||
free(data);
|
free(data);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -432,7 +432,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
|||||||
// Read out deflated content from file
|
// Read out deflated content from file
|
||||||
const auto deflatedData = static_cast<uint8_t*>(malloc(deflatedDataSize));
|
const auto deflatedData = static_cast<uint8_t*>(malloc(deflatedDataSize));
|
||||||
if (deflatedData == nullptr) {
|
if (deflatedData == nullptr) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for decompression buffer\n", millis());
|
LOG_ERR("ZIP", "Failed to allocate memory for decompression buffer");
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@@ -445,7 +445,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dataRead != deflatedDataSize) {
|
if (dataRead != deflatedDataSize) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to read data, expected %d got %d\n", millis(), deflatedDataSize, dataRead);
|
LOG_ERR("ZIP", "Failed to read data, expected %d got %d", deflatedDataSize, dataRead);
|
||||||
free(deflatedData);
|
free(deflatedData);
|
||||||
free(data);
|
free(data);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -455,14 +455,14 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
|||||||
free(deflatedData);
|
free(deflatedData);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to inflate file\n", millis());
|
LOG_ERR("ZIP", "Failed to inflate file");
|
||||||
free(data);
|
free(data);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue out of block with data set
|
// Continue out of block with data set
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [ZIP] Unsupported compression method\n", millis());
|
LOG_ERR("ZIP", "Unsupported compression method");
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@@ -498,7 +498,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
// no deflation, just read content
|
// no deflation, just read content
|
||||||
const auto buffer = static_cast<uint8_t*>(malloc(chunkSize));
|
const auto buffer = static_cast<uint8_t*>(malloc(chunkSize));
|
||||||
if (!buffer) {
|
if (!buffer) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for buffer\n", millis());
|
LOG_ERR("ZIP", "Failed to allocate memory for buffer");
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@@ -509,7 +509,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
while (remaining > 0) {
|
while (remaining > 0) {
|
||||||
const size_t dataRead = file.read(buffer, remaining < chunkSize ? remaining : chunkSize);
|
const size_t dataRead = file.read(buffer, remaining < chunkSize ? remaining : chunkSize);
|
||||||
if (dataRead == 0) {
|
if (dataRead == 0) {
|
||||||
Serial.printf("[%lu] [ZIP] Could not read more bytes\n", millis());
|
LOG_ERR("ZIP", "Could not read more bytes");
|
||||||
free(buffer);
|
free(buffer);
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
@@ -532,7 +532,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
// Setup inflator
|
// Setup inflator
|
||||||
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
|
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
|
||||||
if (!inflator) {
|
if (!inflator) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for inflator\n", millis());
|
LOG_ERR("ZIP", "Failed to allocate memory for inflator");
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@@ -544,7 +544,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
// Setup file read buffer
|
// Setup file read buffer
|
||||||
const auto fileReadBuffer = static_cast<uint8_t*>(malloc(chunkSize));
|
const auto fileReadBuffer = static_cast<uint8_t*>(malloc(chunkSize));
|
||||||
if (!fileReadBuffer) {
|
if (!fileReadBuffer) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for zip file read buffer\n", millis());
|
LOG_ERR("ZIP", "Failed to allocate memory for zip file read buffer");
|
||||||
free(inflator);
|
free(inflator);
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
@@ -554,7 +554,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
|
|
||||||
const auto outputBuffer = static_cast<uint8_t*>(malloc(TINFL_LZ_DICT_SIZE));
|
const auto outputBuffer = static_cast<uint8_t*>(malloc(TINFL_LZ_DICT_SIZE));
|
||||||
if (!outputBuffer) {
|
if (!outputBuffer) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for dictionary\n", millis());
|
LOG_ERR("ZIP", "Failed to allocate memory for dictionary");
|
||||||
free(inflator);
|
free(inflator);
|
||||||
free(fileReadBuffer);
|
free(fileReadBuffer);
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
@@ -605,7 +605,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
if (outBytes > 0) {
|
if (outBytes > 0) {
|
||||||
processedOutputBytes += outBytes;
|
processedOutputBytes += outBytes;
|
||||||
if (out.write(outputBuffer + outputCursor, outBytes) != outBytes) {
|
if (out.write(outputBuffer + outputCursor, outBytes) != outBytes) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to write all output bytes to stream\n", millis());
|
LOG_ERR("ZIP", "Failed to write all output bytes to stream");
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@@ -619,7 +619,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status < 0) {
|
if (status < 0) {
|
||||||
Serial.printf("[%lu] [ZIP] tinfl_decompress() failed with status %d\n", millis(), status);
|
LOG_ERR("ZIP", "tinfl_decompress() failed with status %d", status);
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@@ -630,8 +630,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status == TINFL_STATUS_DONE) {
|
if (status == TINFL_STATUS_DONE) {
|
||||||
Serial.printf("[%lu] [ZIP] Decompressed %d bytes into %d bytes\n", millis(), deflatedDataSize,
|
LOG_ERR("ZIP", "Decompressed %d bytes into %d bytes", deflatedDataSize, inflatedDataSize);
|
||||||
inflatedDataSize);
|
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@@ -643,7 +642,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we get here, EOF reached without TINFL_STATUS_DONE
|
// If we get here, EOF reached without TINFL_STATUS_DONE
|
||||||
Serial.printf("[%lu] [ZIP] Unexpected EOF\n", millis());
|
LOG_ERR("ZIP", "Unexpected EOF");
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@@ -657,6 +656,6 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [ZIP] Unsupported compression method\n", millis());
|
LOG_ERR("ZIP", "Unsupported compression method");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <SdFat.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|||||||
65
lib/hal/HalStorage.cpp
Normal file
65
lib/hal/HalStorage.cpp
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#include "HalStorage.h"
|
||||||
|
|
||||||
|
#include <SDCardManager.h>
|
||||||
|
|
||||||
|
#define SDCard SDCardManager::getInstance()
|
||||||
|
|
||||||
|
HalStorage HalStorage::instance;
|
||||||
|
|
||||||
|
HalStorage::HalStorage() {}
|
||||||
|
|
||||||
|
bool HalStorage::begin() { return SDCard.begin(); }
|
||||||
|
|
||||||
|
bool HalStorage::ready() const { return SDCard.ready(); }
|
||||||
|
|
||||||
|
std::vector<String> HalStorage::listFiles(const char* path, int maxFiles) { return SDCard.listFiles(path, maxFiles); }
|
||||||
|
|
||||||
|
String HalStorage::readFile(const char* path) { return SDCard.readFile(path); }
|
||||||
|
|
||||||
|
bool HalStorage::readFileToStream(const char* path, Print& out, size_t chunkSize) {
|
||||||
|
return SDCard.readFileToStream(path, out, chunkSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t HalStorage::readFileToBuffer(const char* path, char* buffer, size_t bufferSize, size_t maxBytes) {
|
||||||
|
return SDCard.readFileToBuffer(path, buffer, bufferSize, maxBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalStorage::writeFile(const char* path, const String& content) { return SDCard.writeFile(path, content); }
|
||||||
|
|
||||||
|
bool HalStorage::ensureDirectoryExists(const char* path) { return SDCard.ensureDirectoryExists(path); }
|
||||||
|
|
||||||
|
FsFile HalStorage::open(const char* path, const oflag_t oflag) { return SDCard.open(path, oflag); }
|
||||||
|
|
||||||
|
bool HalStorage::mkdir(const char* path, const bool pFlag) { return SDCard.mkdir(path, pFlag); }
|
||||||
|
|
||||||
|
bool HalStorage::exists(const char* path) { return SDCard.exists(path); }
|
||||||
|
|
||||||
|
bool HalStorage::remove(const char* path) { return SDCard.remove(path); }
|
||||||
|
|
||||||
|
bool HalStorage::rmdir(const char* path) { return SDCard.rmdir(path); }
|
||||||
|
|
||||||
|
bool HalStorage::openFileForRead(const char* moduleName, const char* path, FsFile& file) {
|
||||||
|
return SDCard.openFileForRead(moduleName, path, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalStorage::openFileForRead(const char* moduleName, const std::string& path, FsFile& file) {
|
||||||
|
return openFileForRead(moduleName, path.c_str(), file);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalStorage::openFileForRead(const char* moduleName, const String& path, FsFile& file) {
|
||||||
|
return openFileForRead(moduleName, path.c_str(), file);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalStorage::openFileForWrite(const char* moduleName, const char* path, FsFile& file) {
|
||||||
|
return SDCard.openFileForWrite(moduleName, path, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalStorage::openFileForWrite(const char* moduleName, const std::string& path, FsFile& file) {
|
||||||
|
return openFileForWrite(moduleName, path.c_str(), file);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalStorage::openFileForWrite(const char* moduleName, const String& path, FsFile& file) {
|
||||||
|
return openFileForWrite(moduleName, path.c_str(), file);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HalStorage::removeDir(const char* path) { return SDCard.removeDir(path); }
|
||||||
54
lib/hal/HalStorage.h
Normal file
54
lib/hal/HalStorage.h
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDCardManager.h>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class HalStorage {
|
||||||
|
public:
|
||||||
|
HalStorage();
|
||||||
|
bool begin();
|
||||||
|
bool ready() const;
|
||||||
|
std::vector<String> listFiles(const char* path = "/", int maxFiles = 200);
|
||||||
|
// Read the entire file at `path` into a String. Returns empty string on failure.
|
||||||
|
String readFile(const char* path);
|
||||||
|
// Low-memory helpers:
|
||||||
|
// Stream the file contents to a `Print` (e.g. `Serial`, or any `Print`-derived object).
|
||||||
|
// Returns true on success, false on failure.
|
||||||
|
bool readFileToStream(const char* path, Print& out, size_t chunkSize = 256);
|
||||||
|
// Read up to `bufferSize-1` bytes into `buffer`, null-terminating it. Returns bytes read.
|
||||||
|
size_t readFileToBuffer(const char* path, char* buffer, size_t bufferSize, size_t maxBytes = 0);
|
||||||
|
// Write a string to `path` on the SD card. Overwrites existing file.
|
||||||
|
// Returns true on success.
|
||||||
|
bool writeFile(const char* path, const String& content);
|
||||||
|
// Ensure a directory exists, creating it if necessary. Returns true on success.
|
||||||
|
bool ensureDirectoryExists(const char* path);
|
||||||
|
|
||||||
|
FsFile open(const char* path, const oflag_t oflag = O_RDONLY);
|
||||||
|
bool mkdir(const char* path, const bool pFlag = true);
|
||||||
|
bool exists(const char* path);
|
||||||
|
bool remove(const char* path);
|
||||||
|
bool rmdir(const char* path);
|
||||||
|
|
||||||
|
bool openFileForRead(const char* moduleName, const char* path, FsFile& file);
|
||||||
|
bool openFileForRead(const char* moduleName, const std::string& path, FsFile& file);
|
||||||
|
bool openFileForRead(const char* moduleName, const String& path, FsFile& file);
|
||||||
|
bool openFileForWrite(const char* moduleName, const char* path, FsFile& file);
|
||||||
|
bool openFileForWrite(const char* moduleName, const std::string& path, FsFile& file);
|
||||||
|
bool openFileForWrite(const char* moduleName, const String& path, FsFile& file);
|
||||||
|
bool removeDir(const char* path);
|
||||||
|
|
||||||
|
static HalStorage& getInstance() { return instance; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
static HalStorage instance;
|
||||||
|
|
||||||
|
bool initialized = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
#define Storage HalStorage::getInstance()
|
||||||
|
|
||||||
|
// Downstream code must use Storage instead of SdMan
|
||||||
|
#ifdef SdMan
|
||||||
|
#undef SdMan
|
||||||
|
#endif
|
||||||
@@ -54,15 +54,31 @@ extends = base
|
|||||||
build_flags =
|
build_flags =
|
||||||
${base.build_flags}
|
${base.build_flags}
|
||||||
-DCROSSPOINT_VERSION=\"${crosspoint.version}-dev\"
|
-DCROSSPOINT_VERSION=\"${crosspoint.version}-dev\"
|
||||||
|
-DENABLE_SERIAL_LOG
|
||||||
|
-DLOG_LEVEL=2 ; Set log level to debug for development builds
|
||||||
|
|
||||||
|
|
||||||
[env:gh_release]
|
[env:gh_release]
|
||||||
extends = base
|
extends = base
|
||||||
build_flags =
|
build_flags =
|
||||||
${base.build_flags}
|
${base.build_flags}
|
||||||
-DCROSSPOINT_VERSION=\"${crosspoint.version}\"
|
-DCROSSPOINT_VERSION=\"${crosspoint.version}\"
|
||||||
|
-DENABLE_SERIAL_LOG
|
||||||
|
-DLOG_LEVEL=0 ; Set log level to error for release builds
|
||||||
|
|
||||||
[env:gh_release_rc]
|
[env:gh_release_rc]
|
||||||
extends = base
|
extends = base
|
||||||
build_flags =
|
build_flags =
|
||||||
${base.build_flags}
|
${base.build_flags}
|
||||||
-DCROSSPOINT_VERSION=\"${crosspoint.version}-rc+${sysenv.CROSSPOINT_RC_HASH}\"
|
-DCROSSPOINT_VERSION=\"${crosspoint.version}-rc+${sysenv.CROSSPOINT_RC_HASH}\"
|
||||||
|
-DENABLE_SERIAL_LOG
|
||||||
|
-DLOG_LEVEL=1 ; Set log level to info for release candidate builds
|
||||||
|
|
||||||
|
[env:slim]
|
||||||
|
extends = base
|
||||||
|
build_flags =
|
||||||
|
${base.build_flags}
|
||||||
|
-DCROSSPOINT_VERSION=\"${crosspoint.version}-slim\"
|
||||||
|
; serial output is disabled in slim builds to save space
|
||||||
|
-UENABLE_SERIAL_LOG
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import gzip
|
||||||
|
|
||||||
SRC_DIR = "src"
|
SRC_DIR = "src"
|
||||||
|
|
||||||
@@ -40,12 +41,34 @@ for root, _, files in os.walk(SRC_DIR):
|
|||||||
|
|
||||||
# minified = regex.sub("\g<1>", html_content)
|
# minified = regex.sub("\g<1>", html_content)
|
||||||
minified = minify_html(html_content)
|
minified = minify_html(html_content)
|
||||||
|
|
||||||
|
# Compress with gzip (compresslevel 9 is maximum compression)
|
||||||
|
# IMPORTANT: we don't use brotli because Firefox doesn't support brotli with insecured context (only supported on HTTPS)
|
||||||
|
compressed = gzip.compress(minified.encode('utf-8'), compresslevel=9)
|
||||||
|
|
||||||
base_name = f"{os.path.splitext(file)[0]}Html"
|
base_name = f"{os.path.splitext(file)[0]}Html"
|
||||||
header_path = os.path.join(root, f"{base_name}.generated.h")
|
header_path = os.path.join(root, f"{base_name}.generated.h")
|
||||||
|
|
||||||
with open(header_path, "w", encoding="utf-8") as h:
|
with open(header_path, "w", encoding="utf-8") as h:
|
||||||
h.write(f"// THIS FILE IS AUTOGENERATED, DO NOT EDIT MANUALLY\n\n")
|
h.write(f"// THIS FILE IS AUTOGENERATED, DO NOT EDIT MANUALLY\n\n")
|
||||||
h.write(f"#pragma once\n")
|
h.write(f"#pragma once\n")
|
||||||
h.write(f'constexpr char {base_name}[] PROGMEM = R"rawliteral({minified})rawliteral";\n')
|
h.write(f"#include <cstddef>\n\n")
|
||||||
|
|
||||||
|
# Write the compressed data as a byte array
|
||||||
|
h.write(f"constexpr char {base_name}[] PROGMEM = {{\n")
|
||||||
|
|
||||||
|
# Write bytes in rows of 16
|
||||||
|
for i in range(0, len(compressed), 16):
|
||||||
|
chunk = compressed[i:i+16]
|
||||||
|
hex_values = ', '.join(f'0x{b:02x}' for b in chunk)
|
||||||
|
h.write(f" {hex_values},\n")
|
||||||
|
|
||||||
|
h.write(f"}};\n\n")
|
||||||
|
h.write(f"constexpr size_t {base_name}CompressedSize = {len(compressed)};\n")
|
||||||
|
h.write(f"constexpr size_t {base_name}OriginalSize = {len(minified)};\n")
|
||||||
|
|
||||||
print(f"Generated: {header_path}")
|
print(f"Generated: {header_path}")
|
||||||
|
print(f" Original: {len(html_content)} bytes")
|
||||||
|
print(f" Minified: {len(minified)} bytes ({100*len(minified)/len(html_content):.1f}%)")
|
||||||
|
print(f" Compressed: {len(compressed)} bytes ({100*len(compressed)/len(html_content):.1f}%)")
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,73 @@
|
|||||||
import sys
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
ESP32 Serial Monitor with Memory Graph
|
||||||
|
|
||||||
|
This script provides a comprehensive real-time serial monitor for ESP32 devices with
|
||||||
|
integrated memory usage graphing capabilities. It reads serial output, parses memory
|
||||||
|
information, and displays it in both console and graphical form.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Real-time serial output monitoring with color-coded log levels
|
||||||
|
- Interactive memory usage graphing with matplotlib
|
||||||
|
- Command input interface for sending commands to the ESP32 device
|
||||||
|
- Screenshot capture and processing (1-bit black/white format)
|
||||||
|
- Graceful shutdown handling with Ctrl-C signal processing
|
||||||
|
- Configurable filtering and suppression of log messages
|
||||||
|
- Thread-safe operation with coordinated shutdown events
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python debugging_monitor.py [port] [options]
|
||||||
|
|
||||||
|
The script will open a matplotlib window showing memory usage over time and provide
|
||||||
|
an interactive command prompt for sending commands to the device. Press Ctrl-C or
|
||||||
|
close the graph window to exit gracefully.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import glob
|
||||||
|
import platform
|
||||||
import re
|
import re
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from datetime import datetime
|
|
||||||
from collections import deque
|
from collections import deque
|
||||||
import time
|
from datetime import datetime
|
||||||
|
|
||||||
# Try to import potentially missing packages
|
# Try to import potentially missing packages
|
||||||
|
PACKAGE_MAPPING: dict[str, str] = {
|
||||||
|
"serial": "pyserial",
|
||||||
|
"colorama": "colorama",
|
||||||
|
"matplotlib": "matplotlib",
|
||||||
|
"PIL": "Pillow",
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import serial
|
|
||||||
from colorama import init, Fore, Style
|
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
import matplotlib.animation as animation
|
import serial
|
||||||
|
from colorama import Fore, Style, init
|
||||||
|
from matplotlib import animation
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
except ImportError:
|
||||||
|
Image = None
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
missing_package = e.name
|
ERROR_MSG = str(e).lower()
|
||||||
|
missing_packages = [pkg for mod, pkg in PACKAGE_MAPPING.items() if mod in ERROR_MSG]
|
||||||
|
|
||||||
|
if not missing_packages:
|
||||||
|
# Fallback if mapping doesn't cover
|
||||||
|
missing_packages = ["pyserial", "colorama", "matplotlib"]
|
||||||
|
|
||||||
print("\n" + "!" * 50)
|
print("\n" + "!" * 50)
|
||||||
print(f" Error: The required package '{missing_package}' is not installed.")
|
print(f" Error: Required package(s) not installed: {', '.join(missing_packages)}")
|
||||||
print("!" * 50)
|
print("!" * 50)
|
||||||
|
|
||||||
print(f"\nTo fix this, please run the following command in your terminal:\n")
|
print("\nTo fix this, please run the following command in your terminal:\n")
|
||||||
|
INSTALL_CMD = "pip install " if sys.platform.startswith("win") else "pip3 install "
|
||||||
install_cmd = "pip install "
|
print(f" {INSTALL_CMD}{' '.join(missing_packages)}")
|
||||||
packages = []
|
|
||||||
if 'serial' in str(e): packages.append("pyserial")
|
|
||||||
if 'colorama' in str(e): packages.append("colorama")
|
|
||||||
if 'matplotlib' in str(e): packages.append("matplotlib")
|
|
||||||
|
|
||||||
print(f" {install_cmd}{' '.join(packages)}")
|
|
||||||
|
|
||||||
print("\nExiting...")
|
print("\nExiting...")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -34,50 +75,104 @@ except ImportError as e:
|
|||||||
# --- Global Variables for Data Sharing ---
|
# --- Global Variables for Data Sharing ---
|
||||||
# Store last 50 data points
|
# Store last 50 data points
|
||||||
MAX_POINTS = 50
|
MAX_POINTS = 50
|
||||||
time_data = deque(maxlen=MAX_POINTS)
|
time_data: deque[str] = deque(maxlen=MAX_POINTS)
|
||||||
free_mem_data = deque(maxlen=MAX_POINTS)
|
free_mem_data: deque[float] = deque(maxlen=MAX_POINTS)
|
||||||
total_mem_data = deque(maxlen=MAX_POINTS)
|
total_mem_data: deque[float] = deque(maxlen=MAX_POINTS)
|
||||||
data_lock = threading.Lock() # Prevent reading while writing
|
data_lock: threading.Lock = threading.Lock() # Prevent reading while writing
|
||||||
|
|
||||||
|
# Global shutdown flag
|
||||||
|
shutdown_event = threading.Event()
|
||||||
|
|
||||||
# Initialize colors
|
# Initialize colors
|
||||||
init(autoreset=True)
|
init(autoreset=True)
|
||||||
|
|
||||||
def get_color_for_line(line):
|
# Color mapping for log lines
|
||||||
|
COLOR_KEYWORDS: dict[str, list[str]] = {
|
||||||
|
Fore.RED: ["ERROR", "[ERR]", "[SCT]", "FAILED", "WARNING"],
|
||||||
|
Fore.CYAN: ["[MEM]", "FREE:"],
|
||||||
|
Fore.MAGENTA: [
|
||||||
|
"[GFX]",
|
||||||
|
"[ERS]",
|
||||||
|
"DISPLAY",
|
||||||
|
"RAM WRITE",
|
||||||
|
"RAM COMPLETE",
|
||||||
|
"REFRESH",
|
||||||
|
"POWERING ON",
|
||||||
|
"FRAME BUFFER",
|
||||||
|
"LUT",
|
||||||
|
],
|
||||||
|
Fore.GREEN: [
|
||||||
|
"[EBP]",
|
||||||
|
"[BMC]",
|
||||||
|
"[ZIP]",
|
||||||
|
"[PARSER]",
|
||||||
|
"[EHP]",
|
||||||
|
"LOADING EPUB",
|
||||||
|
"CACHE",
|
||||||
|
"DECOMPRESSED",
|
||||||
|
"PARSING",
|
||||||
|
],
|
||||||
|
Fore.YELLOW: ["[ACT]", "ENTERING ACTIVITY", "EXITING ACTIVITY"],
|
||||||
|
Fore.BLUE: ["RENDERED PAGE", "[LOOP]", "DURATION", "WAIT COMPLETE"],
|
||||||
|
Fore.LIGHTYELLOW_EX: [
|
||||||
|
"[CPS]",
|
||||||
|
"SETTINGS",
|
||||||
|
"[CLEAR_CACHE]",
|
||||||
|
"[CHAP]",
|
||||||
|
"[OPDS]",
|
||||||
|
"[COF]",
|
||||||
|
],
|
||||||
|
Fore.LIGHTBLACK_EX: [
|
||||||
|
"ESP-ROM",
|
||||||
|
"BUILD:",
|
||||||
|
"RST:",
|
||||||
|
"BOOT:",
|
||||||
|
"SPIWP:",
|
||||||
|
"MODE:",
|
||||||
|
"LOAD:",
|
||||||
|
"ENTRY",
|
||||||
|
"[SD]",
|
||||||
|
"STARTING CROSSPOINT",
|
||||||
|
"VERSION",
|
||||||
|
],
|
||||||
|
Fore.LIGHTCYAN_EX: ["[RBS]"],
|
||||||
|
Fore.LIGHTMAGENTA_EX: [
|
||||||
|
"[KRS]",
|
||||||
|
"EINKDISPLAY:",
|
||||||
|
"STATIC FRAME",
|
||||||
|
"INITIALIZING",
|
||||||
|
"SPI INITIALIZED",
|
||||||
|
"GPIO PINS",
|
||||||
|
"RESETTING",
|
||||||
|
"SSD1677",
|
||||||
|
"E-INK",
|
||||||
|
],
|
||||||
|
Fore.LIGHTGREEN_EX: ["[FNS]", "FOOTNOTE"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def signal_handler(signum, frame):
|
||||||
|
"""Handle SIGINT (Ctrl-C) by setting the shutdown event."""
|
||||||
|
# frame parameter is required by signal handler signature but not used
|
||||||
|
del frame # Explicitly mark as unused to satisfy linters
|
||||||
|
print(f"\n{Fore.YELLOW}Received signal {signum}. Shutting down...{Style.RESET_ALL}")
|
||||||
|
shutdown_event.set()
|
||||||
|
plt.close("all")
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=R0912
|
||||||
|
def get_color_for_line(line: str) -> str:
|
||||||
"""
|
"""
|
||||||
Classify log lines by type and assign appropriate colors.
|
Classify log lines by type and assign appropriate colors.
|
||||||
"""
|
"""
|
||||||
line_upper = line.upper()
|
line_upper = line.upper()
|
||||||
|
for color, keywords in COLOR_KEYWORDS.items():
|
||||||
if any(keyword in line_upper for keyword in ["ERROR", "[ERR]", "[SCT]", "FAILED", "WARNING"]):
|
if any(keyword in line_upper for keyword in keywords):
|
||||||
return Fore.RED
|
return color
|
||||||
if "[MEM]" in line_upper or "FREE:" in line_upper:
|
|
||||||
return Fore.CYAN
|
|
||||||
if any(keyword in line_upper for keyword in ["[GFX]", "[ERS]", "DISPLAY", "RAM WRITE", "RAM COMPLETE", "REFRESH", "POWERING ON", "FRAME BUFFER", "LUT"]):
|
|
||||||
return Fore.MAGENTA
|
|
||||||
if any(keyword in line_upper for keyword in ["[EBP]", "[BMC]", "[ZIP]", "[PARSER]", "[EHP]", "LOADING EPUB", "CACHE", "DECOMPRESSED", "PARSING"]):
|
|
||||||
return Fore.GREEN
|
|
||||||
if "[ACT]" in line_upper or "ENTERING ACTIVITY" in line_upper or "EXITING ACTIVITY" in line_upper:
|
|
||||||
return Fore.YELLOW
|
|
||||||
if any(keyword in line_upper for keyword in ["RENDERED PAGE", "[LOOP]", "DURATION", "WAIT COMPLETE"]):
|
|
||||||
return Fore.BLUE
|
|
||||||
if any(keyword in line_upper for keyword in ["[CPS]", "SETTINGS", "[CLEAR_CACHE]"]):
|
|
||||||
return Fore.LIGHTYELLOW_EX
|
|
||||||
if any(keyword in line_upper for keyword in ["ESP-ROM", "BUILD:", "RST:", "BOOT:", "SPIWP:", "MODE:", "LOAD:", "ENTRY", "[SD]", "STARTING CROSSPOINT", "VERSION"]):
|
|
||||||
return Fore.LIGHTBLACK_EX
|
|
||||||
if "[RBS]" in line_upper:
|
|
||||||
return Fore.LIGHTCYAN_EX
|
|
||||||
if "[KRS]" in line_upper:
|
|
||||||
return Fore.LIGHTMAGENTA_EX
|
|
||||||
if any(keyword in line_upper for keyword in ["EINKDISPLAY:", "STATIC FRAME", "INITIALIZING", "SPI INITIALIZED", "GPIO PINS", "RESETTING", "SSD1677", "E-INK"]):
|
|
||||||
return Fore.LIGHTMAGENTA_EX
|
|
||||||
if any(keyword in line_upper for keyword in ["[FNS]", "FOOTNOTE"]):
|
|
||||||
return Fore.LIGHTGREEN_EX
|
|
||||||
if any(keyword in line_upper for keyword in ["[CHAP]", "[OPDS]", "[COF]"]):
|
|
||||||
return Fore.LIGHTYELLOW_EX
|
|
||||||
|
|
||||||
return Fore.WHITE
|
return Fore.WHITE
|
||||||
|
|
||||||
def parse_memory_line(line):
|
|
||||||
|
def parse_memory_line(line: str) -> tuple[int | None, int | None]:
|
||||||
"""
|
"""
|
||||||
Extracts Free and Total bytes from the specific log line.
|
Extracts Free and Total bytes from the specific log line.
|
||||||
Format: [MEM] Free: 196344 bytes, Total: 226412 bytes, Min Free: 112620 bytes
|
Format: [MEM] Free: 196344 bytes, Total: 226412 bytes, Min Free: 112620 bytes
|
||||||
@@ -93,25 +188,62 @@ def parse_memory_line(line):
|
|||||||
return None, None
|
return None, None
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
def serial_worker(port, baud):
|
|
||||||
|
def serial_worker(ser, kwargs: dict[str, str]) -> None:
|
||||||
"""
|
"""
|
||||||
Runs in a background thread. Handles reading serial, printing to console,
|
Runs in a background thread. Handles reading serial data, printing to console,
|
||||||
and updating the data lists.
|
updating memory usage data for graphing, and processing screenshot data.
|
||||||
|
Monitors the global shutdown event for graceful termination.
|
||||||
"""
|
"""
|
||||||
print(f"{Fore.CYAN}--- Opening {port} at {baud} baud ---{Style.RESET_ALL}")
|
print(f"{Fore.CYAN}--- Opening serial port ---{Style.RESET_ALL}")
|
||||||
|
filter_keyword = kwargs.get("filter", "").lower()
|
||||||
|
suppress = kwargs.get("suppress", "").lower()
|
||||||
|
if filter_keyword and suppress and filter_keyword == suppress:
|
||||||
|
print(
|
||||||
|
f"{Fore.YELLOW}Warning: Filter and Suppress keywords are the same. "
|
||||||
|
f"This may result in no output.{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
if filter_keyword:
|
||||||
|
print(
|
||||||
|
f"{Fore.YELLOW}Filtering lines to only show those containing: "
|
||||||
|
f"'{filter_keyword}'{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
if suppress:
|
||||||
|
print(
|
||||||
|
f"{Fore.YELLOW}Suppressing lines containing: '{suppress}'{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
|
||||||
|
expecting_screenshot = False
|
||||||
|
screenshot_size = 0
|
||||||
|
screenshot_data = b""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ser = serial.Serial(port, baud, timeout=0.1)
|
while not shutdown_event.is_set():
|
||||||
ser.dtr = False
|
if expecting_screenshot:
|
||||||
ser.rts = False
|
data = ser.read(screenshot_size - len(screenshot_data))
|
||||||
except serial.SerialException as e:
|
if not data:
|
||||||
print(f"{Fore.RED}Error opening port: {e}{Style.RESET_ALL}")
|
continue
|
||||||
return
|
screenshot_data += data
|
||||||
|
if len(screenshot_data) == screenshot_size:
|
||||||
|
if Image:
|
||||||
|
img = Image.frombytes("1", (800, 480), screenshot_data)
|
||||||
|
# We need to rotate the image because the raw data is in landscape mode
|
||||||
|
img = img.transpose(Image.ROTATE_270)
|
||||||
|
img.save("screenshot.bmp")
|
||||||
|
print(
|
||||||
|
f"{Fore.GREEN}Screenshot saved to screenshot.bmp{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
with open("screenshot.raw", "wb") as f:
|
||||||
|
f.write(screenshot_data)
|
||||||
|
print(
|
||||||
|
f"{Fore.GREEN}Screenshot saved to screenshot.raw (PIL not available){Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
expecting_screenshot = False
|
||||||
|
screenshot_data = b""
|
||||||
|
else:
|
||||||
try:
|
try:
|
||||||
while True:
|
raw_data = ser.readline().decode("utf-8", errors="replace")
|
||||||
try:
|
|
||||||
raw_data = ser.readline().decode('utf-8', errors='replace')
|
|
||||||
|
|
||||||
if not raw_data:
|
if not raw_data:
|
||||||
continue
|
continue
|
||||||
@@ -120,6 +252,13 @@ def serial_worker(port, baud):
|
|||||||
if not clean_line:
|
if not clean_line:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if clean_line.startswith("SCREENSHOT_START:"):
|
||||||
|
screenshot_size = int(clean_line.split(":")[1])
|
||||||
|
expecting_screenshot = True
|
||||||
|
continue
|
||||||
|
elif clean_line == "SCREENSHOT_END":
|
||||||
|
continue # ignore
|
||||||
|
|
||||||
# Add PC timestamp
|
# Add PC timestamp
|
||||||
pc_time = datetime.now().strftime("%H:%M:%S")
|
pc_time = datetime.now().strftime("%H:%M:%S")
|
||||||
formatted_line = re.sub(r"^\[\d+\]", f"[{pc_time}]", clean_line)
|
formatted_line = re.sub(r"^\[\d+\]", f"[{pc_time}]", clean_line)
|
||||||
@@ -127,33 +266,57 @@ def serial_worker(port, baud):
|
|||||||
# Check for Memory Line
|
# Check for Memory Line
|
||||||
if "[MEM]" in formatted_line:
|
if "[MEM]" in formatted_line:
|
||||||
free_val, total_val = parse_memory_line(formatted_line)
|
free_val, total_val = parse_memory_line(formatted_line)
|
||||||
if free_val is not None:
|
if free_val is not None and total_val is not None:
|
||||||
with data_lock:
|
with data_lock:
|
||||||
time_data.append(pc_time)
|
time_data.append(pc_time)
|
||||||
free_mem_data.append(free_val / 1024) # Convert to KB
|
free_mem_data.append(free_val / 1024) # Convert to KB
|
||||||
total_mem_data.append(total_val / 1024) # Convert to KB
|
total_mem_data.append(total_val / 1024) # Convert to KB
|
||||||
|
# Apply filters
|
||||||
|
if filter_keyword and filter_keyword not in formatted_line.lower():
|
||||||
|
continue
|
||||||
|
if suppress and suppress in formatted_line.lower():
|
||||||
|
continue
|
||||||
# Print to console
|
# Print to console
|
||||||
line_color = get_color_for_line(formatted_line)
|
line_color = get_color_for_line(formatted_line)
|
||||||
print(f"{line_color}{formatted_line}")
|
print(f"{line_color}{formatted_line}")
|
||||||
|
|
||||||
except OSError:
|
except (OSError, UnicodeDecodeError):
|
||||||
print(f"{Fore.RED}Device disconnected.{Style.RESET_ALL}")
|
print(
|
||||||
|
f"{Fore.RED}Device disconnected or data error.{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except KeyboardInterrupt:
|
||||||
# If thread is killed violently (e.g. main exit), silence errors
|
# If thread is killed violently (e.g. main exit), silence errors
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
if 'ser' in locals() and ser.is_open:
|
pass # ser closed in main
|
||||||
ser.close()
|
|
||||||
|
|
||||||
def update_graph(frame):
|
|
||||||
|
def input_worker(ser) -> None:
|
||||||
"""
|
"""
|
||||||
Called by Matplotlib animation to redraw the chart.
|
Runs in a background thread. Handles user input to send commands to the ESP32 device.
|
||||||
|
Monitors the global shutdown event for graceful termination on Ctrl-C.
|
||||||
"""
|
"""
|
||||||
|
while not shutdown_event.is_set():
|
||||||
|
try:
|
||||||
|
cmd = input("Command: ")
|
||||||
|
ser.write(f"CMD:{cmd}\n".encode())
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def update_graph(frame) -> list: # pylint: disable=unused-argument
|
||||||
|
"""
|
||||||
|
Called by Matplotlib animation to redraw the memory usage chart.
|
||||||
|
Monitors the global shutdown event and closes the plot when shutdown is requested.
|
||||||
|
"""
|
||||||
|
if shutdown_event.is_set():
|
||||||
|
plt.close("all")
|
||||||
|
return []
|
||||||
|
|
||||||
with data_lock:
|
with data_lock:
|
||||||
if not time_data:
|
if not time_data:
|
||||||
return
|
return []
|
||||||
|
|
||||||
# Convert deques to lists for plotting
|
# Convert deques to lists for plotting
|
||||||
x = list(time_data)
|
x = list(time_data)
|
||||||
@@ -163,52 +326,183 @@ def update_graph(frame):
|
|||||||
plt.cla() # Clear axis
|
plt.cla() # Clear axis
|
||||||
|
|
||||||
# Plot Total RAM
|
# Plot Total RAM
|
||||||
plt.plot(x, y_total, label='Total RAM (KB)', color='red', linestyle='--')
|
plt.plot(x, y_total, label="Total RAM (KB)", color="red", linestyle="--")
|
||||||
|
|
||||||
# Plot Free RAM
|
# Plot Free RAM
|
||||||
plt.plot(x, y_free, label='Free RAM (KB)', color='green', marker='o')
|
plt.plot(x, y_free, label="Free RAM (KB)", color="green", marker="o")
|
||||||
|
|
||||||
# Fill area under Free RAM
|
# Fill area under Free RAM
|
||||||
plt.fill_between(x, y_free, color='green', alpha=0.1)
|
plt.fill_between(x, y_free, color="green", alpha=0.1)
|
||||||
|
|
||||||
plt.title("ESP32 Memory Monitor")
|
plt.title("ESP32 Memory Monitor")
|
||||||
plt.ylabel("Memory (KB)")
|
plt.ylabel("Memory (KB)")
|
||||||
plt.xlabel("Time")
|
plt.xlabel("Time")
|
||||||
plt.legend(loc='upper left')
|
plt.legend(loc="upper left")
|
||||||
plt.grid(True, linestyle=':', alpha=0.6)
|
plt.grid(True, linestyle=":", alpha=0.6)
|
||||||
|
|
||||||
# Rotate date labels
|
# Rotate date labels
|
||||||
plt.xticks(rotation=45, ha='right')
|
plt.xticks(rotation=45, ha="right")
|
||||||
plt.tight_layout()
|
plt.tight_layout()
|
||||||
|
|
||||||
def main():
|
return []
|
||||||
parser = argparse.ArgumentParser(description="ESP32 Monitor with Graph")
|
|
||||||
parser.add_argument("port", nargs="?", default="/dev/ttyACM0", help="Serial port")
|
|
||||||
parser.add_argument("--baud", type=int, default=115200, help="Baud rate")
|
def get_auto_detected_port() -> list[str]:
|
||||||
|
"""
|
||||||
|
Attempts to auto-detect the serial port for the ESP32 device.
|
||||||
|
Returns a list of all detected ports.
|
||||||
|
If no suitable port is found, the list will be empty.
|
||||||
|
Darwin/Linux logic by jonasdiemer
|
||||||
|
"""
|
||||||
|
port_list = []
|
||||||
|
system = platform.system()
|
||||||
|
# Code for darwin (macOS), linux, and windows
|
||||||
|
if system in ("Darwin", "Linux"):
|
||||||
|
pattern = "/dev/tty.usbmodem*" if system == "Darwin" else "/dev/ttyACM*"
|
||||||
|
port_list = sorted(glob.glob(pattern))
|
||||||
|
elif system == "Windows":
|
||||||
|
from serial.tools import list_ports
|
||||||
|
|
||||||
|
# Be careful with this pattern list - it should be specific
|
||||||
|
# enough to avoid picking up unrelated devices, but broad enough
|
||||||
|
# to catch all common USB-serial adapters used with ESP32
|
||||||
|
# Caveat: localized versions of Windows may have different descriptions,
|
||||||
|
# so we also check for specific VID:PID (but that may not cover all clones)
|
||||||
|
pattern_list = ["CP210x", "CH340", "USB Serial"]
|
||||||
|
found_ports = list_ports.comports()
|
||||||
|
port_list = [
|
||||||
|
port.device
|
||||||
|
for port in found_ports
|
||||||
|
if any(pat in port.description for pat in pattern_list)
|
||||||
|
or port.hwid.startswith(
|
||||||
|
"USB VID:PID=303A:1001"
|
||||||
|
) # Add specific VID:PID for XTEINK X4
|
||||||
|
]
|
||||||
|
|
||||||
|
return port_list
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""
|
||||||
|
Main entry point for the ESP32 monitor application.
|
||||||
|
|
||||||
|
Sets up argument parsing, initializes serial communication, starts background threads
|
||||||
|
for serial monitoring and command input, and launches the memory usage graph.
|
||||||
|
Implements graceful shutdown handling with signal processing for clean termination.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Serial port monitoring with color-coded output
|
||||||
|
- Real-time memory usage graphing
|
||||||
|
- Interactive command interface
|
||||||
|
- Screenshot capture capability
|
||||||
|
- Graceful shutdown on Ctrl-C or window close
|
||||||
|
"""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="ESP32 Serial Monitor with Memory Graph - Real-time monitoring, graphing, and command interface"
|
||||||
|
)
|
||||||
|
default_baudrate = 115200
|
||||||
|
parser.add_argument(
|
||||||
|
"port",
|
||||||
|
nargs="?",
|
||||||
|
default=None,
|
||||||
|
help="Serial port (leave empty for autodetection)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--baud",
|
||||||
|
type=int,
|
||||||
|
default=default_baudrate,
|
||||||
|
help=f"Baud rate (default: {default_baudrate})",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--filter",
|
||||||
|
type=str,
|
||||||
|
default="",
|
||||||
|
help="Only display lines containing this keyword (case-insensitive)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--suppress",
|
||||||
|
type=str,
|
||||||
|
default="",
|
||||||
|
help="Suppress lines containing this keyword (case-insensitive)",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
port = args.port
|
||||||
|
if port is None:
|
||||||
|
port_list = get_auto_detected_port()
|
||||||
|
if len(port_list) == 1:
|
||||||
|
port = port_list[0]
|
||||||
|
print(f"{Fore.CYAN}Auto-detected serial port: {port}{Style.RESET_ALL}")
|
||||||
|
elif len(port_list) > 1:
|
||||||
|
print(f"{Fore.YELLOW}Multiple serial ports found:{Style.RESET_ALL}")
|
||||||
|
for p in port_list:
|
||||||
|
print(f" - {p}")
|
||||||
|
print(
|
||||||
|
f"{Fore.YELLOW}Please specify the desired port as a command-line argument.{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
if port is None:
|
||||||
|
print(f"{Fore.RED}Error: No suitable serial port found.{Style.RESET_ALL}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ser = serial.Serial(port, args.baud, timeout=0.1)
|
||||||
|
ser.dtr = False
|
||||||
|
ser.rts = False
|
||||||
|
except serial.SerialException as e:
|
||||||
|
print(f"{Fore.RED}Error opening port: {e}{Style.RESET_ALL}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Set up signal handler for graceful shutdown
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
|
||||||
# 1. Start the Serial Reader in a separate thread
|
# 1. Start the Serial Reader in a separate thread
|
||||||
# Daemon=True means this thread dies when the main program closes
|
# Daemon=True means this thread dies when the main program closes
|
||||||
t = threading.Thread(target=serial_worker, args=(args.port, args.baud), daemon=True)
|
myargs = vars(args) # Convert Namespace to dict for easier passing
|
||||||
|
t = threading.Thread(target=serial_worker, args=(ser, myargs), daemon=True)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
|
# Start input thread
|
||||||
|
input_thread = threading.Thread(target=input_worker, args=(ser,), daemon=True)
|
||||||
|
input_thread.start()
|
||||||
|
|
||||||
# 2. Set up the Graph (Main Thread)
|
# 2. Set up the Graph (Main Thread)
|
||||||
try:
|
try:
|
||||||
plt.style.use('light_background')
|
import matplotlib.style as mplstyle # pylint: disable=import-outside-toplevel
|
||||||
except:
|
|
||||||
|
default_styles = (
|
||||||
|
"light_background",
|
||||||
|
"ggplot",
|
||||||
|
"seaborn",
|
||||||
|
"dark_background",
|
||||||
|
)
|
||||||
|
styles = list(mplstyle.available)
|
||||||
|
for default_style in default_styles:
|
||||||
|
if default_style in styles:
|
||||||
|
print(
|
||||||
|
f"\n{Fore.CYAN}--- Using Matplotlib style: {default_style} ---{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
mplstyle.use(default_style)
|
||||||
|
break
|
||||||
|
except (AttributeError, ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
fig = plt.figure(figsize=(10, 6))
|
fig = plt.figure(figsize=(10, 6))
|
||||||
|
|
||||||
# Update graph every 1000ms
|
# Update graph every 1000ms
|
||||||
ani = animation.FuncAnimation(fig, update_graph, interval=1000)
|
_ = animation.FuncAnimation(
|
||||||
|
fig, update_graph, interval=1000, cache_frame_data=False
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print(f"{Fore.YELLOW}Starting Graph Window... (Close window to exit){Style.RESET_ALL}")
|
print(
|
||||||
|
f"{Fore.YELLOW}Starting Graph Window... (Close window or press Ctrl-C to exit){Style.RESET_ALL}"
|
||||||
|
)
|
||||||
plt.show()
|
plt.show()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print(f"\n{Fore.YELLOW}Exiting...{Style.RESET_ALL}")
|
print(f"\n{Fore.YELLOW}Exiting...{Style.RESET_ALL}")
|
||||||
plt.close('all') # Force close any lingering plot windows
|
finally:
|
||||||
|
shutdown_event.set() # Ensure all threads know to stop
|
||||||
|
plt.close("all") # Force close any lingering plot windows
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <HalStorage.h>
|
||||||
#include <SDCardManager.h>
|
#include <Logging.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -79,10 +79,10 @@ void applyLegacyFrontButtonLayout(CrossPointSettings& settings) {
|
|||||||
|
|
||||||
bool CrossPointSettings::saveToFile() const {
|
bool CrossPointSettings::saveToFile() const {
|
||||||
// Make sure the directory exists
|
// Make sure the directory exists
|
||||||
SdMan.mkdir("/.crosspoint");
|
Storage.mkdir("/.crosspoint");
|
||||||
|
|
||||||
FsFile outputFile;
|
FsFile outputFile;
|
||||||
if (!SdMan.openFileForWrite("CPS", SETTINGS_FILE, outputFile)) {
|
if (!Storage.openFileForWrite("CPS", SETTINGS_FILE, outputFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,20 +121,20 @@ bool CrossPointSettings::saveToFile() const {
|
|||||||
// New fields added at end for backward compatibility
|
// New fields added at end for backward compatibility
|
||||||
outputFile.close();
|
outputFile.close();
|
||||||
|
|
||||||
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
|
LOG_DBG("CPS", "Settings saved to file");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CrossPointSettings::loadFromFile() {
|
bool CrossPointSettings::loadFromFile() {
|
||||||
FsFile inputFile;
|
FsFile inputFile;
|
||||||
if (!SdMan.openFileForRead("CPS", SETTINGS_FILE, inputFile)) {
|
if (!Storage.openFileForRead("CPS", SETTINGS_FILE, inputFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t version;
|
uint8_t version;
|
||||||
serialization::readPod(inputFile, version);
|
serialization::readPod(inputFile, version);
|
||||||
if (version != SETTINGS_FILE_VERSION) {
|
if (version != SETTINGS_FILE_VERSION) {
|
||||||
Serial.printf("[%lu] [CPS] Deserialization failed: Unknown version %u\n", millis(), version);
|
LOG_ERR("CPS", "Deserialization failed: Unknown version %u", version);
|
||||||
inputFile.close();
|
inputFile.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -233,7 +233,7 @@ bool CrossPointSettings::loadFromFile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inputFile.close();
|
inputFile.close();
|
||||||
Serial.printf("[%lu] [CPS] Settings loaded from file\n", millis());
|
LOG_DBG("CPS", "Settings loaded from file");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <HalStorage.h>
|
||||||
#include <SDCardManager.h>
|
#include <Logging.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -13,7 +13,7 @@ CrossPointState CrossPointState::instance;
|
|||||||
|
|
||||||
bool CrossPointState::saveToFile() const {
|
bool CrossPointState::saveToFile() const {
|
||||||
FsFile outputFile;
|
FsFile outputFile;
|
||||||
if (!SdMan.openFileForWrite("CPS", STATE_FILE, outputFile)) {
|
if (!Storage.openFileForWrite("CPS", STATE_FILE, outputFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,14 +28,14 @@ bool CrossPointState::saveToFile() const {
|
|||||||
|
|
||||||
bool CrossPointState::loadFromFile() {
|
bool CrossPointState::loadFromFile() {
|
||||||
FsFile inputFile;
|
FsFile inputFile;
|
||||||
if (!SdMan.openFileForRead("CPS", STATE_FILE, inputFile)) {
|
if (!Storage.openFileForRead("CPS", STATE_FILE, inputFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t version;
|
uint8_t version;
|
||||||
serialization::readPod(inputFile, version);
|
serialization::readPod(inputFile, version);
|
||||||
if (version > STATE_FILE_VERSION) {
|
if (version > STATE_FILE_VERSION) {
|
||||||
Serial.printf("[%lu] [CPS] Deserialization failed: Unknown version %u\n", millis(), version);
|
LOG_ERR("CPS", "Deserialization failed: Unknown version %u", version);
|
||||||
inputFile.close();
|
inputFile.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class MappedInputManager {
|
|||||||
|
|
||||||
explicit MappedInputManager(HalGPIO& gpio) : gpio(gpio) {}
|
explicit MappedInputManager(HalGPIO& gpio) : gpio(gpio) {}
|
||||||
|
|
||||||
|
void update() const { gpio.update(); }
|
||||||
bool wasPressed(Button button) const;
|
bool wasPressed(Button button) const;
|
||||||
bool wasReleased(Button button) const;
|
bool wasReleased(Button button) const;
|
||||||
bool isPressed(Button button) const;
|
bool isPressed(Button button) const;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
|
|
||||||
#include <Epub.h>
|
#include <Epub.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HalStorage.h>
|
||||||
#include <SDCardManager.h>
|
#include <Logging.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
#include <Xtc.h>
|
#include <Xtc.h>
|
||||||
|
|
||||||
@@ -53,10 +53,10 @@ void RecentBooksStore::updateBook(const std::string& path, const std::string& ti
|
|||||||
|
|
||||||
bool RecentBooksStore::saveToFile() const {
|
bool RecentBooksStore::saveToFile() const {
|
||||||
// Make sure the directory exists
|
// Make sure the directory exists
|
||||||
SdMan.mkdir("/.crosspoint");
|
Storage.mkdir("/.crosspoint");
|
||||||
|
|
||||||
FsFile outputFile;
|
FsFile outputFile;
|
||||||
if (!SdMan.openFileForWrite("RBS", RECENT_BOOKS_FILE, outputFile)) {
|
if (!Storage.openFileForWrite("RBS", RECENT_BOOKS_FILE, outputFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ bool RecentBooksStore::saveToFile() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
outputFile.close();
|
outputFile.close();
|
||||||
Serial.printf("[%lu] [RBS] Recent books saved to file (%d entries)\n", millis(), count);
|
LOG_DBG("RBS", "Recent books saved to file (%d entries)", count);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ RecentBook RecentBooksStore::getDataFromBook(std::string path) const {
|
|||||||
lastBookFileName = path.substr(lastSlash + 1);
|
lastBookFileName = path.substr(lastSlash + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("Loading recent book: %s\n", path.c_str());
|
LOG_DBG("RBS", "Loading recent book: %s", path.c_str());
|
||||||
|
|
||||||
// If epub, try to load the metadata for title/author and cover
|
// If epub, try to load the metadata for title/author and cover
|
||||||
if (StringUtils::checkFileExtension(lastBookFileName, ".epub")) {
|
if (StringUtils::checkFileExtension(lastBookFileName, ".epub")) {
|
||||||
@@ -106,7 +106,7 @@ RecentBook RecentBooksStore::getDataFromBook(std::string path) const {
|
|||||||
|
|
||||||
bool RecentBooksStore::loadFromFile() {
|
bool RecentBooksStore::loadFromFile() {
|
||||||
FsFile inputFile;
|
FsFile inputFile;
|
||||||
if (!SdMan.openFileForRead("RBS", RECENT_BOOKS_FILE, inputFile)) {
|
if (!Storage.openFileForRead("RBS", RECENT_BOOKS_FILE, inputFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ bool RecentBooksStore::loadFromFile() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [RBS] Deserialization failed: Unknown version %u\n", millis(), version);
|
LOG_ERR("RBS", "Deserialization failed: Unknown version %u", version);
|
||||||
inputFile.close();
|
inputFile.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -158,6 +158,6 @@ bool RecentBooksStore::loadFromFile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inputFile.close();
|
inputFile.close();
|
||||||
Serial.printf("[%lu] [RBS] Recent books loaded from file (%d entries)\n", millis(), recentBooks.size());
|
LOG_DBG("RBS", "Recent books loaded from file (%d entries)", recentBooks.size());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
101
src/SettingsList.h
Normal file
101
src/SettingsList.h
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "CrossPointSettings.h"
|
||||||
|
#include "KOReaderCredentialStore.h"
|
||||||
|
#include "activities/settings/SettingsActivity.h"
|
||||||
|
|
||||||
|
// Shared settings list used by both the device settings UI and the web settings API.
|
||||||
|
// Each entry has a key (for JSON API) and category (for grouping).
|
||||||
|
// ACTION-type entries and entries without a key are device-only.
|
||||||
|
inline std::vector<SettingInfo> getSettingsList() {
|
||||||
|
return {
|
||||||
|
// --- Display ---
|
||||||
|
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen,
|
||||||
|
{"Dark", "Light", "Custom", "Cover", "None", "Cover + Custom"}, "sleepScreen", "Display"),
|
||||||
|
SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"},
|
||||||
|
"sleepScreenCoverMode", "Display"),
|
||||||
|
SettingInfo::Enum("Sleep Screen Cover Filter", &CrossPointSettings::sleepScreenCoverFilter,
|
||||||
|
{"None", "Contrast", "Inverted"}, "sleepScreenCoverFilter", "Display"),
|
||||||
|
SettingInfo::Enum(
|
||||||
|
"Status Bar", &CrossPointSettings::statusBar,
|
||||||
|
{"None", "No Progress", "Full w/ Percentage", "Full w/ Book Bar", "Book Bar Only", "Full w/ Chapter Bar"},
|
||||||
|
"statusBar", "Display"),
|
||||||
|
SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"},
|
||||||
|
"hideBatteryPercentage", "Display"),
|
||||||
|
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
|
||||||
|
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}, "refreshFrequency", "Display"),
|
||||||
|
SettingInfo::Enum("UI Theme", &CrossPointSettings::uiTheme, {"Classic", "Lyra"}, "uiTheme", "Display"),
|
||||||
|
SettingInfo::Toggle("Sunlight Fading Fix", &CrossPointSettings::fadingFix, "fadingFix", "Display"),
|
||||||
|
|
||||||
|
// --- Reader ---
|
||||||
|
SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"},
|
||||||
|
"fontFamily", "Reader"),
|
||||||
|
SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}, "fontSize",
|
||||||
|
"Reader"),
|
||||||
|
SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}, "lineSpacing",
|
||||||
|
"Reader"),
|
||||||
|
SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}, "screenMargin", "Reader"),
|
||||||
|
SettingInfo::Enum("Paragraph Alignment", &CrossPointSettings::paragraphAlignment,
|
||||||
|
{"Justify", "Left", "Center", "Right", "Book's Style"}, "paragraphAlignment", "Reader"),
|
||||||
|
SettingInfo::Toggle("Book's Embedded Style", &CrossPointSettings::embeddedStyle, "embeddedStyle", "Reader"),
|
||||||
|
SettingInfo::Toggle("Hyphenation", &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled", "Reader"),
|
||||||
|
SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation,
|
||||||
|
{"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}, "orientation", "Reader"),
|
||||||
|
SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing,
|
||||||
|
"extraParagraphSpacing", "Reader"),
|
||||||
|
SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing, "textAntiAliasing", "Reader"),
|
||||||
|
|
||||||
|
// --- Controls ---
|
||||||
|
SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout,
|
||||||
|
{"Prev, Next", "Next, Prev"}, "sideButtonLayout", "Controls"),
|
||||||
|
SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip, "longPressChapterSkip",
|
||||||
|
"Controls"),
|
||||||
|
SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"},
|
||||||
|
"shortPwrBtn", "Controls"),
|
||||||
|
|
||||||
|
// --- System ---
|
||||||
|
SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout,
|
||||||
|
{"1 min", "5 min", "10 min", "15 min", "30 min"}, "sleepTimeout", "System"),
|
||||||
|
|
||||||
|
// --- KOReader Sync (web-only, uses KOReaderCredentialStore) ---
|
||||||
|
SettingInfo::DynamicString(
|
||||||
|
"KOReader Username", [] { return KOREADER_STORE.getUsername(); },
|
||||||
|
[](const std::string& v) {
|
||||||
|
KOREADER_STORE.setCredentials(v, KOREADER_STORE.getPassword());
|
||||||
|
KOREADER_STORE.saveToFile();
|
||||||
|
},
|
||||||
|
"koUsername", "KOReader Sync"),
|
||||||
|
SettingInfo::DynamicString(
|
||||||
|
"KOReader Password", [] { return KOREADER_STORE.getPassword(); },
|
||||||
|
[](const std::string& v) {
|
||||||
|
KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), v);
|
||||||
|
KOREADER_STORE.saveToFile();
|
||||||
|
},
|
||||||
|
"koPassword", "KOReader Sync"),
|
||||||
|
SettingInfo::DynamicString(
|
||||||
|
"Sync Server URL", [] { return KOREADER_STORE.getServerUrl(); },
|
||||||
|
[](const std::string& v) {
|
||||||
|
KOREADER_STORE.setServerUrl(v);
|
||||||
|
KOREADER_STORE.saveToFile();
|
||||||
|
},
|
||||||
|
"koServerUrl", "KOReader Sync"),
|
||||||
|
SettingInfo::DynamicEnum(
|
||||||
|
"Document Matching", {"Filename", "Binary"},
|
||||||
|
[] { return static_cast<uint8_t>(KOREADER_STORE.getMatchMethod()); },
|
||||||
|
[](uint8_t v) {
|
||||||
|
KOREADER_STORE.setMatchMethod(static_cast<DocumentMatchMethod>(v));
|
||||||
|
KOREADER_STORE.saveToFile();
|
||||||
|
},
|
||||||
|
"koMatchMethod", "KOReader Sync"),
|
||||||
|
|
||||||
|
// --- OPDS Browser (web-only, uses CrossPointSettings char arrays) ---
|
||||||
|
SettingInfo::String("OPDS Server URL", SETTINGS.opdsServerUrl, sizeof(SETTINGS.opdsServerUrl), "opdsServerUrl",
|
||||||
|
"OPDS Browser"),
|
||||||
|
SettingInfo::String("OPDS Username", SETTINGS.opdsUsername, sizeof(SETTINGS.opdsUsername), "opdsUsername",
|
||||||
|
"OPDS Browser"),
|
||||||
|
SettingInfo::String("OPDS Password", SETTINGS.opdsPassword, sizeof(SETTINGS.opdsPassword), "opdsPassword",
|
||||||
|
"OPDS Browser"),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#include "WifiCredentialStore.h"
|
#include "WifiCredentialStore.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <HalStorage.h>
|
||||||
#include <SDCardManager.h>
|
#include <Logging.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
// Initialize the static instance
|
// Initialize the static instance
|
||||||
@@ -9,7 +9,7 @@ WifiCredentialStore WifiCredentialStore::instance;
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// File format version
|
// File format version
|
||||||
constexpr uint8_t WIFI_FILE_VERSION = 1;
|
constexpr uint8_t WIFI_FILE_VERSION = 2; // Increased version
|
||||||
|
|
||||||
// WiFi credentials file path
|
// WiFi credentials file path
|
||||||
constexpr char WIFI_FILE[] = "/.crosspoint/wifi.bin";
|
constexpr char WIFI_FILE[] = "/.crosspoint/wifi.bin";
|
||||||
@@ -21,7 +21,7 @@ constexpr size_t KEY_LENGTH = sizeof(OBFUSCATION_KEY);
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void WifiCredentialStore::obfuscate(std::string& data) const {
|
void WifiCredentialStore::obfuscate(std::string& data) const {
|
||||||
Serial.printf("[%lu] [WCS] Obfuscating/deobfuscating %zu bytes\n", millis(), data.size());
|
LOG_DBG("WCS", "Obfuscating/deobfuscating %zu bytes", data.size());
|
||||||
for (size_t i = 0; i < data.size(); i++) {
|
for (size_t i = 0; i < data.size(); i++) {
|
||||||
data[i] ^= OBFUSCATION_KEY[i % KEY_LENGTH];
|
data[i] ^= OBFUSCATION_KEY[i % KEY_LENGTH];
|
||||||
}
|
}
|
||||||
@@ -29,23 +29,23 @@ void WifiCredentialStore::obfuscate(std::string& data) const {
|
|||||||
|
|
||||||
bool WifiCredentialStore::saveToFile() const {
|
bool WifiCredentialStore::saveToFile() const {
|
||||||
// Make sure the directory exists
|
// Make sure the directory exists
|
||||||
SdMan.mkdir("/.crosspoint");
|
Storage.mkdir("/.crosspoint");
|
||||||
|
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (!SdMan.openFileForWrite("WCS", WIFI_FILE, file)) {
|
if (!Storage.openFileForWrite("WCS", WIFI_FILE, file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write header
|
// Write header
|
||||||
serialization::writePod(file, WIFI_FILE_VERSION);
|
serialization::writePod(file, WIFI_FILE_VERSION);
|
||||||
|
serialization::writeString(file, lastConnectedSsid); // Save last connected SSID
|
||||||
serialization::writePod(file, static_cast<uint8_t>(credentials.size()));
|
serialization::writePod(file, static_cast<uint8_t>(credentials.size()));
|
||||||
|
|
||||||
// Write each credential
|
// Write each credential
|
||||||
for (const auto& cred : credentials) {
|
for (const auto& cred : credentials) {
|
||||||
// Write SSID (plaintext - not sensitive)
|
// Write SSID (plaintext - not sensitive)
|
||||||
serialization::writeString(file, cred.ssid);
|
serialization::writeString(file, cred.ssid);
|
||||||
Serial.printf("[%lu] [WCS] Saving SSID: %s, password length: %zu\n", millis(), cred.ssid.c_str(),
|
LOG_DBG("WCS", "Saving SSID: %s, password length: %zu", cred.ssid.c_str(), cred.password.size());
|
||||||
cred.password.size());
|
|
||||||
|
|
||||||
// Write password (obfuscated)
|
// Write password (obfuscated)
|
||||||
std::string obfuscatedPwd = cred.password;
|
std::string obfuscatedPwd = cred.password;
|
||||||
@@ -54,25 +54,31 @@ bool WifiCredentialStore::saveToFile() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
file.close();
|
file.close();
|
||||||
Serial.printf("[%lu] [WCS] Saved %zu WiFi credentials to file\n", millis(), credentials.size());
|
LOG_DBG("WCS", "Saved %zu WiFi credentials to file", credentials.size());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WifiCredentialStore::loadFromFile() {
|
bool WifiCredentialStore::loadFromFile() {
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (!SdMan.openFileForRead("WCS", WIFI_FILE, file)) {
|
if (!Storage.openFileForRead("WCS", WIFI_FILE, file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read and verify version
|
// Read and verify version
|
||||||
uint8_t version;
|
uint8_t version;
|
||||||
serialization::readPod(file, version);
|
serialization::readPod(file, version);
|
||||||
if (version != WIFI_FILE_VERSION) {
|
if (version > WIFI_FILE_VERSION) {
|
||||||
Serial.printf("[%lu] [WCS] Unknown file version: %u\n", millis(), version);
|
LOG_DBG("WCS", "Unknown file version: %u", version);
|
||||||
file.close();
|
file.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (version >= 2) {
|
||||||
|
serialization::readString(file, lastConnectedSsid);
|
||||||
|
} else {
|
||||||
|
lastConnectedSsid.clear();
|
||||||
|
}
|
||||||
|
|
||||||
// Read credential count
|
// Read credential count
|
||||||
uint8_t count;
|
uint8_t count;
|
||||||
serialization::readPod(file, count);
|
serialization::readPod(file, count);
|
||||||
@@ -87,16 +93,15 @@ bool WifiCredentialStore::loadFromFile() {
|
|||||||
|
|
||||||
// Read and deobfuscate password
|
// Read and deobfuscate password
|
||||||
serialization::readString(file, cred.password);
|
serialization::readString(file, cred.password);
|
||||||
Serial.printf("[%lu] [WCS] Loaded SSID: %s, obfuscated password length: %zu\n", millis(), cred.ssid.c_str(),
|
LOG_DBG("WCS", "Loaded SSID: %s, obfuscated password length: %zu", cred.ssid.c_str(), cred.password.size());
|
||||||
cred.password.size());
|
|
||||||
obfuscate(cred.password); // XOR is symmetric, so same function deobfuscates
|
obfuscate(cred.password); // XOR is symmetric, so same function deobfuscates
|
||||||
Serial.printf("[%lu] [WCS] After deobfuscation, password length: %zu\n", millis(), cred.password.size());
|
LOG_DBG("WCS", "After deobfuscation, password length: %zu", cred.password.size());
|
||||||
|
|
||||||
credentials.push_back(cred);
|
credentials.push_back(cred);
|
||||||
}
|
}
|
||||||
|
|
||||||
file.close();
|
file.close();
|
||||||
Serial.printf("[%lu] [WCS] Loaded %zu WiFi credentials from file\n", millis(), credentials.size());
|
LOG_DBG("WCS", "Loaded %zu WiFi credentials from file", credentials.size());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,19 +111,19 @@ bool WifiCredentialStore::addCredential(const std::string& ssid, const std::stri
|
|||||||
[&ssid](const WifiCredential& cred) { return cred.ssid == ssid; });
|
[&ssid](const WifiCredential& cred) { return cred.ssid == ssid; });
|
||||||
if (cred != credentials.end()) {
|
if (cred != credentials.end()) {
|
||||||
cred->password = password;
|
cred->password = password;
|
||||||
Serial.printf("[%lu] [WCS] Updated credentials for: %s\n", millis(), ssid.c_str());
|
LOG_DBG("WCS", "Updated credentials for: %s", ssid.c_str());
|
||||||
return saveToFile();
|
return saveToFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we've reached the limit
|
// Check if we've reached the limit
|
||||||
if (credentials.size() >= MAX_NETWORKS) {
|
if (credentials.size() >= MAX_NETWORKS) {
|
||||||
Serial.printf("[%lu] [WCS] Cannot add more networks, limit of %zu reached\n", millis(), MAX_NETWORKS);
|
LOG_DBG("WCS", "Cannot add more networks, limit of %zu reached", MAX_NETWORKS);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new credential
|
// Add new credential
|
||||||
credentials.push_back({ssid, password});
|
credentials.push_back({ssid, password});
|
||||||
Serial.printf("[%lu] [WCS] Added credentials for: %s\n", millis(), ssid.c_str());
|
LOG_DBG("WCS", "Added credentials for: %s", ssid.c_str());
|
||||||
return saveToFile();
|
return saveToFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +132,10 @@ bool WifiCredentialStore::removeCredential(const std::string& ssid) {
|
|||||||
[&ssid](const WifiCredential& cred) { return cred.ssid == ssid; });
|
[&ssid](const WifiCredential& cred) { return cred.ssid == ssid; });
|
||||||
if (cred != credentials.end()) {
|
if (cred != credentials.end()) {
|
||||||
credentials.erase(cred);
|
credentials.erase(cred);
|
||||||
Serial.printf("[%lu] [WCS] Removed credentials for: %s\n", millis(), ssid.c_str());
|
LOG_DBG("WCS", "Removed credentials for: %s", ssid.c_str());
|
||||||
|
if (ssid == lastConnectedSsid) {
|
||||||
|
clearLastConnectedSsid();
|
||||||
|
}
|
||||||
return saveToFile();
|
return saveToFile();
|
||||||
}
|
}
|
||||||
return false; // Not found
|
return false; // Not found
|
||||||
@@ -146,8 +154,25 @@ const WifiCredential* WifiCredentialStore::findCredential(const std::string& ssi
|
|||||||
|
|
||||||
bool WifiCredentialStore::hasSavedCredential(const std::string& ssid) const { return findCredential(ssid) != nullptr; }
|
bool WifiCredentialStore::hasSavedCredential(const std::string& ssid) const { return findCredential(ssid) != nullptr; }
|
||||||
|
|
||||||
|
void WifiCredentialStore::setLastConnectedSsid(const std::string& ssid) {
|
||||||
|
if (lastConnectedSsid != ssid) {
|
||||||
|
lastConnectedSsid = ssid;
|
||||||
|
saveToFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& WifiCredentialStore::getLastConnectedSsid() const { return lastConnectedSsid; }
|
||||||
|
|
||||||
|
void WifiCredentialStore::clearLastConnectedSsid() {
|
||||||
|
if (!lastConnectedSsid.empty()) {
|
||||||
|
lastConnectedSsid.clear();
|
||||||
|
saveToFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void WifiCredentialStore::clearAll() {
|
void WifiCredentialStore::clearAll() {
|
||||||
credentials.clear();
|
credentials.clear();
|
||||||
|
lastConnectedSsid.clear();
|
||||||
saveToFile();
|
saveToFile();
|
||||||
Serial.printf("[%lu] [WCS] Cleared all WiFi credentials\n", millis());
|
LOG_DBG("WCS", "Cleared all WiFi credentials");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class WifiCredentialStore {
|
|||||||
private:
|
private:
|
||||||
static WifiCredentialStore instance;
|
static WifiCredentialStore instance;
|
||||||
std::vector<WifiCredential> credentials;
|
std::vector<WifiCredential> credentials;
|
||||||
|
std::string lastConnectedSsid;
|
||||||
|
|
||||||
static constexpr size_t MAX_NETWORKS = 8;
|
static constexpr size_t MAX_NETWORKS = 8;
|
||||||
|
|
||||||
@@ -48,6 +49,11 @@ class WifiCredentialStore {
|
|||||||
// Check if a network is saved
|
// Check if a network is saved
|
||||||
bool hasSavedCredential(const std::string& ssid) const;
|
bool hasSavedCredential(const std::string& ssid) const;
|
||||||
|
|
||||||
|
// Last connected network
|
||||||
|
void setLastConnectedSsid(const std::string& ssid);
|
||||||
|
const std::string& getLastConnectedSsid() const;
|
||||||
|
void clearLastConnectedSsid();
|
||||||
|
|
||||||
// Clear all credentials
|
// Clear all credentials
|
||||||
void clearAll();
|
void clearAll();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -18,8 +18,8 @@ class Activity {
|
|||||||
explicit Activity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput)
|
explicit Activity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||||
: name(std::move(name)), renderer(renderer), mappedInput(mappedInput) {}
|
: name(std::move(name)), renderer(renderer), mappedInput(mappedInput) {}
|
||||||
virtual ~Activity() = default;
|
virtual ~Activity() = default;
|
||||||
virtual void onEnter() { Serial.printf("[%lu] [ACT] Entering activity: %s\n", millis(), name.c_str()); }
|
virtual void onEnter() { LOG_DBG("ACT", "Entering activity: %s", name.c_str()); }
|
||||||
virtual void onExit() { Serial.printf("[%lu] [ACT] Exiting activity: %s\n", millis(), name.c_str()); }
|
virtual void onExit() { LOG_DBG("ACT", "Exiting activity: %s", name.c_str()); }
|
||||||
virtual void loop() {}
|
virtual void loop() {}
|
||||||
virtual bool skipLoopDelay() { return false; }
|
virtual bool skipLoopDelay() { return false; }
|
||||||
virtual bool preventAutoSleep() { return false; }
|
virtual bool preventAutoSleep() { return false; }
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#include <Epub.h>
|
#include <Epub.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
#include <Txt.h>
|
#include <Txt.h>
|
||||||
#include <Xtc.h>
|
#include <Xtc.h>
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ void SleepActivity::onEnter() {
|
|||||||
|
|
||||||
void SleepActivity::renderCustomSleepScreen() const {
|
void SleepActivity::renderCustomSleepScreen() const {
|
||||||
// Check if we have a /sleep directory
|
// Check if we have a /sleep directory
|
||||||
auto dir = SdMan.open("/sleep");
|
auto dir = Storage.open("/sleep");
|
||||||
if (dir && dir.isDirectory()) {
|
if (dir && dir.isDirectory()) {
|
||||||
std::vector<std::string> files;
|
std::vector<std::string> files;
|
||||||
char name[500];
|
char name[500];
|
||||||
@@ -50,13 +50,13 @@ void SleepActivity::renderCustomSleepScreen() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (filename.substr(filename.length() - 4) != ".bmp") {
|
if (filename.substr(filename.length() - 4) != ".bmp") {
|
||||||
Serial.printf("[%lu] [SLP] Skipping non-.bmp file name: %s\n", millis(), name);
|
LOG_DBG("SLP", "Skipping non-.bmp file name: %s", name);
|
||||||
file.close();
|
file.close();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Bitmap bitmap(file);
|
Bitmap bitmap(file);
|
||||||
if (bitmap.parseHeaders() != BmpReaderError::Ok) {
|
if (bitmap.parseHeaders() != BmpReaderError::Ok) {
|
||||||
Serial.printf("[%lu] [SLP] Skipping invalid BMP file: %s\n", millis(), name);
|
LOG_DBG("SLP", "Skipping invalid BMP file: %s", name);
|
||||||
file.close();
|
file.close();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -75,8 +75,8 @@ void SleepActivity::renderCustomSleepScreen() const {
|
|||||||
APP_STATE.saveToFile();
|
APP_STATE.saveToFile();
|
||||||
const auto filename = "/sleep/" + files[randomFileIndex];
|
const auto filename = "/sleep/" + files[randomFileIndex];
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (SdMan.openFileForRead("SLP", filename, file)) {
|
if (Storage.openFileForRead("SLP", filename, file)) {
|
||||||
Serial.printf("[%lu] [SLP] Randomly loading: /sleep/%s\n", millis(), files[randomFileIndex].c_str());
|
LOG_DBG("SLP", "Randomly loading: /sleep/%s", files[randomFileIndex].c_str());
|
||||||
delay(100);
|
delay(100);
|
||||||
Bitmap bitmap(file, true);
|
Bitmap bitmap(file, true);
|
||||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||||
@@ -92,10 +92,10 @@ void SleepActivity::renderCustomSleepScreen() const {
|
|||||||
// Look for sleep.bmp on the root of the sd card to determine if we should
|
// Look for sleep.bmp on the root of the sd card to determine if we should
|
||||||
// render a custom sleep screen instead of the default.
|
// render a custom sleep screen instead of the default.
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (SdMan.openFileForRead("SLP", "/sleep.bmp", file)) {
|
if (Storage.openFileForRead("SLP", "/sleep.bmp", file)) {
|
||||||
Bitmap bitmap(file, true);
|
Bitmap bitmap(file, true);
|
||||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||||
Serial.printf("[%lu] [SLP] Loading: /sleep.bmp\n", millis());
|
LOG_DBG("SLP", "Loading: /sleep.bmp");
|
||||||
renderBitmapSleepScreen(bitmap);
|
renderBitmapSleepScreen(bitmap);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -127,34 +127,33 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
|
|||||||
const auto pageHeight = renderer.getScreenHeight();
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
float cropX = 0, cropY = 0;
|
float cropX = 0, cropY = 0;
|
||||||
|
|
||||||
Serial.printf("[%lu] [SLP] bitmap %d x %d, screen %d x %d\n", millis(), bitmap.getWidth(), bitmap.getHeight(),
|
LOG_DBG("SLP", "bitmap %d x %d, screen %d x %d", bitmap.getWidth(), bitmap.getHeight(), pageWidth, pageHeight);
|
||||||
pageWidth, pageHeight);
|
|
||||||
if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight) {
|
if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight) {
|
||||||
// image will scale, make sure placement is right
|
// image will scale, make sure placement is right
|
||||||
float ratio = static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
|
float ratio = static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
|
||||||
const float screenRatio = static_cast<float>(pageWidth) / static_cast<float>(pageHeight);
|
const float screenRatio = static_cast<float>(pageWidth) / static_cast<float>(pageHeight);
|
||||||
|
|
||||||
Serial.printf("[%lu] [SLP] bitmap ratio: %f, screen ratio: %f\n", millis(), ratio, screenRatio);
|
LOG_DBG("SLP", "bitmap ratio: %f, screen ratio: %f", ratio, screenRatio);
|
||||||
if (ratio > screenRatio) {
|
if (ratio > screenRatio) {
|
||||||
// image wider than viewport ratio, scaled down image needs to be centered vertically
|
// image wider than viewport ratio, scaled down image needs to be centered vertically
|
||||||
if (SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) {
|
if (SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) {
|
||||||
cropX = 1.0f - (screenRatio / ratio);
|
cropX = 1.0f - (screenRatio / ratio);
|
||||||
Serial.printf("[%lu] [SLP] Cropping bitmap x: %f\n", millis(), cropX);
|
LOG_DBG("SLP", "Cropping bitmap x: %f", cropX);
|
||||||
ratio = (1.0f - cropX) * static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
|
ratio = (1.0f - cropX) * static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
|
||||||
}
|
}
|
||||||
x = 0;
|
x = 0;
|
||||||
y = std::round((static_cast<float>(pageHeight) - static_cast<float>(pageWidth) / ratio) / 2);
|
y = std::round((static_cast<float>(pageHeight) - static_cast<float>(pageWidth) / ratio) / 2);
|
||||||
Serial.printf("[%lu] [SLP] Centering with ratio %f to y=%d\n", millis(), ratio, y);
|
LOG_DBG("SLP", "Centering with ratio %f to y=%d", ratio, y);
|
||||||
} else {
|
} else {
|
||||||
// image taller than viewport ratio, scaled down image needs to be centered horizontally
|
// image taller than viewport ratio, scaled down image needs to be centered horizontally
|
||||||
if (SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) {
|
if (SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) {
|
||||||
cropY = 1.0f - (ratio / screenRatio);
|
cropY = 1.0f - (ratio / screenRatio);
|
||||||
Serial.printf("[%lu] [SLP] Cropping bitmap y: %f\n", millis(), cropY);
|
LOG_DBG("SLP", "Cropping bitmap y: %f", cropY);
|
||||||
ratio = static_cast<float>(bitmap.getWidth()) / ((1.0f - cropY) * static_cast<float>(bitmap.getHeight()));
|
ratio = static_cast<float>(bitmap.getWidth()) / ((1.0f - cropY) * static_cast<float>(bitmap.getHeight()));
|
||||||
}
|
}
|
||||||
x = std::round((static_cast<float>(pageWidth) - static_cast<float>(pageHeight) * ratio) / 2);
|
x = std::round((static_cast<float>(pageWidth) - static_cast<float>(pageHeight) * ratio) / 2);
|
||||||
y = 0;
|
y = 0;
|
||||||
Serial.printf("[%lu] [SLP] Centering with ratio %f to x=%d\n", millis(), ratio, x);
|
LOG_DBG("SLP", "Centering with ratio %f to x=%d", ratio, x);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// center the image
|
// center the image
|
||||||
@@ -162,7 +161,7 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
|
|||||||
y = (pageHeight - bitmap.getHeight()) / 2;
|
y = (pageHeight - bitmap.getHeight()) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [SLP] drawing to %d x %d\n", millis(), x, y);
|
LOG_DBG("SLP", "drawing to %d x %d", x, y);
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
|
|
||||||
const bool hasGreyscale = bitmap.hasGreyscale() &&
|
const bool hasGreyscale = bitmap.hasGreyscale() &&
|
||||||
@@ -218,12 +217,12 @@ void SleepActivity::renderCoverSleepScreen() const {
|
|||||||
// Handle XTC file
|
// Handle XTC file
|
||||||
Xtc lastXtc(APP_STATE.openEpubPath, "/.crosspoint");
|
Xtc lastXtc(APP_STATE.openEpubPath, "/.crosspoint");
|
||||||
if (!lastXtc.load()) {
|
if (!lastXtc.load()) {
|
||||||
Serial.println("[SLP] Failed to load last XTC");
|
LOG_ERR("SLP", "Failed to load last XTC");
|
||||||
return (this->*renderNoCoverSleepScreen)();
|
return (this->*renderNoCoverSleepScreen)();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!lastXtc.generateCoverBmp()) {
|
if (!lastXtc.generateCoverBmp()) {
|
||||||
Serial.println("[SLP] Failed to generate XTC cover bmp");
|
LOG_ERR("SLP", "Failed to generate XTC cover bmp");
|
||||||
return (this->*renderNoCoverSleepScreen)();
|
return (this->*renderNoCoverSleepScreen)();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,12 +231,12 @@ void SleepActivity::renderCoverSleepScreen() const {
|
|||||||
// Handle TXT file - looks for cover image in the same folder
|
// Handle TXT file - looks for cover image in the same folder
|
||||||
Txt lastTxt(APP_STATE.openEpubPath, "/.crosspoint");
|
Txt lastTxt(APP_STATE.openEpubPath, "/.crosspoint");
|
||||||
if (!lastTxt.load()) {
|
if (!lastTxt.load()) {
|
||||||
Serial.println("[SLP] Failed to load last TXT");
|
LOG_ERR("SLP", "Failed to load last TXT");
|
||||||
return (this->*renderNoCoverSleepScreen)();
|
return (this->*renderNoCoverSleepScreen)();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!lastTxt.generateCoverBmp()) {
|
if (!lastTxt.generateCoverBmp()) {
|
||||||
Serial.println("[SLP] No cover image found for TXT file");
|
LOG_ERR("SLP", "No cover image found for TXT file");
|
||||||
return (this->*renderNoCoverSleepScreen)();
|
return (this->*renderNoCoverSleepScreen)();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,12 +246,12 @@ void SleepActivity::renderCoverSleepScreen() const {
|
|||||||
Epub lastEpub(APP_STATE.openEpubPath, "/.crosspoint");
|
Epub lastEpub(APP_STATE.openEpubPath, "/.crosspoint");
|
||||||
// Skip loading css since we only need metadata here
|
// Skip loading css since we only need metadata here
|
||||||
if (!lastEpub.load(true, true)) {
|
if (!lastEpub.load(true, true)) {
|
||||||
Serial.println("[SLP] Failed to load last epub");
|
LOG_ERR("SLP", "Failed to load last epub");
|
||||||
return (this->*renderNoCoverSleepScreen)();
|
return (this->*renderNoCoverSleepScreen)();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!lastEpub.generateCoverBmp(cropped)) {
|
if (!lastEpub.generateCoverBmp(cropped)) {
|
||||||
Serial.println("[SLP] Failed to generate cover bmp");
|
LOG_ERR("SLP", "Failed to generate cover bmp");
|
||||||
return (this->*renderNoCoverSleepScreen)();
|
return (this->*renderNoCoverSleepScreen)();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,10 +261,10 @@ void SleepActivity::renderCoverSleepScreen() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (SdMan.openFileForRead("SLP", coverBmpPath, file)) {
|
if (Storage.openFileForRead("SLP", coverBmpPath, file)) {
|
||||||
Bitmap bitmap(file);
|
Bitmap bitmap(file);
|
||||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||||
Serial.printf("[SLP] Rendering sleep cover: %s\n", coverBmpPath.c_str());
|
LOG_DBG("SLP", "Rendering sleep cover: %s", coverBmpPath.c_str());
|
||||||
renderBitmapSleepScreen(bitmap);
|
renderBitmapSleepScreen(bitmap);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#include <Epub.h>
|
#include <Epub.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HardwareSerial.h>
|
#include <Logging.h>
|
||||||
#include <OpdsStream.h>
|
#include <OpdsStream.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
|
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int PAGE_ITEMS = 23;
|
constexpr int PAGE_ITEMS = 23;
|
||||||
constexpr int SKIP_PAGE_MS = 700;
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void OpdsBookBrowserActivity::taskTrampoline(void* param) {
|
void OpdsBookBrowserActivity::taskTrampoline(void* param) {
|
||||||
@@ -79,14 +78,14 @@ void OpdsBookBrowserActivity::loop() {
|
|||||||
// Check if WiFi is still connected
|
// Check if WiFi is still connected
|
||||||
if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) {
|
if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) {
|
||||||
// WiFi connected - just retry fetching the feed
|
// WiFi connected - just retry fetching the feed
|
||||||
Serial.printf("[%lu] [OPDS] Retry: WiFi connected, retrying fetch\n", millis());
|
LOG_DBG("OPDS", "Retry: WiFi connected, retrying fetch");
|
||||||
state = BrowserState::LOADING;
|
state = BrowserState::LOADING;
|
||||||
statusMessage = "Loading...";
|
statusMessage = "Loading...";
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
fetchFeed(currentPath);
|
fetchFeed(currentPath);
|
||||||
} else {
|
} else {
|
||||||
// WiFi not connected - launch WiFi selection
|
// WiFi not connected - launch WiFi selection
|
||||||
Serial.printf("[%lu] [OPDS] Retry: WiFi not connected, launching selection\n", millis());
|
LOG_DBG("OPDS", "Retry: WiFi not connected, launching selection");
|
||||||
launchWifiSelection();
|
launchWifiSelection();
|
||||||
}
|
}
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
@@ -118,12 +117,6 @@ void OpdsBookBrowserActivity::loop() {
|
|||||||
|
|
||||||
// Handle browsing state
|
// Handle browsing state
|
||||||
if (state == BrowserState::BROWSING) {
|
if (state == BrowserState::BROWSING) {
|
||||||
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Left);
|
|
||||||
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Right);
|
|
||||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
if (!entries.empty()) {
|
if (!entries.empty()) {
|
||||||
const auto& entry = entries[selectorIndex];
|
const auto& entry = entries[selectorIndex];
|
||||||
@@ -135,20 +128,29 @@ void OpdsBookBrowserActivity::loop() {
|
|||||||
}
|
}
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
navigateBack();
|
navigateBack();
|
||||||
} else if (prevReleased && !entries.empty()) {
|
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = ((selectorIndex / PAGE_ITEMS - 1) * PAGE_ITEMS + entries.size()) % entries.size();
|
|
||||||
} else {
|
|
||||||
selectorIndex = (selectorIndex + entries.size() - 1) % entries.size();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle navigation
|
||||||
|
if (!entries.empty()) {
|
||||||
|
buttonNavigator.onNextRelease([this] {
|
||||||
|
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, entries.size());
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (nextReleased && !entries.empty()) {
|
});
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % entries.size();
|
buttonNavigator.onPreviousRelease([this] {
|
||||||
} else {
|
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, entries.size());
|
||||||
selectorIndex = (selectorIndex + 1) % entries.size();
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onNextContinuous([this] {
|
||||||
|
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, entries.size(), PAGE_ITEMS);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPreviousContinuous([this] {
|
||||||
|
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, entries.size(), PAGE_ITEMS);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,7 +265,7 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string url = UrlUtils::buildUrl(serverUrl, path);
|
std::string url = UrlUtils::buildUrl(serverUrl, path);
|
||||||
Serial.printf("[%lu] [OPDS] Fetching: %s\n", millis(), url.c_str());
|
LOG_DBG("OPDS", "Fetching: %s", url.c_str());
|
||||||
|
|
||||||
OpdsParser parser;
|
OpdsParser parser;
|
||||||
|
|
||||||
@@ -285,7 +287,7 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
entries = std::move(parser).getEntries();
|
entries = std::move(parser).getEntries();
|
||||||
Serial.printf("[%lu] [OPDS] Found %d entries\n", millis(), entries.size());
|
LOG_DBG("OPDS", "Found %d entries", entries.size());
|
||||||
selectorIndex = 0;
|
selectorIndex = 0;
|
||||||
|
|
||||||
if (entries.empty()) {
|
if (entries.empty()) {
|
||||||
@@ -349,7 +351,7 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
|
|||||||
}
|
}
|
||||||
std::string filename = "/" + StringUtils::sanitizeFilename(baseName) + ".epub";
|
std::string filename = "/" + StringUtils::sanitizeFilename(baseName) + ".epub";
|
||||||
|
|
||||||
Serial.printf("[%lu] [OPDS] Downloading: %s -> %s\n", millis(), downloadUrl.c_str(), filename.c_str());
|
LOG_DBG("OPDS", "Downloading: %s -> %s", downloadUrl.c_str(), filename.c_str());
|
||||||
|
|
||||||
const auto result =
|
const auto result =
|
||||||
HttpDownloader::downloadToFile(downloadUrl, filename, [this](const size_t downloaded, const size_t total) {
|
HttpDownloader::downloadToFile(downloadUrl, filename, [this](const size_t downloaded, const size_t total) {
|
||||||
@@ -359,12 +361,12 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (result == HttpDownloader::OK) {
|
if (result == HttpDownloader::OK) {
|
||||||
Serial.printf("[%lu] [OPDS] Download complete: %s\n", millis(), filename.c_str());
|
LOG_DBG("OPDS", "Download complete: %s", filename.c_str());
|
||||||
|
|
||||||
// Invalidate any existing cache for this file to prevent stale metadata issues
|
// Invalidate any existing cache for this file to prevent stale metadata issues
|
||||||
Epub epub(filename, "/.crosspoint");
|
Epub epub(filename, "/.crosspoint");
|
||||||
epub.clearCache();
|
epub.clearCache();
|
||||||
Serial.printf("[%lu] [OPDS] Cleared cache for: %s\n", millis(), filename.c_str());
|
LOG_DBG("OPDS", "Cleared cache for: %s", filename.c_str());
|
||||||
|
|
||||||
state = BrowserState::BROWSING;
|
state = BrowserState::BROWSING;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
@@ -401,13 +403,13 @@ void OpdsBookBrowserActivity::onWifiSelectionComplete(const bool connected) {
|
|||||||
exitActivity();
|
exitActivity();
|
||||||
|
|
||||||
if (connected) {
|
if (connected) {
|
||||||
Serial.printf("[%lu] [OPDS] WiFi connected via selection, fetching feed\n", millis());
|
LOG_DBG("OPDS", "WiFi connected via selection, fetching feed");
|
||||||
state = BrowserState::LOADING;
|
state = BrowserState::LOADING;
|
||||||
statusMessage = "Loading...";
|
statusMessage = "Loading...";
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
fetchFeed(currentPath);
|
fetchFeed(currentPath);
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [OPDS] WiFi selection cancelled/failed\n", millis());
|
LOG_DBG("OPDS", "WiFi selection cancelled/failed");
|
||||||
// Force disconnect to ensure clean state for next retry
|
// Force disconnect to ensure clean state for next retry
|
||||||
// This prevents stale connection status from interfering
|
// This prevents stale connection status from interfering
|
||||||
WiFi.disconnect();
|
WiFi.disconnect();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "../ActivityWithSubactivity.h"
|
#include "../ActivityWithSubactivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity for browsing and downloading books from an OPDS server.
|
* Activity for browsing and downloading books from an OPDS server.
|
||||||
@@ -37,6 +38,7 @@ class OpdsBookBrowserActivity final : public ActivityWithSubactivity {
|
|||||||
private:
|
private:
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
|
||||||
BrowserState state = BrowserState::LOADING;
|
BrowserState state = BrowserState::LOADING;
|
||||||
@@ -62,4 +64,5 @@ class OpdsBookBrowserActivity final : public ActivityWithSubactivity {
|
|||||||
void navigateToEntry(const OpdsEntry& entry);
|
void navigateToEntry(const OpdsEntry& entry);
|
||||||
void navigateBack();
|
void navigateBack();
|
||||||
void downloadBook(const OpdsEntry& book);
|
void downloadBook(const OpdsEntry& book);
|
||||||
|
bool preventAutoSleep() override { return true; }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
#include <Bitmap.h>
|
#include <Bitmap.h>
|
||||||
#include <Epub.h>
|
#include <Epub.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
#include <Utf8.h>
|
#include <Utf8.h>
|
||||||
#include <Xtc.h>
|
#include <Xtc.h>
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ void HomeActivity::loadRecentBooks(int maxBooks) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Skip if file no longer exists
|
// Skip if file no longer exists
|
||||||
if (!SdMan.exists(book.path.c_str())) {
|
if (!Storage.exists(book.path.c_str())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
|
|||||||
for (RecentBook& book : recentBooks) {
|
for (RecentBook& book : recentBooks) {
|
||||||
if (!book.coverBmpPath.empty()) {
|
if (!book.coverBmpPath.empty()) {
|
||||||
std::string coverPath = UITheme::getCoverThumbPath(book.coverBmpPath, coverHeight);
|
std::string coverPath = UITheme::getCoverThumbPath(book.coverBmpPath, coverHeight);
|
||||||
if (!SdMan.exists(coverPath.c_str())) {
|
if (!Storage.exists(coverPath.c_str())) {
|
||||||
// If epub, try to load the metadata for title/author and cover
|
// If epub, try to load the metadata for title/author and cover
|
||||||
if (StringUtils::checkFileExtension(book.path, ".epub")) {
|
if (StringUtils::checkFileExtension(book.path, ".epub")) {
|
||||||
Epub epub(book.path, "/.crosspoint");
|
Epub epub(book.path, "/.crosspoint");
|
||||||
@@ -196,13 +196,18 @@ void HomeActivity::freeCoverBuffer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void HomeActivity::loop() {
|
void HomeActivity::loop() {
|
||||||
const bool prevPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left);
|
|
||||||
const bool nextPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right);
|
|
||||||
|
|
||||||
const int menuCount = getMenuItemCount();
|
const int menuCount = getMenuItemCount();
|
||||||
|
|
||||||
|
buttonNavigator.onNext([this, menuCount] {
|
||||||
|
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, menuCount);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPrevious([this, menuCount] {
|
||||||
|
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, menuCount);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
// Calculate dynamic indices based on which options are available
|
// Calculate dynamic indices based on which options are available
|
||||||
int idx = 0;
|
int idx = 0;
|
||||||
@@ -226,12 +231,6 @@ void HomeActivity::loop() {
|
|||||||
} else if (menuSelectedIndex == settingsIdx) {
|
} else if (menuSelectedIndex == settingsIdx) {
|
||||||
onSettingsOpen();
|
onSettingsOpen();
|
||||||
}
|
}
|
||||||
} else if (prevPressed) {
|
|
||||||
selectorIndex = (selectorIndex + menuCount - 1) % menuCount;
|
|
||||||
updateRequired = true;
|
|
||||||
} else if (nextPressed) {
|
|
||||||
selectorIndex = (selectorIndex + 1) % menuCount;
|
|
||||||
updateRequired = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "../Activity.h"
|
#include "../Activity.h"
|
||||||
#include "./MyLibraryActivity.h"
|
#include "./MyLibraryActivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
struct RecentBook;
|
struct RecentBook;
|
||||||
struct Rect;
|
struct Rect;
|
||||||
@@ -15,6 +16,7 @@ struct Rect;
|
|||||||
class HomeActivity final : public Activity {
|
class HomeActivity final : public Activity {
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
int selectorIndex = 0;
|
int selectorIndex = 0;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
bool recentsLoading = false;
|
bool recentsLoading = false;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#include "MyLibraryActivity.h"
|
#include "MyLibraryActivity.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
@@ -11,17 +11,58 @@
|
|||||||
#include "util/StringUtils.h"
|
#include "util/StringUtils.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int SKIP_PAGE_MS = 700;
|
|
||||||
constexpr unsigned long GO_HOME_MS = 1000;
|
constexpr unsigned long GO_HOME_MS = 1000;
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void sortFileList(std::vector<std::string>& strs) {
|
void sortFileList(std::vector<std::string>& strs) {
|
||||||
std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) {
|
std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) {
|
||||||
if (str1.back() == '/' && str2.back() != '/') return true;
|
// Directories first
|
||||||
if (str1.back() != '/' && str2.back() == '/') return false;
|
bool isDir1 = str1.back() == '/';
|
||||||
return lexicographical_compare(
|
bool isDir2 = str2.back() == '/';
|
||||||
begin(str1), end(str1), begin(str2), end(str2),
|
if (isDir1 != isDir2) return isDir1;
|
||||||
[](const char& char1, const char& char2) { return tolower(char1) < tolower(char2); });
|
|
||||||
|
// Start naive natural sort
|
||||||
|
const char* s1 = str1.c_str();
|
||||||
|
const char* s2 = str2.c_str();
|
||||||
|
|
||||||
|
// Iterate while both strings have characters
|
||||||
|
while (*s1 && *s2) {
|
||||||
|
// Check if both are at the start of a number
|
||||||
|
if (isdigit(*s1) && isdigit(*s2)) {
|
||||||
|
// Skip leading zeros and track them
|
||||||
|
const char* start1 = s1;
|
||||||
|
const char* start2 = s2;
|
||||||
|
while (*s1 == '0') s1++;
|
||||||
|
while (*s2 == '0') s2++;
|
||||||
|
|
||||||
|
// Count digits to compare lengths first
|
||||||
|
int len1 = 0, len2 = 0;
|
||||||
|
while (isdigit(s1[len1])) len1++;
|
||||||
|
while (isdigit(s2[len2])) len2++;
|
||||||
|
|
||||||
|
// Different length so return smaller integer value
|
||||||
|
if (len1 != len2) return len1 < len2;
|
||||||
|
|
||||||
|
// Same length so compare digit by digit
|
||||||
|
for (int i = 0; i < len1; i++) {
|
||||||
|
if (s1[i] != s2[i]) return s1[i] < s2[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numbers equal so advance pointers
|
||||||
|
s1 += len1;
|
||||||
|
s2 += len2;
|
||||||
|
} else {
|
||||||
|
// Regular case-insensitive character comparison
|
||||||
|
char c1 = tolower(*s1);
|
||||||
|
char c2 = tolower(*s2);
|
||||||
|
if (c1 != c2) return c1 < c2;
|
||||||
|
s1++;
|
||||||
|
s2++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One string is prefix of other
|
||||||
|
return *s1 == '\0' && *s2 != '\0';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +74,7 @@ void MyLibraryActivity::taskTrampoline(void* param) {
|
|||||||
void MyLibraryActivity::loadFiles() {
|
void MyLibraryActivity::loadFiles() {
|
||||||
files.clear();
|
files.clear();
|
||||||
|
|
||||||
auto root = SdMan.open(basepath.c_str());
|
auto root = Storage.open(basepath.c_str());
|
||||||
if (!root || !root.isDirectory()) {
|
if (!root || !root.isDirectory()) {
|
||||||
if (root) root.close();
|
if (root) root.close();
|
||||||
return;
|
return;
|
||||||
@@ -109,13 +150,6 @@ void MyLibraryActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Left) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Up);
|
|
||||||
;
|
|
||||||
const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Right) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Down);
|
|
||||||
|
|
||||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
|
||||||
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false);
|
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false);
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
@@ -157,21 +191,26 @@ void MyLibraryActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int listSize = static_cast<int>(files.size());
|
int listSize = static_cast<int>(files.size());
|
||||||
if (upReleased) {
|
|
||||||
if (skipPage) {
|
buttonNavigator.onNextRelease([this, listSize] {
|
||||||
selectorIndex = std::max(static_cast<int>((selectorIndex / pageItems - 1) * pageItems), 0);
|
selectorIndex = ButtonNavigator::nextIndex(static_cast<int>(selectorIndex), listSize);
|
||||||
} else {
|
|
||||||
selectorIndex = (selectorIndex + listSize - 1) % listSize;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (downReleased) {
|
});
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = std::min(static_cast<int>((selectorIndex / pageItems + 1) * pageItems), listSize - 1);
|
buttonNavigator.onPreviousRelease([this, listSize] {
|
||||||
} else {
|
selectorIndex = ButtonNavigator::previousIndex(static_cast<int>(selectorIndex), listSize);
|
||||||
selectorIndex = (selectorIndex + 1) % listSize;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onNextContinuous([this, listSize, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::nextPageIndex(static_cast<int>(selectorIndex), listSize, pageItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPreviousContinuous([this, listSize, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::previousPageIndex(static_cast<int>(selectorIndex), listSize, pageItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void MyLibraryActivity::displayTaskLoop() {
|
void MyLibraryActivity::displayTaskLoop() {
|
||||||
@@ -207,7 +246,7 @@ void MyLibraryActivity::render() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Help text
|
// Help text
|
||||||
const auto labels = mappedInput.mapLabels("« Home", "Open", "Up", "Down");
|
const auto labels = mappedInput.mapLabels(basepath == "/" ? "« Home" : "« Back", "Open", "Up", "Down");
|
||||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
|
|||||||
@@ -8,11 +8,14 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "../Activity.h"
|
#include "../Activity.h"
|
||||||
|
#include "RecentBooksStore.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
class MyLibraryActivity final : public Activity {
|
class MyLibraryActivity final : public Activity {
|
||||||
private:
|
private:
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
|
|
||||||
size_t selectorIndex = 0;
|
size_t selectorIndex = 0;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#include "RecentBooksActivity.h"
|
#include "RecentBooksActivity.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
@@ -12,7 +12,6 @@
|
|||||||
#include "util/StringUtils.h"
|
#include "util/StringUtils.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int SKIP_PAGE_MS = 700;
|
|
||||||
constexpr unsigned long GO_HOME_MS = 1000;
|
constexpr unsigned long GO_HOME_MS = 1000;
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -28,7 +27,7 @@ void RecentBooksActivity::loadRecentBooks() {
|
|||||||
|
|
||||||
for (const auto& book : books) {
|
for (const auto& book : books) {
|
||||||
// Skip if file no longer exists
|
// Skip if file no longer exists
|
||||||
if (!SdMan.exists(book.path.c_str())) {
|
if (!Storage.exists(book.path.c_str())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
recentBooks.push_back(book);
|
recentBooks.push_back(book);
|
||||||
@@ -70,18 +69,11 @@ void RecentBooksActivity::onExit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RecentBooksActivity::loop() {
|
void RecentBooksActivity::loop() {
|
||||||
const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Left) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Up);
|
|
||||||
;
|
|
||||||
const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Right) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Down);
|
|
||||||
|
|
||||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
|
||||||
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, true);
|
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, true);
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
if (!recentBooks.empty() && selectorIndex < static_cast<int>(recentBooks.size())) {
|
if (!recentBooks.empty() && selectorIndex < static_cast<int>(recentBooks.size())) {
|
||||||
Serial.printf("Selected recent book: %s\n", recentBooks[selectorIndex].path.c_str());
|
LOG_DBG("RBA", "Selected recent book: %s", recentBooks[selectorIndex].path.c_str());
|
||||||
onSelectBook(recentBooks[selectorIndex].path);
|
onSelectBook(recentBooks[selectorIndex].path);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -92,21 +84,26 @@ void RecentBooksActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int listSize = static_cast<int>(recentBooks.size());
|
int listSize = static_cast<int>(recentBooks.size());
|
||||||
if (upReleased) {
|
|
||||||
if (skipPage) {
|
buttonNavigator.onNextRelease([this, listSize] {
|
||||||
selectorIndex = std::max(static_cast<int>((selectorIndex / pageItems - 1) * pageItems), 0);
|
selectorIndex = ButtonNavigator::nextIndex(static_cast<int>(selectorIndex), listSize);
|
||||||
} else {
|
|
||||||
selectorIndex = (selectorIndex + listSize - 1) % listSize;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (downReleased) {
|
});
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = std::min(static_cast<int>((selectorIndex / pageItems + 1) * pageItems), listSize - 1);
|
buttonNavigator.onPreviousRelease([this, listSize] {
|
||||||
} else {
|
selectorIndex = ButtonNavigator::previousIndex(static_cast<int>(selectorIndex), listSize);
|
||||||
selectorIndex = (selectorIndex + 1) % listSize;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onNextContinuous([this, listSize, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::nextPageIndex(static_cast<int>(selectorIndex), listSize, pageItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPreviousContinuous([this, listSize, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::previousPageIndex(static_cast<int>(selectorIndex), listSize, pageItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void RecentBooksActivity::displayTaskLoop() {
|
void RecentBooksActivity::displayTaskLoop() {
|
||||||
|
|||||||
@@ -9,11 +9,13 @@
|
|||||||
|
|
||||||
#include "../Activity.h"
|
#include "../Activity.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
class RecentBooksActivity final : public Activity {
|
class RecentBooksActivity final : public Activity {
|
||||||
private:
|
private:
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
|
|
||||||
size_t selectorIndex = 0;
|
size_t selectorIndex = 0;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ void CalibreConnectActivity::startWebServer() {
|
|||||||
|
|
||||||
if (MDNS.begin(HOSTNAME)) {
|
if (MDNS.begin(HOSTNAME)) {
|
||||||
// mDNS is optional for the Calibre plugin but still helpful for users.
|
// mDNS is optional for the Calibre plugin but still helpful for users.
|
||||||
Serial.printf("[%lu] [CAL] mDNS started: http://%s.local/\n", millis(), HOSTNAME);
|
LOG_DBG("CAL", "mDNS started: http://%s.local/", HOSTNAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
webServer.reset(new CrossPointWebServer());
|
webServer.reset(new CrossPointWebServer());
|
||||||
@@ -131,7 +131,7 @@ void CalibreConnectActivity::loop() {
|
|||||||
if (webServer && webServer->isRunning()) {
|
if (webServer && webServer->isRunning()) {
|
||||||
const unsigned long timeSinceLastHandleClient = millis() - lastHandleClientTime;
|
const unsigned long timeSinceLastHandleClient = millis() - lastHandleClientTime;
|
||||||
if (lastHandleClientTime > 0 && timeSinceLastHandleClient > 100) {
|
if (lastHandleClientTime > 0 && timeSinceLastHandleClient > 100) {
|
||||||
Serial.printf("[%lu] [CAL] WARNING: %lu ms gap since last handleClient\n", millis(), timeSinceLastHandleClient);
|
LOG_DBG("CAL", "WARNING: %lu ms gap since last handleClient", timeSinceLastHandleClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_task_wdt_reset();
|
esp_task_wdt_reset();
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ void CrossPointWebServerActivity::taskTrampoline(void* param) {
|
|||||||
void CrossPointWebServerActivity::onEnter() {
|
void CrossPointWebServerActivity::onEnter() {
|
||||||
ActivityWithSubactivity::onEnter();
|
ActivityWithSubactivity::onEnter();
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onEnter: %d bytes\n", millis(), ESP.getFreeHeap());
|
LOG_DBG("WEBACT] [MEM", "Free heap at onEnter: %d bytes", ESP.getFreeHeap());
|
||||||
|
|
||||||
renderingMutex = xSemaphoreCreateMutex();
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ void CrossPointWebServerActivity::onEnter() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Launch network mode selection subactivity
|
// Launch network mode selection subactivity
|
||||||
Serial.printf("[%lu] [WEBACT] Launching NetworkModeSelectionActivity...\n", millis());
|
LOG_DBG("WEBACT", "Launching NetworkModeSelectionActivity...");
|
||||||
enterNewActivity(new NetworkModeSelectionActivity(
|
enterNewActivity(new NetworkModeSelectionActivity(
|
||||||
renderer, mappedInput, [this](const NetworkMode mode) { onNetworkModeSelected(mode); },
|
renderer, mappedInput, [this](const NetworkMode mode) { onNetworkModeSelected(mode); },
|
||||||
[this]() { onGoBack(); } // Cancel goes back to home
|
[this]() { onGoBack(); } // Cancel goes back to home
|
||||||
@@ -68,7 +68,7 @@ void CrossPointWebServerActivity::onEnter() {
|
|||||||
void CrossPointWebServerActivity::onExit() {
|
void CrossPointWebServerActivity::onExit() {
|
||||||
ActivityWithSubactivity::onExit();
|
ActivityWithSubactivity::onExit();
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onExit start: %d bytes\n", millis(), ESP.getFreeHeap());
|
LOG_DBG("WEBACT] [MEM", "Free heap at onExit start: %d bytes", ESP.getFreeHeap());
|
||||||
|
|
||||||
state = WebServerActivityState::SHUTTING_DOWN;
|
state = WebServerActivityState::SHUTTING_DOWN;
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ void CrossPointWebServerActivity::onExit() {
|
|||||||
|
|
||||||
// Stop DNS server if running (AP mode)
|
// Stop DNS server if running (AP mode)
|
||||||
if (dnsServer) {
|
if (dnsServer) {
|
||||||
Serial.printf("[%lu] [WEBACT] Stopping DNS server...\n", millis());
|
LOG_DBG("WEBACT", "Stopping DNS server...");
|
||||||
dnsServer->stop();
|
dnsServer->stop();
|
||||||
delete dnsServer;
|
delete dnsServer;
|
||||||
dnsServer = nullptr;
|
dnsServer = nullptr;
|
||||||
@@ -91,39 +91,39 @@ void CrossPointWebServerActivity::onExit() {
|
|||||||
|
|
||||||
// Disconnect WiFi gracefully
|
// Disconnect WiFi gracefully
|
||||||
if (isApMode) {
|
if (isApMode) {
|
||||||
Serial.printf("[%lu] [WEBACT] Stopping WiFi AP...\n", millis());
|
LOG_DBG("WEBACT", "Stopping WiFi AP...");
|
||||||
WiFi.softAPdisconnect(true);
|
WiFi.softAPdisconnect(true);
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [WEBACT] Disconnecting WiFi (graceful)...\n", millis());
|
LOG_DBG("WEBACT", "Disconnecting WiFi (graceful)...");
|
||||||
WiFi.disconnect(false); // false = don't erase credentials, send disconnect frame
|
WiFi.disconnect(false); // false = don't erase credentials, send disconnect frame
|
||||||
}
|
}
|
||||||
delay(30); // Allow disconnect frame to be sent
|
delay(30); // Allow disconnect frame to be sent
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEBACT] Setting WiFi mode OFF...\n", millis());
|
LOG_DBG("WEBACT", "Setting WiFi mode OFF...");
|
||||||
WiFi.mode(WIFI_OFF);
|
WiFi.mode(WIFI_OFF);
|
||||||
delay(30); // Allow WiFi hardware to power down
|
delay(30); // Allow WiFi hardware to power down
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEBACT] [MEM] Free heap after WiFi disconnect: %d bytes\n", millis(), ESP.getFreeHeap());
|
LOG_DBG("WEBACT] [MEM", "Free heap after WiFi disconnect: %d bytes", ESP.getFreeHeap());
|
||||||
|
|
||||||
// Acquire mutex before deleting task
|
// Acquire mutex before deleting task
|
||||||
Serial.printf("[%lu] [WEBACT] Acquiring rendering mutex before task deletion...\n", millis());
|
LOG_DBG("WEBACT", "Acquiring rendering mutex before task deletion...");
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
|
||||||
// Delete the display task
|
// Delete the display task
|
||||||
Serial.printf("[%lu] [WEBACT] Deleting display task...\n", millis());
|
LOG_DBG("WEBACT", "Deleting display task...");
|
||||||
if (displayTaskHandle) {
|
if (displayTaskHandle) {
|
||||||
vTaskDelete(displayTaskHandle);
|
vTaskDelete(displayTaskHandle);
|
||||||
displayTaskHandle = nullptr;
|
displayTaskHandle = nullptr;
|
||||||
Serial.printf("[%lu] [WEBACT] Display task deleted\n", millis());
|
LOG_DBG("WEBACT", "Display task deleted");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the mutex
|
// Delete the mutex
|
||||||
Serial.printf("[%lu] [WEBACT] Deleting mutex...\n", millis());
|
LOG_DBG("WEBACT", "Deleting mutex...");
|
||||||
vSemaphoreDelete(renderingMutex);
|
vSemaphoreDelete(renderingMutex);
|
||||||
renderingMutex = nullptr;
|
renderingMutex = nullptr;
|
||||||
Serial.printf("[%lu] [WEBACT] Mutex deleted\n", millis());
|
LOG_DBG("WEBACT", "Mutex deleted");
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onExit end: %d bytes\n", millis(), ESP.getFreeHeap());
|
LOG_DBG("WEBACT] [MEM", "Free heap at onExit end: %d bytes", ESP.getFreeHeap());
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode) {
|
void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode) {
|
||||||
@@ -133,7 +133,7 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode)
|
|||||||
} else if (mode == NetworkMode::CREATE_HOTSPOT) {
|
} else if (mode == NetworkMode::CREATE_HOTSPOT) {
|
||||||
modeName = "Create Hotspot";
|
modeName = "Create Hotspot";
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [WEBACT] Network mode selected: %s\n", millis(), modeName);
|
LOG_DBG("WEBACT", "Network mode selected: %s", modeName);
|
||||||
|
|
||||||
networkMode = mode;
|
networkMode = mode;
|
||||||
isApMode = (mode == NetworkMode::CREATE_HOTSPOT);
|
isApMode = (mode == NetworkMode::CREATE_HOTSPOT);
|
||||||
@@ -155,11 +155,11 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode)
|
|||||||
|
|
||||||
if (mode == NetworkMode::JOIN_NETWORK) {
|
if (mode == NetworkMode::JOIN_NETWORK) {
|
||||||
// STA mode - launch WiFi selection
|
// STA mode - launch WiFi selection
|
||||||
Serial.printf("[%lu] [WEBACT] Turning on WiFi (STA mode)...\n", millis());
|
LOG_DBG("WEBACT", "Turning on WiFi (STA mode)...");
|
||||||
WiFi.mode(WIFI_STA);
|
WiFi.mode(WIFI_STA);
|
||||||
|
|
||||||
state = WebServerActivityState::WIFI_SELECTION;
|
state = WebServerActivityState::WIFI_SELECTION;
|
||||||
Serial.printf("[%lu] [WEBACT] Launching WifiSelectionActivity...\n", millis());
|
LOG_DBG("WEBACT", "Launching WifiSelectionActivity...");
|
||||||
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
||||||
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
||||||
} else {
|
} else {
|
||||||
@@ -171,7 +171,7 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected) {
|
void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected) {
|
||||||
Serial.printf("[%lu] [WEBACT] WifiSelectionActivity completed, connected=%d\n", millis(), connected);
|
LOG_DBG("WEBACT", "WifiSelectionActivity completed, connected=%d", connected);
|
||||||
|
|
||||||
if (connected) {
|
if (connected) {
|
||||||
// Get connection info before exiting subactivity
|
// Get connection info before exiting subactivity
|
||||||
@@ -183,7 +183,7 @@ void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected)
|
|||||||
|
|
||||||
// Start mDNS for hostname resolution
|
// Start mDNS for hostname resolution
|
||||||
if (MDNS.begin(AP_HOSTNAME)) {
|
if (MDNS.begin(AP_HOSTNAME)) {
|
||||||
Serial.printf("[%lu] [WEBACT] mDNS started: http://%s.local/\n", millis(), AP_HOSTNAME);
|
LOG_DBG("WEBACT", "mDNS started: http://%s.local/", AP_HOSTNAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the web server
|
// Start the web server
|
||||||
@@ -199,8 +199,8 @@ void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServerActivity::startAccessPoint() {
|
void CrossPointWebServerActivity::startAccessPoint() {
|
||||||
Serial.printf("[%lu] [WEBACT] Starting Access Point mode...\n", millis());
|
LOG_DBG("WEBACT", "Starting Access Point mode...");
|
||||||
Serial.printf("[%lu] [WEBACT] [MEM] Free heap before AP start: %d bytes\n", millis(), ESP.getFreeHeap());
|
LOG_DBG("WEBACT] [MEM", "Free heap before AP start: %d bytes", ESP.getFreeHeap());
|
||||||
|
|
||||||
// Configure and start the AP
|
// Configure and start the AP
|
||||||
WiFi.mode(WIFI_AP);
|
WiFi.mode(WIFI_AP);
|
||||||
@@ -216,7 +216,7 @@ void CrossPointWebServerActivity::startAccessPoint() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!apStarted) {
|
if (!apStarted) {
|
||||||
Serial.printf("[%lu] [WEBACT] ERROR: Failed to start Access Point!\n", millis());
|
LOG_ERR("WEBACT", "ERROR: Failed to start Access Point!");
|
||||||
onGoBack();
|
onGoBack();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -230,15 +230,15 @@ void CrossPointWebServerActivity::startAccessPoint() {
|
|||||||
connectedIP = ipStr;
|
connectedIP = ipStr;
|
||||||
connectedSSID = AP_SSID;
|
connectedSSID = AP_SSID;
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEBACT] Access Point started!\n", millis());
|
LOG_DBG("WEBACT", "Access Point started!");
|
||||||
Serial.printf("[%lu] [WEBACT] SSID: %s\n", millis(), AP_SSID);
|
LOG_DBG("WEBACT", "SSID: %s", AP_SSID);
|
||||||
Serial.printf("[%lu] [WEBACT] IP: %s\n", millis(), connectedIP.c_str());
|
LOG_DBG("WEBACT", "IP: %s", connectedIP.c_str());
|
||||||
|
|
||||||
// Start mDNS for hostname resolution
|
// Start mDNS for hostname resolution
|
||||||
if (MDNS.begin(AP_HOSTNAME)) {
|
if (MDNS.begin(AP_HOSTNAME)) {
|
||||||
Serial.printf("[%lu] [WEBACT] mDNS started: http://%s.local/\n", millis(), AP_HOSTNAME);
|
LOG_DBG("WEBACT", "mDNS started: http://%s.local/", AP_HOSTNAME);
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [WEBACT] WARNING: mDNS failed to start\n", millis());
|
LOG_DBG("WEBACT", "WARNING: mDNS failed to start");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start DNS server for captive portal behavior
|
// Start DNS server for captive portal behavior
|
||||||
@@ -246,16 +246,16 @@ void CrossPointWebServerActivity::startAccessPoint() {
|
|||||||
dnsServer = new DNSServer();
|
dnsServer = new DNSServer();
|
||||||
dnsServer->setErrorReplyCode(DNSReplyCode::NoError);
|
dnsServer->setErrorReplyCode(DNSReplyCode::NoError);
|
||||||
dnsServer->start(DNS_PORT, "*", apIP);
|
dnsServer->start(DNS_PORT, "*", apIP);
|
||||||
Serial.printf("[%lu] [WEBACT] DNS server started for captive portal\n", millis());
|
LOG_DBG("WEBACT", "DNS server started for captive portal");
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEBACT] [MEM] Free heap after AP start: %d bytes\n", millis(), ESP.getFreeHeap());
|
LOG_DBG("WEBACT] [MEM", "Free heap after AP start: %d bytes", ESP.getFreeHeap());
|
||||||
|
|
||||||
// Start the web server
|
// Start the web server
|
||||||
startWebServer();
|
startWebServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServerActivity::startWebServer() {
|
void CrossPointWebServerActivity::startWebServer() {
|
||||||
Serial.printf("[%lu] [WEBACT] Starting web server...\n", millis());
|
LOG_DBG("WEBACT", "Starting web server...");
|
||||||
|
|
||||||
// Create the web server instance
|
// Create the web server instance
|
||||||
webServer.reset(new CrossPointWebServer());
|
webServer.reset(new CrossPointWebServer());
|
||||||
@@ -263,16 +263,16 @@ void CrossPointWebServerActivity::startWebServer() {
|
|||||||
|
|
||||||
if (webServer->isRunning()) {
|
if (webServer->isRunning()) {
|
||||||
state = WebServerActivityState::SERVER_RUNNING;
|
state = WebServerActivityState::SERVER_RUNNING;
|
||||||
Serial.printf("[%lu] [WEBACT] Web server started successfully\n", millis());
|
LOG_DBG("WEBACT", "Web server started successfully");
|
||||||
|
|
||||||
// Force an immediate render since we're transitioning from a subactivity
|
// Force an immediate render since we're transitioning from a subactivity
|
||||||
// that had its own rendering task. We need to make sure our display is shown.
|
// that had its own rendering task. We need to make sure our display is shown.
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
render();
|
render();
|
||||||
xSemaphoreGive(renderingMutex);
|
xSemaphoreGive(renderingMutex);
|
||||||
Serial.printf("[%lu] [WEBACT] Rendered File Transfer screen\n", millis());
|
LOG_DBG("WEBACT", "Rendered File Transfer screen");
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [WEBACT] ERROR: Failed to start web server!\n", millis());
|
LOG_ERR("WEBACT", "ERROR: Failed to start web server!");
|
||||||
webServer.reset();
|
webServer.reset();
|
||||||
// Go back on error
|
// Go back on error
|
||||||
onGoBack();
|
onGoBack();
|
||||||
@@ -281,9 +281,9 @@ void CrossPointWebServerActivity::startWebServer() {
|
|||||||
|
|
||||||
void CrossPointWebServerActivity::stopWebServer() {
|
void CrossPointWebServerActivity::stopWebServer() {
|
||||||
if (webServer && webServer->isRunning()) {
|
if (webServer && webServer->isRunning()) {
|
||||||
Serial.printf("[%lu] [WEBACT] Stopping web server...\n", millis());
|
LOG_DBG("WEBACT", "Stopping web server...");
|
||||||
webServer->stop();
|
webServer->stop();
|
||||||
Serial.printf("[%lu] [WEBACT] Web server stopped\n", millis());
|
LOG_DBG("WEBACT", "Web server stopped");
|
||||||
}
|
}
|
||||||
webServer.reset();
|
webServer.reset();
|
||||||
}
|
}
|
||||||
@@ -309,7 +309,7 @@ void CrossPointWebServerActivity::loop() {
|
|||||||
lastWifiCheck = millis();
|
lastWifiCheck = millis();
|
||||||
const wl_status_t wifiStatus = WiFi.status();
|
const wl_status_t wifiStatus = WiFi.status();
|
||||||
if (wifiStatus != WL_CONNECTED) {
|
if (wifiStatus != WL_CONNECTED) {
|
||||||
Serial.printf("[%lu] [WEBACT] WiFi disconnected! Status: %d\n", millis(), wifiStatus);
|
LOG_DBG("WEBACT", "WiFi disconnected! Status: %d", wifiStatus);
|
||||||
// Show error and exit gracefully
|
// Show error and exit gracefully
|
||||||
state = WebServerActivityState::SHUTTING_DOWN;
|
state = WebServerActivityState::SHUTTING_DOWN;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
@@ -318,7 +318,7 @@ void CrossPointWebServerActivity::loop() {
|
|||||||
// Log weak signal warnings
|
// Log weak signal warnings
|
||||||
const int rssi = WiFi.RSSI();
|
const int rssi = WiFi.RSSI();
|
||||||
if (rssi < -75) {
|
if (rssi < -75) {
|
||||||
Serial.printf("[%lu] [WEBACT] Warning: Weak WiFi signal: %d dBm\n", millis(), rssi);
|
LOG_DBG("WEBACT", "Warning: Weak WiFi signal: %d dBm", rssi);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -329,8 +329,7 @@ void CrossPointWebServerActivity::loop() {
|
|||||||
|
|
||||||
// Log if there's a significant gap between handleClient calls (>100ms)
|
// Log if there's a significant gap between handleClient calls (>100ms)
|
||||||
if (lastHandleClientTime > 0 && timeSinceLastHandleClient > 100) {
|
if (lastHandleClientTime > 0 && timeSinceLastHandleClient > 100) {
|
||||||
Serial.printf("[%lu] [WEBACT] WARNING: %lu ms gap since last handleClient\n", millis(),
|
LOG_DBG("WEBACT", "WARNING: %lu ms gap since last handleClient", timeSinceLastHandleClient);
|
||||||
timeSinceLastHandleClient);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset watchdog BEFORE processing - HTTP header parsing can be slow
|
// Reset watchdog BEFORE processing - HTTP header parsing can be slow
|
||||||
@@ -348,6 +347,9 @@ void CrossPointWebServerActivity::loop() {
|
|||||||
// Yield and check for exit button every 64 iterations
|
// Yield and check for exit button every 64 iterations
|
||||||
if ((i & 0x3F) == 0x3F) {
|
if ((i & 0x3F) == 0x3F) {
|
||||||
yield();
|
yield();
|
||||||
|
// Force trigger an update of which buttons are being pressed so be have accurate state
|
||||||
|
// for back button checking
|
||||||
|
mappedInput.update();
|
||||||
// Check for exit button inside loop for responsiveness
|
// Check for exit button inside loop for responsiveness
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
onGoBack();
|
onGoBack();
|
||||||
@@ -398,7 +400,7 @@ void drawQRCode(const GfxRenderer& renderer, const int x, const int y, const std
|
|||||||
// The structure to manage the QR code
|
// The structure to manage the QR code
|
||||||
QRCode qrcode;
|
QRCode qrcode;
|
||||||
uint8_t qrcodeBytes[qrcode_getBufferSize(4)];
|
uint8_t qrcodeBytes[qrcode_getBufferSize(4)];
|
||||||
Serial.printf("[%lu] [WEBACT] QR Code (%lu): %s\n", millis(), data.length(), data.c_str());
|
LOG_DBG("WEBACT", "QR Code (%lu): %s", data.length(), data.c_str());
|
||||||
|
|
||||||
qrcode_initText(&qrcode, qrcodeBytes, 4, ECC_LOW, data.c_str());
|
qrcode_initText(&qrcode, qrcodeBytes, 4, ECC_LOW, data.c_str());
|
||||||
const uint8_t px = 6; // pixels per module
|
const uint8_t px = 6; // pixels per module
|
||||||
|
|||||||
@@ -73,18 +73,15 @@ void NetworkModeSelectionActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle navigation
|
// Handle navigation
|
||||||
const bool prevPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
buttonNavigator.onNext([this] {
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left);
|
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEM_COUNT);
|
||||||
const bool nextPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
updateRequired = true;
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right);
|
});
|
||||||
|
|
||||||
if (prevPressed) {
|
buttonNavigator.onPrevious([this] {
|
||||||
selectedIndex = (selectedIndex + MENU_ITEM_COUNT - 1) % MENU_ITEM_COUNT;
|
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, MENU_ITEM_COUNT);
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (nextPressed) {
|
});
|
||||||
selectedIndex = (selectedIndex + 1) % MENU_ITEM_COUNT;
|
|
||||||
updateRequired = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void NetworkModeSelectionActivity::displayTaskLoop() {
|
void NetworkModeSelectionActivity::displayTaskLoop() {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
#include "../Activity.h"
|
#include "../Activity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
// Enum for network mode selection
|
// Enum for network mode selection
|
||||||
enum class NetworkMode { JOIN_NETWORK, CONNECT_CALIBRE, CREATE_HOTSPOT };
|
enum class NetworkMode { JOIN_NETWORK, CONNECT_CALIBRE, CREATE_HOTSPOT };
|
||||||
@@ -22,6 +23,8 @@ enum class NetworkMode { JOIN_NETWORK, CONNECT_CALIBRE, CREATE_HOTSPOT };
|
|||||||
class NetworkModeSelectionActivity final : public Activity {
|
class NetworkModeSelectionActivity final : public Activity {
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
|
|
||||||
int selectedIndex = 0;
|
int selectedIndex = 0;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
const std::function<void(NetworkMode)> onModeSelected;
|
const std::function<void(NetworkMode)> onModeSelected;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "WifiSelectionActivity.h"
|
#include "WifiSelectionActivity.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
#include <Logging.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
@@ -21,7 +22,8 @@ void WifiSelectionActivity::onEnter() {
|
|||||||
|
|
||||||
renderingMutex = xSemaphoreCreateMutex();
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
// Load saved WiFi credentials - SD card operations need lock as we use SPI for both
|
// Load saved WiFi credentials - SD card operations need lock as we use SPI
|
||||||
|
// for both
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
WIFI_STORE.loadFromFile();
|
WIFI_STORE.loadFromFile();
|
||||||
xSemaphoreGive(renderingMutex);
|
xSemaphoreGive(renderingMutex);
|
||||||
@@ -37,6 +39,7 @@ void WifiSelectionActivity::onEnter() {
|
|||||||
usedSavedPassword = false;
|
usedSavedPassword = false;
|
||||||
savePromptSelection = 0;
|
savePromptSelection = 0;
|
||||||
forgetPromptSelection = 0;
|
forgetPromptSelection = 0;
|
||||||
|
autoConnecting = false;
|
||||||
|
|
||||||
// Cache MAC address for display
|
// Cache MAC address for display
|
||||||
uint8_t mac[6];
|
uint8_t mac[6];
|
||||||
@@ -46,9 +49,7 @@ void WifiSelectionActivity::onEnter() {
|
|||||||
mac[5]);
|
mac[5]);
|
||||||
cachedMacAddress = std::string(macStr);
|
cachedMacAddress = std::string(macStr);
|
||||||
|
|
||||||
// Trigger first update to show scanning message
|
// Task creation
|
||||||
updateRequired = true;
|
|
||||||
|
|
||||||
xTaskCreate(&WifiSelectionActivity::taskTrampoline, "WifiSelectionTask",
|
xTaskCreate(&WifiSelectionActivity::taskTrampoline, "WifiSelectionTask",
|
||||||
4096, // Stack size (larger for WiFi operations)
|
4096, // Stack size (larger for WiFi operations)
|
||||||
this, // Parameters
|
this, // Parameters
|
||||||
@@ -56,46 +57,68 @@ void WifiSelectionActivity::onEnter() {
|
|||||||
&displayTaskHandle // Task handle
|
&displayTaskHandle // Task handle
|
||||||
);
|
);
|
||||||
|
|
||||||
// Start WiFi scan
|
// Attempt to auto-connect to the last network
|
||||||
|
if (allowAutoConnect) {
|
||||||
|
const std::string lastSsid = WIFI_STORE.getLastConnectedSsid();
|
||||||
|
if (!lastSsid.empty()) {
|
||||||
|
const auto* cred = WIFI_STORE.findCredential(lastSsid);
|
||||||
|
if (cred) {
|
||||||
|
LOG_DBG("WIFI", "Attempting to auto-connect to %s", lastSsid.c_str());
|
||||||
|
selectedSSID = cred->ssid;
|
||||||
|
enteredPassword = cred->password;
|
||||||
|
selectedRequiresPassword = !cred->password.empty();
|
||||||
|
usedSavedPassword = true;
|
||||||
|
autoConnecting = true;
|
||||||
|
attemptConnection();
|
||||||
|
updateRequired = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to scanning
|
||||||
startWifiScan();
|
startWifiScan();
|
||||||
}
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::onExit() {
|
void WifiSelectionActivity::onExit() {
|
||||||
Activity::onExit();
|
Activity::onExit();
|
||||||
|
|
||||||
Serial.printf("[%lu] [WIFI] [MEM] Free heap at onExit start: %d bytes\n", millis(), ESP.getFreeHeap());
|
LOG_DBG("WIFI] [MEM", "Free heap at onExit start: %d bytes", ESP.getFreeHeap());
|
||||||
|
|
||||||
// Stop any ongoing WiFi scan
|
// Stop any ongoing WiFi scan
|
||||||
Serial.printf("[%lu] [WIFI] Deleting WiFi scan...\n", millis());
|
LOG_DBG("WIFI", "Deleting WiFi scan...");
|
||||||
WiFi.scanDelete();
|
WiFi.scanDelete();
|
||||||
Serial.printf("[%lu] [WIFI] [MEM] Free heap after scanDelete: %d bytes\n", millis(), ESP.getFreeHeap());
|
LOG_DBG("WIFI] [MEM", "Free heap after scanDelete: %d bytes", ESP.getFreeHeap());
|
||||||
|
|
||||||
// Note: We do NOT disconnect WiFi here - the parent activity (CrossPointWebServerActivity)
|
// Note: We do NOT disconnect WiFi here - the parent activity
|
||||||
// manages WiFi connection state. We just clean up the scan and task.
|
// (CrossPointWebServerActivity) manages WiFi connection state. We just clean
|
||||||
|
// up the scan and task.
|
||||||
|
|
||||||
// Acquire mutex before deleting task to ensure task isn't using it
|
// Acquire mutex before deleting task to ensure task isn't using it
|
||||||
// This prevents hangs/crashes if the task holds the mutex when deleted
|
// This prevents hangs/crashes if the task holds the mutex when deleted
|
||||||
Serial.printf("[%lu] [WIFI] Acquiring rendering mutex before task deletion...\n", millis());
|
LOG_DBG("WIFI", "Acquiring rendering mutex before task deletion...");
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
|
||||||
// Delete the display task (we now hold the mutex, so task is blocked if it needs it)
|
// Delete the display task (we now hold the mutex, so task is blocked if it
|
||||||
Serial.printf("[%lu] [WIFI] Deleting display task...\n", millis());
|
// needs it)
|
||||||
|
LOG_DBG("WIFI", "Deleting display task...");
|
||||||
if (displayTaskHandle) {
|
if (displayTaskHandle) {
|
||||||
vTaskDelete(displayTaskHandle);
|
vTaskDelete(displayTaskHandle);
|
||||||
displayTaskHandle = nullptr;
|
displayTaskHandle = nullptr;
|
||||||
Serial.printf("[%lu] [WIFI] Display task deleted\n", millis());
|
LOG_DBG("WIFI", "Display task deleted");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now safe to delete the mutex since we own it
|
// Now safe to delete the mutex since we own it
|
||||||
Serial.printf("[%lu] [WIFI] Deleting mutex...\n", millis());
|
LOG_DBG("WIFI", "Deleting mutex...");
|
||||||
vSemaphoreDelete(renderingMutex);
|
vSemaphoreDelete(renderingMutex);
|
||||||
renderingMutex = nullptr;
|
renderingMutex = nullptr;
|
||||||
Serial.printf("[%lu] [WIFI] Mutex deleted\n", millis());
|
LOG_DBG("WIFI", "Mutex deleted");
|
||||||
|
|
||||||
Serial.printf("[%lu] [WIFI] [MEM] Free heap at onExit end: %d bytes\n", millis(), ESP.getFreeHeap());
|
LOG_DBG("WIFI] [MEM", "Free heap at onExit end: %d bytes", ESP.getFreeHeap());
|
||||||
}
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::startWifiScan() {
|
void WifiSelectionActivity::startWifiScan() {
|
||||||
|
autoConnecting = false;
|
||||||
state = WifiSelectionState::SCANNING;
|
state = WifiSelectionState::SCANNING;
|
||||||
networks.clear();
|
networks.clear();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
@@ -181,6 +204,7 @@ void WifiSelectionActivity::selectNetwork(const int index) {
|
|||||||
selectedRequiresPassword = network.isEncrypted;
|
selectedRequiresPassword = network.isEncrypted;
|
||||||
usedSavedPassword = false;
|
usedSavedPassword = false;
|
||||||
enteredPassword.clear();
|
enteredPassword.clear();
|
||||||
|
autoConnecting = false;
|
||||||
|
|
||||||
// Check if we have saved credentials for this network
|
// Check if we have saved credentials for this network
|
||||||
const auto* savedCred = WIFI_STORE.findCredential(selectedSSID);
|
const auto* savedCred = WIFI_STORE.findCredential(selectedSSID);
|
||||||
@@ -188,8 +212,7 @@ void WifiSelectionActivity::selectNetwork(const int index) {
|
|||||||
// Use saved password - connect directly
|
// Use saved password - connect directly
|
||||||
enteredPassword = savedCred->password;
|
enteredPassword = savedCred->password;
|
||||||
usedSavedPassword = true;
|
usedSavedPassword = true;
|
||||||
Serial.printf("[%lu] [WiFi] Using saved password for %s, length: %zu\n", millis(), selectedSSID.c_str(),
|
LOG_DBG("WiFi", "Using saved password for %s, length: %zu", selectedSSID.c_str(), enteredPassword.size());
|
||||||
enteredPassword.size());
|
|
||||||
attemptConnection();
|
attemptConnection();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -223,7 +246,7 @@ void WifiSelectionActivity::selectNetwork(const int index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::attemptConnection() {
|
void WifiSelectionActivity::attemptConnection() {
|
||||||
state = WifiSelectionState::CONNECTING;
|
state = autoConnecting ? WifiSelectionState::AUTO_CONNECTING : WifiSelectionState::CONNECTING;
|
||||||
connectionStartTime = millis();
|
connectionStartTime = millis();
|
||||||
connectedIP.clear();
|
connectedIP.clear();
|
||||||
connectionError.clear();
|
connectionError.clear();
|
||||||
@@ -239,7 +262,7 @@ void WifiSelectionActivity::attemptConnection() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::checkConnectionStatus() {
|
void WifiSelectionActivity::checkConnectionStatus() {
|
||||||
if (state != WifiSelectionState::CONNECTING) {
|
if (state != WifiSelectionState::CONNECTING && state != WifiSelectionState::AUTO_CONNECTING) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +274,13 @@ void WifiSelectionActivity::checkConnectionStatus() {
|
|||||||
char ipStr[16];
|
char ipStr[16];
|
||||||
snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
|
snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
|
||||||
connectedIP = ipStr;
|
connectedIP = ipStr;
|
||||||
|
autoConnecting = false;
|
||||||
|
|
||||||
|
// Save this as the last connected network - SD card operations need lock as
|
||||||
|
// we use SPI for both
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
WIFI_STORE.setLastConnectedSsid(selectedSSID);
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
|
||||||
// If we entered a new password, ask if user wants to save it
|
// If we entered a new password, ask if user wants to save it
|
||||||
// Otherwise, immediately complete so parent can start web server
|
// Otherwise, immediately complete so parent can start web server
|
||||||
@@ -260,7 +290,9 @@ void WifiSelectionActivity::checkConnectionStatus() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else {
|
} else {
|
||||||
// Using saved password or open network - complete immediately
|
// Using saved password or open network - complete immediately
|
||||||
Serial.printf("[%lu] [WIFI] Connected with saved/open credentials, completing immediately\n", millis());
|
LOG_DBG("WIFI",
|
||||||
|
"Connected with saved/open credentials, "
|
||||||
|
"completing immediately");
|
||||||
onComplete(true);
|
onComplete(true);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -299,7 +331,7 @@ void WifiSelectionActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check connection progress
|
// Check connection progress
|
||||||
if (state == WifiSelectionState::CONNECTING) {
|
if (state == WifiSelectionState::CONNECTING || state == WifiSelectionState::AUTO_CONNECTING) {
|
||||||
checkConnectionStatus();
|
checkConnectionStatus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -368,17 +400,16 @@ void WifiSelectionActivity::loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Go back to network list (whether Cancel or Forget network was selected)
|
// Go back to network list (whether Cancel or Forget network was selected)
|
||||||
state = WifiSelectionState::NETWORK_LIST;
|
startWifiScan();
|
||||||
updateRequired = true;
|
|
||||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
} else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
// Skip forgetting, go back to network list
|
// Skip forgetting, go back to network list
|
||||||
state = WifiSelectionState::NETWORK_LIST;
|
startWifiScan();
|
||||||
updateRequired = true;
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle connected state (should not normally be reached - connection completes immediately)
|
// Handle connected state (should not normally be reached - connection
|
||||||
|
// completes immediately)
|
||||||
if (state == WifiSelectionState::CONNECTED) {
|
if (state == WifiSelectionState::CONNECTED) {
|
||||||
// Safety fallback - immediately complete
|
// Safety fallback - immediately complete
|
||||||
onComplete(true);
|
onComplete(true);
|
||||||
@@ -389,12 +420,14 @@ void WifiSelectionActivity::loop() {
|
|||||||
if (state == WifiSelectionState::CONNECTION_FAILED) {
|
if (state == WifiSelectionState::CONNECTION_FAILED) {
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back) ||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back) ||
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||||
// If we used saved credentials, offer to forget the network
|
// If we were auto-connecting or using a saved credential, offer to forget
|
||||||
if (usedSavedPassword) {
|
// the network
|
||||||
|
if (autoConnecting || usedSavedPassword) {
|
||||||
|
autoConnecting = false;
|
||||||
state = WifiSelectionState::FORGET_PROMPT;
|
state = WifiSelectionState::FORGET_PROMPT;
|
||||||
forgetPromptSelection = 0; // Default to "Cancel"
|
forgetPromptSelection = 0; // Default to "Cancel"
|
||||||
} else {
|
} else {
|
||||||
// Go back to network list on failure
|
// Go back to network list on failure for non-saved credentials
|
||||||
state = WifiSelectionState::NETWORK_LIST;
|
state = WifiSelectionState::NETWORK_LIST;
|
||||||
}
|
}
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
@@ -420,20 +453,33 @@ void WifiSelectionActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle UP/DOWN navigation
|
if (mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
startWifiScan();
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
return;
|
||||||
if (selectedNetworkIndex > 0) {
|
}
|
||||||
selectedNetworkIndex--;
|
|
||||||
|
const bool leftPressed = mappedInput.wasPressed(MappedInputManager::Button::Left);
|
||||||
|
if (leftPressed) {
|
||||||
|
const bool hasSavedPassword = !networks.empty() && networks[selectedNetworkIndex].hasSavedPassword;
|
||||||
|
if (hasSavedPassword) {
|
||||||
|
selectedSSID = networks[selectedNetworkIndex].ssid;
|
||||||
|
state = WifiSelectionState::FORGET_PROMPT;
|
||||||
|
forgetPromptSelection = 0; // Default to "Cancel"
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
}
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
|
||||||
if (!networks.empty() && selectedNetworkIndex < static_cast<int>(networks.size()) - 1) {
|
// Handle navigation
|
||||||
selectedNetworkIndex++;
|
buttonNavigator.onNext([this] {
|
||||||
|
selectedNetworkIndex = ButtonNavigator::nextIndex(selectedNetworkIndex, networks.size());
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
buttonNavigator.onPrevious([this] {
|
||||||
|
selectedNetworkIndex = ButtonNavigator::previousIndex(selectedNetworkIndex, networks.size());
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,6 +529,9 @@ void WifiSelectionActivity::render() const {
|
|||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
|
case WifiSelectionState::AUTO_CONNECTING:
|
||||||
|
renderConnecting();
|
||||||
|
break;
|
||||||
case WifiSelectionState::SCANNING:
|
case WifiSelectionState::SCANNING:
|
||||||
renderConnecting(); // Reuse connecting screen with different message
|
renderConnecting(); // Reuse connecting screen with different message
|
||||||
break;
|
break;
|
||||||
@@ -586,7 +635,11 @@ 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", "", "");
|
|
||||||
|
const bool hasSavedPassword = !networks.empty() && networks[selectedNetworkIndex].hasSavedPassword;
|
||||||
|
const char* forgetLabel = hasSavedPassword ? "Forget" : "";
|
||||||
|
|
||||||
|
const auto labels = mappedInput.mapLabels("« Back", "Connect", forgetLabel, "Refresh");
|
||||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,8 +743,7 @@ void WifiSelectionActivity::renderForgetPrompt() const {
|
|||||||
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
|
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
|
||||||
const auto top = (pageHeight - height * 3) / 2;
|
const auto top = (pageHeight - height * 3) / 2;
|
||||||
|
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Connection Failed", true, EpdFontFamily::BOLD);
|
renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Forget Network", true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
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.replace(25, ssidInfo.length() - 25, "...");
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
// Structure to hold WiFi network information
|
// Structure to hold WiFi network information
|
||||||
struct WifiNetworkInfo {
|
struct WifiNetworkInfo {
|
||||||
@@ -21,6 +22,7 @@ struct WifiNetworkInfo {
|
|||||||
|
|
||||||
// WiFi selection states
|
// WiFi selection states
|
||||||
enum class WifiSelectionState {
|
enum class WifiSelectionState {
|
||||||
|
AUTO_CONNECTING, // Trying to connect to the last known network
|
||||||
SCANNING, // Scanning for networks
|
SCANNING, // Scanning for networks
|
||||||
NETWORK_LIST, // Displaying available networks
|
NETWORK_LIST, // Displaying available networks
|
||||||
PASSWORD_ENTRY, // Entering password for selected network
|
PASSWORD_ENTRY, // Entering password for selected network
|
||||||
@@ -45,6 +47,7 @@ enum class WifiSelectionState {
|
|||||||
class WifiSelectionActivity final : public ActivityWithSubactivity {
|
class WifiSelectionActivity final : public ActivityWithSubactivity {
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
WifiSelectionState state = WifiSelectionState::SCANNING;
|
WifiSelectionState state = WifiSelectionState::SCANNING;
|
||||||
int selectedNetworkIndex = 0;
|
int selectedNetworkIndex = 0;
|
||||||
@@ -68,6 +71,12 @@ class WifiSelectionActivity final : public ActivityWithSubactivity {
|
|||||||
// Whether network was connected using a saved password (skip save prompt)
|
// Whether network was connected using a saved password (skip save prompt)
|
||||||
bool usedSavedPassword = false;
|
bool usedSavedPassword = false;
|
||||||
|
|
||||||
|
// Whether to attempt auto-connect on entry
|
||||||
|
const bool allowAutoConnect;
|
||||||
|
|
||||||
|
// Whether we are attempting to auto-connect
|
||||||
|
bool autoConnecting = false;
|
||||||
|
|
||||||
// Save/forget prompt selection (0 = Yes, 1 = No)
|
// Save/forget prompt selection (0 = Yes, 1 = No)
|
||||||
int savePromptSelection = 0;
|
int savePromptSelection = 0;
|
||||||
int forgetPromptSelection = 0;
|
int forgetPromptSelection = 0;
|
||||||
@@ -96,8 +105,10 @@ class WifiSelectionActivity final : public ActivityWithSubactivity {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
explicit WifiSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
explicit WifiSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
const std::function<void(bool connected)>& onComplete)
|
const std::function<void(bool connected)>& onComplete, bool autoConnect = true)
|
||||||
: ActivityWithSubactivity("WifiSelection", renderer, mappedInput), onComplete(onComplete) {}
|
: ActivityWithSubactivity("WifiSelection", renderer, mappedInput),
|
||||||
|
onComplete(onComplete),
|
||||||
|
allowAutoConnect(autoConnect) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
#include <Epub/Page.h>
|
#include <Epub/Page.h>
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
@@ -77,14 +78,14 @@ void EpubReaderActivity::onEnter() {
|
|||||||
epub->setupCacheDir();
|
epub->setupCacheDir();
|
||||||
|
|
||||||
FsFile f;
|
FsFile f;
|
||||||
if (SdMan.openFileForRead("ERS", epub->getCachePath() + "/progress.bin", f)) {
|
if (Storage.openFileForRead("ERS", epub->getCachePath() + "/progress.bin", f)) {
|
||||||
uint8_t data[6];
|
uint8_t data[6];
|
||||||
int dataSize = f.read(data, 6);
|
int dataSize = f.read(data, 6);
|
||||||
if (dataSize == 4 || dataSize == 6) {
|
if (dataSize == 4 || dataSize == 6) {
|
||||||
currentSpineIndex = data[0] + (data[1] << 8);
|
currentSpineIndex = data[0] + (data[1] << 8);
|
||||||
nextPageNumber = data[2] + (data[3] << 8);
|
nextPageNumber = data[2] + (data[3] << 8);
|
||||||
cachedSpineIndex = currentSpineIndex;
|
cachedSpineIndex = currentSpineIndex;
|
||||||
Serial.printf("[%lu] [ERS] Loaded cache: %d, %d\n", millis(), currentSpineIndex, nextPageNumber);
|
LOG_DBG("ERS", "Loaded cache: %d, %d", currentSpineIndex, nextPageNumber);
|
||||||
}
|
}
|
||||||
if (dataSize == 6) {
|
if (dataSize == 6) {
|
||||||
cachedChapterTotalPageCount = data[4] + (data[5] << 8);
|
cachedChapterTotalPageCount = data[4] + (data[5] << 8);
|
||||||
@@ -97,8 +98,7 @@ void EpubReaderActivity::onEnter() {
|
|||||||
int textSpineIndex = epub->getSpineIndexForTextReference();
|
int textSpineIndex = epub->getSpineIndexForTextReference();
|
||||||
if (textSpineIndex != 0) {
|
if (textSpineIndex != 0) {
|
||||||
currentSpineIndex = textSpineIndex;
|
currentSpineIndex = textSpineIndex;
|
||||||
Serial.printf("[%lu] [ERS] Opened for first time, navigating to text reference at index %d\n", millis(),
|
LOG_DBG("ERS", "Opened for first time, navigating to text reference at index %d", textSpineIndex);
|
||||||
textSpineIndex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,15 +204,15 @@ void EpubReaderActivity::loop() {
|
|||||||
xSemaphoreGive(renderingMutex);
|
xSemaphoreGive(renderingMutex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Long press BACK (1s+) goes directly to home
|
// Long press BACK (1s+) goes to file selection
|
||||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) {
|
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) {
|
||||||
onGoHome();
|
onGoBack();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Short press BACK goes to file selection
|
// Short press BACK goes directly to home
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) {
|
||||||
onGoBack();
|
onGoHome();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,7 +567,7 @@ void EpubReaderActivity::renderScreen() {
|
|||||||
|
|
||||||
if (!section) {
|
if (!section) {
|
||||||
const auto filepath = epub->getSpineItem(currentSpineIndex).href;
|
const auto filepath = epub->getSpineItem(currentSpineIndex).href;
|
||||||
Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex);
|
LOG_DBG("ERS", "Loading file: %s, index: %d", filepath.c_str(), currentSpineIndex);
|
||||||
section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
|
section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
|
||||||
|
|
||||||
const uint16_t viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
|
const uint16_t viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
|
||||||
@@ -576,19 +576,19 @@ void EpubReaderActivity::renderScreen() {
|
|||||||
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||||
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle)) {
|
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle)) {
|
||||||
Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis());
|
LOG_DBG("ERS", "Cache not found, building...");
|
||||||
|
|
||||||
const auto popupFn = [this]() { GUI.drawPopup(renderer, "Indexing..."); };
|
const auto popupFn = [this]() { GUI.drawPopup(renderer, "Indexing..."); };
|
||||||
|
|
||||||
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||||
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, popupFn)) {
|
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, popupFn)) {
|
||||||
Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis());
|
LOG_ERR("ERS", "Failed to persist page data to SD");
|
||||||
section.reset();
|
section.reset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [ERS] Cache found, skipping build...\n", millis());
|
LOG_DBG("ERS", "Cache found, skipping build...");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextPageNumber == UINT16_MAX) {
|
if (nextPageNumber == UINT16_MAX) {
|
||||||
@@ -622,7 +622,7 @@ void EpubReaderActivity::renderScreen() {
|
|||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
|
|
||||||
if (section->pageCount == 0) {
|
if (section->pageCount == 0) {
|
||||||
Serial.printf("[%lu] [ERS] No pages to render\n", millis());
|
LOG_DBG("ERS", "No pages to render");
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Empty chapter", true, EpdFontFamily::BOLD);
|
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Empty chapter", true, EpdFontFamily::BOLD);
|
||||||
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
@@ -630,7 +630,7 @@ void EpubReaderActivity::renderScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (section->currentPage < 0 || section->currentPage >= section->pageCount) {
|
if (section->currentPage < 0 || section->currentPage >= section->pageCount) {
|
||||||
Serial.printf("[%lu] [ERS] Page out of bounds: %d (max %d)\n", millis(), section->currentPage, section->pageCount);
|
LOG_DBG("ERS", "Page out of bounds: %d (max %d)", section->currentPage, section->pageCount);
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Out of bounds", true, EpdFontFamily::BOLD);
|
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Out of bounds", true, EpdFontFamily::BOLD);
|
||||||
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
@@ -640,21 +640,21 @@ void EpubReaderActivity::renderScreen() {
|
|||||||
{
|
{
|
||||||
auto p = section->loadPageFromSectionFile();
|
auto p = section->loadPageFromSectionFile();
|
||||||
if (!p) {
|
if (!p) {
|
||||||
Serial.printf("[%lu] [ERS] Failed to load page from SD - clearing section cache\n", millis());
|
LOG_ERR("ERS", "Failed to load page from SD - clearing section cache");
|
||||||
section->clearCache();
|
section->clearCache();
|
||||||
section.reset();
|
section.reset();
|
||||||
return renderScreen();
|
return renderScreen();
|
||||||
}
|
}
|
||||||
const auto start = millis();
|
const auto start = millis();
|
||||||
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
||||||
Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start);
|
LOG_DBG("ERS", "Rendered page in %dms", millis() - start);
|
||||||
}
|
}
|
||||||
saveProgress(currentSpineIndex, section->currentPage, section->pageCount);
|
saveProgress(currentSpineIndex, section->currentPage, section->pageCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) {
|
void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) {
|
||||||
FsFile f;
|
FsFile f;
|
||||||
if (SdMan.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) {
|
if (Storage.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) {
|
||||||
uint8_t data[6];
|
uint8_t data[6];
|
||||||
data[0] = currentSpineIndex & 0xFF;
|
data[0] = currentSpineIndex & 0xFF;
|
||||||
data[1] = (currentSpineIndex >> 8) & 0xFF;
|
data[1] = (currentSpineIndex >> 8) & 0xFF;
|
||||||
@@ -664,9 +664,9 @@ void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageC
|
|||||||
data[5] = (pageCount >> 8) & 0xFF;
|
data[5] = (pageCount >> 8) & 0xFF;
|
||||||
f.write(data, 6);
|
f.write(data, 6);
|
||||||
f.close();
|
f.close();
|
||||||
Serial.printf("[ERS] Progress saved: Chapter %d, Page %d\n", spineIndex, currentPage);
|
LOG_DBG("ERS", "Progress saved: Chapter %d, Page %d", spineIndex, currentPage);
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[ERS] Could not save progress!\n");
|
LOG_ERR("ERS", "Could not save progress!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int orientedMarginTop,
|
void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int orientedMarginTop,
|
||||||
|
|||||||
@@ -6,11 +6,6 @@
|
|||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
namespace {
|
|
||||||
// Time threshold for treating a long press as a page-up/page-down
|
|
||||||
constexpr int SKIP_PAGE_MS = 700;
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
int EpubReaderChapterSelectionActivity::getTotalItems() const { return epub->getTocItemsCount(); }
|
int EpubReaderChapterSelectionActivity::getTotalItems() const { return epub->getTocItemsCount(); }
|
||||||
|
|
||||||
int EpubReaderChapterSelectionActivity::getPageItems() const {
|
int EpubReaderChapterSelectionActivity::getPageItems() const {
|
||||||
@@ -77,12 +72,6 @@ void EpubReaderChapterSelectionActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Left);
|
|
||||||
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Right);
|
|
||||||
|
|
||||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
|
||||||
const int pageItems = getPageItems();
|
const int pageItems = getPageItems();
|
||||||
const int totalItems = getTotalItems();
|
const int totalItems = getTotalItems();
|
||||||
|
|
||||||
@@ -95,21 +84,27 @@ void EpubReaderChapterSelectionActivity::loop() {
|
|||||||
}
|
}
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
onGoBack();
|
onGoBack();
|
||||||
} else if (prevReleased) {
|
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + totalItems) % totalItems;
|
|
||||||
} else {
|
|
||||||
selectorIndex = (selectorIndex + totalItems - 1) % totalItems;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buttonNavigator.onNextRelease([this, totalItems] {
|
||||||
|
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (nextReleased) {
|
});
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % totalItems;
|
buttonNavigator.onPreviousRelease([this, totalItems] {
|
||||||
} else {
|
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
|
||||||
selectorIndex = (selectorIndex + 1) % totalItems;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onNextContinuous([this, totalItems, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderChapterSelectionActivity::displayTaskLoop() {
|
void EpubReaderChapterSelectionActivity::displayTaskLoop() {
|
||||||
|
|||||||
@@ -7,12 +7,14 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "../ActivityWithSubactivity.h"
|
#include "../ActivityWithSubactivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity {
|
class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity {
|
||||||
std::shared_ptr<Epub> epub;
|
std::shared_ptr<Epub> epub;
|
||||||
std::string epubPath;
|
std::string epubPath;
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
int currentSpineIndex = 0;
|
int currentSpineIndex = 0;
|
||||||
int currentPage = 0;
|
int currentPage = 0;
|
||||||
int totalPagesInSpine = 0;
|
int totalPagesInSpine = 0;
|
||||||
|
|||||||
@@ -48,16 +48,19 @@ void EpubReaderMenuActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle navigation
|
||||||
|
buttonNavigator.onNext([this] {
|
||||||
|
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPrevious([this] {
|
||||||
|
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
// Use local variables for items we need to check after potential deletion
|
// Use local variables for items we need to check after potential deletion
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Up) ||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Left)) {
|
|
||||||
selectedIndex = (selectedIndex + menuItems.size() - 1) % menuItems.size();
|
|
||||||
updateRequired = true;
|
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
|
||||||
selectedIndex = (selectedIndex + 1) % menuItems.size();
|
|
||||||
updateRequired = true;
|
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
|
||||||
const auto selectedAction = menuItems[selectedIndex].action;
|
const auto selectedAction = menuItems[selectedIndex].action;
|
||||||
if (selectedAction == MenuAction::ROTATE_SCREEN) {
|
if (selectedAction == MenuAction::ROTATE_SCREEN) {
|
||||||
// Cycle orientation preview locally; actual rotation happens on menu exit.
|
// Cycle orientation preview locally; actual rotation happens on menu exit.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "../ActivityWithSubactivity.h"
|
#include "../ActivityWithSubactivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
||||||
public:
|
public:
|
||||||
@@ -48,6 +49,7 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
|||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
std::string title = "Reader Menu";
|
std::string title = "Reader Menu";
|
||||||
uint8_t pendingOrientation = 0;
|
uint8_t pendingOrientation = 0;
|
||||||
const std::vector<const char*> orientationLabels = {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"};
|
const std::vector<const char*> orientationLabels = {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"};
|
||||||
|
|||||||
@@ -79,25 +79,11 @@ void EpubReaderPercentSelectionActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Left)) {
|
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { adjustPercent(-kSmallStep); });
|
||||||
adjustPercent(-kSmallStep);
|
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { adjustPercent(kSmallStep); });
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this] { adjustPercent(kLargeStep); });
|
||||||
adjustPercent(kSmallStep);
|
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] { adjustPercent(-kLargeStep); });
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Up)) {
|
|
||||||
adjustPercent(kLargeStep);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Down)) {
|
|
||||||
adjustPercent(-kLargeStep);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderPercentSelectionActivity::renderScreen() {
|
void EpubReaderPercentSelectionActivity::renderScreen() {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
class EpubReaderPercentSelectionActivity final : public ActivityWithSubactivity {
|
class EpubReaderPercentSelectionActivity final : public ActivityWithSubactivity {
|
||||||
public:
|
public:
|
||||||
@@ -31,6 +32,7 @@ class EpubReaderPercentSelectionActivity final : public ActivityWithSubactivity
|
|||||||
// FreeRTOS task and mutex for rendering.
|
// FreeRTOS task and mutex for rendering.
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
|
|
||||||
// Callback invoked when the user confirms a percent.
|
// Callback invoked when the user confirms a percent.
|
||||||
const std::function<void(int)> onSelect;
|
const std::function<void(int)> onSelect;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "KOReaderSyncActivity.h"
|
#include "KOReaderSyncActivity.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
#include <Logging.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <esp_sntp.h>
|
#include <esp_sntp.h>
|
||||||
|
|
||||||
@@ -32,9 +33,9 @@ void syncTimeWithNTP() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (retry < maxRetries) {
|
if (retry < maxRetries) {
|
||||||
Serial.printf("[%lu] [KOSync] NTP time synced\n", millis());
|
LOG_DBG("KOSync", "NTP time synced");
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [KOSync] NTP sync timeout, using fallback\n", millis());
|
LOG_DBG("KOSync", "NTP sync timeout, using fallback");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -48,12 +49,12 @@ void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
|
|||||||
exitActivity();
|
exitActivity();
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Serial.printf("[%lu] [KOSync] WiFi connection failed, exiting\n", millis());
|
LOG_DBG("KOSync", "WiFi connection failed, exiting");
|
||||||
onCancel();
|
onCancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [KOSync] WiFi connected, starting sync\n", millis());
|
LOG_DBG("KOSync", "WiFi connected, starting sync");
|
||||||
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
state = SYNCING;
|
state = SYNCING;
|
||||||
@@ -88,7 +89,7 @@ void KOReaderSyncActivity::performSync() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [KOSync] Document hash: %s\n", millis(), documentHash.c_str());
|
LOG_DBG("KOSync", "Document hash: %s", documentHash.c_str());
|
||||||
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
statusMessage = "Fetching remote progress...";
|
statusMessage = "Fetching remote progress...";
|
||||||
@@ -188,12 +189,12 @@ void KOReaderSyncActivity::onEnter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Turn on WiFi
|
// Turn on WiFi
|
||||||
Serial.printf("[%lu] [KOSync] Turning on WiFi...\n", millis());
|
LOG_DBG("KOSync", "Turning on WiFi...");
|
||||||
WiFi.mode(WIFI_STA);
|
WiFi.mode(WIFI_STA);
|
||||||
|
|
||||||
// Check if already connected
|
// Check if already connected
|
||||||
if (WiFi.status() == WL_CONNECTED) {
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
Serial.printf("[%lu] [KOSync] Already connected to WiFi\n", millis());
|
LOG_DBG("KOSync", "Already connected to WiFi");
|
||||||
state = SYNCING;
|
state = SYNCING;
|
||||||
statusMessage = "Syncing time...";
|
statusMessage = "Syncing time...";
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
@@ -216,7 +217,7 @@ void KOReaderSyncActivity::onEnter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Launch WiFi selection subactivity
|
// Launch WiFi selection subactivity
|
||||||
Serial.printf("[%lu] [KOSync] Launching WifiSelectionActivity...\n", millis());
|
LOG_DBG("KOSync", "Launching WifiSelectionActivity...");
|
||||||
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
||||||
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
||||||
}
|
}
|
||||||
@@ -317,7 +318,6 @@ void KOReaderSyncActivity::render() {
|
|||||||
localProgress.percentage * 100);
|
localProgress.percentage * 100);
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, 320, localPageStr);
|
renderer.drawText(UI_10_FONT_ID, 20, 320, localPageStr);
|
||||||
|
|
||||||
// Options
|
|
||||||
const int optionY = 350;
|
const int optionY = 350;
|
||||||
const int optionHeight = 30;
|
const int optionHeight = 30;
|
||||||
|
|
||||||
@@ -333,13 +333,8 @@ void KOReaderSyncActivity::render() {
|
|||||||
}
|
}
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, optionY + optionHeight, "Upload local progress", selectedOption != 1);
|
renderer.drawText(UI_10_FONT_ID, 20, optionY + optionHeight, "Upload local progress", selectedOption != 1);
|
||||||
|
|
||||||
// Cancel option
|
// Bottom button hints: show Back and Select
|
||||||
if (selectedOption == 2) {
|
const auto labels = mappedInput.mapLabels("Back", "Select", "", "");
|
||||||
renderer.fillRect(0, optionY + optionHeight * 2 - 2, pageWidth - 1, optionHeight);
|
|
||||||
}
|
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, optionY + optionHeight * 2, "Cancel", selectedOption != 2);
|
|
||||||
|
|
||||||
const auto labels = mappedInput.mapLabels("", "Select", "", "");
|
|
||||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
return;
|
return;
|
||||||
@@ -349,7 +344,7 @@ void KOReaderSyncActivity::render() {
|
|||||||
renderer.drawCenteredText(UI_10_FONT_ID, 280, "No remote progress found", true, EpdFontFamily::BOLD);
|
renderer.drawCenteredText(UI_10_FONT_ID, 280, "No remote progress found", true, EpdFontFamily::BOLD);
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, 320, "Upload current position?");
|
renderer.drawCenteredText(UI_10_FONT_ID, 320, "Upload current position?");
|
||||||
|
|
||||||
const auto labels = mappedInput.mapLabels("Cancel", "Upload", "", "");
|
const auto labels = mappedInput.mapLabels("Back", "Upload", "", "");
|
||||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
return;
|
return;
|
||||||
@@ -392,11 +387,11 @@ void KOReaderSyncActivity::loop() {
|
|||||||
// Navigate options
|
// Navigate options
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
||||||
selectedOption = (selectedOption + 2) % 3; // Wrap around
|
selectedOption = (selectedOption + 1) % 2; // Wrap around among 2 options
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
||||||
selectedOption = (selectedOption + 1) % 3;
|
selectedOption = (selectedOption + 1) % 2; // Wrap around among 2 options
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,9 +402,6 @@ void KOReaderSyncActivity::loop() {
|
|||||||
} else if (selectedOption == 1) {
|
} else if (selectedOption == 1) {
|
||||||
// Upload local progress
|
// Upload local progress
|
||||||
performUpload();
|
performUpload();
|
||||||
} else {
|
|
||||||
// Cancel
|
|
||||||
onCancel();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
* 1. Connect to WiFi (if not connected)
|
* 1. Connect to WiFi (if not connected)
|
||||||
* 2. Calculate document hash
|
* 2. Calculate document hash
|
||||||
* 3. Fetch remote progress
|
* 3. Fetch remote progress
|
||||||
* 4. Show comparison and options (Apply/Upload/Cancel)
|
* 4. Show comparison and options (Apply/Upload)
|
||||||
* 5. Apply or upload progress
|
* 5. Apply or upload progress
|
||||||
*/
|
*/
|
||||||
class KOReaderSyncActivity final : public ActivityWithSubactivity {
|
class KOReaderSyncActivity final : public ActivityWithSubactivity {
|
||||||
@@ -82,7 +82,7 @@ class KOReaderSyncActivity final : public ActivityWithSubactivity {
|
|||||||
// Local progress as KOReader format (for display)
|
// Local progress as KOReader format (for display)
|
||||||
KOReaderPosition localProgress;
|
KOReaderPosition localProgress;
|
||||||
|
|
||||||
// Selection in result screen (0=Apply, 1=Upload, 2=Cancel)
|
// Selection in result screen (0=Apply, 1=Upload)
|
||||||
int selectedOption = 0;
|
int selectedOption = 0;
|
||||||
|
|
||||||
OnCancelCallback onCancel;
|
OnCancelCallback onCancel;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "ReaderActivity.h"
|
#include "ReaderActivity.h"
|
||||||
|
|
||||||
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include "Epub.h"
|
#include "Epub.h"
|
||||||
#include "EpubReaderActivity.h"
|
#include "EpubReaderActivity.h"
|
||||||
#include "Txt.h"
|
#include "Txt.h"
|
||||||
@@ -27,8 +29,8 @@ bool ReaderActivity::isTxtFile(const std::string& path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Epub> ReaderActivity::loadEpub(const std::string& path) {
|
std::unique_ptr<Epub> ReaderActivity::loadEpub(const std::string& path) {
|
||||||
if (!SdMan.exists(path.c_str())) {
|
if (!Storage.exists(path.c_str())) {
|
||||||
Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str());
|
LOG_ERR("READER", "File does not exist: %s", path.c_str());
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,13 +39,13 @@ std::unique_ptr<Epub> ReaderActivity::loadEpub(const std::string& path) {
|
|||||||
return epub;
|
return epub;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [ ] Failed to load epub\n", millis());
|
LOG_ERR("READER", "Failed to load epub");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Xtc> ReaderActivity::loadXtc(const std::string& path) {
|
std::unique_ptr<Xtc> ReaderActivity::loadXtc(const std::string& path) {
|
||||||
if (!SdMan.exists(path.c_str())) {
|
if (!Storage.exists(path.c_str())) {
|
||||||
Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str());
|
LOG_ERR("READER", "File does not exist: %s", path.c_str());
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,13 +54,13 @@ std::unique_ptr<Xtc> ReaderActivity::loadXtc(const std::string& path) {
|
|||||||
return xtc;
|
return xtc;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [ ] Failed to load XTC\n", millis());
|
LOG_ERR("READER", "Failed to load XTC");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Txt> ReaderActivity::loadTxt(const std::string& path) {
|
std::unique_ptr<Txt> ReaderActivity::loadTxt(const std::string& path) {
|
||||||
if (!SdMan.exists(path.c_str())) {
|
if (!Storage.exists(path.c_str())) {
|
||||||
Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str());
|
LOG_ERR("READER", "File does not exist: %s", path.c_str());
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +69,7 @@ std::unique_ptr<Txt> ReaderActivity::loadTxt(const std::string& path) {
|
|||||||
return txt;
|
return txt;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [ ] Failed to load TXT\n", millis());
|
LOG_ERR("READER", "Failed to load TXT");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#include "TxtReaderActivity.h"
|
#include "TxtReaderActivity.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
#include <Utf8.h>
|
#include <Utf8.h>
|
||||||
|
|
||||||
@@ -102,15 +102,15 @@ void TxtReaderActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Long press BACK (1s+) goes directly to home
|
// Long press BACK (1s+) goes to file selection
|
||||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) {
|
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) {
|
||||||
onGoHome();
|
onGoBack();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Short press BACK goes to file selection
|
// Short press BACK goes directly to home
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) {
|
||||||
onGoBack();
|
onGoHome();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,8 +191,7 @@ void TxtReaderActivity::initializeReader() {
|
|||||||
linesPerPage = viewportHeight / lineHeight;
|
linesPerPage = viewportHeight / lineHeight;
|
||||||
if (linesPerPage < 1) linesPerPage = 1;
|
if (linesPerPage < 1) linesPerPage = 1;
|
||||||
|
|
||||||
Serial.printf("[%lu] [TRS] Viewport: %dx%d, lines per page: %d\n", millis(), viewportWidth, viewportHeight,
|
LOG_DBG("TRS", "Viewport: %dx%d, lines per page: %d", viewportWidth, viewportHeight, linesPerPage);
|
||||||
linesPerPage);
|
|
||||||
|
|
||||||
// Try to load cached page index first
|
// Try to load cached page index first
|
||||||
if (!loadPageIndexCache()) {
|
if (!loadPageIndexCache()) {
|
||||||
@@ -215,7 +214,7 @@ void TxtReaderActivity::buildPageIndex() {
|
|||||||
size_t offset = 0;
|
size_t offset = 0;
|
||||||
const size_t fileSize = txt->getFileSize();
|
const size_t fileSize = txt->getFileSize();
|
||||||
|
|
||||||
Serial.printf("[%lu] [TRS] Building page index for %zu bytes...\n", millis(), fileSize);
|
LOG_DBG("TRS", "Building page index for %zu bytes...", fileSize);
|
||||||
|
|
||||||
GUI.drawPopup(renderer, "Indexing...");
|
GUI.drawPopup(renderer, "Indexing...");
|
||||||
|
|
||||||
@@ -244,7 +243,7 @@ void TxtReaderActivity::buildPageIndex() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
totalPages = pageOffsets.size();
|
totalPages = pageOffsets.size();
|
||||||
Serial.printf("[%lu] [TRS] Built page index: %d pages\n", millis(), totalPages);
|
LOG_DBG("TRS", "Built page index: %d pages", totalPages);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector<std::string>& outLines, size_t& nextOffset) {
|
bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector<std::string>& outLines, size_t& nextOffset) {
|
||||||
@@ -259,7 +258,7 @@ bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector<std::string>
|
|||||||
size_t chunkSize = std::min(CHUNK_SIZE, fileSize - offset);
|
size_t chunkSize = std::min(CHUNK_SIZE, fileSize - offset);
|
||||||
auto* buffer = static_cast<uint8_t*>(malloc(chunkSize + 1));
|
auto* buffer = static_cast<uint8_t*>(malloc(chunkSize + 1));
|
||||||
if (!buffer) {
|
if (!buffer) {
|
||||||
Serial.printf("[%lu] [TRS] Failed to allocate %zu bytes\n", millis(), chunkSize);
|
LOG_ERR("TRS", "Failed to allocate %zu bytes", chunkSize);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,7 +564,7 @@ void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int
|
|||||||
|
|
||||||
void TxtReaderActivity::saveProgress() const {
|
void TxtReaderActivity::saveProgress() const {
|
||||||
FsFile f;
|
FsFile f;
|
||||||
if (SdMan.openFileForWrite("TRS", txt->getCachePath() + "/progress.bin", f)) {
|
if (Storage.openFileForWrite("TRS", txt->getCachePath() + "/progress.bin", f)) {
|
||||||
uint8_t data[4];
|
uint8_t data[4];
|
||||||
data[0] = currentPage & 0xFF;
|
data[0] = currentPage & 0xFF;
|
||||||
data[1] = (currentPage >> 8) & 0xFF;
|
data[1] = (currentPage >> 8) & 0xFF;
|
||||||
@@ -578,7 +577,7 @@ void TxtReaderActivity::saveProgress() const {
|
|||||||
|
|
||||||
void TxtReaderActivity::loadProgress() {
|
void TxtReaderActivity::loadProgress() {
|
||||||
FsFile f;
|
FsFile f;
|
||||||
if (SdMan.openFileForRead("TRS", txt->getCachePath() + "/progress.bin", f)) {
|
if (Storage.openFileForRead("TRS", txt->getCachePath() + "/progress.bin", f)) {
|
||||||
uint8_t data[4];
|
uint8_t data[4];
|
||||||
if (f.read(data, 4) == 4) {
|
if (f.read(data, 4) == 4) {
|
||||||
currentPage = data[0] + (data[1] << 8);
|
currentPage = data[0] + (data[1] << 8);
|
||||||
@@ -588,7 +587,7 @@ void TxtReaderActivity::loadProgress() {
|
|||||||
if (currentPage < 0) {
|
if (currentPage < 0) {
|
||||||
currentPage = 0;
|
currentPage = 0;
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [TRS] Loaded progress: page %d/%d\n", millis(), currentPage, totalPages);
|
LOG_DBG("TRS", "Loaded progress: page %d/%d", currentPage, totalPages);
|
||||||
}
|
}
|
||||||
f.close();
|
f.close();
|
||||||
}
|
}
|
||||||
@@ -609,8 +608,8 @@ bool TxtReaderActivity::loadPageIndexCache() {
|
|||||||
|
|
||||||
std::string cachePath = txt->getCachePath() + "/index.bin";
|
std::string cachePath = txt->getCachePath() + "/index.bin";
|
||||||
FsFile f;
|
FsFile f;
|
||||||
if (!SdMan.openFileForRead("TRS", cachePath, f)) {
|
if (!Storage.openFileForRead("TRS", cachePath, f)) {
|
||||||
Serial.printf("[%lu] [TRS] No page index cache found\n", millis());
|
LOG_DBG("TRS", "No page index cache found");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,7 +617,7 @@ bool TxtReaderActivity::loadPageIndexCache() {
|
|||||||
uint32_t magic;
|
uint32_t magic;
|
||||||
serialization::readPod(f, magic);
|
serialization::readPod(f, magic);
|
||||||
if (magic != CACHE_MAGIC) {
|
if (magic != CACHE_MAGIC) {
|
||||||
Serial.printf("[%lu] [TRS] Cache magic mismatch, rebuilding\n", millis());
|
LOG_DBG("TRS", "Cache magic mismatch, rebuilding");
|
||||||
f.close();
|
f.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -626,7 +625,7 @@ bool TxtReaderActivity::loadPageIndexCache() {
|
|||||||
uint8_t version;
|
uint8_t version;
|
||||||
serialization::readPod(f, version);
|
serialization::readPod(f, version);
|
||||||
if (version != CACHE_VERSION) {
|
if (version != CACHE_VERSION) {
|
||||||
Serial.printf("[%lu] [TRS] Cache version mismatch (%d != %d), rebuilding\n", millis(), version, CACHE_VERSION);
|
LOG_DBG("TRS", "Cache version mismatch (%d != %d), rebuilding", version, CACHE_VERSION);
|
||||||
f.close();
|
f.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -634,7 +633,7 @@ bool TxtReaderActivity::loadPageIndexCache() {
|
|||||||
uint32_t fileSize;
|
uint32_t fileSize;
|
||||||
serialization::readPod(f, fileSize);
|
serialization::readPod(f, fileSize);
|
||||||
if (fileSize != txt->getFileSize()) {
|
if (fileSize != txt->getFileSize()) {
|
||||||
Serial.printf("[%lu] [TRS] Cache file size mismatch, rebuilding\n", millis());
|
LOG_DBG("TRS", "Cache file size mismatch, rebuilding");
|
||||||
f.close();
|
f.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -642,7 +641,7 @@ bool TxtReaderActivity::loadPageIndexCache() {
|
|||||||
int32_t cachedWidth;
|
int32_t cachedWidth;
|
||||||
serialization::readPod(f, cachedWidth);
|
serialization::readPod(f, cachedWidth);
|
||||||
if (cachedWidth != viewportWidth) {
|
if (cachedWidth != viewportWidth) {
|
||||||
Serial.printf("[%lu] [TRS] Cache viewport width mismatch, rebuilding\n", millis());
|
LOG_DBG("TRS", "Cache viewport width mismatch, rebuilding");
|
||||||
f.close();
|
f.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -650,7 +649,7 @@ bool TxtReaderActivity::loadPageIndexCache() {
|
|||||||
int32_t cachedLines;
|
int32_t cachedLines;
|
||||||
serialization::readPod(f, cachedLines);
|
serialization::readPod(f, cachedLines);
|
||||||
if (cachedLines != linesPerPage) {
|
if (cachedLines != linesPerPage) {
|
||||||
Serial.printf("[%lu] [TRS] Cache lines per page mismatch, rebuilding\n", millis());
|
LOG_DBG("TRS", "Cache lines per page mismatch, rebuilding");
|
||||||
f.close();
|
f.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -658,7 +657,7 @@ bool TxtReaderActivity::loadPageIndexCache() {
|
|||||||
int32_t fontId;
|
int32_t fontId;
|
||||||
serialization::readPod(f, fontId);
|
serialization::readPod(f, fontId);
|
||||||
if (fontId != cachedFontId) {
|
if (fontId != cachedFontId) {
|
||||||
Serial.printf("[%lu] [TRS] Cache font ID mismatch (%d != %d), rebuilding\n", millis(), fontId, cachedFontId);
|
LOG_DBG("TRS", "Cache font ID mismatch (%d != %d), rebuilding", fontId, cachedFontId);
|
||||||
f.close();
|
f.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -666,7 +665,7 @@ bool TxtReaderActivity::loadPageIndexCache() {
|
|||||||
int32_t margin;
|
int32_t margin;
|
||||||
serialization::readPod(f, margin);
|
serialization::readPod(f, margin);
|
||||||
if (margin != cachedScreenMargin) {
|
if (margin != cachedScreenMargin) {
|
||||||
Serial.printf("[%lu] [TRS] Cache screen margin mismatch, rebuilding\n", millis());
|
LOG_DBG("TRS", "Cache screen margin mismatch, rebuilding");
|
||||||
f.close();
|
f.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -674,7 +673,7 @@ bool TxtReaderActivity::loadPageIndexCache() {
|
|||||||
uint8_t alignment;
|
uint8_t alignment;
|
||||||
serialization::readPod(f, alignment);
|
serialization::readPod(f, alignment);
|
||||||
if (alignment != cachedParagraphAlignment) {
|
if (alignment != cachedParagraphAlignment) {
|
||||||
Serial.printf("[%lu] [TRS] Cache paragraph alignment mismatch, rebuilding\n", millis());
|
LOG_DBG("TRS", "Cache paragraph alignment mismatch, rebuilding");
|
||||||
f.close();
|
f.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -694,15 +693,15 @@ bool TxtReaderActivity::loadPageIndexCache() {
|
|||||||
|
|
||||||
f.close();
|
f.close();
|
||||||
totalPages = pageOffsets.size();
|
totalPages = pageOffsets.size();
|
||||||
Serial.printf("[%lu] [TRS] Loaded page index cache: %d pages\n", millis(), totalPages);
|
LOG_DBG("TRS", "Loaded page index cache: %d pages", totalPages);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TxtReaderActivity::savePageIndexCache() const {
|
void TxtReaderActivity::savePageIndexCache() const {
|
||||||
std::string cachePath = txt->getCachePath() + "/index.bin";
|
std::string cachePath = txt->getCachePath() + "/index.bin";
|
||||||
FsFile f;
|
FsFile f;
|
||||||
if (!SdMan.openFileForWrite("TRS", cachePath, f)) {
|
if (!Storage.openFileForWrite("TRS", cachePath, f)) {
|
||||||
Serial.printf("[%lu] [TRS] Failed to save page index cache\n", millis());
|
LOG_ERR("TRS", "Failed to save page index cache");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -723,5 +722,5 @@ void TxtReaderActivity::savePageIndexCache() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
f.close();
|
f.close();
|
||||||
Serial.printf("[%lu] [TRS] Saved page index cache: %d pages\n", millis(), totalPages);
|
LOG_DBG("TRS", "Saved page index cache: %d pages", totalPages);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
@@ -102,15 +102,15 @@ void XtcReaderActivity::loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Long press BACK (1s+) goes directly to home
|
// Long press BACK (1s+) goes to file selection
|
||||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) {
|
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) {
|
||||||
onGoHome();
|
onGoBack();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Short press BACK goes to file selection
|
// Short press BACK goes directly to home
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) {
|
||||||
onGoBack();
|
onGoHome();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +206,7 @@ void XtcReaderActivity::renderPage() {
|
|||||||
// Allocate page buffer
|
// Allocate page buffer
|
||||||
uint8_t* pageBuffer = static_cast<uint8_t*>(malloc(pageBufferSize));
|
uint8_t* pageBuffer = static_cast<uint8_t*>(malloc(pageBufferSize));
|
||||||
if (!pageBuffer) {
|
if (!pageBuffer) {
|
||||||
Serial.printf("[%lu] [XTR] Failed to allocate page buffer (%lu bytes)\n", millis(), pageBufferSize);
|
LOG_ERR("XTR", "Failed to allocate page buffer (%lu bytes)", pageBufferSize);
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Memory error", true, EpdFontFamily::BOLD);
|
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Memory error", true, EpdFontFamily::BOLD);
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
@@ -216,7 +216,7 @@ void XtcReaderActivity::renderPage() {
|
|||||||
// Load page data
|
// Load page data
|
||||||
size_t bytesRead = xtc->loadPage(currentPage, pageBuffer, pageBufferSize);
|
size_t bytesRead = xtc->loadPage(currentPage, pageBuffer, pageBufferSize);
|
||||||
if (bytesRead == 0) {
|
if (bytesRead == 0) {
|
||||||
Serial.printf("[%lu] [XTR] Failed to load page %lu\n", millis(), currentPage);
|
LOG_ERR("XTR", "Failed to load page %lu", currentPage);
|
||||||
free(pageBuffer);
|
free(pageBuffer);
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Page load error", true, EpdFontFamily::BOLD);
|
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Page load error", true, EpdFontFamily::BOLD);
|
||||||
@@ -265,8 +265,8 @@ void XtcReaderActivity::renderPage() {
|
|||||||
pixelCounts[getPixelValue(x, y)]++;
|
pixelCounts[getPixelValue(x, y)]++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Serial.printf("[%lu] [XTR] Pixel distribution: White=%lu, DarkGrey=%lu, LightGrey=%lu, Black=%lu\n", millis(),
|
LOG_DBG("XTR", "Pixel distribution: White=%lu, DarkGrey=%lu, LightGrey=%lu, Black=%lu", pixelCounts[0],
|
||||||
pixelCounts[0], pixelCounts[1], pixelCounts[2], pixelCounts[3]);
|
pixelCounts[1], pixelCounts[2], pixelCounts[3]);
|
||||||
|
|
||||||
// Pass 1: BW buffer - draw all non-white pixels as black
|
// Pass 1: BW buffer - draw all non-white pixels as black
|
||||||
for (uint16_t y = 0; y < pageHeight; y++) {
|
for (uint16_t y = 0; y < pageHeight; y++) {
|
||||||
@@ -329,8 +329,7 @@ void XtcReaderActivity::renderPage() {
|
|||||||
|
|
||||||
free(pageBuffer);
|
free(pageBuffer);
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTR] Rendered page %lu/%lu (2-bit grayscale)\n", millis(), currentPage + 1,
|
LOG_DBG("XTR", "Rendered page %lu/%lu (2-bit grayscale)", currentPage + 1, xtc->getPageCount());
|
||||||
xtc->getPageCount());
|
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
// 1-bit mode: 8 pixels per byte, MSB first
|
// 1-bit mode: 8 pixels per byte, MSB first
|
||||||
@@ -366,13 +365,12 @@ void XtcReaderActivity::renderPage() {
|
|||||||
pagesUntilFullRefresh--;
|
pagesUntilFullRefresh--;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTR] Rendered page %lu/%lu (%u-bit)\n", millis(), currentPage + 1, xtc->getPageCount(),
|
LOG_DBG("XTR", "Rendered page %lu/%lu (%u-bit)", currentPage + 1, xtc->getPageCount(), bitDepth);
|
||||||
bitDepth);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void XtcReaderActivity::saveProgress() const {
|
void XtcReaderActivity::saveProgress() const {
|
||||||
FsFile f;
|
FsFile f;
|
||||||
if (SdMan.openFileForWrite("XTR", xtc->getCachePath() + "/progress.bin", f)) {
|
if (Storage.openFileForWrite("XTR", xtc->getCachePath() + "/progress.bin", f)) {
|
||||||
uint8_t data[4];
|
uint8_t data[4];
|
||||||
data[0] = currentPage & 0xFF;
|
data[0] = currentPage & 0xFF;
|
||||||
data[1] = (currentPage >> 8) & 0xFF;
|
data[1] = (currentPage >> 8) & 0xFF;
|
||||||
@@ -385,11 +383,11 @@ void XtcReaderActivity::saveProgress() const {
|
|||||||
|
|
||||||
void XtcReaderActivity::loadProgress() {
|
void XtcReaderActivity::loadProgress() {
|
||||||
FsFile f;
|
FsFile f;
|
||||||
if (SdMan.openFileForRead("XTR", xtc->getCachePath() + "/progress.bin", f)) {
|
if (Storage.openFileForRead("XTR", xtc->getCachePath() + "/progress.bin", f)) {
|
||||||
uint8_t data[4];
|
uint8_t data[4];
|
||||||
if (f.read(data, 4) == 4) {
|
if (f.read(data, 4) == 4) {
|
||||||
currentPage = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
|
currentPage = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
|
||||||
Serial.printf("[%lu] [XTR] Loaded progress: page %lu\n", millis(), currentPage);
|
LOG_DBG("XTR", "Loaded progress: page %lu", currentPage);
|
||||||
|
|
||||||
// Validate page number
|
// Validate page number
|
||||||
if (currentPage >= xtc->getPageCount()) {
|
if (currentPage >= xtc->getPageCount()) {
|
||||||
|
|||||||
@@ -8,10 +8,6 @@
|
|||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
namespace {
|
|
||||||
constexpr int SKIP_PAGE_MS = 700;
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
int XtcReaderChapterSelectionActivity::getPageItems() const {
|
int XtcReaderChapterSelectionActivity::getPageItems() const {
|
||||||
constexpr int lineHeight = 30;
|
constexpr int lineHeight = 30;
|
||||||
|
|
||||||
@@ -78,13 +74,8 @@ void XtcReaderChapterSelectionActivity::onExit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void XtcReaderChapterSelectionActivity::loop() {
|
void XtcReaderChapterSelectionActivity::loop() {
|
||||||
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Left);
|
|
||||||
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Right);
|
|
||||||
|
|
||||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
|
||||||
const int pageItems = getPageItems();
|
const int pageItems = getPageItems();
|
||||||
|
const int totalItems = static_cast<int>(xtc->getChapters().size());
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
const auto& chapters = xtc->getChapters();
|
const auto& chapters = xtc->getChapters();
|
||||||
@@ -93,29 +84,27 @@ void XtcReaderChapterSelectionActivity::loop() {
|
|||||||
}
|
}
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
onGoBack();
|
onGoBack();
|
||||||
} else if (prevReleased) {
|
|
||||||
const int total = static_cast<int>(xtc->getChapters().size());
|
|
||||||
if (total == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + total) % total;
|
|
||||||
} else {
|
|
||||||
selectorIndex = (selectorIndex + total - 1) % total;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buttonNavigator.onNextRelease([this, totalItems] {
|
||||||
|
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (nextReleased) {
|
});
|
||||||
const int total = static_cast<int>(xtc->getChapters().size());
|
|
||||||
if (total == 0) {
|
buttonNavigator.onPreviousRelease([this, totalItems] {
|
||||||
return;
|
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
|
||||||
}
|
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % total;
|
|
||||||
} else {
|
|
||||||
selectorIndex = (selectorIndex + 1) % total;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onNextContinuous([this, totalItems, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void XtcReaderChapterSelectionActivity::displayTaskLoop() {
|
void XtcReaderChapterSelectionActivity::displayTaskLoop() {
|
||||||
|
|||||||
@@ -7,11 +7,13 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "../Activity.h"
|
#include "../Activity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
class XtcReaderChapterSelectionActivity final : public Activity {
|
class XtcReaderChapterSelectionActivity final : public Activity {
|
||||||
std::shared_ptr<Xtc> xtc;
|
std::shared_ptr<Xtc> xtc;
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
uint32_t currentPage = 0;
|
uint32_t currentPage = 0;
|
||||||
int selectorIndex = 0;
|
int selectorIndex = 0;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
|||||||
@@ -63,15 +63,16 @@ void CalibreSettingsActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
// Handle navigation
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
buttonNavigator.onNext([this] {
|
||||||
selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS;
|
|
||||||
updateRequired = true;
|
|
||||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
|
||||||
selectedIndex = (selectedIndex + 1) % MENU_ITEMS;
|
selectedIndex = (selectedIndex + 1) % MENU_ITEMS;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPrevious([this] {
|
||||||
|
selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS;
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void CalibreSettingsActivity::handleSelection() {
|
void CalibreSettingsActivity::handleSelection() {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submenu for OPDS Browser settings.
|
* Submenu for OPDS Browser settings.
|
||||||
@@ -24,6 +25,7 @@ class CalibreSettingsActivity final : public ActivityWithSubactivity {
|
|||||||
private:
|
private:
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
|
||||||
int selectedIndex = 0;
|
int selectedIndex = 0;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#include "ClearCacheActivity.h"
|
#include "ClearCacheActivity.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HalStorage.h>
|
||||||
#include <SDCardManager.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
@@ -104,12 +104,12 @@ void ClearCacheActivity::render() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ClearCacheActivity::clearCache() {
|
void ClearCacheActivity::clearCache() {
|
||||||
Serial.printf("[%lu] [CLEAR_CACHE] Clearing cache...\n", millis());
|
LOG_DBG("CLEAR_CACHE", "Clearing cache...");
|
||||||
|
|
||||||
// Open .crosspoint directory
|
// Open .crosspoint directory
|
||||||
auto root = SdMan.open("/.crosspoint");
|
auto root = Storage.open("/.crosspoint");
|
||||||
if (!root || !root.isDirectory()) {
|
if (!root || !root.isDirectory()) {
|
||||||
Serial.printf("[%lu] [CLEAR_CACHE] Failed to open cache directory\n", millis());
|
LOG_DBG("CLEAR_CACHE", "Failed to open cache directory");
|
||||||
if (root) root.close();
|
if (root) root.close();
|
||||||
state = FAILED;
|
state = FAILED;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
@@ -128,14 +128,14 @@ void ClearCacheActivity::clearCache() {
|
|||||||
// Only delete directories starting with epub_ or xtc_
|
// Only delete directories starting with epub_ or xtc_
|
||||||
if (file.isDirectory() && (itemName.startsWith("epub_") || itemName.startsWith("xtc_"))) {
|
if (file.isDirectory() && (itemName.startsWith("epub_") || itemName.startsWith("xtc_"))) {
|
||||||
String fullPath = "/.crosspoint/" + itemName;
|
String fullPath = "/.crosspoint/" + itemName;
|
||||||
Serial.printf("[%lu] [CLEAR_CACHE] Removing cache: %s\n", millis(), fullPath.c_str());
|
LOG_DBG("CLEAR_CACHE", "Removing cache: %s", fullPath.c_str());
|
||||||
|
|
||||||
file.close(); // Close before attempting to delete
|
file.close(); // Close before attempting to delete
|
||||||
|
|
||||||
if (SdMan.removeDir(fullPath.c_str())) {
|
if (Storage.removeDir(fullPath.c_str())) {
|
||||||
clearedCount++;
|
clearedCount++;
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [CLEAR_CACHE] Failed to remove: %s\n", millis(), fullPath.c_str());
|
LOG_ERR("CLEAR_CACHE", "Failed to remove: %s", fullPath.c_str());
|
||||||
failedCount++;
|
failedCount++;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -144,7 +144,7 @@ void ClearCacheActivity::clearCache() {
|
|||||||
}
|
}
|
||||||
root.close();
|
root.close();
|
||||||
|
|
||||||
Serial.printf("[%lu] [CLEAR_CACHE] Cache cleared: %d removed, %d failed\n", millis(), clearedCount, failedCount);
|
LOG_DBG("CLEAR_CACHE", "Cache cleared: %d removed, %d failed", clearedCount, failedCount);
|
||||||
|
|
||||||
state = SUCCESS;
|
state = SUCCESS;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
@@ -153,7 +153,7 @@ void ClearCacheActivity::clearCache() {
|
|||||||
void ClearCacheActivity::loop() {
|
void ClearCacheActivity::loop() {
|
||||||
if (state == WARNING) {
|
if (state == WARNING) {
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||||
Serial.printf("[%lu] [CLEAR_CACHE] User confirmed, starting cache clear\n", millis());
|
LOG_DBG("CLEAR_CACHE", "User confirmed, starting cache clear");
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
state = CLEARING;
|
state = CLEARING;
|
||||||
xSemaphoreGive(renderingMutex);
|
xSemaphoreGive(renderingMutex);
|
||||||
@@ -164,7 +164,7 @@ void ClearCacheActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
Serial.printf("[%lu] [CLEAR_CACHE] User cancelled\n", millis());
|
LOG_DBG("CLEAR_CACHE", "User cancelled");
|
||||||
goBack();
|
goBack();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -64,15 +64,16 @@ void KOReaderSettingsActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
// Handle navigation
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
buttonNavigator.onNext([this] {
|
||||||
selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS;
|
|
||||||
updateRequired = true;
|
|
||||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
|
||||||
selectedIndex = (selectedIndex + 1) % MENU_ITEMS;
|
selectedIndex = (selectedIndex + 1) % MENU_ITEMS;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPrevious([this] {
|
||||||
|
selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS;
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void KOReaderSettingsActivity::handleSelection() {
|
void KOReaderSettingsActivity::handleSelection() {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submenu for KOReader Sync settings.
|
* Submenu for KOReader Sync settings.
|
||||||
@@ -24,6 +25,7 @@ class KOReaderSettingsActivity final : public ActivityWithSubactivity {
|
|||||||
private:
|
private:
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
|
||||||
int selectedIndex = 0;
|
int selectedIndex = 0;
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ void OtaUpdateActivity::onWifiSelectionComplete(const bool success) {
|
|||||||
exitActivity();
|
exitActivity();
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Serial.printf("[%lu] [OTA] WiFi connection failed, exiting\n", millis());
|
LOG_ERR("OTA", "WiFi connection failed, exiting");
|
||||||
goBack();
|
goBack();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [OTA] WiFi connected, checking for update\n", millis());
|
LOG_DBG("OTA", "WiFi connected, checking for update");
|
||||||
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
state = CHECKING_FOR_UPDATE;
|
state = CHECKING_FOR_UPDATE;
|
||||||
@@ -32,7 +32,7 @@ void OtaUpdateActivity::onWifiSelectionComplete(const bool success) {
|
|||||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
const auto res = updater.checkForUpdate();
|
const auto res = updater.checkForUpdate();
|
||||||
if (res != OtaUpdater::OK) {
|
if (res != OtaUpdater::OK) {
|
||||||
Serial.printf("[%lu] [OTA] Update check failed: %d\n", millis(), res);
|
LOG_DBG("OTA", "Update check failed: %d", res);
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
state = FAILED;
|
state = FAILED;
|
||||||
xSemaphoreGive(renderingMutex);
|
xSemaphoreGive(renderingMutex);
|
||||||
@@ -41,7 +41,7 @@ void OtaUpdateActivity::onWifiSelectionComplete(const bool success) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!updater.isUpdateNewer()) {
|
if (!updater.isUpdateNewer()) {
|
||||||
Serial.printf("[%lu] [OTA] No new update available\n", millis());
|
LOG_DBG("OTA", "No new update available");
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
state = NO_UPDATE;
|
state = NO_UPDATE;
|
||||||
xSemaphoreGive(renderingMutex);
|
xSemaphoreGive(renderingMutex);
|
||||||
@@ -68,11 +68,11 @@ void OtaUpdateActivity::onEnter() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Turn on WiFi immediately
|
// Turn on WiFi immediately
|
||||||
Serial.printf("[%lu] [OTA] Turning on WiFi...\n", millis());
|
LOG_DBG("OTA", "Turning on WiFi...");
|
||||||
WiFi.mode(WIFI_STA);
|
WiFi.mode(WIFI_STA);
|
||||||
|
|
||||||
// Launch WiFi selection subactivity
|
// Launch WiFi selection subactivity
|
||||||
Serial.printf("[%lu] [OTA] Launching WifiSelectionActivity...\n", millis());
|
LOG_DBG("OTA", "Launching WifiSelectionActivity...");
|
||||||
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
||||||
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
||||||
}
|
}
|
||||||
@@ -116,8 +116,7 @@ void OtaUpdateActivity::render() {
|
|||||||
|
|
||||||
float updaterProgress = 0;
|
float updaterProgress = 0;
|
||||||
if (state == UPDATE_IN_PROGRESS) {
|
if (state == UPDATE_IN_PROGRESS) {
|
||||||
Serial.printf("[%lu] [OTA] Update progress: %d / %d\n", millis(), updater.getProcessedSize(),
|
LOG_DBG("OTA", "Update progress: %d / %d", updater.getProcessedSize(), updater.getTotalSize());
|
||||||
updater.getTotalSize());
|
|
||||||
updaterProgress = static_cast<float>(updater.getProcessedSize()) / static_cast<float>(updater.getTotalSize());
|
updaterProgress = static_cast<float>(updater.getProcessedSize()) / static_cast<float>(updater.getTotalSize());
|
||||||
// Only update every 2% at the most
|
// Only update every 2% at the most
|
||||||
if (static_cast<int>(updaterProgress * 50) == lastUpdaterPercentage / 2) {
|
if (static_cast<int>(updaterProgress * 50) == lastUpdaterPercentage / 2) {
|
||||||
@@ -190,7 +189,7 @@ void OtaUpdateActivity::loop() {
|
|||||||
|
|
||||||
if (state == WAITING_CONFIRMATION) {
|
if (state == WAITING_CONFIRMATION) {
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||||
Serial.printf("[%lu] [OTA] New update available, starting download...\n", millis());
|
LOG_DBG("OTA", "New update available, starting download...");
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
state = UPDATE_IN_PROGRESS;
|
state = UPDATE_IN_PROGRESS;
|
||||||
xSemaphoreGive(renderingMutex);
|
xSemaphoreGive(renderingMutex);
|
||||||
@@ -199,7 +198,7 @@ void OtaUpdateActivity::loop() {
|
|||||||
const auto res = updater.installUpdate();
|
const auto res = updater.installUpdate();
|
||||||
|
|
||||||
if (res != OtaUpdater::OK) {
|
if (res != OtaUpdater::OK) {
|
||||||
Serial.printf("[%lu] [OTA] Update failed: %d\n", millis(), res);
|
LOG_DBG("OTA", "Update failed: %d", res);
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
state = FAILED;
|
state = FAILED;
|
||||||
xSemaphoreGive(renderingMutex);
|
xSemaphoreGive(renderingMutex);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#include "SettingsActivity.h"
|
#include "SettingsActivity.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HardwareSerial.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
#include "ButtonRemapActivity.h"
|
#include "ButtonRemapActivity.h"
|
||||||
#include "CalibreSettingsActivity.h"
|
#include "CalibreSettingsActivity.h"
|
||||||
@@ -10,63 +10,13 @@
|
|||||||
#include "KOReaderSettingsActivity.h"
|
#include "KOReaderSettingsActivity.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "OtaUpdateActivity.h"
|
#include "OtaUpdateActivity.h"
|
||||||
|
#include "SettingsList.h"
|
||||||
|
#include "activities/network/WifiSelectionActivity.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"};
|
const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"};
|
||||||
|
|
||||||
namespace {
|
|
||||||
constexpr int changeTabsMs = 700;
|
|
||||||
constexpr int displaySettingsCount = 8;
|
|
||||||
const SettingInfo displaySettings[displaySettingsCount] = {
|
|
||||||
// Should match with SLEEP_SCREEN_MODE
|
|
||||||
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen,
|
|
||||||
{"Dark", "Light", "Custom", "Cover", "None", "Cover + Custom"}),
|
|
||||||
SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}),
|
|
||||||
SettingInfo::Enum("Sleep Screen Cover Filter", &CrossPointSettings::sleepScreenCoverFilter,
|
|
||||||
{"None", "Contrast", "Inverted"}),
|
|
||||||
SettingInfo::Enum(
|
|
||||||
"Status Bar", &CrossPointSettings::statusBar,
|
|
||||||
{"None", "No Progress", "Full w/ Percentage", "Full w/ Book Bar", "Book Bar Only", "Full w/ Chapter Bar"}),
|
|
||||||
SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}),
|
|
||||||
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
|
|
||||||
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}),
|
|
||||||
SettingInfo::Enum("UI Theme", &CrossPointSettings::uiTheme, {"Classic", "Lyra"}),
|
|
||||||
SettingInfo::Toggle("Sunlight Fading Fix", &CrossPointSettings::fadingFix),
|
|
||||||
};
|
|
||||||
|
|
||||||
constexpr int readerSettingsCount = 10;
|
|
||||||
const SettingInfo readerSettings[readerSettingsCount] = {
|
|
||||||
SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"}),
|
|
||||||
SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}),
|
|
||||||
SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}),
|
|
||||||
SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}),
|
|
||||||
SettingInfo::Enum("Paragraph Alignment", &CrossPointSettings::paragraphAlignment,
|
|
||||||
{"Justify", "Left", "Center", "Right", "Book's Style"}),
|
|
||||||
SettingInfo::Toggle("Book's Embedded Style", &CrossPointSettings::embeddedStyle),
|
|
||||||
SettingInfo::Toggle("Hyphenation", &CrossPointSettings::hyphenationEnabled),
|
|
||||||
SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation,
|
|
||||||
{"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}),
|
|
||||||
SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing),
|
|
||||||
SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing)};
|
|
||||||
|
|
||||||
constexpr int controlsSettingsCount = 4;
|
|
||||||
const SettingInfo controlsSettings[controlsSettingsCount] = {
|
|
||||||
// Launches the remap wizard for front buttons.
|
|
||||||
SettingInfo::Action("Remap Front Buttons"),
|
|
||||||
SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout,
|
|
||||||
{"Prev, Next", "Next, Prev"}),
|
|
||||||
SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip),
|
|
||||||
SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"})};
|
|
||||||
|
|
||||||
constexpr int systemSettingsCount = 5;
|
|
||||||
const SettingInfo systemSettings[systemSettingsCount] = {
|
|
||||||
SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout,
|
|
||||||
{"1 min", "5 min", "10 min", "15 min", "30 min"}),
|
|
||||||
SettingInfo::Action("KOReader Sync"), SettingInfo::Action("OPDS Browser"), SettingInfo::Action("Clear Cache"),
|
|
||||||
SettingInfo::Action("Check for updates")};
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
void SettingsActivity::taskTrampoline(void* param) {
|
void SettingsActivity::taskTrampoline(void* param) {
|
||||||
auto* self = static_cast<SettingsActivity*>(param);
|
auto* self = static_cast<SettingsActivity*>(param);
|
||||||
self->displayTaskLoop();
|
self->displayTaskLoop();
|
||||||
@@ -76,13 +26,42 @@ void SettingsActivity::onEnter() {
|
|||||||
Activity::onEnter();
|
Activity::onEnter();
|
||||||
renderingMutex = xSemaphoreCreateMutex();
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
|
// Build per-category vectors from the shared settings list
|
||||||
|
displaySettings.clear();
|
||||||
|
readerSettings.clear();
|
||||||
|
controlsSettings.clear();
|
||||||
|
systemSettings.clear();
|
||||||
|
|
||||||
|
for (auto& setting : getSettingsList()) {
|
||||||
|
if (!setting.category) continue;
|
||||||
|
if (strcmp(setting.category, "Display") == 0) {
|
||||||
|
displaySettings.push_back(std::move(setting));
|
||||||
|
} else if (strcmp(setting.category, "Reader") == 0) {
|
||||||
|
readerSettings.push_back(std::move(setting));
|
||||||
|
} else if (strcmp(setting.category, "Controls") == 0) {
|
||||||
|
controlsSettings.push_back(std::move(setting));
|
||||||
|
} else if (strcmp(setting.category, "System") == 0) {
|
||||||
|
systemSettings.push_back(std::move(setting));
|
||||||
|
}
|
||||||
|
// Web-only categories (KOReader Sync, OPDS Browser) are skipped for device UI
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append device-only ACTION items
|
||||||
|
controlsSettings.insert(controlsSettings.begin(),
|
||||||
|
SettingInfo::Action("Remap Front Buttons", SettingAction::RemapFrontButtons));
|
||||||
|
systemSettings.push_back(SettingInfo::Action("Network", SettingAction::Network));
|
||||||
|
systemSettings.push_back(SettingInfo::Action("KOReader Sync", SettingAction::KOReaderSync));
|
||||||
|
systemSettings.push_back(SettingInfo::Action("OPDS Browser", SettingAction::OPDSBrowser));
|
||||||
|
systemSettings.push_back(SettingInfo::Action("Clear Cache", SettingAction::ClearCache));
|
||||||
|
systemSettings.push_back(SettingInfo::Action("Check for updates", SettingAction::CheckForUpdates));
|
||||||
|
|
||||||
// Reset selection to first category
|
// Reset selection to first category
|
||||||
selectedCategoryIndex = 0;
|
selectedCategoryIndex = 0;
|
||||||
selectedSettingIndex = 0;
|
selectedSettingIndex = 0;
|
||||||
|
|
||||||
// Initialize with first category (Display)
|
// Initialize with first category (Display)
|
||||||
settingsList = displaySettings;
|
currentSettings = &displaySettings;
|
||||||
settingsCount = displaySettingsCount;
|
settingsCount = static_cast<int>(displaySettings.size());
|
||||||
|
|
||||||
// Trigger first update
|
// Trigger first update
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
@@ -136,49 +115,46 @@ void SettingsActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Up);
|
|
||||||
const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Down);
|
|
||||||
const bool leftReleased = mappedInput.wasReleased(MappedInputManager::Button::Left);
|
|
||||||
const bool rightReleased = mappedInput.wasReleased(MappedInputManager::Button::Right);
|
|
||||||
const bool changeTab = mappedInput.getHeldTime() > changeTabsMs;
|
|
||||||
|
|
||||||
// Handle navigation
|
// Handle navigation
|
||||||
if (upReleased && changeTab) {
|
buttonNavigator.onNextRelease([this] {
|
||||||
|
selectedSettingIndex = ButtonNavigator::nextIndex(selectedSettingIndex, settingsCount + 1);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPreviousRelease([this] {
|
||||||
|
selectedSettingIndex = ButtonNavigator::previousIndex(selectedSettingIndex, settingsCount + 1);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onNextContinuous([this, &hasChangedCategory] {
|
||||||
hasChangedCategory = true;
|
hasChangedCategory = true;
|
||||||
selectedCategoryIndex = (selectedCategoryIndex > 0) ? (selectedCategoryIndex - 1) : (categoryCount - 1);
|
selectedCategoryIndex = ButtonNavigator::nextIndex(selectedCategoryIndex, categoryCount);
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (downReleased && changeTab) {
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPreviousContinuous([this, &hasChangedCategory] {
|
||||||
hasChangedCategory = true;
|
hasChangedCategory = true;
|
||||||
selectedCategoryIndex = (selectedCategoryIndex < categoryCount - 1) ? (selectedCategoryIndex + 1) : 0;
|
selectedCategoryIndex = ButtonNavigator::previousIndex(selectedCategoryIndex, categoryCount);
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (upReleased || leftReleased) {
|
});
|
||||||
selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount);
|
|
||||||
updateRequired = true;
|
|
||||||
} else if (rightReleased || downReleased) {
|
|
||||||
selectedSettingIndex = (selectedSettingIndex < settingsCount) ? (selectedSettingIndex + 1) : 0;
|
|
||||||
updateRequired = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasChangedCategory) {
|
if (hasChangedCategory) {
|
||||||
selectedSettingIndex = (selectedSettingIndex == 0) ? 0 : 1;
|
selectedSettingIndex = (selectedSettingIndex == 0) ? 0 : 1;
|
||||||
switch (selectedCategoryIndex) {
|
switch (selectedCategoryIndex) {
|
||||||
case 0: // Display
|
case 0:
|
||||||
settingsList = displaySettings;
|
currentSettings = &displaySettings;
|
||||||
settingsCount = displaySettingsCount;
|
|
||||||
break;
|
break;
|
||||||
case 1: // Reader
|
case 1:
|
||||||
settingsList = readerSettings;
|
currentSettings = &readerSettings;
|
||||||
settingsCount = readerSettingsCount;
|
|
||||||
break;
|
break;
|
||||||
case 2: // Controls
|
case 2:
|
||||||
settingsList = controlsSettings;
|
currentSettings = &controlsSettings;
|
||||||
settingsCount = controlsSettingsCount;
|
|
||||||
break;
|
break;
|
||||||
case 3: // System
|
case 3:
|
||||||
settingsList = systemSettings;
|
currentSettings = &systemSettings;
|
||||||
settingsCount = systemSettingsCount;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
settingsCount = static_cast<int>(currentSettings->size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +164,7 @@ void SettingsActivity::toggleCurrentSetting() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& setting = settingsList[selectedSetting];
|
const auto& setting = (*currentSettings)[selectedSetting];
|
||||||
|
|
||||||
if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {
|
if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {
|
||||||
// Toggle the boolean value using the member pointer
|
// Toggle the boolean value using the member pointer
|
||||||
@@ -205,46 +181,45 @@ void SettingsActivity::toggleCurrentSetting() {
|
|||||||
SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step;
|
SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step;
|
||||||
}
|
}
|
||||||
} else if (setting.type == SettingType::ACTION) {
|
} else if (setting.type == SettingType::ACTION) {
|
||||||
if (strcmp(setting.name, "Remap Front Buttons") == 0) {
|
auto enterSubActivity = [this](Activity* activity) {
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new ButtonRemapActivity(renderer, mappedInput, [this] {
|
enterNewActivity(activity);
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto onComplete = [this] {
|
||||||
exitActivity();
|
exitActivity();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}));
|
};
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
} else if (strcmp(setting.name, "KOReader Sync") == 0) {
|
auto onCompleteBool = [this](bool) {
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
exitActivity();
|
|
||||||
enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] {
|
|
||||||
exitActivity();
|
exitActivity();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}));
|
};
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
} else if (strcmp(setting.name, "OPDS Browser") == 0) {
|
switch (setting.action) {
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
case SettingAction::RemapFrontButtons:
|
||||||
exitActivity();
|
enterSubActivity(new ButtonRemapActivity(renderer, mappedInput, onComplete));
|
||||||
enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] {
|
break;
|
||||||
exitActivity();
|
case SettingAction::KOReaderSync:
|
||||||
updateRequired = true;
|
enterSubActivity(new KOReaderSettingsActivity(renderer, mappedInput, onComplete));
|
||||||
}));
|
break;
|
||||||
xSemaphoreGive(renderingMutex);
|
case SettingAction::OPDSBrowser:
|
||||||
} else if (strcmp(setting.name, "Clear Cache") == 0) {
|
enterSubActivity(new CalibreSettingsActivity(renderer, mappedInput, onComplete));
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
break;
|
||||||
exitActivity();
|
case SettingAction::Network:
|
||||||
enterNewActivity(new ClearCacheActivity(renderer, mappedInput, [this] {
|
enterSubActivity(new WifiSelectionActivity(renderer, mappedInput, onCompleteBool, false));
|
||||||
exitActivity();
|
break;
|
||||||
updateRequired = true;
|
case SettingAction::ClearCache:
|
||||||
}));
|
enterSubActivity(new ClearCacheActivity(renderer, mappedInput, onComplete));
|
||||||
xSemaphoreGive(renderingMutex);
|
break;
|
||||||
} else if (strcmp(setting.name, "Check for updates") == 0) {
|
case SettingAction::CheckForUpdates:
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
enterSubActivity(new OtaUpdateActivity(renderer, mappedInput, onComplete));
|
||||||
exitActivity();
|
break;
|
||||||
enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] {
|
case SettingAction::None:
|
||||||
exitActivity();
|
// Do nothing
|
||||||
updateRequired = true;
|
break;
|
||||||
}));
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
@@ -283,24 +258,24 @@ void SettingsActivity::render() const {
|
|||||||
GUI.drawTabBar(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, tabs,
|
GUI.drawTabBar(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, tabs,
|
||||||
selectedSettingIndex == 0);
|
selectedSettingIndex == 0);
|
||||||
|
|
||||||
|
const auto& settings = *currentSettings;
|
||||||
GUI.drawList(
|
GUI.drawList(
|
||||||
renderer,
|
renderer,
|
||||||
Rect{0, metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing, pageWidth,
|
Rect{0, metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing, pageWidth,
|
||||||
pageHeight - (metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.buttonHintsHeight +
|
pageHeight - (metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.buttonHintsHeight +
|
||||||
metrics.verticalSpacing * 2)},
|
metrics.verticalSpacing * 2)},
|
||||||
settingsCount, selectedSettingIndex - 1, [this](int index) { return std::string(settingsList[index].name); },
|
settingsCount, selectedSettingIndex - 1, [&settings](int index) { return std::string(settings[index].name); },
|
||||||
nullptr, nullptr,
|
nullptr, nullptr,
|
||||||
[this](int i) {
|
[&settings](int i) {
|
||||||
const auto& setting = settingsList[i];
|
|
||||||
std::string valueText = "";
|
std::string valueText = "";
|
||||||
if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) {
|
if (settings[i].type == SettingType::TOGGLE && settings[i].valuePtr != nullptr) {
|
||||||
const bool value = SETTINGS.*(settingsList[i].valuePtr);
|
const bool value = SETTINGS.*(settings[i].valuePtr);
|
||||||
valueText = value ? "ON" : "OFF";
|
valueText = value ? "ON" : "OFF";
|
||||||
} else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) {
|
} else if (settings[i].type == SettingType::ENUM && settings[i].valuePtr != nullptr) {
|
||||||
const uint8_t value = SETTINGS.*(settingsList[i].valuePtr);
|
const uint8_t value = SETTINGS.*(settings[i].valuePtr);
|
||||||
valueText = settingsList[i].enumValues[value];
|
valueText = settings[i].enumValues[value];
|
||||||
} else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) {
|
} else if (settings[i].type == SettingType::VALUE && settings[i].valuePtr != nullptr) {
|
||||||
valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr));
|
valueText = std::to_string(SETTINGS.*(settings[i].valuePtr));
|
||||||
}
|
}
|
||||||
return valueText;
|
return valueText;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,47 +8,147 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
class CrossPointSettings;
|
class CrossPointSettings;
|
||||||
|
|
||||||
enum class SettingType { TOGGLE, ENUM, ACTION, VALUE };
|
enum class SettingType { TOGGLE, ENUM, ACTION, VALUE, STRING };
|
||||||
|
|
||||||
|
enum class SettingAction {
|
||||||
|
None,
|
||||||
|
RemapFrontButtons,
|
||||||
|
KOReaderSync,
|
||||||
|
OPDSBrowser,
|
||||||
|
Network,
|
||||||
|
ClearCache,
|
||||||
|
CheckForUpdates,
|
||||||
|
};
|
||||||
|
|
||||||
struct SettingInfo {
|
struct SettingInfo {
|
||||||
const char* name;
|
const char* name;
|
||||||
SettingType type;
|
SettingType type;
|
||||||
uint8_t CrossPointSettings::* valuePtr;
|
uint8_t CrossPointSettings::* valuePtr = nullptr;
|
||||||
std::vector<std::string> enumValues;
|
std::vector<std::string> enumValues;
|
||||||
|
SettingAction action = SettingAction::None;
|
||||||
|
|
||||||
struct ValueRange {
|
struct ValueRange {
|
||||||
uint8_t min;
|
uint8_t min;
|
||||||
uint8_t max;
|
uint8_t max;
|
||||||
uint8_t step;
|
uint8_t step;
|
||||||
};
|
};
|
||||||
ValueRange valueRange;
|
ValueRange valueRange = {};
|
||||||
|
|
||||||
static SettingInfo Toggle(const char* name, uint8_t CrossPointSettings::* ptr) {
|
const char* key = nullptr; // JSON API key (nullptr for ACTION types)
|
||||||
return {name, SettingType::TOGGLE, ptr};
|
const char* category = nullptr; // Category for web UI grouping
|
||||||
|
|
||||||
|
// Direct char[] string fields (for settings stored in CrossPointSettings)
|
||||||
|
char* stringPtr = nullptr;
|
||||||
|
size_t stringMaxLen = 0;
|
||||||
|
|
||||||
|
// Dynamic accessors (for settings stored outside CrossPointSettings, e.g. KOReaderCredentialStore)
|
||||||
|
std::function<uint8_t()> valueGetter;
|
||||||
|
std::function<void(uint8_t)> valueSetter;
|
||||||
|
std::function<std::string()> stringGetter;
|
||||||
|
std::function<void(const std::string&)> stringSetter;
|
||||||
|
|
||||||
|
static SettingInfo Toggle(const char* name, uint8_t CrossPointSettings::* ptr, const char* key = nullptr,
|
||||||
|
const char* category = nullptr) {
|
||||||
|
SettingInfo s;
|
||||||
|
s.name = name;
|
||||||
|
s.type = SettingType::TOGGLE;
|
||||||
|
s.valuePtr = ptr;
|
||||||
|
s.key = key;
|
||||||
|
s.category = category;
|
||||||
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
static SettingInfo Enum(const char* name, uint8_t CrossPointSettings::* ptr, std::vector<std::string> values) {
|
static SettingInfo Enum(const char* name, uint8_t CrossPointSettings::* ptr, std::vector<std::string> values,
|
||||||
return {name, SettingType::ENUM, ptr, std::move(values)};
|
const char* key = nullptr, const char* category = nullptr) {
|
||||||
|
SettingInfo s;
|
||||||
|
s.name = name;
|
||||||
|
s.type = SettingType::ENUM;
|
||||||
|
s.valuePtr = ptr;
|
||||||
|
s.enumValues = std::move(values);
|
||||||
|
s.key = key;
|
||||||
|
s.category = category;
|
||||||
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
static SettingInfo Action(const char* name) { return {name, SettingType::ACTION, nullptr}; }
|
static SettingInfo Action(const char* name, SettingAction action) {
|
||||||
|
SettingInfo s;
|
||||||
|
s.name = name;
|
||||||
|
s.type = SettingType::ACTION;
|
||||||
|
s.action = action;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
static SettingInfo Value(const char* name, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange) {
|
static SettingInfo Value(const char* name, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange,
|
||||||
return {name, SettingType::VALUE, ptr, {}, valueRange};
|
const char* key = nullptr, const char* category = nullptr) {
|
||||||
|
SettingInfo s;
|
||||||
|
s.name = name;
|
||||||
|
s.type = SettingType::VALUE;
|
||||||
|
s.valuePtr = ptr;
|
||||||
|
s.valueRange = valueRange;
|
||||||
|
s.key = key;
|
||||||
|
s.category = category;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SettingInfo String(const char* name, char* ptr, size_t maxLen, const char* key = nullptr,
|
||||||
|
const char* category = nullptr) {
|
||||||
|
SettingInfo s;
|
||||||
|
s.name = name;
|
||||||
|
s.type = SettingType::STRING;
|
||||||
|
s.stringPtr = ptr;
|
||||||
|
s.stringMaxLen = maxLen;
|
||||||
|
s.key = key;
|
||||||
|
s.category = category;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SettingInfo DynamicEnum(const char* name, std::vector<std::string> values, std::function<uint8_t()> getter,
|
||||||
|
std::function<void(uint8_t)> setter, const char* key = nullptr,
|
||||||
|
const char* category = nullptr) {
|
||||||
|
SettingInfo s;
|
||||||
|
s.name = name;
|
||||||
|
s.type = SettingType::ENUM;
|
||||||
|
s.enumValues = std::move(values);
|
||||||
|
s.valueGetter = std::move(getter);
|
||||||
|
s.valueSetter = std::move(setter);
|
||||||
|
s.key = key;
|
||||||
|
s.category = category;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SettingInfo DynamicString(const char* name, std::function<std::string()> getter,
|
||||||
|
std::function<void(const std::string&)> setter, const char* key = nullptr,
|
||||||
|
const char* category = nullptr) {
|
||||||
|
SettingInfo s;
|
||||||
|
s.name = name;
|
||||||
|
s.type = SettingType::STRING;
|
||||||
|
s.stringGetter = std::move(getter);
|
||||||
|
s.stringSetter = std::move(setter);
|
||||||
|
s.key = key;
|
||||||
|
s.category = category;
|
||||||
|
return s;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class SettingsActivity final : public ActivityWithSubactivity {
|
class SettingsActivity final : public ActivityWithSubactivity {
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
int selectedCategoryIndex = 0; // Currently selected category
|
int selectedCategoryIndex = 0; // Currently selected category
|
||||||
int selectedSettingIndex = 0;
|
int selectedSettingIndex = 0;
|
||||||
int settingsCount = 0;
|
int settingsCount = 0;
|
||||||
const SettingInfo* settingsList = nullptr;
|
|
||||||
|
// Per-category settings derived from shared list + device-only actions
|
||||||
|
std::vector<SettingInfo> displaySettings;
|
||||||
|
std::vector<SettingInfo> readerSettings;
|
||||||
|
std::vector<SettingInfo> controlsSettings;
|
||||||
|
std::vector<SettingInfo> systemSettings;
|
||||||
|
const std::vector<SettingInfo>* currentSettings = nullptr;
|
||||||
|
|
||||||
const std::function<void()> onGoHome;
|
const std::function<void()> onGoHome;
|
||||||
|
|
||||||
|
|||||||
@@ -142,37 +142,24 @@ void KeyboardEntryActivity::handleKeyPress() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void KeyboardEntryActivity::loop() {
|
void KeyboardEntryActivity::loop() {
|
||||||
// Navigation
|
// Handle navigation
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up)) {
|
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this] {
|
||||||
if (selectedRow > 0) {
|
selectedRow = ButtonNavigator::previousIndex(selectedRow, NUM_ROWS);
|
||||||
selectedRow--;
|
|
||||||
// Clamp column to valid range for new row
|
|
||||||
const int maxCol = getRowLength(selectedRow) - 1;
|
|
||||||
if (selectedCol > maxCol) selectedCol = maxCol;
|
|
||||||
} else {
|
|
||||||
// Wrap to bottom row
|
|
||||||
selectedRow = NUM_ROWS - 1;
|
|
||||||
const int maxCol = getRowLength(selectedRow) - 1;
|
|
||||||
if (selectedCol > maxCol) selectedCol = maxCol;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Down)) {
|
|
||||||
if (selectedRow < NUM_ROWS - 1) {
|
|
||||||
selectedRow++;
|
|
||||||
const int maxCol = getRowLength(selectedRow) - 1;
|
const int maxCol = getRowLength(selectedRow) - 1;
|
||||||
if (selectedCol > maxCol) selectedCol = maxCol;
|
if (selectedCol > maxCol) selectedCol = maxCol;
|
||||||
} else {
|
|
||||||
// Wrap to top row
|
|
||||||
selectedRow = 0;
|
|
||||||
const int maxCol = getRowLength(selectedRow) - 1;
|
|
||||||
if (selectedCol > maxCol) selectedCol = maxCol;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] {
|
||||||
|
selectedRow = ButtonNavigator::nextIndex(selectedRow, NUM_ROWS);
|
||||||
|
|
||||||
|
const int maxCol = getRowLength(selectedRow) - 1;
|
||||||
|
if (selectedCol > maxCol) selectedCol = maxCol;
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] {
|
||||||
const int maxCol = getRowLength(selectedRow) - 1;
|
const int maxCol = getRowLength(selectedRow) - 1;
|
||||||
|
|
||||||
// Special bottom row case
|
// Special bottom row case
|
||||||
@@ -191,20 +178,14 @@ void KeyboardEntryActivity::loop() {
|
|||||||
// At done button, move to backspace
|
// At done button, move to backspace
|
||||||
selectedCol = BACKSPACE_COL;
|
selectedCol = BACKSPACE_COL;
|
||||||
}
|
}
|
||||||
updateRequired = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedCol > 0) {
|
|
||||||
selectedCol--;
|
|
||||||
} else {
|
} else {
|
||||||
// Wrap to end of current row
|
selectedCol = ButtonNavigator::previousIndex(selectedCol, maxCol + 1);
|
||||||
selectedCol = maxCol;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] {
|
||||||
const int maxCol = getRowLength(selectedRow) - 1;
|
const int maxCol = getRowLength(selectedRow) - 1;
|
||||||
|
|
||||||
// Special bottom row case
|
// Special bottom row case
|
||||||
@@ -223,18 +204,11 @@ void KeyboardEntryActivity::loop() {
|
|||||||
// At done button, wrap to beginning of row
|
// At done button, wrap to beginning of row
|
||||||
selectedCol = SHIFT_COL;
|
selectedCol = SHIFT_COL;
|
||||||
}
|
}
|
||||||
updateRequired = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedCol < maxCol) {
|
|
||||||
selectedCol++;
|
|
||||||
} else {
|
} else {
|
||||||
// Wrap to beginning of current row
|
selectedCol = ButtonNavigator::nextIndex(selectedCol, maxCol + 1);
|
||||||
selectedCol = 0;
|
|
||||||
}
|
}
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
|
|
||||||
// Selection
|
// Selection
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "../Activity.h"
|
#include "../Activity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reusable keyboard entry activity for text input.
|
* Reusable keyboard entry activity for text input.
|
||||||
@@ -65,6 +66,7 @@ class KeyboardEntryActivity : public Activity {
|
|||||||
bool isPassword;
|
bool isPassword;
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
|
||||||
// Keyboard state
|
// Keyboard state
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "UITheme.h"
|
#include "UITheme.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
@@ -23,12 +24,12 @@ void UITheme::reload() {
|
|||||||
void UITheme::setTheme(CrossPointSettings::UI_THEME type) {
|
void UITheme::setTheme(CrossPointSettings::UI_THEME type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case CrossPointSettings::UI_THEME::CLASSIC:
|
case CrossPointSettings::UI_THEME::CLASSIC:
|
||||||
Serial.printf("[%lu] [UI] Using Classic theme\n", millis());
|
LOG_DBG("UI", "Using Classic theme");
|
||||||
currentTheme = new BaseTheme();
|
currentTheme = new BaseTheme();
|
||||||
currentMetrics = &BaseMetrics::values;
|
currentMetrics = &BaseMetrics::values;
|
||||||
break;
|
break;
|
||||||
case CrossPointSettings::UI_THEME::LYRA:
|
case CrossPointSettings::UI_THEME::LYRA:
|
||||||
Serial.printf("[%lu] [UI] Using Lyra theme\n", millis());
|
LOG_DBG("UI", "Using Lyra theme");
|
||||||
currentTheme = new LyraTheme();
|
currentTheme = new LyraTheme();
|
||||||
currentMetrics = &LyraMetrics::values;
|
currentMetrics = &LyraMetrics::values;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
#include "BaseTheme.h"
|
#include "BaseTheme.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
#include <Logging.h>
|
||||||
#include <Utf8.h>
|
#include <Utf8.h>
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
@@ -308,10 +309,10 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
|||||||
|
|
||||||
// First time: load cover from SD and render
|
// First time: load cover from SD and render
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (SdMan.openFileForRead("HOME", coverBmpPath, file)) {
|
if (Storage.openFileForRead("HOME", coverBmpPath, file)) {
|
||||||
Bitmap bitmap(file);
|
Bitmap bitmap(file);
|
||||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||||
Serial.printf("Rendering bmp\n");
|
LOG_DBG("THEME", "Rendering bmp");
|
||||||
// Calculate position to center image within the book card
|
// Calculate position to center image within the book card
|
||||||
int coverX, coverY;
|
int coverX, coverY;
|
||||||
|
|
||||||
@@ -345,7 +346,7 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
|||||||
|
|
||||||
// First render: if selected, draw selection indicators now
|
// First render: if selected, draw selection indicators now
|
||||||
if (bookSelected) {
|
if (bookSelected) {
|
||||||
Serial.printf("Drawing selection\n");
|
LOG_DBG("THEME", "Drawing selection");
|
||||||
renderer.drawRect(bookX + 1, bookY + 1, bookWidth - 2, bookHeight - 2);
|
renderer.drawRect(bookX + 1, bookY + 1, bookWidth - 2, bookHeight - 2);
|
||||||
renderer.drawRect(bookX + 2, bookY + 2, bookWidth - 4, bookHeight - 4);
|
renderer.drawRect(bookX + 2, bookY + 2, bookWidth - 4, bookHeight - 4);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#include "LyraTheme.h"
|
#include "LyraTheme.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -283,7 +283,7 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
|||||||
|
|
||||||
// First time: load cover from SD and render
|
// First time: load cover from SD and render
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (SdMan.openFileForRead("HOME", coverBmpPath, file)) {
|
if (Storage.openFileForRead("HOME", coverBmpPath, file)) {
|
||||||
Bitmap bitmap(file);
|
Bitmap bitmap(file);
|
||||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||||
float coverHeight = static_cast<float>(bitmap.getHeight());
|
float coverHeight = static_cast<float>(bitmap.getHeight());
|
||||||
|
|||||||
57
src/main.cpp
57
src/main.cpp
@@ -3,7 +3,8 @@
|
|||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HalDisplay.h>
|
#include <HalDisplay.h>
|
||||||
#include <HalGPIO.h>
|
#include <HalGPIO.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
#include <Logging.h>
|
||||||
#include <SPI.h>
|
#include <SPI.h>
|
||||||
#include <builtinFonts/all.h>
|
#include <builtinFonts/all.h>
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
#include "activities/util/FullScreenMessageActivity.h"
|
#include "activities/util/FullScreenMessageActivity.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
HalDisplay display;
|
HalDisplay display;
|
||||||
HalGPIO gpio;
|
HalGPIO gpio;
|
||||||
@@ -200,8 +202,8 @@ void enterDeepSleep() {
|
|||||||
enterNewActivity(new SleepActivity(renderer, mappedInputManager));
|
enterNewActivity(new SleepActivity(renderer, mappedInputManager));
|
||||||
|
|
||||||
display.deepSleep();
|
display.deepSleep();
|
||||||
Serial.printf("[%lu] [ ] Power button press calibration value: %lu ms\n", millis(), t2 - t1);
|
LOG_DBG("MAIN", "Power button press calibration value: %lu ms", t2 - t1);
|
||||||
Serial.printf("[%lu] [ ] Entering deep sleep.\n", millis());
|
LOG_DBG("MAIN", "Entering deep sleep");
|
||||||
|
|
||||||
gpio.startDeepSleep();
|
gpio.startDeepSleep();
|
||||||
}
|
}
|
||||||
@@ -254,7 +256,7 @@ void onGoHome() {
|
|||||||
void setupDisplayAndFonts() {
|
void setupDisplayAndFonts() {
|
||||||
display.begin();
|
display.begin();
|
||||||
renderer.begin();
|
renderer.begin();
|
||||||
Serial.printf("[%lu] [ ] Display initialized\n", millis());
|
LOG_DBG("MAIN", "Display initialized");
|
||||||
renderer.insertFont(BOOKERLY_14_FONT_ID, bookerly14FontFamily);
|
renderer.insertFont(BOOKERLY_14_FONT_ID, bookerly14FontFamily);
|
||||||
#ifndef OMIT_FONTS
|
#ifndef OMIT_FONTS
|
||||||
renderer.insertFont(BOOKERLY_12_FONT_ID, bookerly12FontFamily);
|
renderer.insertFont(BOOKERLY_12_FONT_ID, bookerly12FontFamily);
|
||||||
@@ -273,7 +275,7 @@ void setupDisplayAndFonts() {
|
|||||||
renderer.insertFont(UI_10_FONT_ID, ui10FontFamily);
|
renderer.insertFont(UI_10_FONT_ID, ui10FontFamily);
|
||||||
renderer.insertFont(UI_12_FONT_ID, ui12FontFamily);
|
renderer.insertFont(UI_12_FONT_ID, ui12FontFamily);
|
||||||
renderer.insertFont(SMALL_FONT_ID, smallFontFamily);
|
renderer.insertFont(SMALL_FONT_ID, smallFontFamily);
|
||||||
Serial.printf("[%lu] [ ] Fonts setup\n", millis());
|
LOG_DBG("MAIN", "Fonts setup");
|
||||||
}
|
}
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
@@ -293,8 +295,8 @@ void setup() {
|
|||||||
|
|
||||||
// SD Card Initialization
|
// SD Card Initialization
|
||||||
// We need 6 open files concurrently when parsing a new chapter
|
// We need 6 open files concurrently when parsing a new chapter
|
||||||
if (!SdMan.begin()) {
|
if (!Storage.begin()) {
|
||||||
Serial.printf("[%lu] [ ] SD card initialization failed\n", millis());
|
LOG_ERR("MAIN", "SD card initialization failed");
|
||||||
setupDisplayAndFonts();
|
setupDisplayAndFonts();
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new FullScreenMessageActivity(renderer, mappedInputManager, "SD card error", EpdFontFamily::BOLD));
|
enterNewActivity(new FullScreenMessageActivity(renderer, mappedInputManager, "SD card error", EpdFontFamily::BOLD));
|
||||||
@@ -304,16 +306,17 @@ void setup() {
|
|||||||
SETTINGS.loadFromFile();
|
SETTINGS.loadFromFile();
|
||||||
KOREADER_STORE.loadFromFile();
|
KOREADER_STORE.loadFromFile();
|
||||||
UITheme::getInstance().reload();
|
UITheme::getInstance().reload();
|
||||||
|
ButtonNavigator::setMappedInputManager(mappedInputManager);
|
||||||
|
|
||||||
switch (gpio.getWakeupReason()) {
|
switch (gpio.getWakeupReason()) {
|
||||||
case HalGPIO::WakeupReason::PowerButton:
|
case HalGPIO::WakeupReason::PowerButton:
|
||||||
// For normal wakeups, verify power button press duration
|
// For normal wakeups, verify power button press duration
|
||||||
Serial.printf("[%lu] [ ] Verifying power button press duration\n", millis());
|
LOG_DBG("MAIN", "Verifying power button press duration");
|
||||||
verifyPowerButtonDuration();
|
verifyPowerButtonDuration();
|
||||||
break;
|
break;
|
||||||
case HalGPIO::WakeupReason::AfterUSBPower:
|
case HalGPIO::WakeupReason::AfterUSBPower:
|
||||||
// If USB power caused a cold boot, go back to sleep
|
// If USB power caused a cold boot, go back to sleep
|
||||||
Serial.printf("[%lu] [ ] Wakeup reason: After USB Power\n", millis());
|
LOG_DBG("MAIN", "Wakeup reason: After USB Power");
|
||||||
gpio.startDeepSleep();
|
gpio.startDeepSleep();
|
||||||
break;
|
break;
|
||||||
case HalGPIO::WakeupReason::AfterFlash:
|
case HalGPIO::WakeupReason::AfterFlash:
|
||||||
@@ -324,7 +327,7 @@ void setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// First serial output only here to avoid timing inconsistencies for power button press duration verification
|
// 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());
|
LOG_DBG("MAIN", "Starting CrossPoint version " CROSSPOINT_VERSION);
|
||||||
|
|
||||||
setupDisplayAndFonts();
|
setupDisplayAndFonts();
|
||||||
|
|
||||||
@@ -362,11 +365,27 @@ void loop() {
|
|||||||
renderer.setFadingFix(SETTINGS.fadingFix);
|
renderer.setFadingFix(SETTINGS.fadingFix);
|
||||||
|
|
||||||
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(),
|
LOG_INF("MEM", "Free: %d bytes, Total: %d bytes, Min Free: %d bytes", ESP.getFreeHeap(), ESP.getHeapSize(),
|
||||||
ESP.getHeapSize(), ESP.getMinFreeHeap());
|
ESP.getMinFreeHeap());
|
||||||
lastMemPrint = millis();
|
lastMemPrint = millis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle incoming serial commands,
|
||||||
|
// nb: we use logSerial from logging to avoid deprecation warnings
|
||||||
|
if (logSerial.available() > 0) {
|
||||||
|
String line = logSerial.readStringUntil('\n');
|
||||||
|
if (line.startsWith("CMD:")) {
|
||||||
|
String cmd = line.substring(4);
|
||||||
|
cmd.trim();
|
||||||
|
if (cmd == "SCREENSHOT") {
|
||||||
|
logSerial.printf("SCREENSHOT_START:%d\n", HalDisplay::BUFFER_SIZE);
|
||||||
|
uint8_t* buf = display.getFrameBuffer();
|
||||||
|
logSerial.write(buf, HalDisplay::BUFFER_SIZE);
|
||||||
|
logSerial.printf("SCREENSHOT_END\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check for any user activity (button press or release) or active background work
|
// Check for any user activity (button press or release) or active background work
|
||||||
static unsigned long lastActivityTime = millis();
|
static unsigned long lastActivityTime = millis();
|
||||||
if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || (currentActivity && currentActivity->preventAutoSleep())) {
|
if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || (currentActivity && currentActivity->preventAutoSleep())) {
|
||||||
@@ -375,7 +394,7 @@ void loop() {
|
|||||||
|
|
||||||
const unsigned long sleepTimeoutMs = SETTINGS.getSleepTimeoutMs();
|
const unsigned long sleepTimeoutMs = SETTINGS.getSleepTimeoutMs();
|
||||||
if (millis() - lastActivityTime >= sleepTimeoutMs) {
|
if (millis() - lastActivityTime >= sleepTimeoutMs) {
|
||||||
Serial.printf("[%lu] [SLP] Auto-sleep triggered after %lu ms of inactivity\n", millis(), sleepTimeoutMs);
|
LOG_DBG("SLP", "Auto-sleep triggered after %lu ms of inactivity", sleepTimeoutMs);
|
||||||
enterDeepSleep();
|
enterDeepSleep();
|
||||||
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start
|
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start
|
||||||
return;
|
return;
|
||||||
@@ -397,8 +416,7 @@ void loop() {
|
|||||||
if (loopDuration > maxLoopDuration) {
|
if (loopDuration > maxLoopDuration) {
|
||||||
maxLoopDuration = loopDuration;
|
maxLoopDuration = loopDuration;
|
||||||
if (maxLoopDuration > 50) {
|
if (maxLoopDuration > 50) {
|
||||||
Serial.printf("[%lu] [LOOP] New max loop duration: %lu ms (activity: %lu ms)\n", millis(), maxLoopDuration,
|
LOG_DBG("LOOP", "New max loop duration: %lu ms (activity: %lu ms)", maxLoopDuration, activityDuration);
|
||||||
activityDuration);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,6 +426,13 @@ void loop() {
|
|||||||
if (currentActivity && currentActivity->skipLoopDelay()) {
|
if (currentActivity && currentActivity->skipLoopDelay()) {
|
||||||
yield(); // Give FreeRTOS a chance to run tasks, but return immediately
|
yield(); // Give FreeRTOS a chance to run tasks, but return immediately
|
||||||
} else {
|
} else {
|
||||||
delay(10); // Normal delay when no activity requires fast response
|
static constexpr unsigned long IDLE_POWER_SAVING_MS = 3000; // 3 seconds
|
||||||
|
if (millis() - lastActivityTime >= IDLE_POWER_SAVING_MS) {
|
||||||
|
// If we've been inactive for a while, increase the delay to save power
|
||||||
|
delay(50);
|
||||||
|
} else {
|
||||||
|
// Short delay to prevent tight loop while still being responsive
|
||||||
|
delay(10);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,18 @@
|
|||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include <Epub.h>
|
#include <Epub.h>
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
|
#include <Logging.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <esp_task_wdt.h>
|
#include <esp_task_wdt.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "CrossPointSettings.h"
|
||||||
|
#include "SettingsList.h"
|
||||||
#include "html/FilesPageHtml.generated.h"
|
#include "html/FilesPageHtml.generated.h"
|
||||||
#include "html/HomePageHtml.generated.h"
|
#include "html/HomePageHtml.generated.h"
|
||||||
|
#include "html/SettingsPageHtml.generated.h"
|
||||||
#include "util/StringUtils.h"
|
#include "util/StringUtils.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -41,7 +45,7 @@ void clearEpubCacheIfNeeded(const String& filePath) {
|
|||||||
// Only clear cache for .epub files
|
// Only clear cache for .epub files
|
||||||
if (StringUtils::checkFileExtension(filePath, ".epub")) {
|
if (StringUtils::checkFileExtension(filePath, ".epub")) {
|
||||||
Epub(filePath.c_str(), "/.crosspoint").clearCache();
|
Epub(filePath.c_str(), "/.crosspoint").clearCache();
|
||||||
Serial.printf("[%lu] [WEB] Cleared epub cache for: %s\n", millis(), filePath.c_str());
|
LOG_DBG("WEB", "Cleared epub cache for: %s", filePath.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +90,7 @@ CrossPointWebServer::~CrossPointWebServer() { stop(); }
|
|||||||
|
|
||||||
void CrossPointWebServer::begin() {
|
void CrossPointWebServer::begin() {
|
||||||
if (running) {
|
if (running) {
|
||||||
Serial.printf("[%lu] [WEB] Web server already running\n", millis());
|
LOG_DBG("WEB", "Web server already running");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,18 +100,17 @@ void CrossPointWebServer::begin() {
|
|||||||
const bool isInApMode = (wifiMode & WIFI_MODE_AP) && (WiFi.softAPgetStationNum() >= 0); // AP is running
|
const bool isInApMode = (wifiMode & WIFI_MODE_AP) && (WiFi.softAPgetStationNum() >= 0); // AP is running
|
||||||
|
|
||||||
if (!isStaConnected && !isInApMode) {
|
if (!isStaConnected && !isInApMode) {
|
||||||
Serial.printf("[%lu] [WEB] Cannot start webserver - no valid network (mode=%d, status=%d)\n", millis(), wifiMode,
|
LOG_DBG("WEB", "Cannot start webserver - no valid network (mode=%d, status=%d)", wifiMode, WiFi.status());
|
||||||
WiFi.status());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store AP mode flag for later use (e.g., in handleStatus)
|
// Store AP mode flag for later use (e.g., in handleStatus)
|
||||||
apMode = isInApMode;
|
apMode = isInApMode;
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] [MEM] Free heap before begin: %d bytes\n", millis(), ESP.getFreeHeap());
|
LOG_DBG("WEB", "[MEM] Free heap before begin: %d bytes", ESP.getFreeHeap());
|
||||||
Serial.printf("[%lu] [WEB] Network mode: %s\n", millis(), apMode ? "AP" : "STA");
|
LOG_DBG("WEB", "Network mode: %s", apMode ? "AP" : "STA");
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] Creating web server on port %d...\n", millis(), port);
|
LOG_DBG("WEB", "Creating web server on port %d...", port);
|
||||||
server.reset(new WebServer(port));
|
server.reset(new WebServer(port));
|
||||||
|
|
||||||
// Disable WiFi sleep to improve responsiveness and prevent 'unreachable' errors.
|
// Disable WiFi sleep to improve responsiveness and prevent 'unreachable' errors.
|
||||||
@@ -117,15 +120,15 @@ void CrossPointWebServer::begin() {
|
|||||||
// Note: WebServer class doesn't have setNoDelay() in the standard ESP32 library.
|
// Note: WebServer class doesn't have setNoDelay() in the standard ESP32 library.
|
||||||
// We rely on disabling WiFi sleep for responsiveness.
|
// We rely on disabling WiFi sleep for responsiveness.
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] [MEM] Free heap after WebServer allocation: %d bytes\n", millis(), ESP.getFreeHeap());
|
LOG_DBG("WEB", "[MEM] Free heap after WebServer allocation: %d bytes", ESP.getFreeHeap());
|
||||||
|
|
||||||
if (!server) {
|
if (!server) {
|
||||||
Serial.printf("[%lu] [WEB] Failed to create WebServer!\n", millis());
|
LOG_ERR("WEB", "Failed to create WebServer!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup routes
|
// Setup routes
|
||||||
Serial.printf("[%lu] [WEB] Setting up routes...\n", millis());
|
LOG_DBG("WEB", "Setting up routes...");
|
||||||
server->on("/", HTTP_GET, [this] { handleRoot(); });
|
server->on("/", HTTP_GET, [this] { handleRoot(); });
|
||||||
server->on("/files", HTTP_GET, [this] { handleFileList(); });
|
server->on("/files", HTTP_GET, [this] { handleFileList(); });
|
||||||
|
|
||||||
@@ -148,44 +151,47 @@ void CrossPointWebServer::begin() {
|
|||||||
// Delete file/folder endpoint
|
// Delete file/folder endpoint
|
||||||
server->on("/delete", HTTP_POST, [this] { handleDelete(); });
|
server->on("/delete", HTTP_POST, [this] { handleDelete(); });
|
||||||
|
|
||||||
|
// Settings endpoints
|
||||||
|
server->on("/settings", HTTP_GET, [this] { handleSettingsPage(); });
|
||||||
|
server->on("/api/settings", HTTP_GET, [this] { handleGetSettings(); });
|
||||||
|
server->on("/api/settings", HTTP_POST, [this] { handlePostSettings(); });
|
||||||
|
|
||||||
server->onNotFound([this] { handleNotFound(); });
|
server->onNotFound([this] { handleNotFound(); });
|
||||||
Serial.printf("[%lu] [WEB] [MEM] Free heap after route setup: %d bytes\n", millis(), ESP.getFreeHeap());
|
LOG_DBG("WEB", "[MEM] Free heap after route setup: %d bytes", ESP.getFreeHeap());
|
||||||
|
|
||||||
server->begin();
|
server->begin();
|
||||||
|
|
||||||
// Start WebSocket server for fast binary uploads
|
// Start WebSocket server for fast binary uploads
|
||||||
Serial.printf("[%lu] [WEB] Starting WebSocket server on port %d...\n", millis(), wsPort);
|
LOG_DBG("WEB", "Starting WebSocket server on port %d...", wsPort);
|
||||||
wsServer.reset(new WebSocketsServer(wsPort));
|
wsServer.reset(new WebSocketsServer(wsPort));
|
||||||
wsInstance = const_cast<CrossPointWebServer*>(this);
|
wsInstance = const_cast<CrossPointWebServer*>(this);
|
||||||
wsServer->begin();
|
wsServer->begin();
|
||||||
wsServer->onEvent(wsEventCallback);
|
wsServer->onEvent(wsEventCallback);
|
||||||
Serial.printf("[%lu] [WEB] WebSocket server started\n", millis());
|
LOG_DBG("WEB", "WebSocket server started");
|
||||||
|
|
||||||
udpActive = udp.begin(LOCAL_UDP_PORT);
|
udpActive = udp.begin(LOCAL_UDP_PORT);
|
||||||
Serial.printf("[%lu] [WEB] Discovery UDP %s on port %d\n", millis(), udpActive ? "enabled" : "failed",
|
LOG_DBG("WEB", "Discovery UDP %s on port %d", udpActive ? "enabled" : "failed", LOCAL_UDP_PORT);
|
||||||
LOCAL_UDP_PORT);
|
|
||||||
|
|
||||||
running = true;
|
running = true;
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] Web server started on port %d\n", millis(), port);
|
LOG_DBG("WEB", "Web server started on port %d", port);
|
||||||
// Show the correct IP based on network mode
|
// Show the correct IP based on network mode
|
||||||
const String ipAddr = apMode ? WiFi.softAPIP().toString() : WiFi.localIP().toString();
|
const String ipAddr = apMode ? WiFi.softAPIP().toString() : WiFi.localIP().toString();
|
||||||
Serial.printf("[%lu] [WEB] Access at http://%s/\n", millis(), ipAddr.c_str());
|
LOG_DBG("WEB", "Access at http://%s/", ipAddr.c_str());
|
||||||
Serial.printf("[%lu] [WEB] WebSocket at ws://%s:%d/\n", millis(), ipAddr.c_str(), wsPort);
|
LOG_DBG("WEB", "WebSocket at ws://%s:%d/", ipAddr.c_str(), wsPort);
|
||||||
Serial.printf("[%lu] [WEB] [MEM] Free heap after server.begin(): %d bytes\n", millis(), ESP.getFreeHeap());
|
LOG_DBG("WEB", "[MEM] Free heap after server.begin(): %d bytes", ESP.getFreeHeap());
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServer::stop() {
|
void CrossPointWebServer::stop() {
|
||||||
if (!running || !server) {
|
if (!running || !server) {
|
||||||
Serial.printf("[%lu] [WEB] stop() called but already stopped (running=%d, server=%p)\n", millis(), running,
|
LOG_DBG("WEB", "stop() called but already stopped (running=%d, server=%p)", running, server.get());
|
||||||
server.get());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] STOP INITIATED - setting running=false first\n", millis());
|
LOG_DBG("WEB", "STOP INITIATED - setting running=false first");
|
||||||
running = false; // Set this FIRST to prevent handleClient from using server
|
running = false; // Set this FIRST to prevent handleClient from using server
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] [MEM] Free heap before stop: %d bytes\n", millis(), ESP.getFreeHeap());
|
LOG_DBG("WEB", "[MEM] Free heap before stop: %d bytes", ESP.getFreeHeap());
|
||||||
|
|
||||||
// Close any in-progress WebSocket upload
|
// Close any in-progress WebSocket upload
|
||||||
if (wsUploadInProgress && wsUploadFile) {
|
if (wsUploadInProgress && wsUploadFile) {
|
||||||
@@ -195,11 +201,11 @@ void CrossPointWebServer::stop() {
|
|||||||
|
|
||||||
// Stop WebSocket server
|
// Stop WebSocket server
|
||||||
if (wsServer) {
|
if (wsServer) {
|
||||||
Serial.printf("[%lu] [WEB] Stopping WebSocket server...\n", millis());
|
LOG_DBG("WEB", "Stopping WebSocket server...");
|
||||||
wsServer->close();
|
wsServer->close();
|
||||||
wsServer.reset();
|
wsServer.reset();
|
||||||
wsInstance = nullptr;
|
wsInstance = nullptr;
|
||||||
Serial.printf("[%lu] [WEB] WebSocket server stopped\n", millis());
|
LOG_DBG("WEB", "WebSocket server stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (udpActive) {
|
if (udpActive) {
|
||||||
@@ -211,18 +217,18 @@ void CrossPointWebServer::stop() {
|
|||||||
delay(20);
|
delay(20);
|
||||||
|
|
||||||
server->stop();
|
server->stop();
|
||||||
Serial.printf("[%lu] [WEB] [MEM] Free heap after server->stop(): %d bytes\n", millis(), ESP.getFreeHeap());
|
LOG_DBG("WEB", "[MEM] Free heap after server->stop(): %d bytes", ESP.getFreeHeap());
|
||||||
|
|
||||||
// Brief delay before deletion
|
// Brief delay before deletion
|
||||||
delay(10);
|
delay(10);
|
||||||
|
|
||||||
server.reset();
|
server.reset();
|
||||||
Serial.printf("[%lu] [WEB] Web server stopped and deleted\n", millis());
|
LOG_DBG("WEB", "Web server stopped and deleted");
|
||||||
Serial.printf("[%lu] [WEB] [MEM] Free heap after delete server: %d bytes\n", millis(), ESP.getFreeHeap());
|
LOG_DBG("WEB", "[MEM] Free heap after delete server: %d bytes", ESP.getFreeHeap());
|
||||||
|
|
||||||
// Note: Static upload variables (uploadFileName, uploadPath, uploadError) are declared
|
// Note: Static upload variables (uploadFileName, uploadPath, uploadError) are declared
|
||||||
// later in the file and will be cleared when they go out of scope or on next upload
|
// later in the file and will be cleared when they go out of scope or on next upload
|
||||||
Serial.printf("[%lu] [WEB] [MEM] Free heap final: %d bytes\n", millis(), ESP.getFreeHeap());
|
LOG_DBG("WEB", "[MEM] Free heap final: %d bytes", ESP.getFreeHeap());
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServer::handleClient() {
|
void CrossPointWebServer::handleClient() {
|
||||||
@@ -235,13 +241,13 @@ void CrossPointWebServer::handleClient() {
|
|||||||
|
|
||||||
// Double-check server pointer is valid
|
// Double-check server pointer is valid
|
||||||
if (!server) {
|
if (!server) {
|
||||||
Serial.printf("[%lu] [WEB] WARNING: handleClient called with null server!\n", millis());
|
LOG_DBG("WEB", "WARNING: handleClient called with null server!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print debug every 10 seconds to confirm handleClient is being called
|
// Print debug every 10 seconds to confirm handleClient is being called
|
||||||
if (millis() - lastDebugPrint > 10000) {
|
if (millis() - lastDebugPrint > 10000) {
|
||||||
Serial.printf("[%lu] [WEB] handleClient active, server running on port %d\n", millis(), port);
|
LOG_DBG("WEB", "handleClient active, server running on port %d", port);
|
||||||
lastDebugPrint = millis();
|
lastDebugPrint = millis();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,9 +293,14 @@ CrossPointWebServer::WsUploadStatus CrossPointWebServer::getWsUploadStatus() con
|
|||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void sendHtmlContent(WebServer* server, const char* data, size_t len) {
|
||||||
|
server->sendHeader("Content-Encoding", "gzip");
|
||||||
|
server->send_P(200, "text/html", data, len);
|
||||||
|
}
|
||||||
|
|
||||||
void CrossPointWebServer::handleRoot() const {
|
void CrossPointWebServer::handleRoot() const {
|
||||||
server->send(200, "text/html", HomePageHtml);
|
sendHtmlContent(server.get(), HomePageHtml, sizeof(HomePageHtml));
|
||||||
Serial.printf("[%lu] [WEB] Served root page\n", millis());
|
LOG_DBG("WEB", "Served root page");
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServer::handleNotFound() const {
|
void CrossPointWebServer::handleNotFound() const {
|
||||||
@@ -316,19 +327,19 @@ void CrossPointWebServer::handleStatus() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServer::scanFiles(const char* path, const std::function<void(FileInfo)>& callback) const {
|
void CrossPointWebServer::scanFiles(const char* path, const std::function<void(FileInfo)>& callback) const {
|
||||||
FsFile root = SdMan.open(path);
|
FsFile root = Storage.open(path);
|
||||||
if (!root) {
|
if (!root) {
|
||||||
Serial.printf("[%lu] [WEB] Failed to open directory: %s\n", millis(), path);
|
LOG_DBG("WEB", "Failed to open directory: %s", path);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!root.isDirectory()) {
|
if (!root.isDirectory()) {
|
||||||
Serial.printf("[%lu] [WEB] Not a directory: %s\n", millis(), path);
|
LOG_DBG("WEB", "Not a directory: %s", path);
|
||||||
root.close();
|
root.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] Scanning files in: %s\n", millis(), path);
|
LOG_DBG("WEB", "Scanning files in: %s", path);
|
||||||
|
|
||||||
FsFile file = root.openNextFile();
|
FsFile file = root.openNextFile();
|
||||||
char name[500];
|
char name[500];
|
||||||
@@ -379,7 +390,9 @@ bool CrossPointWebServer::isEpubFile(const String& filename) const {
|
|||||||
return lower.endsWith(".epub");
|
return lower.endsWith(".epub");
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServer::handleFileList() const { server->send(200, "text/html", FilesPageHtml); }
|
void CrossPointWebServer::handleFileList() const {
|
||||||
|
sendHtmlContent(server.get(), FilesPageHtml, sizeof(FilesPageHtml));
|
||||||
|
}
|
||||||
|
|
||||||
void CrossPointWebServer::handleFileListData() const {
|
void CrossPointWebServer::handleFileListData() const {
|
||||||
// Get current path from query string (default to root)
|
// Get current path from query string (default to root)
|
||||||
@@ -414,7 +427,7 @@ void CrossPointWebServer::handleFileListData() const {
|
|||||||
const size_t written = serializeJson(doc, output, outputSize);
|
const size_t written = serializeJson(doc, output, outputSize);
|
||||||
if (written >= outputSize) {
|
if (written >= outputSize) {
|
||||||
// JSON output truncated; skip this entry to avoid sending malformed JSON
|
// JSON output truncated; skip this entry to avoid sending malformed JSON
|
||||||
Serial.printf("[%lu] [WEB] Skipping file entry with oversized JSON for name: %s\n", millis(), info.name.c_str());
|
LOG_DBG("WEB", "Skipping file entry with oversized JSON for name: %s", info.name.c_str());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,7 +441,7 @@ void CrossPointWebServer::handleFileListData() const {
|
|||||||
server->sendContent("]");
|
server->sendContent("]");
|
||||||
// End of streamed response, empty chunk to signal client
|
// End of streamed response, empty chunk to signal client
|
||||||
server->sendContent("");
|
server->sendContent("");
|
||||||
Serial.printf("[%lu] [WEB] Served file listing page for path: %s\n", millis(), currentPath.c_str());
|
LOG_DBG("WEB", "Served file listing page for path: %s", currentPath.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServer::handleDownload() const {
|
void CrossPointWebServer::handleDownload() const {
|
||||||
@@ -458,12 +471,12 @@ void CrossPointWebServer::handleDownload() const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SdMan.exists(itemPath.c_str())) {
|
if (!Storage.exists(itemPath.c_str())) {
|
||||||
server->send(404, "text/plain", "Item not found");
|
server->send(404, "text/plain", "Item not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
FsFile file = SdMan.open(itemPath.c_str());
|
FsFile file = Storage.open(itemPath.c_str());
|
||||||
if (!file) {
|
if (!file) {
|
||||||
server->send(500, "text/plain", "Failed to open file");
|
server->send(500, "text/plain", "Failed to open file");
|
||||||
return;
|
return;
|
||||||
@@ -509,8 +522,7 @@ static bool flushUploadBuffer(CrossPointWebServer::UploadState& state) {
|
|||||||
esp_task_wdt_reset(); // Reset watchdog after SD write
|
esp_task_wdt_reset(); // Reset watchdog after SD write
|
||||||
|
|
||||||
if (written != state.bufferPos) {
|
if (written != state.bufferPos) {
|
||||||
Serial.printf("[%lu] [WEB] [UPLOAD] Buffer flush failed: expected %d, wrote %d\n", millis(), state.bufferPos,
|
LOG_DBG("WEB", "[UPLOAD] Buffer flush failed: expected %d, wrote %d", state.bufferPos, written);
|
||||||
written);
|
|
||||||
state.bufferPos = 0;
|
state.bufferPos = 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -527,7 +539,7 @@ void CrossPointWebServer::handleUpload(UploadState& state) const {
|
|||||||
|
|
||||||
// Safety check: ensure server is still valid
|
// Safety check: ensure server is still valid
|
||||||
if (!running || !server) {
|
if (!running || !server) {
|
||||||
Serial.printf("[%lu] [WEB] [UPLOAD] ERROR: handleUpload called but server not running!\n", millis());
|
LOG_DBG("WEB", "[UPLOAD] ERROR: handleUpload called but server not running!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,8 +576,8 @@ void CrossPointWebServer::handleUpload(UploadState& state) const {
|
|||||||
state.path = "/";
|
state.path = "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] [UPLOAD] START: %s to path: %s\n", millis(), state.fileName.c_str(), state.path.c_str());
|
LOG_DBG("WEB", "[UPLOAD] START: %s to path: %s", state.fileName.c_str(), state.path.c_str());
|
||||||
Serial.printf("[%lu] [WEB] [UPLOAD] Free heap: %d bytes\n", millis(), ESP.getFreeHeap());
|
LOG_DBG("WEB", "[UPLOAD] Free heap: %d bytes", ESP.getFreeHeap());
|
||||||
|
|
||||||
// Create file path
|
// Create file path
|
||||||
String filePath = state.path;
|
String filePath = state.path;
|
||||||
@@ -574,22 +586,22 @@ void CrossPointWebServer::handleUpload(UploadState& state) const {
|
|||||||
|
|
||||||
// Check if file already exists - SD operations can be slow
|
// Check if file already exists - SD operations can be slow
|
||||||
esp_task_wdt_reset();
|
esp_task_wdt_reset();
|
||||||
if (SdMan.exists(filePath.c_str())) {
|
if (Storage.exists(filePath.c_str())) {
|
||||||
Serial.printf("[%lu] [WEB] [UPLOAD] Overwriting existing file: %s\n", millis(), filePath.c_str());
|
LOG_DBG("WEB", "[UPLOAD] Overwriting existing file: %s", filePath.c_str());
|
||||||
esp_task_wdt_reset();
|
esp_task_wdt_reset();
|
||||||
SdMan.remove(filePath.c_str());
|
Storage.remove(filePath.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open file for writing - this can be slow due to FAT cluster allocation
|
// Open file for writing - this can be slow due to FAT cluster allocation
|
||||||
esp_task_wdt_reset();
|
esp_task_wdt_reset();
|
||||||
if (!SdMan.openFileForWrite("WEB", filePath, state.file)) {
|
if (!Storage.openFileForWrite("WEB", filePath, state.file)) {
|
||||||
state.error = "Failed to create file on SD card";
|
state.error = "Failed to create file on SD card";
|
||||||
Serial.printf("[%lu] [WEB] [UPLOAD] FAILED to create file: %s\n", millis(), filePath.c_str());
|
LOG_DBG("WEB", "[UPLOAD] FAILED to create file: %s", filePath.c_str());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
esp_task_wdt_reset();
|
esp_task_wdt_reset();
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] [UPLOAD] File created successfully: %s\n", millis(), filePath.c_str());
|
LOG_DBG("WEB", "[UPLOAD] File created successfully: %s", filePath.c_str());
|
||||||
} else if (upload.status == UPLOAD_FILE_WRITE) {
|
} else if (upload.status == UPLOAD_FILE_WRITE) {
|
||||||
if (state.file && state.error.isEmpty()) {
|
if (state.file && state.error.isEmpty()) {
|
||||||
// Buffer incoming data and flush when buffer is full
|
// Buffer incoming data and flush when buffer is full
|
||||||
@@ -622,8 +634,8 @@ void CrossPointWebServer::handleUpload(UploadState& state) const {
|
|||||||
if (state.size - lastLoggedSize >= 102400) {
|
if (state.size - lastLoggedSize >= 102400) {
|
||||||
const unsigned long elapsed = millis() - uploadStartTime;
|
const unsigned long elapsed = millis() - uploadStartTime;
|
||||||
const float kbps = (elapsed > 0) ? (state.size / 1024.0) / (elapsed / 1000.0) : 0;
|
const float kbps = (elapsed > 0) ? (state.size / 1024.0) / (elapsed / 1000.0) : 0;
|
||||||
Serial.printf("[%lu] [WEB] [UPLOAD] %d bytes (%.1f KB), %.1f KB/s, %d writes\n", millis(), state.size,
|
LOG_DBG("WEB", "[UPLOAD] %d bytes (%.1f KB), %.1f KB/s, %d writes", state.size, state.size / 1024.0, kbps,
|
||||||
state.size / 1024.0, kbps, writeCount);
|
writeCount);
|
||||||
lastLoggedSize = state.size;
|
lastLoggedSize = state.size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -640,10 +652,10 @@ void CrossPointWebServer::handleUpload(UploadState& state) const {
|
|||||||
const unsigned long elapsed = millis() - uploadStartTime;
|
const unsigned long elapsed = millis() - uploadStartTime;
|
||||||
const float avgKbps = (elapsed > 0) ? (state.size / 1024.0) / (elapsed / 1000.0) : 0;
|
const float avgKbps = (elapsed > 0) ? (state.size / 1024.0) / (elapsed / 1000.0) : 0;
|
||||||
const float writePercent = (elapsed > 0) ? (totalWriteTime * 100.0 / elapsed) : 0;
|
const float writePercent = (elapsed > 0) ? (totalWriteTime * 100.0 / elapsed) : 0;
|
||||||
Serial.printf("[%lu] [WEB] [UPLOAD] Complete: %s (%d bytes in %lu ms, avg %.1f KB/s)\n", millis(),
|
LOG_DBG("WEB", "[UPLOAD] Complete: %s (%d bytes in %lu ms, avg %.1f KB/s)", state.fileName.c_str(), state.size,
|
||||||
state.fileName.c_str(), state.size, elapsed, avgKbps);
|
elapsed, avgKbps);
|
||||||
Serial.printf("[%lu] [WEB] [UPLOAD] Diagnostics: %d writes, total write time: %lu ms (%.1f%%)\n", millis(),
|
LOG_DBG("WEB", "[UPLOAD] Diagnostics: %d writes, total write time: %lu ms (%.1f%%)", writeCount, totalWriteTime,
|
||||||
writeCount, totalWriteTime, writePercent);
|
writePercent);
|
||||||
|
|
||||||
// Clear epub cache to prevent stale metadata issues when overwriting files
|
// Clear epub cache to prevent stale metadata issues when overwriting files
|
||||||
String filePath = state.path;
|
String filePath = state.path;
|
||||||
@@ -660,10 +672,10 @@ void CrossPointWebServer::handleUpload(UploadState& state) const {
|
|||||||
String filePath = state.path;
|
String filePath = state.path;
|
||||||
if (!filePath.endsWith("/")) filePath += "/";
|
if (!filePath.endsWith("/")) filePath += "/";
|
||||||
filePath += state.fileName;
|
filePath += state.fileName;
|
||||||
SdMan.remove(filePath.c_str());
|
Storage.remove(filePath.c_str());
|
||||||
}
|
}
|
||||||
state.error = "Upload aborted";
|
state.error = "Upload aborted";
|
||||||
Serial.printf("[%lu] [WEB] Upload aborted\n", millis());
|
LOG_DBG("WEB", "Upload aborted");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -708,20 +720,20 @@ void CrossPointWebServer::handleCreateFolder() const {
|
|||||||
if (!folderPath.endsWith("/")) folderPath += "/";
|
if (!folderPath.endsWith("/")) folderPath += "/";
|
||||||
folderPath += folderName;
|
folderPath += folderName;
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] Creating folder: %s\n", millis(), folderPath.c_str());
|
LOG_DBG("WEB", "Creating folder: %s", folderPath.c_str());
|
||||||
|
|
||||||
// Check if already exists
|
// Check if already exists
|
||||||
if (SdMan.exists(folderPath.c_str())) {
|
if (Storage.exists(folderPath.c_str())) {
|
||||||
server->send(400, "text/plain", "Folder already exists");
|
server->send(400, "text/plain", "Folder already exists");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the folder
|
// Create the folder
|
||||||
if (SdMan.mkdir(folderPath.c_str())) {
|
if (Storage.mkdir(folderPath.c_str())) {
|
||||||
Serial.printf("[%lu] [WEB] Folder created successfully: %s\n", millis(), folderPath.c_str());
|
LOG_DBG("WEB", "Folder created successfully: %s", folderPath.c_str());
|
||||||
server->send(200, "text/plain", "Folder created: " + folderName);
|
server->send(200, "text/plain", "Folder created: " + folderName);
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [WEB] Failed to create folder: %s\n", millis(), folderPath.c_str());
|
LOG_DBG("WEB", "Failed to create folder: %s", folderPath.c_str());
|
||||||
server->send(500, "text/plain", "Failed to create folder");
|
server->send(500, "text/plain", "Failed to create folder");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -763,12 +775,12 @@ void CrossPointWebServer::handleRename() const {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SdMan.exists(itemPath.c_str())) {
|
if (!Storage.exists(itemPath.c_str())) {
|
||||||
server->send(404, "text/plain", "Item not found");
|
server->send(404, "text/plain", "Item not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
FsFile file = SdMan.open(itemPath.c_str());
|
FsFile file = Storage.open(itemPath.c_str());
|
||||||
if (!file) {
|
if (!file) {
|
||||||
server->send(500, "text/plain", "Failed to open file");
|
server->send(500, "text/plain", "Failed to open file");
|
||||||
return;
|
return;
|
||||||
@@ -789,7 +801,7 @@ void CrossPointWebServer::handleRename() const {
|
|||||||
}
|
}
|
||||||
newPath += newName;
|
newPath += newName;
|
||||||
|
|
||||||
if (SdMan.exists(newPath.c_str())) {
|
if (Storage.exists(newPath.c_str())) {
|
||||||
file.close();
|
file.close();
|
||||||
server->send(409, "text/plain", "Target already exists");
|
server->send(409, "text/plain", "Target already exists");
|
||||||
return;
|
return;
|
||||||
@@ -800,10 +812,10 @@ void CrossPointWebServer::handleRename() const {
|
|||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
Serial.printf("[%lu] [WEB] Renamed file: %s -> %s\n", millis(), itemPath.c_str(), newPath.c_str());
|
LOG_DBG("WEB", "Renamed file: %s -> %s", itemPath.c_str(), newPath.c_str());
|
||||||
server->send(200, "text/plain", "Renamed successfully");
|
server->send(200, "text/plain", "Renamed successfully");
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [WEB] Failed to rename file: %s -> %s\n", millis(), itemPath.c_str(), newPath.c_str());
|
LOG_ERR("WEB", "Failed to rename file: %s -> %s", itemPath.c_str(), newPath.c_str());
|
||||||
server->send(500, "text/plain", "Failed to rename file");
|
server->send(500, "text/plain", "Failed to rename file");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -839,12 +851,12 @@ void CrossPointWebServer::handleMove() const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SdMan.exists(itemPath.c_str())) {
|
if (!Storage.exists(itemPath.c_str())) {
|
||||||
server->send(404, "text/plain", "Item not found");
|
server->send(404, "text/plain", "Item not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
FsFile file = SdMan.open(itemPath.c_str());
|
FsFile file = Storage.open(itemPath.c_str());
|
||||||
if (!file) {
|
if (!file) {
|
||||||
server->send(500, "text/plain", "Failed to open file");
|
server->send(500, "text/plain", "Failed to open file");
|
||||||
return;
|
return;
|
||||||
@@ -855,12 +867,12 @@ void CrossPointWebServer::handleMove() const {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SdMan.exists(destPath.c_str())) {
|
if (!Storage.exists(destPath.c_str())) {
|
||||||
file.close();
|
file.close();
|
||||||
server->send(404, "text/plain", "Destination not found");
|
server->send(404, "text/plain", "Destination not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
FsFile destDir = SdMan.open(destPath.c_str());
|
FsFile destDir = Storage.open(destPath.c_str());
|
||||||
if (!destDir || !destDir.isDirectory()) {
|
if (!destDir || !destDir.isDirectory()) {
|
||||||
if (destDir) {
|
if (destDir) {
|
||||||
destDir.close();
|
destDir.close();
|
||||||
@@ -882,7 +894,7 @@ void CrossPointWebServer::handleMove() const {
|
|||||||
server->send(200, "text/plain", "Already in destination");
|
server->send(200, "text/plain", "Already in destination");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (SdMan.exists(newPath.c_str())) {
|
if (Storage.exists(newPath.c_str())) {
|
||||||
file.close();
|
file.close();
|
||||||
server->send(409, "text/plain", "Target already exists");
|
server->send(409, "text/plain", "Target already exists");
|
||||||
return;
|
return;
|
||||||
@@ -893,10 +905,10 @@ void CrossPointWebServer::handleMove() const {
|
|||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
Serial.printf("[%lu] [WEB] Moved file: %s -> %s\n", millis(), itemPath.c_str(), newPath.c_str());
|
LOG_DBG("WEB", "Moved file: %s -> %s", itemPath.c_str(), newPath.c_str());
|
||||||
server->send(200, "text/plain", "Moved successfully");
|
server->send(200, "text/plain", "Moved successfully");
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [WEB] Failed to move file: %s -> %s\n", millis(), itemPath.c_str(), newPath.c_str());
|
LOG_ERR("WEB", "Failed to move file: %s -> %s", itemPath.c_str(), newPath.c_str());
|
||||||
server->send(500, "text/plain", "Failed to move file");
|
server->send(500, "text/plain", "Failed to move file");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -927,7 +939,7 @@ void CrossPointWebServer::handleDelete() const {
|
|||||||
|
|
||||||
// Check if item starts with a dot (hidden/system file)
|
// Check if item starts with a dot (hidden/system file)
|
||||||
if (itemName.startsWith(".")) {
|
if (itemName.startsWith(".")) {
|
||||||
Serial.printf("[%lu] [WEB] Delete rejected - hidden/system item: %s\n", millis(), itemPath.c_str());
|
LOG_DBG("WEB", "Delete rejected - hidden/system item: %s", itemPath.c_str());
|
||||||
server->send(403, "text/plain", "Cannot delete system files");
|
server->send(403, "text/plain", "Cannot delete system files");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -935,26 +947,26 @@ void CrossPointWebServer::handleDelete() const {
|
|||||||
// Check against explicitly protected items
|
// Check against explicitly protected items
|
||||||
for (size_t i = 0; i < HIDDEN_ITEMS_COUNT; i++) {
|
for (size_t i = 0; i < HIDDEN_ITEMS_COUNT; i++) {
|
||||||
if (itemName.equals(HIDDEN_ITEMS[i])) {
|
if (itemName.equals(HIDDEN_ITEMS[i])) {
|
||||||
Serial.printf("[%lu] [WEB] Delete rejected - protected item: %s\n", millis(), itemPath.c_str());
|
LOG_DBG("WEB", "Delete rejected - protected item: %s", itemPath.c_str());
|
||||||
server->send(403, "text/plain", "Cannot delete protected items");
|
server->send(403, "text/plain", "Cannot delete protected items");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if item exists
|
// Check if item exists
|
||||||
if (!SdMan.exists(itemPath.c_str())) {
|
if (!Storage.exists(itemPath.c_str())) {
|
||||||
Serial.printf("[%lu] [WEB] Delete failed - item not found: %s\n", millis(), itemPath.c_str());
|
LOG_DBG("WEB", "Delete failed - item not found: %s", itemPath.c_str());
|
||||||
server->send(404, "text/plain", "Item not found");
|
server->send(404, "text/plain", "Item not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] Attempting to delete %s: %s\n", millis(), itemType.c_str(), itemPath.c_str());
|
LOG_DBG("WEB", "Attempting to delete %s: %s", itemType.c_str(), itemPath.c_str());
|
||||||
|
|
||||||
bool success = false;
|
bool success = false;
|
||||||
|
|
||||||
if (itemType == "folder") {
|
if (itemType == "folder") {
|
||||||
// For folders, try to remove (will fail if not empty)
|
// For folders, try to remove (will fail if not empty)
|
||||||
FsFile dir = SdMan.open(itemPath.c_str());
|
FsFile dir = Storage.open(itemPath.c_str());
|
||||||
if (dir && dir.isDirectory()) {
|
if (dir && dir.isDirectory()) {
|
||||||
// Check if folder is empty
|
// Check if folder is empty
|
||||||
FsFile entry = dir.openNextFile();
|
FsFile entry = dir.openNextFile();
|
||||||
@@ -962,27 +974,189 @@ void CrossPointWebServer::handleDelete() const {
|
|||||||
// Folder is not empty
|
// Folder is not empty
|
||||||
entry.close();
|
entry.close();
|
||||||
dir.close();
|
dir.close();
|
||||||
Serial.printf("[%lu] [WEB] Delete failed - folder not empty: %s\n", millis(), itemPath.c_str());
|
LOG_DBG("WEB", "Delete failed - folder not empty: %s", itemPath.c_str());
|
||||||
server->send(400, "text/plain", "Folder is not empty. Delete contents first.");
|
server->send(400, "text/plain", "Folder is not empty. Delete contents first.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dir.close();
|
dir.close();
|
||||||
}
|
}
|
||||||
success = SdMan.rmdir(itemPath.c_str());
|
success = Storage.rmdir(itemPath.c_str());
|
||||||
} else {
|
} else {
|
||||||
// For files, use remove
|
// For files, use remove
|
||||||
success = SdMan.remove(itemPath.c_str());
|
success = Storage.remove(itemPath.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
Serial.printf("[%lu] [WEB] Successfully deleted: %s\n", millis(), itemPath.c_str());
|
LOG_DBG("WEB", "Successfully deleted: %s", itemPath.c_str());
|
||||||
server->send(200, "text/plain", "Deleted successfully");
|
server->send(200, "text/plain", "Deleted successfully");
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [WEB] Failed to delete: %s\n", millis(), itemPath.c_str());
|
LOG_ERR("WEB", "Failed to delete: %s", itemPath.c_str());
|
||||||
server->send(500, "text/plain", "Failed to delete item");
|
server->send(500, "text/plain", "Failed to delete item");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CrossPointWebServer::handleSettingsPage() const {
|
||||||
|
sendHtmlContent(server.get(), SettingsPageHtml, sizeof(SettingsPageHtml));
|
||||||
|
LOG_DBG("WEB", "Served settings page");
|
||||||
|
}
|
||||||
|
|
||||||
|
void CrossPointWebServer::handleGetSettings() const {
|
||||||
|
auto settings = getSettingsList();
|
||||||
|
|
||||||
|
server->setContentLength(CONTENT_LENGTH_UNKNOWN);
|
||||||
|
server->send(200, "application/json", "");
|
||||||
|
server->sendContent("[");
|
||||||
|
|
||||||
|
char output[512];
|
||||||
|
constexpr size_t outputSize = sizeof(output);
|
||||||
|
bool seenFirst = false;
|
||||||
|
JsonDocument doc;
|
||||||
|
|
||||||
|
for (const auto& s : settings) {
|
||||||
|
if (!s.key) continue; // Skip ACTION-only entries
|
||||||
|
|
||||||
|
doc.clear();
|
||||||
|
doc["key"] = s.key;
|
||||||
|
doc["name"] = s.name;
|
||||||
|
doc["category"] = s.category;
|
||||||
|
|
||||||
|
switch (s.type) {
|
||||||
|
case SettingType::TOGGLE: {
|
||||||
|
doc["type"] = "toggle";
|
||||||
|
if (s.valuePtr) {
|
||||||
|
doc["value"] = static_cast<int>(SETTINGS.*(s.valuePtr));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SettingType::ENUM: {
|
||||||
|
doc["type"] = "enum";
|
||||||
|
if (s.valuePtr) {
|
||||||
|
doc["value"] = static_cast<int>(SETTINGS.*(s.valuePtr));
|
||||||
|
} else if (s.valueGetter) {
|
||||||
|
doc["value"] = static_cast<int>(s.valueGetter());
|
||||||
|
}
|
||||||
|
JsonArray options = doc["options"].to<JsonArray>();
|
||||||
|
for (const auto& opt : s.enumValues) {
|
||||||
|
options.add(opt);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SettingType::VALUE: {
|
||||||
|
doc["type"] = "value";
|
||||||
|
if (s.valuePtr) {
|
||||||
|
doc["value"] = static_cast<int>(SETTINGS.*(s.valuePtr));
|
||||||
|
}
|
||||||
|
doc["min"] = s.valueRange.min;
|
||||||
|
doc["max"] = s.valueRange.max;
|
||||||
|
doc["step"] = s.valueRange.step;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SettingType::STRING: {
|
||||||
|
doc["type"] = "string";
|
||||||
|
if (s.stringGetter) {
|
||||||
|
doc["value"] = s.stringGetter();
|
||||||
|
} else if (s.stringPtr) {
|
||||||
|
doc["value"] = s.stringPtr;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t written = serializeJson(doc, output, outputSize);
|
||||||
|
if (written >= outputSize) {
|
||||||
|
LOG_DBG("WEB", "Skipping oversized setting JSON for: %s", s.key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seenFirst) {
|
||||||
|
server->sendContent(",");
|
||||||
|
} else {
|
||||||
|
seenFirst = true;
|
||||||
|
}
|
||||||
|
server->sendContent(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
server->sendContent("]");
|
||||||
|
server->sendContent("");
|
||||||
|
LOG_DBG("WEB", "Served settings API");
|
||||||
|
}
|
||||||
|
|
||||||
|
void CrossPointWebServer::handlePostSettings() {
|
||||||
|
if (!server->hasArg("plain")) {
|
||||||
|
server->send(400, "text/plain", "Missing JSON body");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const String body = server->arg("plain");
|
||||||
|
JsonDocument doc;
|
||||||
|
const DeserializationError err = deserializeJson(doc, body);
|
||||||
|
if (err) {
|
||||||
|
server->send(400, "text/plain", String("Invalid JSON: ") + err.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto settings = getSettingsList();
|
||||||
|
int applied = 0;
|
||||||
|
|
||||||
|
for (auto& s : settings) {
|
||||||
|
if (!s.key) continue;
|
||||||
|
if (!doc[s.key].is<JsonVariant>()) continue;
|
||||||
|
|
||||||
|
switch (s.type) {
|
||||||
|
case SettingType::TOGGLE: {
|
||||||
|
const int val = doc[s.key].as<int>() ? 1 : 0;
|
||||||
|
if (s.valuePtr) {
|
||||||
|
SETTINGS.*(s.valuePtr) = val;
|
||||||
|
}
|
||||||
|
applied++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SettingType::ENUM: {
|
||||||
|
const int val = doc[s.key].as<int>();
|
||||||
|
if (val >= 0 && val < static_cast<int>(s.enumValues.size())) {
|
||||||
|
if (s.valuePtr) {
|
||||||
|
SETTINGS.*(s.valuePtr) = static_cast<uint8_t>(val);
|
||||||
|
} else if (s.valueSetter) {
|
||||||
|
s.valueSetter(static_cast<uint8_t>(val));
|
||||||
|
}
|
||||||
|
applied++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SettingType::VALUE: {
|
||||||
|
const int val = doc[s.key].as<int>();
|
||||||
|
if (val >= s.valueRange.min && val <= s.valueRange.max) {
|
||||||
|
if (s.valuePtr) {
|
||||||
|
SETTINGS.*(s.valuePtr) = static_cast<uint8_t>(val);
|
||||||
|
}
|
||||||
|
applied++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SettingType::STRING: {
|
||||||
|
const std::string val = doc[s.key].as<std::string>();
|
||||||
|
if (s.stringSetter) {
|
||||||
|
s.stringSetter(val);
|
||||||
|
} else if (s.stringPtr && s.stringMaxLen > 0) {
|
||||||
|
strncpy(s.stringPtr, val.c_str(), s.stringMaxLen - 1);
|
||||||
|
s.stringPtr[s.stringMaxLen - 1] = '\0';
|
||||||
|
}
|
||||||
|
applied++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SETTINGS.saveToFile();
|
||||||
|
|
||||||
|
LOG_DBG("WEB", "Applied %d setting(s)", applied);
|
||||||
|
server->send(200, "text/plain", String("Applied ") + String(applied) + " setting(s)");
|
||||||
|
}
|
||||||
|
|
||||||
// WebSocket callback trampoline
|
// WebSocket callback trampoline
|
||||||
void CrossPointWebServer::wsEventCallback(uint8_t num, WStype_t type, uint8_t* payload, size_t length) {
|
void CrossPointWebServer::wsEventCallback(uint8_t num, WStype_t type, uint8_t* payload, size_t length) {
|
||||||
if (wsInstance) {
|
if (wsInstance) {
|
||||||
@@ -999,7 +1173,7 @@ void CrossPointWebServer::wsEventCallback(uint8_t num, WStype_t type, uint8_t* p
|
|||||||
void CrossPointWebServer::onWebSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) {
|
void CrossPointWebServer::onWebSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case WStype_DISCONNECTED:
|
case WStype_DISCONNECTED:
|
||||||
Serial.printf("[%lu] [WS] Client %u disconnected\n", millis(), num);
|
LOG_DBG("WS", "Client %u disconnected", num);
|
||||||
// Clean up any in-progress upload
|
// Clean up any in-progress upload
|
||||||
if (wsUploadInProgress && wsUploadFile) {
|
if (wsUploadInProgress && wsUploadFile) {
|
||||||
wsUploadFile.close();
|
wsUploadFile.close();
|
||||||
@@ -1007,21 +1181,21 @@ void CrossPointWebServer::onWebSocketEvent(uint8_t num, WStype_t type, uint8_t*
|
|||||||
String filePath = wsUploadPath;
|
String filePath = wsUploadPath;
|
||||||
if (!filePath.endsWith("/")) filePath += "/";
|
if (!filePath.endsWith("/")) filePath += "/";
|
||||||
filePath += wsUploadFileName;
|
filePath += wsUploadFileName;
|
||||||
SdMan.remove(filePath.c_str());
|
Storage.remove(filePath.c_str());
|
||||||
Serial.printf("[%lu] [WS] Deleted incomplete upload: %s\n", millis(), filePath.c_str());
|
LOG_DBG("WS", "Deleted incomplete upload: %s", filePath.c_str());
|
||||||
}
|
}
|
||||||
wsUploadInProgress = false;
|
wsUploadInProgress = false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WStype_CONNECTED: {
|
case WStype_CONNECTED: {
|
||||||
Serial.printf("[%lu] [WS] Client %u connected\n", millis(), num);
|
LOG_DBG("WS", "Client %u connected", num);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case WStype_TEXT: {
|
case WStype_TEXT: {
|
||||||
// Parse control messages
|
// Parse control messages
|
||||||
String msg = String((char*)payload);
|
String msg = String((char*)payload);
|
||||||
Serial.printf("[%lu] [WS] Text from client %u: %s\n", millis(), num, msg.c_str());
|
LOG_DBG("WS", "Text from client %u: %s", num, msg.c_str());
|
||||||
|
|
||||||
if (msg.startsWith("START:")) {
|
if (msg.startsWith("START:")) {
|
||||||
// Parse: START:<filename>:<size>:<path>
|
// Parse: START:<filename>:<size>:<path>
|
||||||
@@ -1046,18 +1220,18 @@ void CrossPointWebServer::onWebSocketEvent(uint8_t num, WStype_t type, uint8_t*
|
|||||||
if (!filePath.endsWith("/")) filePath += "/";
|
if (!filePath.endsWith("/")) filePath += "/";
|
||||||
filePath += wsUploadFileName;
|
filePath += wsUploadFileName;
|
||||||
|
|
||||||
Serial.printf("[%lu] [WS] Starting upload: %s (%d bytes) to %s\n", millis(), wsUploadFileName.c_str(),
|
LOG_DBG("WS", "Starting upload: %s (%d bytes) to %s", wsUploadFileName.c_str(), wsUploadSize,
|
||||||
wsUploadSize, filePath.c_str());
|
filePath.c_str());
|
||||||
|
|
||||||
// Check if file exists and remove it
|
// Check if file exists and remove it
|
||||||
esp_task_wdt_reset();
|
esp_task_wdt_reset();
|
||||||
if (SdMan.exists(filePath.c_str())) {
|
if (Storage.exists(filePath.c_str())) {
|
||||||
SdMan.remove(filePath.c_str());
|
Storage.remove(filePath.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open file for writing
|
// Open file for writing
|
||||||
esp_task_wdt_reset();
|
esp_task_wdt_reset();
|
||||||
if (!SdMan.openFileForWrite("WS", filePath, wsUploadFile)) {
|
if (!Storage.openFileForWrite("WS", filePath, wsUploadFile)) {
|
||||||
wsServer->sendTXT(num, "ERROR:Failed to create file");
|
wsServer->sendTXT(num, "ERROR:Failed to create file");
|
||||||
wsUploadInProgress = false;
|
wsUploadInProgress = false;
|
||||||
return;
|
return;
|
||||||
@@ -1113,8 +1287,8 @@ void CrossPointWebServer::onWebSocketEvent(uint8_t num, WStype_t type, uint8_t*
|
|||||||
unsigned long elapsed = millis() - wsUploadStartTime;
|
unsigned long elapsed = millis() - wsUploadStartTime;
|
||||||
float kbps = (elapsed > 0) ? (wsUploadSize / 1024.0) / (elapsed / 1000.0) : 0;
|
float kbps = (elapsed > 0) ? (wsUploadSize / 1024.0) / (elapsed / 1000.0) : 0;
|
||||||
|
|
||||||
Serial.printf("[%lu] [WS] Upload complete: %s (%d bytes in %lu ms, %.1f KB/s)\n", millis(),
|
LOG_DBG("WS", "Upload complete: %s (%d bytes in %lu ms, %.1f KB/s)", wsUploadFileName.c_str(), wsUploadSize,
|
||||||
wsUploadFileName.c_str(), wsUploadSize, elapsed, kbps);
|
elapsed, kbps);
|
||||||
|
|
||||||
// Clear epub cache to prevent stale metadata issues when overwriting files
|
// Clear epub cache to prevent stale metadata issues when overwriting files
|
||||||
String filePath = wsUploadPath;
|
String filePath = wsUploadPath;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <SDCardManager.h>
|
#include <HalStorage.h>
|
||||||
#include <WebServer.h>
|
#include <WebServer.h>
|
||||||
#include <WebSocketsServer.h>
|
#include <WebSocketsServer.h>
|
||||||
#include <WiFiUdp.h>
|
#include <WiFiUdp.h>
|
||||||
@@ -100,4 +100,9 @@ class CrossPointWebServer {
|
|||||||
void handleRename() const;
|
void handleRename() const;
|
||||||
void handleMove() const;
|
void handleMove() const;
|
||||||
void handleDelete() const;
|
void handleDelete() const;
|
||||||
|
|
||||||
|
// Settings handlers
|
||||||
|
void handleSettingsPage() const;
|
||||||
|
void handleGetSettings() const;
|
||||||
|
void handlePostSettings();
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user