mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 22:57:50 +03:00
Compare commits
5 Commits
5bf603a38e
...
75fd818c93
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75fd818c93 | ||
|
|
09e73b34b5 | ||
|
|
1107590b56 | ||
|
|
66ddb52103 | ||
|
|
9f4f71fabe |
@ -1,11 +1,11 @@
|
|||||||
#include "Epub.h"
|
#include "Epub.h"
|
||||||
|
|
||||||
|
#include <FsHelpers.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <JpegToBmpConverter.h>
|
#include <JpegToBmpConverter.h>
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
#include <ZipFile.h>
|
#include <ZipFile.h>
|
||||||
|
|
||||||
#include "Epub/FsHelpers.h"
|
|
||||||
#include "Epub/parsers/ContainerParser.h"
|
#include "Epub/parsers/ContainerParser.h"
|
||||||
#include "Epub/parsers/ContentOpfParser.h"
|
#include "Epub/parsers/ContentOpfParser.h"
|
||||||
#include "Epub/parsers/TocNcxParser.h"
|
#include "Epub/parsers/TocNcxParser.h"
|
||||||
@ -42,7 +42,7 @@ bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Epub::parseContentOpf(bool useCache) {
|
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());
|
Serial.printf("[%lu] [EBP] Could not find content.opf in zip\n", millis());
|
||||||
@ -59,7 +59,9 @@ bool Epub::parseContentOpf(bool useCache) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentOpfParser opfParser(getBasePath(), contentOpfSize, useCache ? spineTocCache.get() : nullptr);
|
ContentOpfParser opfParser(getCachePath(), getBasePath(), contentOpfSize, bookMetadataCache.get());
|
||||||
|
Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(),
|
||||||
|
ESP.getHeapSize(), ESP.getMinFreeHeap());
|
||||||
|
|
||||||
if (!opfParser.setup()) {
|
if (!opfParser.setup()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not setup content.opf parser\n", millis());
|
Serial.printf("[%lu] [EBP] Could not setup content.opf parser\n", millis());
|
||||||
@ -72,10 +74,10 @@ bool Epub::parseContentOpf(bool useCache) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Grab data from opfParser into epub
|
// Grab data from opfParser into epub
|
||||||
title = opfParser.title;
|
bookMetadata.title = opfParser.title;
|
||||||
// if (!opfParser.coverItemId.empty() && opfParser.items.count(opfParser.coverItemId) > 0) {
|
// TODO: Parse author
|
||||||
// coverImageItem = opfParser.items.at(opfParser.coverItemId);
|
bookMetadata.author = "";
|
||||||
// }
|
bookMetadata.coverItemHref = opfParser.coverItemHref;
|
||||||
|
|
||||||
if (!opfParser.tocNcxPath.empty()) {
|
if (!opfParser.tocNcxPath.empty()) {
|
||||||
tocNcxItem = opfParser.tocNcxPath;
|
tocNcxItem = opfParser.tocNcxPath;
|
||||||
@ -95,13 +97,18 @@ bool Epub::parseTocNcxFile() const {
|
|||||||
Serial.printf("[%lu] [EBP] Parsing toc ncx file: %s\n", millis(), tocNcxItem.c_str());
|
Serial.printf("[%lu] [EBP] Parsing toc ncx file: %s\n", millis(), tocNcxItem.c_str());
|
||||||
|
|
||||||
const auto tmpNcxPath = getCachePath() + "/toc.ncx";
|
const auto tmpNcxPath = getCachePath() + "/toc.ncx";
|
||||||
File tempNcxFile = SD.open(tmpNcxPath.c_str(), FILE_WRITE);
|
File tempNcxFile;
|
||||||
|
if (!FsHelpers::openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
readItemContentsToStream(tocNcxItem, tempNcxFile, 1024);
|
readItemContentsToStream(tocNcxItem, tempNcxFile, 1024);
|
||||||
tempNcxFile.close();
|
tempNcxFile.close();
|
||||||
tempNcxFile = SD.open(tmpNcxPath.c_str(), FILE_READ);
|
if (!FsHelpers::openFileForRead("EBP", tmpNcxPath, tempNcxFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const auto ncxSize = tempNcxFile.size();
|
const auto ncxSize = tempNcxFile.size();
|
||||||
|
|
||||||
TocNcxParser ncxParser(contentBasePath, ncxSize, spineTocCache.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());
|
Serial.printf("[%lu] [EBP] Could not setup toc ncx parser\n", millis());
|
||||||
@ -139,18 +146,10 @@ bool Epub::load() {
|
|||||||
Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str());
|
Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str());
|
||||||
|
|
||||||
// Initialize spine/TOC cache
|
// Initialize spine/TOC cache
|
||||||
spineTocCache.reset(new SpineTocCache(cachePath));
|
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
||||||
|
|
||||||
// Try to load existing cache first
|
// Try to load existing cache first
|
||||||
if (spineTocCache->load()) {
|
if (bookMetadataCache->load()) {
|
||||||
Serial.printf("[%lu] [EBP] Loaded spine/TOC from cache\n", millis());
|
|
||||||
|
|
||||||
// Still need to parse content.opf for title and cover
|
|
||||||
if (!parseContentOpf(false)) {
|
|
||||||
Serial.printf("[%lu] [EBP] Could not parse content.opf\n", millis());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -160,33 +159,59 @@ bool Epub::load() {
|
|||||||
setupCacheDir();
|
setupCacheDir();
|
||||||
|
|
||||||
// Begin building cache - stream entries to disk immediately
|
// Begin building cache - stream entries to disk immediately
|
||||||
if (!spineTocCache->beginWrite()) {
|
if (!bookMetadataCache->beginWrite()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not begin writing cache\n", millis());
|
Serial.printf("[%lu] [EBP] Could not begin writing cache\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!parseContentOpf(true)) {
|
|
||||||
|
// OPF Pass
|
||||||
|
BookMetadataCache::BookMetadata bookMetadata;
|
||||||
|
if (!bookMetadataCache->beginContentOpfPass()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not begin writing content.opf pass\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!parseContentOpf(bookMetadata)) {
|
||||||
Serial.printf("[%lu] [EBP] Could not parse content.opf\n", millis());
|
Serial.printf("[%lu] [EBP] Could not parse content.opf\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!bookMetadataCache->endContentOpfPass()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not end writing content.opf pass\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TOC Pass
|
||||||
|
if (!bookMetadataCache->beginTocPass()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not begin writing toc pass\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!parseTocNcxFile()) {
|
if (!parseTocNcxFile()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not parse toc\n", millis());
|
Serial.printf("[%lu] [EBP] Could not parse toc\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!bookMetadataCache->endTocPass()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not end writing toc pass\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Close the cache files
|
// Close the cache files
|
||||||
if (!spineTocCache->endWrite()) {
|
if (!bookMetadataCache->endWrite()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not end writing cache\n", millis());
|
Serial.printf("[%lu] [EBP] Could not end writing cache\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now compute mappings and sizes (this loads entries temporarily, computes, then rewrites)
|
// Build final book.bin
|
||||||
if (!spineTocCache->updateMapsAndSizes(filepath)) {
|
if (!bookMetadataCache->buildBookBin(filepath, bookMetadata)) {
|
||||||
Serial.printf("[%lu] [EBP] Could not update mappings and sizes\n", millis());
|
Serial.printf("[%lu] [EBP] Could not update mappings and sizes\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!bookMetadataCache->cleanupTmpFiles()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not cleanup tmp files - ignoring\n", millis());
|
||||||
|
}
|
||||||
|
|
||||||
// Reload the cache from disk so it's in the correct state
|
// Reload the cache from disk so it's in the correct state
|
||||||
spineTocCache.reset(new SpineTocCache(cachePath));
|
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
||||||
if (!spineTocCache->load()) {
|
if (!bookMetadataCache->load()) {
|
||||||
Serial.printf("[%lu] [EBP] Failed to reload cache after writing\n", millis());
|
Serial.printf("[%lu] [EBP] Failed to reload cache after writing\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -228,7 +253,14 @@ const std::string& Epub::getCachePath() const { return cachePath; }
|
|||||||
|
|
||||||
const std::string& Epub::getPath() const { return filepath; }
|
const std::string& Epub::getPath() const { return filepath; }
|
||||||
|
|
||||||
const std::string& Epub::getTitle() const { return title; }
|
const std::string& Epub::getTitle() const {
|
||||||
|
static std::string blank;
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
return blank;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bookMetadataCache->coreMetadata.title;
|
||||||
|
}
|
||||||
|
|
||||||
std::string Epub::getCoverBmpPath() const { return cachePath + "/cover.bmp"; }
|
std::string Epub::getCoverBmpPath() const { return cachePath + "/cover.bmp"; }
|
||||||
|
|
||||||
@ -238,24 +270,42 @@ bool Epub::generateCoverBmp() const {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coverImageItem.empty()) {
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Cannot generate cover BMP, cache not loaded\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||||
|
if (coverImageHref.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] No known cover image\n", millis());
|
Serial.printf("[%lu] [EBP] No known cover image\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coverImageItem.substr(coverImageItem.length() - 4) == ".jpg" ||
|
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
||||||
coverImageItem.substr(coverImageItem.length() - 5) == ".jpeg") {
|
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
||||||
Serial.printf("[%lu] [EBP] Generating BMP from JPG cover image\n", millis());
|
Serial.printf("[%lu] [EBP] Generating BMP from JPG cover image\n", millis());
|
||||||
File coverJpg = SD.open((getCachePath() + "/.cover.jpg").c_str(), FILE_WRITE, true);
|
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||||
readItemContentsToStream(coverImageItem, coverJpg, 1024);
|
|
||||||
|
File coverJpg;
|
||||||
|
if (!FsHelpers::openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
|
|
||||||
coverJpg = SD.open((getCachePath() + "/.cover.jpg").c_str(), FILE_READ);
|
if (!FsHelpers::openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||||
File coverBmp = SD.open(getCoverBmpPath().c_str(), FILE_WRITE, true);
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
File coverBmp;
|
||||||
|
if (!FsHelpers::openFileForWrite("EBP", getCoverBmpPath(), coverBmp)) {
|
||||||
|
coverJpg.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp);
|
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp);
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
coverBmp.close();
|
coverBmp.close();
|
||||||
SD.remove((getCachePath() + "/.cover.jpg").c_str());
|
SD.remove(coverJpgTempPath.c_str());
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Serial.printf("[%lu] [EBP] Failed to generate BMP from JPG cover image\n", millis());
|
Serial.printf("[%lu] [EBP] Failed to generate BMP from JPG cover image\n", millis());
|
||||||
@ -301,75 +351,63 @@ bool Epub::getItemSize(const ZipFile& zip, const std::string& itemHref, size_t*
|
|||||||
}
|
}
|
||||||
|
|
||||||
int Epub::getSpineItemsCount() const {
|
int Epub::getSpineItemsCount() const {
|
||||||
if (!spineTocCache || !spineTocCache->isLoaded()) {
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return spineTocCache->getSpineCount();
|
return bookMetadataCache->getSpineCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const {
|
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return getSpineItem(spineIndex).cumulativeSize; }
|
||||||
if (!spineTocCache || !spineTocCache->isLoaded()) {
|
|
||||||
Serial.printf("[%lu] [EBP] getCumulativeSpineItemSize called but cache not loaded\n", millis());
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (spineIndex < 0 || spineIndex >= spineTocCache->getSpineCount()) {
|
BookMetadataCache::SpineEntry Epub::getSpineItem(const int spineIndex) const {
|
||||||
Serial.printf("[%lu] [EBP] getCumulativeSpineItemSize index:%d is out of range\n", millis(), spineIndex);
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return spineTocCache->getSpineEntry(spineIndex).cumulativeSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string Epub::getSpineHref(const int spineIndex) const {
|
|
||||||
if (!spineTocCache || !spineTocCache->isLoaded()) {
|
|
||||||
Serial.printf("[%lu] [EBP] getSpineItem called but cache not loaded\n", millis());
|
Serial.printf("[%lu] [EBP] getSpineItem called but cache not loaded\n", millis());
|
||||||
return "";
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (spineIndex < 0 || spineIndex >= spineTocCache->getSpineCount()) {
|
if (spineIndex < 0 || spineIndex >= bookMetadataCache->getSpineCount()) {
|
||||||
Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex);
|
Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex);
|
||||||
return spineTocCache->getSpineEntry(0).href;
|
return bookMetadataCache->getSpineEntry(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return spineTocCache->getSpineEntry(spineIndex).href;
|
return bookMetadataCache->getSpineEntry(spineIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
SpineTocCache::TocEntry Epub::getTocItem(const int tocIndex) const {
|
BookMetadataCache::TocEntry Epub::getTocItem(const int tocIndex) const {
|
||||||
if (!spineTocCache || !spineTocCache->isLoaded()) {
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
Serial.printf("[%lu] [EBP] getTocItem called but cache not loaded\n", millis());
|
Serial.printf("[%lu] [EBP] getTocItem called but cache not loaded\n", millis());
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tocIndex < 0 || tocIndex >= spineTocCache->getTocCount()) {
|
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
|
||||||
Serial.printf("[%lu] [EBP] getTocItem index:%d is out of range\n", millis(), tocIndex);
|
Serial.printf("[%lu] [EBP] getTocItem index:%d is out of range\n", millis(), tocIndex);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return spineTocCache->getTocEntry(tocIndex);
|
return bookMetadataCache->getTocEntry(tocIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
int Epub::getTocItemsCount() const {
|
int Epub::getTocItemsCount() const {
|
||||||
if (!spineTocCache || !spineTocCache->isLoaded()) {
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return spineTocCache->getTocCount();
|
return bookMetadataCache->getTocCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 (!spineTocCache || !spineTocCache->isLoaded()) {
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex called but cache not loaded\n", millis());
|
Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex called but cache not loaded\n", millis());
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tocIndex < 0 || tocIndex >= spineTocCache->getTocCount()) {
|
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
|
||||||
Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex: tocIndex %d out of range\n", millis(), tocIndex);
|
Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex: tocIndex %d out of range\n", millis(), tocIndex);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int spineIndex = spineTocCache->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);
|
Serial.printf("[%lu] [EBP] Section not found for TOC index %d\n", millis(), tocIndex);
|
||||||
return 0;
|
return 0;
|
||||||
@ -378,22 +416,10 @@ int Epub::getSpineIndexForTocIndex(const int tocIndex) const {
|
|||||||
return spineIndex;
|
return spineIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
int Epub::getTocIndexForSpineIndex(const int spineIndex) const {
|
int Epub::getTocIndexForSpineIndex(const int spineIndex) const { return getSpineItem(spineIndex).tocIndex; }
|
||||||
if (!spineTocCache || !spineTocCache->isLoaded()) {
|
|
||||||
Serial.printf("[%lu] [EBP] getTocIndexForSpineIndex called but cache not loaded\n", millis());
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (spineIndex < 0 || spineIndex >= spineTocCache->getSpineCount()) {
|
|
||||||
Serial.printf("[%lu] [EBP] getTocIndexForSpineIndex: spineIndex %d out of range\n", millis(), spineIndex);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return spineTocCache->getSpineEntry(spineIndex).tocIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t Epub::getBookSize() const {
|
size_t Epub::getBookSize() const {
|
||||||
if (!spineTocCache || !spineTocCache->isLoaded() || spineTocCache->getSpineCount() == 0) {
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded() || bookMetadataCache->getSpineCount() == 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return getCumulativeSpineItemSize(getSpineItemsCount() - 1);
|
return getCumulativeSpineItemSize(getSpineItemsCount() - 1);
|
||||||
|
|||||||
@ -6,15 +6,11 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "Epub/SpineTocCache.h"
|
#include "Epub/BookMetadataCache.h"
|
||||||
|
|
||||||
class ZipFile;
|
class ZipFile;
|
||||||
|
|
||||||
class Epub {
|
class Epub {
|
||||||
// the title read from the EPUB meta data
|
|
||||||
std::string title;
|
|
||||||
// the cover image
|
|
||||||
std::string coverImageItem;
|
|
||||||
// the ncx file
|
// the ncx file
|
||||||
std::string tocNcxItem;
|
std::string tocNcxItem;
|
||||||
// where is the EPUBfile?
|
// where is the EPUBfile?
|
||||||
@ -24,10 +20,10 @@ class Epub {
|
|||||||
// Uniq cache key based on filepath
|
// Uniq cache key based on filepath
|
||||||
std::string cachePath;
|
std::string cachePath;
|
||||||
// Spine and TOC cache
|
// Spine and TOC cache
|
||||||
std::unique_ptr<SpineTocCache> spineTocCache;
|
std::unique_ptr<BookMetadataCache> bookMetadataCache;
|
||||||
|
|
||||||
bool findContentOpfFile(std::string* contentOpfFile) const;
|
bool findContentOpfFile(std::string* contentOpfFile) const;
|
||||||
bool parseContentOpf(bool useCache);
|
bool parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata);
|
||||||
bool parseTocNcxFile() const;
|
bool parseTocNcxFile() const;
|
||||||
static bool getItemSize(const ZipFile& zip, const std::string& itemHref, size_t* size);
|
static bool getItemSize(const ZipFile& zip, const std::string& itemHref, size_t* size);
|
||||||
|
|
||||||
@ -50,13 +46,13 @@ class Epub {
|
|||||||
bool trailingNullByte = false) const;
|
bool trailingNullByte = false) const;
|
||||||
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
||||||
bool getItemSize(const std::string& itemHref, size_t* size) const;
|
bool getItemSize(const std::string& itemHref, size_t* size) const;
|
||||||
std::string getSpineHref(int spineIndex) const;
|
BookMetadataCache::SpineEntry getSpineItem(int spineIndex) const;
|
||||||
|
BookMetadataCache::TocEntry getTocItem(int tocIndex) const;
|
||||||
int getSpineItemsCount() const;
|
int getSpineItemsCount() const;
|
||||||
size_t getCumulativeSpineItemSize(int spineIndex) const;
|
|
||||||
SpineTocCache::TocEntry getTocItem(int tocIndex) const;
|
|
||||||
int getTocItemsCount() const;
|
int getTocItemsCount() const;
|
||||||
int getSpineIndexForTocIndex(int tocIndex) const;
|
int getSpineIndexForTocIndex(int tocIndex) const;
|
||||||
int getTocIndexForSpineIndex(int spineIndex) const;
|
int getTocIndexForSpineIndex(int spineIndex) const;
|
||||||
|
size_t getCumulativeSpineItemSize(int spineIndex) const;
|
||||||
|
|
||||||
size_t getBookSize() const;
|
size_t getBookSize() const;
|
||||||
uint8_t calculateProgress(const int currentSpineIndex, const float currentSpineRead) const;
|
uint8_t calculateProgress(const int currentSpineIndex, const float currentSpineRead) const;
|
||||||
|
|||||||
323
lib/Epub/Epub/BookMetadataCache.cpp
Normal file
323
lib/Epub/Epub/BookMetadataCache.cpp
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
#include "BookMetadataCache.h"
|
||||||
|
|
||||||
|
#include <HardwareSerial.h>
|
||||||
|
#include <SD.h>
|
||||||
|
#include <Serialization.h>
|
||||||
|
#include <ZipFile.h>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "FsHelpers.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr uint8_t BOOK_CACHE_VERSION = 1;
|
||||||
|
constexpr char bookBinFile[] = "/book.bin";
|
||||||
|
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
||||||
|
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
/* ============= WRITING / BUILDING FUNCTIONS ================ */
|
||||||
|
|
||||||
|
bool BookMetadataCache::beginWrite() {
|
||||||
|
buildMode = true;
|
||||||
|
spineCount = 0;
|
||||||
|
tocCount = 0;
|
||||||
|
Serial.printf("[%lu] [BMC] Entering write mode\n", millis());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::beginContentOpfPass() {
|
||||||
|
Serial.printf("[%lu] [BMC] Beginning content opf pass\n", millis());
|
||||||
|
|
||||||
|
// Open spine file for writing
|
||||||
|
return FsHelpers::openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::endContentOpfPass() {
|
||||||
|
spineFile.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::beginTocPass() {
|
||||||
|
Serial.printf("[%lu] [BMC] Beginning toc pass\n", millis());
|
||||||
|
|
||||||
|
// Open spine file for reading
|
||||||
|
if (!FsHelpers::openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!FsHelpers::openFileForWrite("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
||||||
|
spineFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::endTocPass() {
|
||||||
|
tocFile.close();
|
||||||
|
spineFile.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::endWrite() {
|
||||||
|
if (!buildMode) {
|
||||||
|
Serial.printf("[%lu] [BMC] endWrite called but not in build mode\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildMode = false;
|
||||||
|
Serial.printf("[%lu] [BMC] Wrote %d spine, %d TOC entries\n", millis(), spineCount, tocCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMetadata& metadata) {
|
||||||
|
// Open all three files, writing to meta, reading from spine and toc
|
||||||
|
if (!FsHelpers::openFileForWrite("BMC", cachePath + bookBinFile, bookFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!FsHelpers::openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||||
|
bookFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!FsHelpers::openFileForRead("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
||||||
|
bookFile.close();
|
||||||
|
spineFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr size_t headerASize =
|
||||||
|
sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(size_t) + sizeof(spineCount) + sizeof(tocCount);
|
||||||
|
const size_t metadataSize =
|
||||||
|
metadata.title.size() + metadata.author.size() + metadata.coverItemHref.size() + sizeof(uint32_t) * 3;
|
||||||
|
const size_t lutSize = sizeof(size_t) * spineCount + sizeof(size_t) * tocCount;
|
||||||
|
const size_t lutOffset = headerASize + metadataSize;
|
||||||
|
|
||||||
|
// Header A
|
||||||
|
serialization::writePod(bookFile, BOOK_CACHE_VERSION);
|
||||||
|
serialization::writePod(bookFile, lutOffset);
|
||||||
|
serialization::writePod(bookFile, spineCount);
|
||||||
|
serialization::writePod(bookFile, tocCount);
|
||||||
|
// Metadata
|
||||||
|
serialization::writeString(bookFile, metadata.title);
|
||||||
|
serialization::writeString(bookFile, metadata.author);
|
||||||
|
serialization::writeString(bookFile, metadata.coverItemHref);
|
||||||
|
|
||||||
|
// Loop through spine entries, writing LUT positions
|
||||||
|
spineFile.seek(0);
|
||||||
|
for (int i = 0; i < spineCount; i++) {
|
||||||
|
auto spineEntry = readSpineEntry(spineFile);
|
||||||
|
serialization::writePod(bookFile, spineFile.position() + lutOffset + lutSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through toc entries, writing LUT positions
|
||||||
|
tocFile.seek(0);
|
||||||
|
for (int i = 0; i < tocCount; i++) {
|
||||||
|
auto tocEntry = readTocEntry(tocFile);
|
||||||
|
serialization::writePod(bookFile, tocFile.position() + lutOffset + lutSize + spineFile.position());
|
||||||
|
}
|
||||||
|
|
||||||
|
// LUTs complete
|
||||||
|
// Loop through spines from spine file matching up TOC indexes, calculating cumulative size and writing to book.bin
|
||||||
|
|
||||||
|
const ZipFile zip("/sd" + epubPath);
|
||||||
|
size_t cumSize = 0;
|
||||||
|
spineFile.seek(0);
|
||||||
|
for (int i = 0; i < spineCount; i++) {
|
||||||
|
auto spineEntry = readSpineEntry(spineFile);
|
||||||
|
|
||||||
|
tocFile.seek(0);
|
||||||
|
for (int j = 0; j < tocCount; j++) {
|
||||||
|
auto tocEntry = readTocEntry(tocFile);
|
||||||
|
if (tocEntry.spineIndex == i) {
|
||||||
|
spineEntry.tocIndex = j;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
if (spineEntry.tocIndex == -1) {
|
||||||
|
Serial.printf("[%lu] [BMC] Warning: Could not find TOC entry for spine item %d: %s\n", millis(), i,
|
||||||
|
spineEntry.href.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate size for cumulative size
|
||||||
|
size_t itemSize = 0;
|
||||||
|
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
||||||
|
if (zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||||
|
cumSize += itemSize;
|
||||||
|
spineEntry.cumulativeSize = cumSize;
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write out spine data to book.bin
|
||||||
|
writeSpineEntry(bookFile, spineEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through toc entries from toc file writing to book.bin
|
||||||
|
tocFile.seek(0);
|
||||||
|
for (int i = 0; i < tocCount; i++) {
|
||||||
|
auto tocEntry = readTocEntry(tocFile);
|
||||||
|
writeTocEntry(bookFile, tocEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
bookFile.close();
|
||||||
|
spineFile.close();
|
||||||
|
tocFile.close();
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [BMC] Successfully built book.bin\n", millis());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::cleanupTmpFiles() const {
|
||||||
|
if (SD.exists((cachePath + tmpSpineBinFile).c_str())) {
|
||||||
|
SD.remove((cachePath + tmpSpineBinFile).c_str());
|
||||||
|
}
|
||||||
|
if (SD.exists((cachePath + tmpTocBinFile).c_str())) {
|
||||||
|
SD.remove((cachePath + tmpTocBinFile).c_str());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t BookMetadataCache::writeSpineEntry(File& file, const SpineEntry& entry) const {
|
||||||
|
const auto pos = file.position();
|
||||||
|
serialization::writeString(file, entry.href);
|
||||||
|
serialization::writePod(file, entry.cumulativeSize);
|
||||||
|
serialization::writePod(file, entry.tocIndex);
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t BookMetadataCache::writeTocEntry(File& file, const TocEntry& entry) const {
|
||||||
|
const auto pos = file.position();
|
||||||
|
serialization::writeString(file, entry.title);
|
||||||
|
serialization::writeString(file, entry.href);
|
||||||
|
serialization::writeString(file, entry.anchor);
|
||||||
|
serialization::writePod(file, entry.level);
|
||||||
|
serialization::writePod(file, entry.spineIndex);
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: for the LUT to be accurate, this **MUST** be called for all spine items before `addTocEntry` is ever called
|
||||||
|
// this is because in this function we're marking positions of the items
|
||||||
|
void BookMetadataCache::createSpineEntry(const std::string& href) {
|
||||||
|
if (!buildMode || !spineFile) {
|
||||||
|
Serial.printf("[%lu] [BMC] createSpineEntry called but not in build mode\n", millis());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SpineEntry entry(href, 0, -1);
|
||||||
|
writeSpineEntry(spineFile, entry);
|
||||||
|
spineCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BookMetadataCache::createTocEntry(const std::string& title, const std::string& href, const std::string& anchor,
|
||||||
|
const uint8_t level) {
|
||||||
|
if (!buildMode || !tocFile || !spineFile) {
|
||||||
|
Serial.printf("[%lu] [BMC] createTocEntry called but not in build mode\n", millis());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int spineIndex = -1;
|
||||||
|
// find spine index
|
||||||
|
spineFile.seek(0);
|
||||||
|
for (int i = 0; i < spineCount; i++) {
|
||||||
|
auto spineEntry = readSpineEntry(spineFile);
|
||||||
|
if (spineEntry.href == href) {
|
||||||
|
spineIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spineIndex == -1) {
|
||||||
|
Serial.printf("[%lu] [BMC] addTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
const TocEntry entry(title, href, anchor, level, spineIndex);
|
||||||
|
writeTocEntry(tocFile, entry);
|
||||||
|
tocCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============= READING / LOADING FUNCTIONS ================ */
|
||||||
|
|
||||||
|
bool BookMetadataCache::load() {
|
||||||
|
if (!FsHelpers::openFileForRead("BMC", cachePath + bookBinFile, bookFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t version;
|
||||||
|
serialization::readPod(bookFile, version);
|
||||||
|
if (version != BOOK_CACHE_VERSION) {
|
||||||
|
Serial.printf("[%lu] [BMC] Cache version mismatch: expected %d, got %d\n", millis(), BOOK_CACHE_VERSION, version);
|
||||||
|
bookFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
serialization::readPod(bookFile, lutOffset);
|
||||||
|
serialization::readPod(bookFile, spineCount);
|
||||||
|
serialization::readPod(bookFile, tocCount);
|
||||||
|
|
||||||
|
loaded = true;
|
||||||
|
Serial.printf("[%lu] [BMC] Loaded cache data: %d spine, %d TOC entries\n", millis(), spineCount, tocCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index) {
|
||||||
|
if (!loaded) {
|
||||||
|
Serial.printf("[%lu] [BMC] getSpineEntry called but cache not loaded\n", millis());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0 || index >= static_cast<int>(spineCount)) {
|
||||||
|
Serial.printf("[%lu] [BMC] getSpineEntry index %d out of range\n", millis(), index);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek to spine LUT item, read from LUT and get out data
|
||||||
|
bookFile.seek(lutOffset + sizeof(size_t) * index);
|
||||||
|
size_t spineEntryPos;
|
||||||
|
serialization::readPod(bookFile, spineEntryPos);
|
||||||
|
bookFile.seek(spineEntryPos);
|
||||||
|
return readSpineEntry(bookFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
|
||||||
|
if (!loaded) {
|
||||||
|
Serial.printf("[%lu] [BMC] getTocEntry called but cache not loaded\n", millis());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0 || index >= static_cast<int>(tocCount)) {
|
||||||
|
Serial.printf("[%lu] [BMC] getTocEntry index %d out of range\n", millis(), index);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek to TOC LUT item, read from LUT and get out data
|
||||||
|
bookFile.seek(lutOffset + sizeof(size_t) * spineCount + sizeof(size_t) * index);
|
||||||
|
size_t tocEntryPos;
|
||||||
|
serialization::readPod(bookFile, tocEntryPos);
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [BMC] getTocEntry tocEntryPos: %d\n", millis(), tocEntryPos);
|
||||||
|
|
||||||
|
bookFile.seek(tocEntryPos);
|
||||||
|
return readTocEntry(tocFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(File& file) const {
|
||||||
|
SpineEntry entry;
|
||||||
|
serialization::readString(file, entry.href);
|
||||||
|
serialization::readPod(file, entry.cumulativeSize);
|
||||||
|
serialization::readPod(file, entry.tocIndex);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(File& file) const {
|
||||||
|
TocEntry entry;
|
||||||
|
serialization::readString(file, entry.title);
|
||||||
|
serialization::readString(file, entry.href);
|
||||||
|
serialization::readString(file, entry.anchor);
|
||||||
|
serialization::readPod(file, entry.level);
|
||||||
|
serialization::readPod(file, entry.spineIndex);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
@ -4,8 +4,14 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
class SpineTocCache {
|
class BookMetadataCache {
|
||||||
public:
|
public:
|
||||||
|
struct BookMetadata {
|
||||||
|
std::string title;
|
||||||
|
std::string author;
|
||||||
|
std::string coverItemHref;
|
||||||
|
};
|
||||||
|
|
||||||
struct SpineEntry {
|
struct SpineEntry {
|
||||||
std::string href;
|
std::string href;
|
||||||
size_t cumulativeSize;
|
size_t cumulativeSize;
|
||||||
@ -34,13 +40,14 @@ class SpineTocCache {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
std::string cachePath;
|
std::string cachePath;
|
||||||
|
size_t lutOffset;
|
||||||
uint16_t spineCount;
|
uint16_t spineCount;
|
||||||
uint16_t tocCount;
|
uint16_t tocCount;
|
||||||
bool loaded;
|
bool loaded;
|
||||||
bool buildMode;
|
bool buildMode;
|
||||||
|
|
||||||
|
File bookFile;
|
||||||
// Temp file handles during build
|
// Temp file handles during build
|
||||||
File metaFile;
|
|
||||||
File spineFile;
|
File spineFile;
|
||||||
File tocFile;
|
File tocFile;
|
||||||
|
|
||||||
@ -50,24 +57,31 @@ class SpineTocCache {
|
|||||||
TocEntry readTocEntry(File& file) const;
|
TocEntry readTocEntry(File& file) const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit SpineTocCache(std::string cachePath)
|
BookMetadata coreMetadata;
|
||||||
: cachePath(std::move(cachePath)), spineCount(0), tocCount(0), loaded(false), buildMode(false) {}
|
|
||||||
~SpineTocCache() = default;
|
explicit BookMetadataCache(std::string cachePath)
|
||||||
|
: cachePath(std::move(cachePath)), lutOffset(0), spineCount(0), tocCount(0), loaded(false), buildMode(false) {}
|
||||||
|
~BookMetadataCache() = default;
|
||||||
|
|
||||||
// Building phase (stream to disk immediately)
|
// Building phase (stream to disk immediately)
|
||||||
bool beginWrite();
|
bool beginWrite();
|
||||||
void addSpineEntry(const std::string& href);
|
bool beginContentOpfPass();
|
||||||
void addTocEntry(const std::string& title, const std::string& href, const std::string& anchor, uint8_t level);
|
void createSpineEntry(const std::string& href);
|
||||||
|
bool endContentOpfPass();
|
||||||
|
bool beginTocPass();
|
||||||
|
void createTocEntry(const std::string& title, const std::string& href, const std::string& anchor, uint8_t level);
|
||||||
|
bool endTocPass();
|
||||||
bool endWrite();
|
bool endWrite();
|
||||||
|
bool cleanupTmpFiles() const;
|
||||||
|
|
||||||
// Post-processing to update mappings and sizes
|
// Post-processing to update mappings and sizes
|
||||||
bool updateMapsAndSizes(const std::string& epubPath);
|
bool buildBookBin(const std::string& epubPath, const BookMetadata& metadata);
|
||||||
|
|
||||||
// Reading phase (read mode)
|
// Reading phase (read mode)
|
||||||
bool load();
|
bool load();
|
||||||
SpineEntry getSpineEntry(int index);
|
SpineEntry getSpineEntry(int index);
|
||||||
TocEntry getTocEntry(int index);
|
TocEntry getTocEntry(int index);
|
||||||
int getSpineCount() const;
|
int getSpineCount() const { return spineCount; }
|
||||||
int getTocCount() const;
|
int getTocCount() const { return tocCount; }
|
||||||
bool isLoaded() const;
|
bool isLoaded() const { return loaded; }
|
||||||
};
|
};
|
||||||
@ -16,7 +16,7 @@ bool FsHelpers::openFileForRead(const char* moduleName, const std::string& path,
|
|||||||
bool FsHelpers::openFileForWrite(const char* moduleName, const std::string& path, File& file) {
|
bool FsHelpers::openFileForWrite(const char* moduleName, const std::string& path, File& file) {
|
||||||
file = SD.open(path.c_str(), FILE_WRITE, true);
|
file = SD.open(path.c_str(), FILE_WRITE, true);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
Serial.printf("[%lu] [%s] Failed to open spine file for writing: %s\n", millis(), moduleName, path.c_str());
|
Serial.printf("[%lu] [%s] Failed to open file for writing: %s\n", millis(), moduleName, path.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -9,21 +9,21 @@ constexpr uint8_t PAGE_FILE_VERSION = 3;
|
|||||||
|
|
||||||
void PageLine::render(GfxRenderer& renderer, const int fontId) { block->render(renderer, fontId, xPos, yPos); }
|
void PageLine::render(GfxRenderer& renderer, const int fontId) { block->render(renderer, fontId, xPos, yPos); }
|
||||||
|
|
||||||
void PageLine::serialize(std::ostream& os) {
|
void PageLine::serialize(File& file) {
|
||||||
serialization::writePod(os, xPos);
|
serialization::writePod(file, xPos);
|
||||||
serialization::writePod(os, yPos);
|
serialization::writePod(file, yPos);
|
||||||
|
|
||||||
// serialize TextBlock pointed to by PageLine
|
// serialize TextBlock pointed to by PageLine
|
||||||
block->serialize(os);
|
block->serialize(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<PageLine> PageLine::deserialize(std::istream& is) {
|
std::unique_ptr<PageLine> PageLine::deserialize(File& file) {
|
||||||
int16_t xPos;
|
int16_t xPos;
|
||||||
int16_t yPos;
|
int16_t yPos;
|
||||||
serialization::readPod(is, xPos);
|
serialization::readPod(file, xPos);
|
||||||
serialization::readPod(is, yPos);
|
serialization::readPod(file, yPos);
|
||||||
|
|
||||||
auto tb = TextBlock::deserialize(is);
|
auto tb = TextBlock::deserialize(file);
|
||||||
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
|
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,22 +33,22 @@ void Page::render(GfxRenderer& renderer, const int fontId) const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Page::serialize(std::ostream& os) const {
|
void Page::serialize(File& file) const {
|
||||||
serialization::writePod(os, PAGE_FILE_VERSION);
|
serialization::writePod(file, PAGE_FILE_VERSION);
|
||||||
|
|
||||||
const uint32_t count = elements.size();
|
const uint32_t count = elements.size();
|
||||||
serialization::writePod(os, count);
|
serialization::writePod(file, count);
|
||||||
|
|
||||||
for (const auto& el : elements) {
|
for (const auto& el : elements) {
|
||||||
// Only PageLine exists currently
|
// Only PageLine exists currently
|
||||||
serialization::writePod(os, static_cast<uint8_t>(TAG_PageLine));
|
serialization::writePod(file, static_cast<uint8_t>(TAG_PageLine));
|
||||||
el->serialize(os);
|
el->serialize(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Page> Page::deserialize(std::istream& is) {
|
std::unique_ptr<Page> Page::deserialize(File& file) {
|
||||||
uint8_t version;
|
uint8_t version;
|
||||||
serialization::readPod(is, version);
|
serialization::readPod(file, version);
|
||||||
if (version != PAGE_FILE_VERSION) {
|
if (version != PAGE_FILE_VERSION) {
|
||||||
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown version %u\n", millis(), version);
|
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown version %u\n", millis(), version);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@ -57,14 +57,14 @@ std::unique_ptr<Page> Page::deserialize(std::istream& is) {
|
|||||||
auto page = std::unique_ptr<Page>(new Page());
|
auto page = std::unique_ptr<Page>(new Page());
|
||||||
|
|
||||||
uint32_t count;
|
uint32_t count;
|
||||||
serialization::readPod(is, count);
|
serialization::readPod(file, count);
|
||||||
|
|
||||||
for (uint32_t i = 0; i < count; i++) {
|
for (uint32_t i = 0; i < count; i++) {
|
||||||
uint8_t tag;
|
uint8_t tag;
|
||||||
serialization::readPod(is, tag);
|
serialization::readPod(file, tag);
|
||||||
|
|
||||||
if (tag == TAG_PageLine) {
|
if (tag == TAG_PageLine) {
|
||||||
auto pl = PageLine::deserialize(is);
|
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);
|
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown tag %u\n", millis(), tag);
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include <FS.h>
|
||||||
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@ -16,7 +18,7 @@ class PageElement {
|
|||||||
explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {}
|
explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {}
|
||||||
virtual ~PageElement() = default;
|
virtual ~PageElement() = default;
|
||||||
virtual void render(GfxRenderer& renderer, int fontId) = 0;
|
virtual void render(GfxRenderer& renderer, int fontId) = 0;
|
||||||
virtual void serialize(std::ostream& os) = 0;
|
virtual void serialize(File& file) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// a line from a block element
|
// a line from a block element
|
||||||
@ -27,8 +29,8 @@ class PageLine final : public PageElement {
|
|||||||
PageLine(std::shared_ptr<TextBlock> block, const int16_t xPos, const int16_t yPos)
|
PageLine(std::shared_ptr<TextBlock> block, const int16_t xPos, const int16_t yPos)
|
||||||
: PageElement(xPos, yPos), block(std::move(block)) {}
|
: PageElement(xPos, yPos), block(std::move(block)) {}
|
||||||
void render(GfxRenderer& renderer, int fontId) override;
|
void render(GfxRenderer& renderer, int fontId) override;
|
||||||
void serialize(std::ostream& os) override;
|
void serialize(File& file) override;
|
||||||
static std::unique_ptr<PageLine> deserialize(std::istream& is);
|
static std::unique_ptr<PageLine> deserialize(File& file);
|
||||||
};
|
};
|
||||||
|
|
||||||
class Page {
|
class Page {
|
||||||
@ -36,6 +38,6 @@ class Page {
|
|||||||
// the list of block index and line numbers on this page
|
// the list of block index and line numbers on this page
|
||||||
std::vector<std::shared_ptr<PageElement>> elements;
|
std::vector<std::shared_ptr<PageElement>> elements;
|
||||||
void render(GfxRenderer& renderer, int fontId) const;
|
void render(GfxRenderer& renderer, int fontId) const;
|
||||||
void serialize(std::ostream& os) const;
|
void serialize(File& file) const;
|
||||||
static std::unique_ptr<Page> deserialize(std::istream& is);
|
static std::unique_ptr<Page> deserialize(File& file);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
#include "Section.h"
|
#include "Section.h"
|
||||||
|
|
||||||
|
#include <FsHelpers.h>
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
#include <fstream>
|
|
||||||
|
|
||||||
#include "FsHelpers.h"
|
|
||||||
#include "Page.h"
|
#include "Page.h"
|
||||||
#include "parsers/ChapterHtmlSlimParser.h"
|
#include "parsers/ChapterHtmlSlimParser.h"
|
||||||
|
|
||||||
@ -16,7 +14,10 @@ constexpr uint8_t SECTION_FILE_VERSION = 5;
|
|||||||
void Section::onPageComplete(std::unique_ptr<Page> page) {
|
void Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||||
const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin";
|
const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin";
|
||||||
|
|
||||||
std::ofstream outputFile("/sd" + filePath);
|
File outputFile;
|
||||||
|
if (!FsHelpers::openFileForWrite("SCT", filePath, outputFile)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
page->serialize(outputFile);
|
page->serialize(outputFile);
|
||||||
outputFile.close();
|
outputFile.close();
|
||||||
|
|
||||||
@ -28,7 +29,10 @@ void Section::onPageComplete(std::unique_ptr<Page> page) {
|
|||||||
void Section::writeCacheMetadata(const int fontId, const float lineCompression, const int marginTop,
|
void Section::writeCacheMetadata(const int fontId, const float lineCompression, const int marginTop,
|
||||||
const int marginRight, const int marginBottom, const int marginLeft,
|
const int marginRight, const int marginBottom, const int marginLeft,
|
||||||
const bool extraParagraphSpacing) const {
|
const bool extraParagraphSpacing) const {
|
||||||
std::ofstream outputFile(("/sd" + cachePath + "/section.bin").c_str());
|
File outputFile;
|
||||||
|
if (!FsHelpers::openFileForWrite("SCT", cachePath + "/section.bin", outputFile)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
serialization::writePod(outputFile, SECTION_FILE_VERSION);
|
serialization::writePod(outputFile, SECTION_FILE_VERSION);
|
||||||
serialization::writePod(outputFile, fontId);
|
serialization::writePod(outputFile, fontId);
|
||||||
serialization::writePod(outputFile, lineCompression);
|
serialization::writePod(outputFile, lineCompression);
|
||||||
@ -44,17 +48,12 @@ void Section::writeCacheMetadata(const int fontId, const float lineCompression,
|
|||||||
bool Section::loadCacheMetadata(const int fontId, const float lineCompression, const int marginTop,
|
bool Section::loadCacheMetadata(const int fontId, const float lineCompression, const int marginTop,
|
||||||
const int marginRight, const int marginBottom, const int marginLeft,
|
const int marginRight, const int marginBottom, const int marginLeft,
|
||||||
const bool extraParagraphSpacing) {
|
const bool extraParagraphSpacing) {
|
||||||
if (!SD.exists(cachePath.c_str())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto sectionFilePath = cachePath + "/section.bin";
|
const auto sectionFilePath = cachePath + "/section.bin";
|
||||||
if (!SD.exists(sectionFilePath.c_str())) {
|
File inputFile;
|
||||||
|
if (!FsHelpers::openFileForRead("SCT", sectionFilePath, inputFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::ifstream inputFile(("/sd" + sectionFilePath).c_str());
|
|
||||||
|
|
||||||
// Match parameters
|
// Match parameters
|
||||||
{
|
{
|
||||||
uint8_t version;
|
uint8_t version;
|
||||||
@ -117,15 +116,14 @@ bool Section::clearCache() const {
|
|||||||
bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const int marginTop,
|
bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const int marginTop,
|
||||||
const int marginRight, const int marginBottom, const int marginLeft,
|
const int marginRight, const int marginBottom, const int marginLeft,
|
||||||
const bool extraParagraphSpacing) {
|
const bool extraParagraphSpacing) {
|
||||||
const auto localPath = epub->getSpineHref(spineIndex);
|
const auto localPath = epub->getSpineItem(spineIndex).href;
|
||||||
|
|
||||||
// TODO: Should we get rid of this file all together?
|
|
||||||
// It currently saves us a bit of memory by allowing for all the inflation bits to be released
|
|
||||||
// before loading the XML parser
|
|
||||||
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
|
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
|
||||||
File f = SD.open(tmpHtmlPath.c_str(), FILE_WRITE, true);
|
File tmpHtml;
|
||||||
bool success = epub->readItemContentsToStream(localPath, f, 1024);
|
if (!FsHelpers::openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
|
||||||
f.close();
|
return false;
|
||||||
|
}
|
||||||
|
bool success = epub->readItemContentsToStream(localPath, tmpHtml, 1024);
|
||||||
|
tmpHtml.close();
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Serial.printf("[%lu] [SCT] Failed to stream item contents to temp file\n", millis());
|
Serial.printf("[%lu] [SCT] Failed to stream item contents to temp file\n", millis());
|
||||||
@ -134,10 +132,8 @@ bool Section::persistPageDataToSD(const int fontId, const float lineCompression,
|
|||||||
|
|
||||||
Serial.printf("[%lu] [SCT] Streamed temp HTML to %s\n", millis(), tmpHtmlPath.c_str());
|
Serial.printf("[%lu] [SCT] Streamed temp HTML to %s\n", millis(), tmpHtmlPath.c_str());
|
||||||
|
|
||||||
const auto sdTmpHtmlPath = "/sd" + tmpHtmlPath;
|
ChapterHtmlSlimParser visitor(tmpHtmlPath, renderer, fontId, lineCompression, marginTop, marginRight, marginBottom,
|
||||||
|
marginLeft, extraParagraphSpacing,
|
||||||
ChapterHtmlSlimParser visitor(sdTmpHtmlPath.c_str(), renderer, fontId, lineCompression, marginTop, marginRight,
|
|
||||||
marginBottom, marginLeft, extraParagraphSpacing,
|
|
||||||
[this](std::unique_ptr<Page> page) { this->onPageComplete(std::move(page)); });
|
[this](std::unique_ptr<Page> page) { this->onPageComplete(std::move(page)); });
|
||||||
success = visitor.parseAndBuildPages();
|
success = visitor.parseAndBuildPages();
|
||||||
|
|
||||||
@ -153,13 +149,12 @@ bool Section::persistPageDataToSD(const int fontId, const float lineCompression,
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Page> Section::loadPageFromSD() const {
|
std::unique_ptr<Page> Section::loadPageFromSD() const {
|
||||||
const auto filePath = "/sd" + cachePath + "/page_" + std::to_string(currentPage) + ".bin";
|
const auto filePath = cachePath + "/page_" + std::to_string(currentPage) + ".bin";
|
||||||
if (!SD.exists(filePath.c_str() + 3)) {
|
|
||||||
Serial.printf("[%lu] [SCT] Page file does not exist: %s\n", millis(), filePath.c_str());
|
File inputFile;
|
||||||
|
if (!FsHelpers::openFileForRead("SCT", filePath, inputFile)) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::ifstream inputFile(filePath);
|
|
||||||
auto page = Page::deserialize(inputFile);
|
auto page = Page::deserialize(inputFile);
|
||||||
inputFile.close();
|
inputFile.close();
|
||||||
return page;
|
return page;
|
||||||
|
|||||||
@ -1,311 +0,0 @@
|
|||||||
#include "SpineTocCache.h"
|
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
|
||||||
#include <SD.h>
|
|
||||||
#include <Serialization.h>
|
|
||||||
#include <ZipFile.h>
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "FsHelpers.h"
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
constexpr uint8_t SPINE_TOC_CACHE_VERSION = 1;
|
|
||||||
constexpr size_t SPINE_TOC_META_HEADER_SIZE = sizeof(SPINE_TOC_CACHE_VERSION) + sizeof(uint16_t) * 2;
|
|
||||||
constexpr char spineTocMetaBinFile[] = "/spine_toc_meta.bin";
|
|
||||||
constexpr char spineBinFile[] = "/spine.bin";
|
|
||||||
constexpr char tocBinFile[] = "/toc.bin";
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
bool SpineTocCache::beginWrite() {
|
|
||||||
buildMode = true;
|
|
||||||
spineCount = 0;
|
|
||||||
tocCount = 0;
|
|
||||||
|
|
||||||
Serial.printf("[%lu] [STC] Beginning write to cache path: %s\n", millis(), cachePath.c_str());
|
|
||||||
|
|
||||||
// Open spine file for writing
|
|
||||||
if (!FsHelpers::openFileForWrite("STC", cachePath + spineBinFile, spineFile)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open TOC file for writing
|
|
||||||
if (!FsHelpers::openFileForWrite("STC", cachePath + tocBinFile, tocFile)) {
|
|
||||||
spineFile.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open meta file for writing
|
|
||||||
if (!FsHelpers::openFileForWrite("STC", cachePath + spineTocMetaBinFile, metaFile)) {
|
|
||||||
spineFile.close();
|
|
||||||
tocFile.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write 0s into first slots, LUT is written during `addSpineEntry` and `addTocEntry`, and counts are rewritten at
|
|
||||||
// the end
|
|
||||||
serialization::writePod(metaFile, SPINE_TOC_CACHE_VERSION);
|
|
||||||
serialization::writePod(metaFile, spineCount);
|
|
||||||
serialization::writePod(metaFile, tocCount);
|
|
||||||
|
|
||||||
Serial.printf("[%lu] [STC] Began writing cache files\n", millis());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t SpineTocCache::writeSpineEntry(File& file, const SpineEntry& entry) const {
|
|
||||||
const auto pos = file.position();
|
|
||||||
serialization::writeString(file, entry.href);
|
|
||||||
serialization::writePod(file, entry.cumulativeSize);
|
|
||||||
serialization::writePod(file, entry.tocIndex);
|
|
||||||
return pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t SpineTocCache::writeTocEntry(File& file, const TocEntry& entry) const {
|
|
||||||
const auto pos = file.position();
|
|
||||||
serialization::writeString(file, entry.title);
|
|
||||||
serialization::writeString(file, entry.href);
|
|
||||||
serialization::writeString(file, entry.anchor);
|
|
||||||
serialization::writePod(file, entry.level);
|
|
||||||
serialization::writePod(file, entry.spineIndex);
|
|
||||||
return pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: for the LUT to be accurate, this **MUST** be called for all spine items before `addTocEntry` is ever called
|
|
||||||
// this is because in this function we're marking positions of the items
|
|
||||||
void SpineTocCache::addSpineEntry(const std::string& href) {
|
|
||||||
if (!buildMode || !spineFile || !metaFile) {
|
|
||||||
Serial.printf("[%lu] [STC] addSpineEntry called but not in build mode\n", millis());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SpineEntry entry(href, 0, -1);
|
|
||||||
const auto position = writeSpineEntry(spineFile, entry);
|
|
||||||
serialization::writePod(metaFile, position);
|
|
||||||
spineCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SpineTocCache::addTocEntry(const std::string& title, const std::string& href, const std::string& anchor,
|
|
||||||
const uint8_t level) {
|
|
||||||
if (!buildMode || !tocFile || !metaFile) {
|
|
||||||
Serial.printf("[%lu] [STC] addTocEntry called but not in build mode\n", millis());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TocEntry entry(title, href, anchor, level, -1);
|
|
||||||
const auto position = writeTocEntry(tocFile, entry);
|
|
||||||
serialization::writePod(metaFile, position);
|
|
||||||
tocCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SpineTocCache::endWrite() {
|
|
||||||
if (!buildMode) {
|
|
||||||
Serial.printf("[%lu] [STC] endWrite called but not in build mode\n", millis());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
spineFile.close();
|
|
||||||
tocFile.close();
|
|
||||||
|
|
||||||
// Write correct counts into meta file
|
|
||||||
metaFile.seek(sizeof(SPINE_TOC_CACHE_VERSION));
|
|
||||||
serialization::writePod(metaFile, spineCount);
|
|
||||||
serialization::writePod(metaFile, tocCount);
|
|
||||||
metaFile.close();
|
|
||||||
|
|
||||||
buildMode = false;
|
|
||||||
Serial.printf("[%lu] [STC] Wrote %d spine, %d TOC entries\n", millis(), spineCount, tocCount);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
SpineTocCache::SpineEntry SpineTocCache::readSpineEntry(File& file) const {
|
|
||||||
SpineEntry entry;
|
|
||||||
serialization::readString(file, entry.href);
|
|
||||||
serialization::readPod(file, entry.cumulativeSize);
|
|
||||||
serialization::readPod(file, entry.tocIndex);
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
SpineTocCache::TocEntry SpineTocCache::readTocEntry(File& file) const {
|
|
||||||
TocEntry entry;
|
|
||||||
serialization::readString(file, entry.title);
|
|
||||||
serialization::readString(file, entry.href);
|
|
||||||
serialization::readString(file, entry.anchor);
|
|
||||||
serialization::readPod(file, entry.level);
|
|
||||||
serialization::readPod(file, entry.spineIndex);
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SpineTocCache::updateMapsAndSizes(const std::string& epubPath) {
|
|
||||||
Serial.printf("[%lu] [STC] Computing mappings and sizes for %d spine, %d TOC entries\n", millis(), spineCount,
|
|
||||||
tocCount);
|
|
||||||
|
|
||||||
std::vector<SpineEntry> spineEntries;
|
|
||||||
spineEntries.reserve(spineCount);
|
|
||||||
|
|
||||||
// Load only the spine items, update them in memory while loading one TOC at a time and storing it
|
|
||||||
{
|
|
||||||
if (!FsHelpers::openFileForRead("STC", cachePath + spineBinFile, spineFile)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < spineCount; i++) {
|
|
||||||
spineEntries.push_back(readSpineEntry(spineFile));
|
|
||||||
}
|
|
||||||
spineFile.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterate over TOC entries and update them with the spine mapping
|
|
||||||
// We do this by moving the TOC file and then making a new one parsing through both at the same time
|
|
||||||
{
|
|
||||||
SD.rename((cachePath + tocBinFile).c_str(), (cachePath + tocBinFile + ".tmp").c_str());
|
|
||||||
File tempTocFile;
|
|
||||||
if (!FsHelpers::openFileForRead("STC", cachePath + tocBinFile + ".tmp", tempTocFile)) {
|
|
||||||
SD.remove((cachePath + tocBinFile + ".tmp").c_str());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!FsHelpers::openFileForWrite("STC", cachePath + tocBinFile, tocFile)) {
|
|
||||||
tempTocFile.close();
|
|
||||||
SD.remove((cachePath + tocBinFile + ".tmp").c_str());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < tocCount; i++) {
|
|
||||||
auto tocEntry = readTocEntry(tempTocFile);
|
|
||||||
|
|
||||||
// Find the matching spine entry
|
|
||||||
for (int j = 0; j < spineCount; j++) {
|
|
||||||
if (spineEntries[j].href == tocEntry.href) {
|
|
||||||
tocEntry.spineIndex = static_cast<int16_t>(j);
|
|
||||||
// Point the spine to the first TOC entry we come across (in the case that there are multiple)
|
|
||||||
if (spineEntries[j].tocIndex == -1) spineEntries[j].tocIndex = static_cast<int16_t>(i);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writeTocEntry(tocFile, tocEntry);
|
|
||||||
}
|
|
||||||
tocFile.close();
|
|
||||||
tempTocFile.close();
|
|
||||||
SD.remove((cachePath + tocBinFile + ".tmp").c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
// By this point all the spine items in memory should have the right `tocIndex` and the TOC file is complete
|
|
||||||
|
|
||||||
// Next, compute cumulative sizes
|
|
||||||
{
|
|
||||||
const ZipFile zip("/sd" + epubPath);
|
|
||||||
size_t cumSize = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < spineCount; i++) {
|
|
||||||
size_t itemSize = 0;
|
|
||||||
const std::string path = FsHelpers::normalisePath(spineEntries[i].href);
|
|
||||||
if (zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
|
||||||
cumSize += itemSize;
|
|
||||||
spineEntries[i].cumulativeSize = cumSize;
|
|
||||||
} else {
|
|
||||||
Serial.printf("[%lu] [STC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Serial.printf("[%lu] [STC] Book size: %lu\n", millis(), cumSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rewrite spine file with updated data
|
|
||||||
{
|
|
||||||
if (!FsHelpers::openFileForWrite("STC", cachePath + spineBinFile, spineFile)) {
|
|
||||||
// metaFile.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (const auto& entry : spineEntries) {
|
|
||||||
writeSpineEntry(spineFile, entry);
|
|
||||||
}
|
|
||||||
spineFile.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear vectors to free memory
|
|
||||||
spineEntries.clear();
|
|
||||||
spineEntries.shrink_to_fit();
|
|
||||||
|
|
||||||
Serial.printf("[%lu] [STC] Updated cache with mappings and sizes\n", millis());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Opens (and leaves open all three files for fast access)
|
|
||||||
bool SpineTocCache::load() {
|
|
||||||
// Load metadata
|
|
||||||
if (!FsHelpers::openFileForRead("STC", cachePath + spineTocMetaBinFile, metaFile)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t version;
|
|
||||||
serialization::readPod(metaFile, version);
|
|
||||||
if (version != SPINE_TOC_CACHE_VERSION) {
|
|
||||||
Serial.printf("[%lu] [STC] Cache version mismatch: expected %d, got %d\n", millis(), SPINE_TOC_CACHE_VERSION,
|
|
||||||
version);
|
|
||||||
metaFile.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!FsHelpers::openFileForRead("STC", cachePath + spineBinFile, spineFile)) {
|
|
||||||
metaFile.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!FsHelpers::openFileForRead("STC", cachePath + tocBinFile, tocFile)) {
|
|
||||||
metaFile.close();
|
|
||||||
spineFile.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
serialization::readPod(metaFile, spineCount);
|
|
||||||
serialization::readPod(metaFile, tocCount);
|
|
||||||
|
|
||||||
loaded = true;
|
|
||||||
Serial.printf("[%lu] [STC] Loaded cache metadata: %d spine, %d TOC entries\n", millis(), spineCount, tocCount);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
SpineTocCache::SpineEntry SpineTocCache::getSpineEntry(const int index) {
|
|
||||||
if (!loaded) {
|
|
||||||
Serial.printf("[%lu] [STC] getSpineEntry called but cache not loaded\n", millis());
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index < 0 || index >= static_cast<int>(spineCount)) {
|
|
||||||
Serial.printf("[%lu] [STC] getSpineEntry index %d out of range\n", millis(), index);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seek to spine LUT item, read from LUT and get out data
|
|
||||||
metaFile.seek(SPINE_TOC_META_HEADER_SIZE + sizeof(size_t) * index);
|
|
||||||
size_t spineEntryPos;
|
|
||||||
serialization::readPod(metaFile, spineEntryPos);
|
|
||||||
spineFile.seek(spineEntryPos);
|
|
||||||
auto entry = readSpineEntry(spineFile);
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
SpineTocCache::TocEntry SpineTocCache::getTocEntry(const int index) {
|
|
||||||
if (!loaded) {
|
|
||||||
Serial.printf("[%lu] [STC] getTocEntry called but cache not loaded\n", millis());
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index < 0 || index >= static_cast<int>(tocCount)) {
|
|
||||||
Serial.printf("[%lu] [STC] getTocEntry index %d out of range\n", millis(), index);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seek to TOC LUT item, read from LUT and get out data
|
|
||||||
metaFile.seek(SPINE_TOC_META_HEADER_SIZE + sizeof(size_t) * spineCount + sizeof(size_t) * index);
|
|
||||||
size_t tocEntryPos;
|
|
||||||
serialization::readPod(metaFile, tocEntryPos);
|
|
||||||
tocFile.seek(tocEntryPos);
|
|
||||||
auto entry = readTocEntry(tocFile);
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
int SpineTocCache::getSpineCount() const { return spineCount; }
|
|
||||||
|
|
||||||
int SpineTocCache::getTocCount() const { return tocCount; }
|
|
||||||
|
|
||||||
bool SpineTocCache::isLoaded() const { return loaded; }
|
|
||||||
@ -17,27 +17,27 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextBlock::serialize(std::ostream& os) const {
|
void TextBlock::serialize(File& file) const {
|
||||||
// words
|
// words
|
||||||
const uint32_t wc = words.size();
|
const uint32_t wc = words.size();
|
||||||
serialization::writePod(os, wc);
|
serialization::writePod(file, wc);
|
||||||
for (const auto& w : words) serialization::writeString(os, w);
|
for (const auto& w : words) serialization::writeString(file, w);
|
||||||
|
|
||||||
// wordXpos
|
// wordXpos
|
||||||
const uint32_t xc = wordXpos.size();
|
const uint32_t xc = wordXpos.size();
|
||||||
serialization::writePod(os, xc);
|
serialization::writePod(file, xc);
|
||||||
for (auto x : wordXpos) serialization::writePod(os, x);
|
for (auto x : wordXpos) serialization::writePod(file, x);
|
||||||
|
|
||||||
// wordStyles
|
// wordStyles
|
||||||
const uint32_t sc = wordStyles.size();
|
const uint32_t sc = wordStyles.size();
|
||||||
serialization::writePod(os, sc);
|
serialization::writePod(file, sc);
|
||||||
for (auto s : wordStyles) serialization::writePod(os, s);
|
for (auto s : wordStyles) serialization::writePod(file, s);
|
||||||
|
|
||||||
// style
|
// style
|
||||||
serialization::writePod(os, style);
|
serialization::writePod(file, style);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<TextBlock> TextBlock::deserialize(std::istream& is) {
|
std::unique_ptr<TextBlock> TextBlock::deserialize(File& file) {
|
||||||
uint32_t wc, xc, sc;
|
uint32_t wc, xc, sc;
|
||||||
std::list<std::string> words;
|
std::list<std::string> words;
|
||||||
std::list<uint16_t> wordXpos;
|
std::list<uint16_t> wordXpos;
|
||||||
@ -45,22 +45,22 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(std::istream& is) {
|
|||||||
BLOCK_STYLE style;
|
BLOCK_STYLE style;
|
||||||
|
|
||||||
// words
|
// words
|
||||||
serialization::readPod(is, wc);
|
serialization::readPod(file, wc);
|
||||||
words.resize(wc);
|
words.resize(wc);
|
||||||
for (auto& w : words) serialization::readString(is, w);
|
for (auto& w : words) serialization::readString(file, w);
|
||||||
|
|
||||||
// wordXpos
|
// wordXpos
|
||||||
serialization::readPod(is, xc);
|
serialization::readPod(file, xc);
|
||||||
wordXpos.resize(xc);
|
wordXpos.resize(xc);
|
||||||
for (auto& x : wordXpos) serialization::readPod(is, x);
|
for (auto& x : wordXpos) serialization::readPod(file, x);
|
||||||
|
|
||||||
// wordStyles
|
// wordStyles
|
||||||
serialization::readPod(is, sc);
|
serialization::readPod(file, sc);
|
||||||
wordStyles.resize(sc);
|
wordStyles.resize(sc);
|
||||||
for (auto& s : wordStyles) serialization::readPod(is, s);
|
for (auto& s : wordStyles) serialization::readPod(file, s);
|
||||||
|
|
||||||
// style
|
// style
|
||||||
serialization::readPod(is, style);
|
serialization::readPod(file, style);
|
||||||
|
|
||||||
return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), style));
|
return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), style));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <EpdFontFamily.h>
|
#include <EpdFontFamily.h>
|
||||||
|
#include <FS.h>
|
||||||
|
|
||||||
#include <list>
|
#include <list>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@ -35,6 +36,6 @@ class TextBlock final : public Block {
|
|||||||
// given a renderer works out where to break the words into lines
|
// given a renderer works out where to break the words into lines
|
||||||
void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
|
void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
|
||||||
BlockType getType() override { return TEXT_BLOCK; }
|
BlockType getType() override { return TEXT_BLOCK; }
|
||||||
void serialize(std::ostream& os) const;
|
void serialize(File& file) const;
|
||||||
static std::unique_ptr<TextBlock> deserialize(std::istream& is);
|
static std::unique_ptr<TextBlock> deserialize(File& file);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
#include "ChapterHtmlSlimParser.h"
|
#include "ChapterHtmlSlimParser.h"
|
||||||
|
|
||||||
|
#include <FsHelpers.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <expat.h>
|
#include <expat.h>
|
||||||
@ -214,9 +215,8 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
FILE* file = fopen(filepath, "r");
|
File file;
|
||||||
if (!file) {
|
if (!FsHelpers::openFileForRead("EHP", filepath, file)) {
|
||||||
Serial.printf("[%lu] [EHP] Couldn't open file %s\n", millis(), filepath);
|
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -233,23 +233,23 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
XML_SetCharacterDataHandler(parser, nullptr);
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
fclose(file);
|
file.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const size_t len = fread(buf, 1, 1024, file);
|
const size_t len = file.read(static_cast<uint8_t*>(buf), 1024);
|
||||||
|
|
||||||
if (ferror(file)) {
|
if (len == 0) {
|
||||||
Serial.printf("[%lu] [EHP] File read error\n", millis());
|
Serial.printf("[%lu] [EHP] File read error\n", millis());
|
||||||
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);
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
fclose(file);
|
file.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
done = feof(file);
|
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),
|
Serial.printf("[%lu] [EHP] Parse error at line %lu:\n%s\n", millis(), XML_GetCurrentLineNumber(parser),
|
||||||
@ -258,7 +258,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
XML_SetCharacterDataHandler(parser, nullptr);
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
fclose(file);
|
file.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} while (!done);
|
} while (!done);
|
||||||
@ -267,7 +267,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
XML_SetCharacterDataHandler(parser, nullptr);
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
fclose(file);
|
file.close();
|
||||||
|
|
||||||
// Process last page if there is still text
|
// Process last page if there is still text
|
||||||
if (currentTextBlock) {
|
if (currentTextBlock) {
|
||||||
|
|||||||
@ -15,7 +15,7 @@ class GfxRenderer;
|
|||||||
#define MAX_WORD_SIZE 200
|
#define MAX_WORD_SIZE 200
|
||||||
|
|
||||||
class ChapterHtmlSlimParser {
|
class ChapterHtmlSlimParser {
|
||||||
const char* filepath;
|
const std::string& filepath;
|
||||||
GfxRenderer& renderer;
|
GfxRenderer& renderer;
|
||||||
std::function<void(std::unique_ptr<Page>)> completePageFn;
|
std::function<void(std::unique_ptr<Page>)> completePageFn;
|
||||||
int depth = 0;
|
int depth = 0;
|
||||||
@ -45,7 +45,7 @@ class ChapterHtmlSlimParser {
|
|||||||
static void XMLCALL endElement(void* userData, const XML_Char* name);
|
static void XMLCALL endElement(void* userData, const XML_Char* name);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ChapterHtmlSlimParser(const char* filepath, GfxRenderer& renderer, const int fontId,
|
explicit ChapterHtmlSlimParser(const std::string& filepath, GfxRenderer& renderer, const int fontId,
|
||||||
const float lineCompression, const int marginTop, const int marginRight,
|
const float lineCompression, const int marginTop, const int marginRight,
|
||||||
const int marginBottom, const int marginLeft, const bool extraParagraphSpacing,
|
const int marginBottom, const int marginLeft, const bool extraParagraphSpacing,
|
||||||
const std::function<void(std::unique_ptr<Page>)>& completePageFn)
|
const std::function<void(std::unique_ptr<Page>)>& completePageFn)
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
#include "ContentOpfParser.h"
|
#include "ContentOpfParser.h"
|
||||||
|
|
||||||
|
#include <FsHelpers.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
#include <ZipFile.h>
|
#include <ZipFile.h>
|
||||||
|
|
||||||
|
#include "../BookMetadataCache.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml";
|
constexpr char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml";
|
||||||
}
|
constexpr char itemCacheFile[] = "/.items.bin";
|
||||||
|
} // namespace
|
||||||
|
|
||||||
bool ContentOpfParser::setup() {
|
bool ContentOpfParser::setup() {
|
||||||
parser = XML_ParserCreate(nullptr);
|
parser = XML_ParserCreate(nullptr);
|
||||||
@ -29,6 +33,12 @@ ContentOpfParser::~ContentOpfParser() {
|
|||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
parser = nullptr;
|
parser = nullptr;
|
||||||
}
|
}
|
||||||
|
if (tempItemStore) {
|
||||||
|
tempItemStore.close();
|
||||||
|
}
|
||||||
|
if (SD.exists((cachePath + itemCacheFile).c_str())) {
|
||||||
|
SD.remove((cachePath + itemCacheFile).c_str());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t ContentOpfParser::write(const uint8_t data) { return write(&data, 1); }
|
size_t ContentOpfParser::write(const uint8_t data) { return write(&data, 1); }
|
||||||
@ -95,13 +105,21 @@ 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;
|
||||||
self->tempItemStore = SD.open("/.crosspoint/.tmp-items.bin", FILE_WRITE, true);
|
if (!FsHelpers::openFileForWrite("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||||
|
Serial.printf(
|
||||||
|
"[%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;
|
||||||
self->tempItemStore = SD.open("/.crosspoint/.tmp-items.bin", FILE_READ);
|
if (!FsHelpers::openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||||
|
Serial.printf(
|
||||||
|
"[%lu] [COF] Couldn't open temp items file for reading. This is probably going to be a fatal error.\n",
|
||||||
|
millis());
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,10 +156,13 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write items down to SD card
|
||||||
serialization::writeString(self->tempItemStore, itemId);
|
serialization::writeString(self->tempItemStore, itemId);
|
||||||
serialization::writeString(self->tempItemStore, href);
|
serialization::writeString(self->tempItemStore, href);
|
||||||
// // Write items down to SD card
|
|
||||||
// self->items[itemId] = href;
|
if (itemId == self->coverItemId) {
|
||||||
|
self->coverItemHref = href;
|
||||||
|
}
|
||||||
|
|
||||||
if (mediaType == MEDIA_TYPE_NCX) {
|
if (mediaType == MEDIA_TYPE_NCX) {
|
||||||
if (self->tocNcxPath.empty()) {
|
if (self->tocNcxPath.empty()) {
|
||||||
@ -154,7 +175,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: This relies on spine appearing after item manifest
|
// NOTE: This relies on spine appearing after item manifest (which is pretty safe as it's part of the EPUB spec)
|
||||||
// Only run the spine parsing if there's a cache to add it to
|
// Only run the spine parsing if there's a cache to add it to
|
||||||
if (self->cache) {
|
if (self->cache) {
|
||||||
if (self->state == IN_SPINE && (strcmp(name, "itemref") == 0 || strcmp(name, "opf:itemref") == 0)) {
|
if (self->state == IN_SPINE && (strcmp(name, "itemref") == 0 || strcmp(name, "opf:itemref") == 0)) {
|
||||||
@ -169,7 +190,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
serialization::readString(self->tempItemStore, itemId);
|
serialization::readString(self->tempItemStore, itemId);
|
||||||
serialization::readString(self->tempItemStore, href);
|
serialization::readString(self->tempItemStore, href);
|
||||||
if (itemId == idref) {
|
if (itemId == idref) {
|
||||||
self->cache->addSpineEntry(href);
|
self->cache->createSpineEntry(href);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,9 +2,10 @@
|
|||||||
#include <Print.h>
|
#include <Print.h>
|
||||||
|
|
||||||
#include "Epub.h"
|
#include "Epub.h"
|
||||||
#include "Epub/SpineTocCache.h"
|
|
||||||
#include "expat.h"
|
#include "expat.h"
|
||||||
|
|
||||||
|
class BookMetadataCache;
|
||||||
|
|
||||||
class ContentOpfParser final : public Print {
|
class ContentOpfParser final : public Print {
|
||||||
enum ParserState {
|
enum ParserState {
|
||||||
START,
|
START,
|
||||||
@ -15,12 +16,14 @@ class ContentOpfParser final : public Print {
|
|||||||
IN_SPINE,
|
IN_SPINE,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const std::string& cachePath;
|
||||||
const std::string& baseContentPath;
|
const std::string& baseContentPath;
|
||||||
size_t remainingSize;
|
size_t remainingSize;
|
||||||
XML_Parser parser = nullptr;
|
XML_Parser parser = nullptr;
|
||||||
ParserState state = START;
|
ParserState state = START;
|
||||||
SpineTocCache* cache;
|
BookMetadataCache* cache;
|
||||||
File tempItemStore;
|
File tempItemStore;
|
||||||
|
std::string coverItemId;
|
||||||
|
|
||||||
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||||
static void characterData(void* userData, const XML_Char* s, int len);
|
static void characterData(void* userData, const XML_Char* s, int len);
|
||||||
@ -29,10 +32,11 @@ class ContentOpfParser final : public Print {
|
|||||||
public:
|
public:
|
||||||
std::string title;
|
std::string title;
|
||||||
std::string tocNcxPath;
|
std::string tocNcxPath;
|
||||||
std::string coverItemId;
|
std::string coverItemHref;
|
||||||
|
|
||||||
explicit ContentOpfParser(const std::string& baseContentPath, const size_t xmlSize, SpineTocCache* cache)
|
explicit ContentOpfParser(const std::string& cachePath, const std::string& baseContentPath, const size_t xmlSize,
|
||||||
: baseContentPath(baseContentPath), remainingSize(xmlSize), cache(cache) {}
|
BookMetadataCache* cache)
|
||||||
|
: cachePath(cachePath), baseContentPath(baseContentPath), remainingSize(xmlSize), cache(cache) {}
|
||||||
~ContentOpfParser() override;
|
~ContentOpfParser() override;
|
||||||
|
|
||||||
bool setup();
|
bool setup();
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
#include "TocNcxParser.h"
|
#include "TocNcxParser.h"
|
||||||
|
|
||||||
#include <Esp.h>
|
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
|
|
||||||
|
#include "../BookMetadataCache.h"
|
||||||
|
|
||||||
bool TocNcxParser::setup() {
|
bool TocNcxParser::setup() {
|
||||||
parser = XML_ParserCreate(nullptr);
|
parser = XML_ParserCreate(nullptr);
|
||||||
if (!parser) {
|
if (!parser) {
|
||||||
@ -168,7 +169,7 @@ void XMLCALL TocNcxParser::endElement(void* userData, const XML_Char* name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (self->cache) {
|
if (self->cache) {
|
||||||
self->cache->addTocEntry(self->currentLabel, href, anchor, self->currentDepth);
|
self->cache->createTocEntry(self->currentLabel, href, anchor, self->currentDepth);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear them so we don't re-add them if there are weird XML structures
|
// Clear them so we don't re-add them if there are weird XML structures
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <Print.h>
|
#include <Print.h>
|
||||||
|
#include <expat.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "Epub/SpineTocCache.h"
|
class BookMetadataCache;
|
||||||
#include "expat.h"
|
|
||||||
|
|
||||||
class TocNcxParser final : public Print {
|
class TocNcxParser final : public Print {
|
||||||
enum ParserState { START, IN_NCX, IN_NAV_MAP, IN_NAV_POINT, IN_NAV_LABEL, IN_NAV_LABEL_TEXT, IN_CONTENT };
|
enum ParserState { START, IN_NCX, IN_NAV_MAP, IN_NAV_POINT, IN_NAV_LABEL, IN_NAV_LABEL_TEXT, IN_CONTENT };
|
||||||
@ -13,7 +13,7 @@ class TocNcxParser final : public Print {
|
|||||||
size_t remainingSize;
|
size_t remainingSize;
|
||||||
XML_Parser parser = nullptr;
|
XML_Parser parser = nullptr;
|
||||||
ParserState state = START;
|
ParserState state = START;
|
||||||
SpineTocCache* cache;
|
BookMetadataCache* cache;
|
||||||
|
|
||||||
std::string currentLabel;
|
std::string currentLabel;
|
||||||
std::string currentSrc;
|
std::string currentSrc;
|
||||||
@ -24,7 +24,7 @@ class TocNcxParser final : public Print {
|
|||||||
static void endElement(void* userData, const XML_Char* name);
|
static void endElement(void* userData, const XML_Char* name);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit TocNcxParser(const std::string& baseContentPath, const size_t xmlSize, SpineTocCache* cache)
|
explicit TocNcxParser(const std::string& baseContentPath, const size_t xmlSize, BookMetadataCache* cache)
|
||||||
: baseContentPath(baseContentPath), remainingSize(xmlSize), cache(cache) {}
|
: baseContentPath(baseContentPath), remainingSize(xmlSize), cache(cache) {}
|
||||||
~TocNcxParser() override;
|
~TocNcxParser() override;
|
||||||
|
|
||||||
|
|||||||
112
lib/FsHelpers/FsHelpers.cpp
Normal file
112
lib/FsHelpers/FsHelpers.cpp
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
#include "FsHelpers.h"
|
||||||
|
|
||||||
|
#include <SD.h>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
bool FsHelpers::openFileForRead(const char* moduleName, const char* path, File& file) {
|
||||||
|
if (!SD.exists(path)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
file = SD.open(path, FILE_READ);
|
||||||
|
if (!file) {
|
||||||
|
Serial.printf("[%lu] [%s] Failed to open file for reading: %s\n", millis(), moduleName, path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FsHelpers::openFileForRead(const char* moduleName, const std::string& path, File& file) {
|
||||||
|
return openFileForRead(moduleName, path.c_str(), file);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FsHelpers::openFileForRead(const char* moduleName, const String& path, File& file) {
|
||||||
|
return openFileForRead(moduleName, path.c_str(), file);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FsHelpers::openFileForWrite(const char* moduleName, const char* path, File& file) {
|
||||||
|
file = SD.open(path, FILE_WRITE, true);
|
||||||
|
if (!file) {
|
||||||
|
Serial.printf("[%lu] [%s] Failed to open file for writing: %s\n", millis(), moduleName, path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FsHelpers::openFileForWrite(const char* moduleName, const std::string& path, File& file) {
|
||||||
|
return openFileForWrite(moduleName, path.c_str(), file);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FsHelpers::openFileForWrite(const char* moduleName, const String& path, File& file) {
|
||||||
|
return openFileForWrite(moduleName, path.c_str(), file);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FsHelpers::removeDir(const char* path) {
|
||||||
|
// 1. Open the directory
|
||||||
|
File dir = SD.open(path);
|
||||||
|
if (!dir) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!dir.isDirectory()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
File file = dir.openNextFile();
|
||||||
|
while (file) {
|
||||||
|
String filePath = path;
|
||||||
|
if (!filePath.endsWith("/")) {
|
||||||
|
filePath += "/";
|
||||||
|
}
|
||||||
|
filePath += file.name();
|
||||||
|
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
if (!removeDir(filePath.c_str())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!SD.remove(filePath.c_str())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file = dir.openNextFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SD.rmdir(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string FsHelpers::normalisePath(const std::string& path) {
|
||||||
|
std::vector<std::string> components;
|
||||||
|
std::string component;
|
||||||
|
|
||||||
|
for (const auto c : path) {
|
||||||
|
if (c == '/') {
|
||||||
|
if (!component.empty()) {
|
||||||
|
if (component == "..") {
|
||||||
|
if (!components.empty()) {
|
||||||
|
components.pop_back();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
components.push_back(component);
|
||||||
|
}
|
||||||
|
component.clear();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
component += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!component.empty()) {
|
||||||
|
components.push_back(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string result;
|
||||||
|
for (const auto& c : components) {
|
||||||
|
if (!result.empty()) {
|
||||||
|
result += "/";
|
||||||
|
}
|
||||||
|
result += c;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
14
lib/FsHelpers/FsHelpers.h
Normal file
14
lib/FsHelpers/FsHelpers.h
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <FS.h>
|
||||||
|
|
||||||
|
class FsHelpers {
|
||||||
|
public:
|
||||||
|
static bool openFileForRead(const char* moduleName, const char* path, File& file);
|
||||||
|
static bool openFileForRead(const char* moduleName, const std::string& path, File& file);
|
||||||
|
static bool openFileForRead(const char* moduleName, const String& path, File& file);
|
||||||
|
static bool openFileForWrite(const char* moduleName, const char* path, File& file);
|
||||||
|
static bool openFileForWrite(const char* moduleName, const std::string& path, File& file);
|
||||||
|
static bool openFileForWrite(const char* moduleName, const String& path, File& file);
|
||||||
|
static bool removeDir(const char* path);
|
||||||
|
static std::string normalisePath(const std::string& path);
|
||||||
|
};
|
||||||
@ -3,7 +3,7 @@ crosspoint_version = 0.8.1
|
|||||||
default_envs = default
|
default_envs = default
|
||||||
|
|
||||||
[base]
|
[base]
|
||||||
platform = espressif32
|
platform = espressif32 @ 6.12.0
|
||||||
board = esp32-c3-devkitm-1
|
board = esp32-c3-devkitm-1
|
||||||
framework = arduino
|
framework = arduino
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
|
|||||||
@ -1,26 +1,28 @@
|
|||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
|
|
||||||
|
#include <FsHelpers.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <fstream>
|
|
||||||
|
|
||||||
// Initialize the static instance
|
// Initialize the static instance
|
||||||
CrossPointSettings CrossPointSettings::instance;
|
CrossPointSettings CrossPointSettings::instance;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
||||||
constexpr uint8_t SETTINGS_COUNT = 3;
|
constexpr uint8_t SETTINGS_COUNT = 3;
|
||||||
constexpr char SETTINGS_FILE[] = "/sd/.crosspoint/settings.bin";
|
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool CrossPointSettings::saveToFile() const {
|
bool CrossPointSettings::saveToFile() const {
|
||||||
// Make sure the directory exists
|
// Make sure the directory exists
|
||||||
SD.mkdir("/.crosspoint");
|
SD.mkdir("/.crosspoint");
|
||||||
|
|
||||||
std::ofstream outputFile(SETTINGS_FILE);
|
File outputFile;
|
||||||
|
if (!FsHelpers::openFileForWrite("CPS", SETTINGS_FILE, outputFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
serialization::writePod(outputFile, SETTINGS_FILE_VERSION);
|
serialization::writePod(outputFile, SETTINGS_FILE_VERSION);
|
||||||
serialization::writePod(outputFile, SETTINGS_COUNT);
|
serialization::writePod(outputFile, SETTINGS_COUNT);
|
||||||
serialization::writePod(outputFile, sleepScreen);
|
serialization::writePod(outputFile, sleepScreen);
|
||||||
@ -33,13 +35,11 @@ bool CrossPointSettings::saveToFile() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool CrossPointSettings::loadFromFile() {
|
bool CrossPointSettings::loadFromFile() {
|
||||||
if (!SD.exists(SETTINGS_FILE + 3)) { // +3 to skip "/sd" prefix
|
File inputFile;
|
||||||
Serial.printf("[%lu] [CPS] Settings file does not exist, using defaults\n", millis());
|
if (!FsHelpers::openFileForRead("CPS", SETTINGS_FILE, inputFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::ifstream inputFile(SETTINGS_FILE);
|
|
||||||
|
|
||||||
uint8_t version;
|
uint8_t version;
|
||||||
serialization::readPod(inputFile, version);
|
serialization::readPod(inputFile, version);
|
||||||
if (version != SETTINGS_FILE_VERSION) {
|
if (version != SETTINGS_FILE_VERSION) {
|
||||||
|
|||||||
@ -1,20 +1,22 @@
|
|||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
|
|
||||||
|
#include <FsHelpers.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <SD.h>
|
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
#include <fstream>
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr uint8_t STATE_FILE_VERSION = 1;
|
constexpr uint8_t STATE_FILE_VERSION = 1;
|
||||||
constexpr char STATE_FILE[] = "/sd/.crosspoint/state.bin";
|
constexpr char STATE_FILE[] = "/.crosspoint/state.bin";
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
CrossPointState CrossPointState::instance;
|
CrossPointState CrossPointState::instance;
|
||||||
|
|
||||||
bool CrossPointState::saveToFile() const {
|
bool CrossPointState::saveToFile() const {
|
||||||
std::ofstream outputFile(STATE_FILE);
|
File outputFile;
|
||||||
|
if (!FsHelpers::openFileForWrite("CPS", STATE_FILE, outputFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
serialization::writePod(outputFile, STATE_FILE_VERSION);
|
serialization::writePod(outputFile, STATE_FILE_VERSION);
|
||||||
serialization::writeString(outputFile, openEpubPath);
|
serialization::writeString(outputFile, openEpubPath);
|
||||||
outputFile.close();
|
outputFile.close();
|
||||||
@ -22,7 +24,10 @@ bool CrossPointState::saveToFile() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool CrossPointState::loadFromFile() {
|
bool CrossPointState::loadFromFile() {
|
||||||
std::ifstream inputFile(STATE_FILE);
|
File inputFile;
|
||||||
|
if (!FsHelpers::openFileForRead("CPS", STATE_FILE, inputFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t version;
|
uint8_t version;
|
||||||
serialization::readPod(inputFile, version);
|
serialization::readPod(inputFile, version);
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
#include "WifiCredentialStore.h"
|
#include "WifiCredentialStore.h"
|
||||||
|
|
||||||
|
#include <FsHelpers.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
#include <fstream>
|
|
||||||
|
|
||||||
// Initialize the static instance
|
// Initialize the static instance
|
||||||
WifiCredentialStore WifiCredentialStore::instance;
|
WifiCredentialStore WifiCredentialStore::instance;
|
||||||
|
|
||||||
@ -14,7 +13,7 @@ namespace {
|
|||||||
constexpr uint8_t WIFI_FILE_VERSION = 1;
|
constexpr uint8_t WIFI_FILE_VERSION = 1;
|
||||||
|
|
||||||
// WiFi credentials file path
|
// WiFi credentials file path
|
||||||
constexpr char WIFI_FILE[] = "/sd/.crosspoint/wifi.bin";
|
constexpr char WIFI_FILE[] = "/.crosspoint/wifi.bin";
|
||||||
|
|
||||||
// Obfuscation key - "CrossPoint" in ASCII
|
// Obfuscation key - "CrossPoint" in ASCII
|
||||||
// This is NOT cryptographic security, just prevents casual file reading
|
// This is NOT cryptographic security, just prevents casual file reading
|
||||||
@ -33,9 +32,8 @@ bool WifiCredentialStore::saveToFile() const {
|
|||||||
// Make sure the directory exists
|
// Make sure the directory exists
|
||||||
SD.mkdir("/.crosspoint");
|
SD.mkdir("/.crosspoint");
|
||||||
|
|
||||||
std::ofstream file(WIFI_FILE, std::ios::binary);
|
File file;
|
||||||
if (!file) {
|
if (!FsHelpers::openFileForWrite("WCS", WIFI_FILE, file)) {
|
||||||
Serial.printf("[%lu] [WCS] Failed to open wifi.bin for writing\n", millis());
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,14 +60,8 @@ bool WifiCredentialStore::saveToFile() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool WifiCredentialStore::loadFromFile() {
|
bool WifiCredentialStore::loadFromFile() {
|
||||||
if (!SD.exists(WIFI_FILE + 3)) { // +3 to skip "/sd" prefix
|
File file;
|
||||||
Serial.printf("[%lu] [WCS] WiFi credentials file does not exist\n", millis());
|
if (!FsHelpers::openFileForRead("WCS", WIFI_FILE, file)) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::ifstream file(WIFI_FILE, std::ios::binary);
|
|
||||||
if (!file) {
|
|
||||||
Serial.printf("[%lu] [WCS] Failed to open wifi.bin for reading\n", millis());
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
#include "SleepActivity.h"
|
#include "SleepActivity.h"
|
||||||
|
|
||||||
#include <Epub.h>
|
#include <Epub.h>
|
||||||
|
#include <FsHelpers.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
|
|
||||||
@ -76,8 +77,8 @@ void SleepActivity::renderCustomSleepScreen() const {
|
|||||||
// Generate a random number between 1 and numFiles
|
// Generate a random number between 1 and numFiles
|
||||||
const auto randomFileIndex = random(numFiles);
|
const auto randomFileIndex = random(numFiles);
|
||||||
const auto filename = "/sleep/" + files[randomFileIndex];
|
const auto filename = "/sleep/" + files[randomFileIndex];
|
||||||
auto file = SD.open(filename.c_str());
|
File file;
|
||||||
if (file) {
|
if (FsHelpers::openFileForRead("SLP", filename, file)) {
|
||||||
Serial.printf("[%lu] [SLP] Randomly loading: /sleep/%s\n", millis(), files[randomFileIndex].c_str());
|
Serial.printf("[%lu] [SLP] Randomly loading: /sleep/%s\n", millis(), files[randomFileIndex].c_str());
|
||||||
delay(100);
|
delay(100);
|
||||||
Bitmap bitmap(file);
|
Bitmap bitmap(file);
|
||||||
@ -93,8 +94,8 @@ 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.
|
||||||
auto file = SD.open("/sleep.bmp");
|
File file;
|
||||||
if (file) {
|
if (FsHelpers::openFileForRead("SLP", "/sleep.bmp", file)) {
|
||||||
Bitmap bitmap(file);
|
Bitmap bitmap(file);
|
||||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||||
Serial.printf("[%lu] [SLP] Loading: /sleep.bmp\n", millis());
|
Serial.printf("[%lu] [SLP] Loading: /sleep.bmp\n", millis());
|
||||||
@ -186,8 +187,8 @@ void SleepActivity::renderCoverSleepScreen() const {
|
|||||||
return renderDefaultSleepScreen();
|
return renderDefaultSleepScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto file = SD.open(lastEpub.getCoverBmpPath().c_str(), FILE_READ);
|
File file;
|
||||||
if (file) {
|
if (FsHelpers::openFileForRead("SLP", lastEpub.getCoverBmpPath(), file)) {
|
||||||
Bitmap bitmap(file);
|
Bitmap bitmap(file);
|
||||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||||
renderBitmapSleepScreen(bitmap);
|
renderBitmapSleepScreen(bitmap);
|
||||||
|
|||||||
@ -1,12 +1,28 @@
|
|||||||
#include "CrossPointWebServerActivity.h"
|
#include "CrossPointWebServerActivity.h"
|
||||||
|
|
||||||
|
#include <DNSServer.h>
|
||||||
|
#include <ESPmDNS.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <InputManager.h>
|
#include <InputManager.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
|
|
||||||
|
#include "NetworkModeSelectionActivity.h"
|
||||||
#include "WifiSelectionActivity.h"
|
#include "WifiSelectionActivity.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// AP Mode configuration
|
||||||
|
constexpr const char* AP_SSID = "CrossPoint-Reader";
|
||||||
|
constexpr const char* AP_PASSWORD = nullptr; // Open network for ease of use
|
||||||
|
constexpr const char* AP_HOSTNAME = "crosspoint";
|
||||||
|
constexpr uint8_t AP_CHANNEL = 1;
|
||||||
|
constexpr uint8_t AP_MAX_CONNECTIONS = 4;
|
||||||
|
|
||||||
|
// DNS server for captive portal (redirects all DNS queries to our IP)
|
||||||
|
DNSServer* dnsServer = nullptr;
|
||||||
|
constexpr uint16_t DNS_PORT = 53;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void CrossPointWebServerActivity::taskTrampoline(void* param) {
|
void CrossPointWebServerActivity::taskTrampoline(void* param) {
|
||||||
auto* self = static_cast<CrossPointWebServerActivity*>(param);
|
auto* self = static_cast<CrossPointWebServerActivity*>(param);
|
||||||
self->displayTaskLoop();
|
self->displayTaskLoop();
|
||||||
@ -20,7 +36,9 @@ void CrossPointWebServerActivity::onEnter() {
|
|||||||
renderingMutex = xSemaphoreCreateMutex();
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
// Reset state
|
// Reset state
|
||||||
state = WebServerActivityState::WIFI_SELECTION;
|
state = WebServerActivityState::MODE_SELECTION;
|
||||||
|
networkMode = NetworkMode::JOIN_NETWORK;
|
||||||
|
isApMode = false;
|
||||||
connectedIP.clear();
|
connectedIP.clear();
|
||||||
connectedSSID.clear();
|
connectedSSID.clear();
|
||||||
lastHandleClientTime = 0;
|
lastHandleClientTime = 0;
|
||||||
@ -33,14 +51,12 @@ void CrossPointWebServerActivity::onEnter() {
|
|||||||
&displayTaskHandle // Task handle
|
&displayTaskHandle // Task handle
|
||||||
);
|
);
|
||||||
|
|
||||||
// Turn on WiFi immediately
|
// Launch network mode selection subactivity
|
||||||
Serial.printf("[%lu] [WEBACT] Turning on WiFi...\n", millis());
|
Serial.printf("[%lu] [WEBACT] Launching NetworkModeSelectionActivity...\n", millis());
|
||||||
WiFi.mode(WIFI_STA);
|
enterNewActivity(new NetworkModeSelectionActivity(
|
||||||
|
renderer, inputManager, [this](const NetworkMode mode) { onNetworkModeSelected(mode); },
|
||||||
// Launch WiFi selection subactivity
|
[this]() { onGoBack(); } // Cancel goes back to home
|
||||||
Serial.printf("[%lu] [WEBACT] Launching WifiSelectionActivity...\n", millis());
|
));
|
||||||
enterNewActivity(new WifiSelectionActivity(renderer, inputManager,
|
|
||||||
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServerActivity::onExit() {
|
void CrossPointWebServerActivity::onExit() {
|
||||||
@ -53,14 +69,30 @@ void CrossPointWebServerActivity::onExit() {
|
|||||||
// Stop the web server first (before disconnecting WiFi)
|
// Stop the web server first (before disconnecting WiFi)
|
||||||
stopWebServer();
|
stopWebServer();
|
||||||
|
|
||||||
|
// Stop mDNS
|
||||||
|
MDNS.end();
|
||||||
|
|
||||||
|
// Stop DNS server if running (AP mode)
|
||||||
|
if (dnsServer) {
|
||||||
|
Serial.printf("[%lu] [WEBACT] Stopping DNS server...\n", millis());
|
||||||
|
dnsServer->stop();
|
||||||
|
delete dnsServer;
|
||||||
|
dnsServer = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
// CRITICAL: Wait for LWIP stack to flush any pending packets
|
// CRITICAL: Wait for LWIP stack to flush any pending packets
|
||||||
Serial.printf("[%lu] [WEBACT] Waiting 500ms for network stack to flush pending packets...\n", millis());
|
Serial.printf("[%lu] [WEBACT] Waiting 500ms for network stack to flush pending packets...\n", millis());
|
||||||
delay(500);
|
delay(500);
|
||||||
|
|
||||||
// Disconnect WiFi gracefully
|
// Disconnect WiFi gracefully
|
||||||
Serial.printf("[%lu] [WEBACT] Disconnecting WiFi (graceful)...\n", millis());
|
if (isApMode) {
|
||||||
WiFi.disconnect(false); // false = don't erase credentials, send disconnect frame
|
Serial.printf("[%lu] [WEBACT] Stopping WiFi AP...\n", millis());
|
||||||
delay(100); // Allow disconnect frame to be sent
|
WiFi.softAPdisconnect(true);
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [WEBACT] Disconnecting WiFi (graceful)...\n", millis());
|
||||||
|
WiFi.disconnect(false); // false = don't erase credentials, send disconnect frame
|
||||||
|
}
|
||||||
|
delay(100); // Allow disconnect frame to be sent
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEBACT] Setting WiFi mode OFF...\n", millis());
|
Serial.printf("[%lu] [WEBACT] Setting WiFi mode OFF...\n", millis());
|
||||||
WiFi.mode(WIFI_OFF);
|
WiFi.mode(WIFI_OFF);
|
||||||
@ -89,6 +121,33 @@ void CrossPointWebServerActivity::onExit() {
|
|||||||
Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onExit end: %d bytes\n", millis(), ESP.getFreeHeap());
|
Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onExit end: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode) {
|
||||||
|
Serial.printf("[%lu] [WEBACT] Network mode selected: %s\n", millis(),
|
||||||
|
mode == NetworkMode::JOIN_NETWORK ? "Join Network" : "Create Hotspot");
|
||||||
|
|
||||||
|
networkMode = mode;
|
||||||
|
isApMode = (mode == NetworkMode::CREATE_HOTSPOT);
|
||||||
|
|
||||||
|
// Exit mode selection subactivity
|
||||||
|
exitActivity();
|
||||||
|
|
||||||
|
if (mode == NetworkMode::JOIN_NETWORK) {
|
||||||
|
// STA mode - launch WiFi selection
|
||||||
|
Serial.printf("[%lu] [WEBACT] Turning on WiFi (STA mode)...\n", millis());
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
|
||||||
|
state = WebServerActivityState::WIFI_SELECTION;
|
||||||
|
Serial.printf("[%lu] [WEBACT] Launching WifiSelectionActivity...\n", millis());
|
||||||
|
enterNewActivity(new WifiSelectionActivity(renderer, inputManager,
|
||||||
|
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
||||||
|
} else {
|
||||||
|
// AP mode - start access point
|
||||||
|
state = WebServerActivityState::AP_STARTING;
|
||||||
|
updateRequired = true;
|
||||||
|
startAccessPoint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected) {
|
void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected) {
|
||||||
Serial.printf("[%lu] [WEBACT] WifiSelectionActivity completed, connected=%d\n", millis(), connected);
|
Serial.printf("[%lu] [WEBACT] WifiSelectionActivity completed, connected=%d\n", millis(), connected);
|
||||||
|
|
||||||
@ -96,17 +155,83 @@ void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected)
|
|||||||
// Get connection info before exiting subactivity
|
// Get connection info before exiting subactivity
|
||||||
connectedIP = static_cast<WifiSelectionActivity*>(subActivity.get())->getConnectedIP();
|
connectedIP = static_cast<WifiSelectionActivity*>(subActivity.get())->getConnectedIP();
|
||||||
connectedSSID = WiFi.SSID().c_str();
|
connectedSSID = WiFi.SSID().c_str();
|
||||||
|
isApMode = false;
|
||||||
|
|
||||||
exitActivity();
|
exitActivity();
|
||||||
|
|
||||||
|
// Start mDNS for hostname resolution
|
||||||
|
if (MDNS.begin(AP_HOSTNAME)) {
|
||||||
|
Serial.printf("[%lu] [WEBACT] mDNS started: http://%s.local/\n", millis(), AP_HOSTNAME);
|
||||||
|
}
|
||||||
|
|
||||||
// Start the web server
|
// Start the web server
|
||||||
startWebServer();
|
startWebServer();
|
||||||
} else {
|
} else {
|
||||||
// User cancelled - go back
|
// User cancelled - go back to mode selection
|
||||||
onGoBack();
|
exitActivity();
|
||||||
|
state = WebServerActivityState::MODE_SELECTION;
|
||||||
|
enterNewActivity(new NetworkModeSelectionActivity(
|
||||||
|
renderer, inputManager, [this](const NetworkMode mode) { onNetworkModeSelected(mode); },
|
||||||
|
[this]() { onGoBack(); }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CrossPointWebServerActivity::startAccessPoint() {
|
||||||
|
Serial.printf("[%lu] [WEBACT] Starting Access Point mode...\n", millis());
|
||||||
|
Serial.printf("[%lu] [WEBACT] [MEM] Free heap before AP start: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
|
||||||
|
// Configure and start the AP
|
||||||
|
WiFi.mode(WIFI_AP);
|
||||||
|
delay(100);
|
||||||
|
|
||||||
|
// Start soft AP
|
||||||
|
bool apStarted;
|
||||||
|
if (AP_PASSWORD && strlen(AP_PASSWORD) >= 8) {
|
||||||
|
apStarted = WiFi.softAP(AP_SSID, AP_PASSWORD, AP_CHANNEL, false, AP_MAX_CONNECTIONS);
|
||||||
|
} else {
|
||||||
|
// Open network (no password)
|
||||||
|
apStarted = WiFi.softAP(AP_SSID, nullptr, AP_CHANNEL, false, AP_MAX_CONNECTIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!apStarted) {
|
||||||
|
Serial.printf("[%lu] [WEBACT] ERROR: Failed to start Access Point!\n", millis());
|
||||||
|
onGoBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(100); // Wait for AP to fully initialize
|
||||||
|
|
||||||
|
// Get AP IP address
|
||||||
|
const IPAddress apIP = WiFi.softAPIP();
|
||||||
|
char ipStr[16];
|
||||||
|
snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", apIP[0], apIP[1], apIP[2], apIP[3]);
|
||||||
|
connectedIP = ipStr;
|
||||||
|
connectedSSID = AP_SSID;
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [WEBACT] Access Point started!\n", millis());
|
||||||
|
Serial.printf("[%lu] [WEBACT] SSID: %s\n", millis(), AP_SSID);
|
||||||
|
Serial.printf("[%lu] [WEBACT] IP: %s\n", millis(), connectedIP.c_str());
|
||||||
|
|
||||||
|
// Start mDNS for hostname resolution
|
||||||
|
if (MDNS.begin(AP_HOSTNAME)) {
|
||||||
|
Serial.printf("[%lu] [WEBACT] mDNS started: http://%s.local/\n", millis(), AP_HOSTNAME);
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [WEBACT] WARNING: mDNS failed to start\n", millis());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start DNS server for captive portal behavior
|
||||||
|
// This redirects all DNS queries to our IP, making any domain typed resolve to us
|
||||||
|
dnsServer = new DNSServer();
|
||||||
|
dnsServer->setErrorReplyCode(DNSReplyCode::NoError);
|
||||||
|
dnsServer->start(DNS_PORT, "*", apIP);
|
||||||
|
Serial.printf("[%lu] [WEBACT] DNS server started for captive portal\n", millis());
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [WEBACT] [MEM] Free heap after AP start: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
|
||||||
|
// Start the web server
|
||||||
|
startWebServer();
|
||||||
|
}
|
||||||
|
|
||||||
void CrossPointWebServerActivity::startWebServer() {
|
void CrossPointWebServerActivity::startWebServer() {
|
||||||
Serial.printf("[%lu] [WEBACT] Starting web server...\n", millis());
|
Serial.printf("[%lu] [WEBACT] Starting web server...\n", millis());
|
||||||
|
|
||||||
@ -150,6 +275,11 @@ void CrossPointWebServerActivity::loop() {
|
|||||||
|
|
||||||
// Handle different states
|
// Handle different states
|
||||||
if (state == WebServerActivityState::SERVER_RUNNING) {
|
if (state == WebServerActivityState::SERVER_RUNNING) {
|
||||||
|
// Handle DNS requests for captive portal (AP mode only)
|
||||||
|
if (isApMode && dnsServer) {
|
||||||
|
dnsServer->processNextRequest();
|
||||||
|
}
|
||||||
|
|
||||||
// Handle web server requests - call handleClient multiple times per loop
|
// Handle web server requests - call handleClient multiple times per loop
|
||||||
// to improve responsiveness and upload throughput
|
// to improve responsiveness and upload throughput
|
||||||
if (webServer && webServer->isRunning()) {
|
if (webServer && webServer->isRunning()) {
|
||||||
@ -193,35 +323,71 @@ void CrossPointWebServerActivity::displayTaskLoop() {
|
|||||||
|
|
||||||
void CrossPointWebServerActivity::render() const {
|
void CrossPointWebServerActivity::render() const {
|
||||||
// Only render our own UI when server is running
|
// Only render our own UI when server is running
|
||||||
// WiFi selection handles its own rendering
|
// Subactivities handle their own rendering
|
||||||
if (state == WebServerActivityState::SERVER_RUNNING) {
|
if (state == WebServerActivityState::SERVER_RUNNING) {
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
renderServerRunning();
|
renderServerRunning();
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
|
} else if (state == WebServerActivityState::AP_STARTING) {
|
||||||
|
renderer.clearScreen();
|
||||||
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
renderer.drawCenteredText(READER_FONT_ID, pageHeight / 2 - 20, "Starting Hotspot...", true, BOLD);
|
||||||
|
renderer.displayBuffer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServerActivity::renderServerRunning() const {
|
void CrossPointWebServerActivity::renderServerRunning() const {
|
||||||
const auto pageHeight = renderer.getScreenHeight();
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
const auto height = renderer.getLineHeight(UI_FONT_ID);
|
|
||||||
const auto top = (pageHeight - height * 5) / 2;
|
|
||||||
|
|
||||||
renderer.drawCenteredText(READER_FONT_ID, top - 30, "File Transfer", true, BOLD);
|
// Use consistent line spacing
|
||||||
|
constexpr int LINE_SPACING = 28; // Space between lines
|
||||||
|
|
||||||
std::string ssidInfo = "Network: " + connectedSSID;
|
renderer.drawCenteredText(READER_FONT_ID, 15, "File Transfer", true, BOLD);
|
||||||
if (ssidInfo.length() > 28) {
|
|
||||||
ssidInfo.replace(25, ssidInfo.length() - 25, "...");
|
if (isApMode) {
|
||||||
|
// AP mode display - center the content block
|
||||||
|
const int startY = 55;
|
||||||
|
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, startY, "Hotspot Mode", true, BOLD);
|
||||||
|
|
||||||
|
std::string ssidInfo = "Network: " + connectedSSID;
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, startY + LINE_SPACING, ssidInfo.c_str(), true, REGULAR);
|
||||||
|
|
||||||
|
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 2, "Connect your device to this WiFi network",
|
||||||
|
true, REGULAR);
|
||||||
|
|
||||||
|
// Show primary URL (hostname)
|
||||||
|
std::string hostnameUrl = std::string("http://") + AP_HOSTNAME + ".local/";
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str(), true, BOLD);
|
||||||
|
|
||||||
|
// Show IP address as fallback
|
||||||
|
std::string ipUrl = "or http://" + connectedIP + "/";
|
||||||
|
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, ipUrl.c_str(), true, REGULAR);
|
||||||
|
|
||||||
|
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, "Open this URL in your browser", true, REGULAR);
|
||||||
|
} else {
|
||||||
|
// STA mode display (original behavior)
|
||||||
|
const int startY = 65;
|
||||||
|
|
||||||
|
std::string ssidInfo = "Network: " + connectedSSID;
|
||||||
|
if (ssidInfo.length() > 28) {
|
||||||
|
ssidInfo.replace(25, ssidInfo.length() - 25, "...");
|
||||||
|
}
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, startY, ssidInfo.c_str(), true, REGULAR);
|
||||||
|
|
||||||
|
std::string ipInfo = "IP Address: " + connectedIP;
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, startY + LINE_SPACING, ipInfo.c_str(), true, REGULAR);
|
||||||
|
|
||||||
|
// Show web server URL prominently
|
||||||
|
std::string webInfo = "http://" + connectedIP + "/";
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, startY + LINE_SPACING * 2, webInfo.c_str(), true, BOLD);
|
||||||
|
|
||||||
|
// Also show hostname URL
|
||||||
|
std::string hostnameUrl = std::string("or http://") + AP_HOSTNAME + ".local/";
|
||||||
|
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str(), true, REGULAR);
|
||||||
|
|
||||||
|
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, "Open this URL in your browser", true, REGULAR);
|
||||||
}
|
}
|
||||||
renderer.drawCenteredText(UI_FONT_ID, top + 10, ssidInfo.c_str(), true, REGULAR);
|
|
||||||
|
|
||||||
std::string ipInfo = "IP Address: " + connectedIP;
|
|
||||||
renderer.drawCenteredText(UI_FONT_ID, top + 40, ipInfo.c_str(), true, REGULAR);
|
|
||||||
|
|
||||||
// Show web server URL prominently
|
|
||||||
std::string webInfo = "http://" + connectedIP + "/";
|
|
||||||
renderer.drawCenteredText(UI_FONT_ID, top + 70, webInfo.c_str(), true, BOLD);
|
|
||||||
|
|
||||||
renderer.drawCenteredText(SMALL_FONT_ID, top + 110, "Open this URL in your browser", true, REGULAR);
|
|
||||||
|
|
||||||
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press BACK to exit", true, REGULAR);
|
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press BACK to exit", true, REGULAR);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,12 +7,15 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include "NetworkModeSelectionActivity.h"
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
#include "network/CrossPointWebServer.h"
|
#include "network/CrossPointWebServer.h"
|
||||||
|
|
||||||
// Web server activity states
|
// Web server activity states
|
||||||
enum class WebServerActivityState {
|
enum class WebServerActivityState {
|
||||||
WIFI_SELECTION, // WiFi selection subactivity is active
|
MODE_SELECTION, // Choosing between Join Network and Create Hotspot
|
||||||
|
WIFI_SELECTION, // WiFi selection subactivity is active (for Join Network mode)
|
||||||
|
AP_STARTING, // Starting Access Point mode
|
||||||
SERVER_RUNNING, // Web server is running and handling requests
|
SERVER_RUNNING, // Web server is running and handling requests
|
||||||
SHUTTING_DOWN // Shutting down server and WiFi
|
SHUTTING_DOWN // Shutting down server and WiFi
|
||||||
};
|
};
|
||||||
@ -20,8 +23,10 @@ enum class WebServerActivityState {
|
|||||||
/**
|
/**
|
||||||
* CrossPointWebServerActivity is the entry point for file transfer functionality.
|
* CrossPointWebServerActivity is the entry point for file transfer functionality.
|
||||||
* It:
|
* It:
|
||||||
* - Immediately turns on WiFi and launches WifiSelectionActivity on enter
|
* - First presents a choice between "Join a Network" (STA) and "Create Hotspot" (AP)
|
||||||
* - When WifiSelectionActivity completes successfully, starts the CrossPointWebServer
|
* - For STA mode: Launches WifiSelectionActivity to connect to an existing network
|
||||||
|
* - For AP mode: Creates an Access Point that clients can connect to
|
||||||
|
* - Starts the CrossPointWebServer when connected
|
||||||
* - Handles client requests in its loop() function
|
* - Handles client requests in its loop() function
|
||||||
* - Cleans up the server and shuts down WiFi on exit
|
* - Cleans up the server and shuts down WiFi on exit
|
||||||
*/
|
*/
|
||||||
@ -29,15 +34,19 @@ class CrossPointWebServerActivity final : public ActivityWithSubactivity {
|
|||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
WebServerActivityState state = WebServerActivityState::WIFI_SELECTION;
|
WebServerActivityState state = WebServerActivityState::MODE_SELECTION;
|
||||||
const std::function<void()> onGoBack;
|
const std::function<void()> onGoBack;
|
||||||
|
|
||||||
|
// Network mode
|
||||||
|
NetworkMode networkMode = NetworkMode::JOIN_NETWORK;
|
||||||
|
bool isApMode = false;
|
||||||
|
|
||||||
// Web server - owned by this activity
|
// Web server - owned by this activity
|
||||||
std::unique_ptr<CrossPointWebServer> webServer;
|
std::unique_ptr<CrossPointWebServer> webServer;
|
||||||
|
|
||||||
// Server status
|
// Server status
|
||||||
std::string connectedIP;
|
std::string connectedIP;
|
||||||
std::string connectedSSID;
|
std::string connectedSSID; // For STA mode: network name, For AP mode: AP name
|
||||||
|
|
||||||
// Performance monitoring
|
// Performance monitoring
|
||||||
unsigned long lastHandleClientTime = 0;
|
unsigned long lastHandleClientTime = 0;
|
||||||
@ -47,7 +56,9 @@ class CrossPointWebServerActivity final : public ActivityWithSubactivity {
|
|||||||
void render() const;
|
void render() const;
|
||||||
void renderServerRunning() const;
|
void renderServerRunning() const;
|
||||||
|
|
||||||
|
void onNetworkModeSelected(NetworkMode mode);
|
||||||
void onWifiSelectionComplete(bool connected);
|
void onWifiSelectionComplete(bool connected);
|
||||||
|
void startAccessPoint();
|
||||||
void startWebServer();
|
void startWebServer();
|
||||||
void stopWebServer();
|
void stopWebServer();
|
||||||
|
|
||||||
|
|||||||
128
src/activities/network/NetworkModeSelectionActivity.cpp
Normal file
128
src/activities/network/NetworkModeSelectionActivity.cpp
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
#include "NetworkModeSelectionActivity.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <InputManager.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr int MENU_ITEM_COUNT = 2;
|
||||||
|
const char* MENU_ITEMS[MENU_ITEM_COUNT] = {"Join a Network", "Create Hotspot"};
|
||||||
|
const char* MENU_DESCRIPTIONS[MENU_ITEM_COUNT] = {"Connect to an existing WiFi network",
|
||||||
|
"Create a WiFi network others can join"};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void NetworkModeSelectionActivity::taskTrampoline(void* param) {
|
||||||
|
auto* self = static_cast<NetworkModeSelectionActivity*>(param);
|
||||||
|
self->displayTaskLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkModeSelectionActivity::onEnter() {
|
||||||
|
Activity::onEnter();
|
||||||
|
|
||||||
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
|
// Reset selection
|
||||||
|
selectedIndex = 0;
|
||||||
|
|
||||||
|
// Trigger first update
|
||||||
|
updateRequired = true;
|
||||||
|
|
||||||
|
xTaskCreate(&NetworkModeSelectionActivity::taskTrampoline, "NetworkModeTask",
|
||||||
|
2048, // Stack size
|
||||||
|
this, // Parameters
|
||||||
|
1, // Priority
|
||||||
|
&displayTaskHandle // Task handle
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkModeSelectionActivity::onExit() {
|
||||||
|
Activity::onExit();
|
||||||
|
|
||||||
|
// Wait until not rendering to delete task
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
if (displayTaskHandle) {
|
||||||
|
vTaskDelete(displayTaskHandle);
|
||||||
|
displayTaskHandle = nullptr;
|
||||||
|
}
|
||||||
|
vSemaphoreDelete(renderingMutex);
|
||||||
|
renderingMutex = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkModeSelectionActivity::loop() {
|
||||||
|
// Handle back button - cancel
|
||||||
|
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
||||||
|
onCancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle confirm button - select current option
|
||||||
|
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
||||||
|
const NetworkMode mode = (selectedIndex == 0) ? NetworkMode::JOIN_NETWORK : NetworkMode::CREATE_HOTSPOT;
|
||||||
|
onModeSelected(mode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle navigation
|
||||||
|
const bool prevPressed =
|
||||||
|
inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT);
|
||||||
|
const bool nextPressed =
|
||||||
|
inputManager.wasPressed(InputManager::BTN_DOWN) || inputManager.wasPressed(InputManager::BTN_RIGHT);
|
||||||
|
|
||||||
|
if (prevPressed) {
|
||||||
|
selectedIndex = (selectedIndex + MENU_ITEM_COUNT - 1) % MENU_ITEM_COUNT;
|
||||||
|
updateRequired = true;
|
||||||
|
} else if (nextPressed) {
|
||||||
|
selectedIndex = (selectedIndex + 1) % MENU_ITEM_COUNT;
|
||||||
|
updateRequired = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkModeSelectionActivity::displayTaskLoop() {
|
||||||
|
while (true) {
|
||||||
|
if (updateRequired) {
|
||||||
|
updateRequired = false;
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
render();
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
}
|
||||||
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkModeSelectionActivity::render() const {
|
||||||
|
renderer.clearScreen();
|
||||||
|
|
||||||
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
|
// Draw header
|
||||||
|
renderer.drawCenteredText(READER_FONT_ID, 10, "File Transfer", true, BOLD);
|
||||||
|
|
||||||
|
// Draw subtitle
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, 50, "How would you like to connect?", true, REGULAR);
|
||||||
|
|
||||||
|
// Draw menu items centered on screen
|
||||||
|
constexpr int itemHeight = 50; // Height for each menu item (including description)
|
||||||
|
const int startY = (pageHeight - (MENU_ITEM_COUNT * itemHeight)) / 2 + 10;
|
||||||
|
|
||||||
|
for (int i = 0; i < MENU_ITEM_COUNT; i++) {
|
||||||
|
const int itemY = startY + i * itemHeight;
|
||||||
|
const bool isSelected = (i == selectedIndex);
|
||||||
|
|
||||||
|
// Draw selection highlight (black fill) for selected item
|
||||||
|
if (isSelected) {
|
||||||
|
renderer.fillRect(20, itemY - 2, pageWidth - 40, itemHeight - 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw text: black=false (white text) when selected (on black background)
|
||||||
|
// black=true (black text) when not selected (on white background)
|
||||||
|
renderer.drawText(UI_FONT_ID, 30, itemY, MENU_ITEMS[i], /*black=*/!isSelected);
|
||||||
|
renderer.drawText(SMALL_FONT_ID, 30, itemY + 22, MENU_DESCRIPTIONS[i], /*black=*/!isSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw help text at bottom
|
||||||
|
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press OK to select, BACK to cancel", true, REGULAR);
|
||||||
|
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
41
src/activities/network/NetworkModeSelectionActivity.h
Normal file
41
src/activities/network/NetworkModeSelectionActivity.h
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#include "../Activity.h"
|
||||||
|
|
||||||
|
// Enum for network mode selection
|
||||||
|
enum class NetworkMode { JOIN_NETWORK, CREATE_HOTSPOT };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NetworkModeSelectionActivity presents the user with a choice:
|
||||||
|
* - "Join a Network" - Connect to an existing WiFi network (STA mode)
|
||||||
|
* - "Create Hotspot" - Create an Access Point that others can connect to (AP mode)
|
||||||
|
*
|
||||||
|
* The onModeSelected callback is called with the user's choice.
|
||||||
|
* The onCancel callback is called if the user presses back.
|
||||||
|
*/
|
||||||
|
class NetworkModeSelectionActivity final : public Activity {
|
||||||
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
int selectedIndex = 0;
|
||||||
|
bool updateRequired = false;
|
||||||
|
const std::function<void(NetworkMode)> onModeSelected;
|
||||||
|
const std::function<void()> onCancel;
|
||||||
|
|
||||||
|
static void taskTrampoline(void* param);
|
||||||
|
[[noreturn]] void displayTaskLoop();
|
||||||
|
void render() const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit NetworkModeSelectionActivity(GfxRenderer& renderer, InputManager& inputManager,
|
||||||
|
const std::function<void(NetworkMode)>& onModeSelected,
|
||||||
|
const std::function<void()>& onCancel)
|
||||||
|
: Activity("NetworkModeSelection", renderer, inputManager), onModeSelected(onModeSelected), onCancel(onCancel) {}
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void loop() override;
|
||||||
|
};
|
||||||
@ -1,9 +1,9 @@
|
|||||||
#include "EpubReaderActivity.h"
|
#include "EpubReaderActivity.h"
|
||||||
|
|
||||||
#include <Epub/Page.h>
|
#include <Epub/Page.h>
|
||||||
|
#include <FsHelpers.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <InputManager.h>
|
#include <InputManager.h>
|
||||||
#include <SD.h>
|
|
||||||
|
|
||||||
#include "Battery.h"
|
#include "Battery.h"
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
@ -37,8 +37,8 @@ void EpubReaderActivity::onEnter() {
|
|||||||
|
|
||||||
epub->setupCacheDir();
|
epub->setupCacheDir();
|
||||||
|
|
||||||
File f = SD.open((epub->getCachePath() + "/progress.bin").c_str());
|
File f;
|
||||||
if (f) {
|
if (FsHelpers::openFileForRead("ERS", epub->getCachePath() + "/progress.bin", f)) {
|
||||||
uint8_t data[4];
|
uint8_t data[4];
|
||||||
if (f.read(data, 4) == 4) {
|
if (f.read(data, 4) == 4) {
|
||||||
currentSpineIndex = data[0] + (data[1] << 8);
|
currentSpineIndex = data[0] + (data[1] << 8);
|
||||||
@ -212,7 +212,7 @@ void EpubReaderActivity::renderScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!section) {
|
if (!section) {
|
||||||
const auto filepath = epub->getSpineHref(currentSpineIndex);
|
const auto filepath = epub->getSpineItem(currentSpineIndex).href;
|
||||||
Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex);
|
Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex);
|
||||||
section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
|
section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
|
||||||
if (!section->loadCacheMetadata(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom, marginLeft,
|
if (!section->loadCacheMetadata(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom, marginLeft,
|
||||||
@ -282,14 +282,16 @@ void EpubReaderActivity::renderScreen() {
|
|||||||
Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start);
|
Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start);
|
||||||
}
|
}
|
||||||
|
|
||||||
File f = SD.open((epub->getCachePath() + "/progress.bin").c_str(), FILE_WRITE);
|
File f;
|
||||||
uint8_t data[4];
|
if (FsHelpers::openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) {
|
||||||
data[0] = currentSpineIndex & 0xFF;
|
uint8_t data[4];
|
||||||
data[1] = (currentSpineIndex >> 8) & 0xFF;
|
data[0] = currentSpineIndex & 0xFF;
|
||||||
data[2] = section->currentPage & 0xFF;
|
data[1] = (currentSpineIndex >> 8) & 0xFF;
|
||||||
data[3] = (section->currentPage >> 8) & 0xFF;
|
data[2] = section->currentPage & 0xFF;
|
||||||
f.write(data, 4);
|
data[3] = (section->currentPage >> 8) & 0xFF;
|
||||||
f.close();
|
f.write(data, 4);
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderActivity::renderContents(std::unique_ptr<Page> page) {
|
void EpubReaderActivity::renderContents(std::unique_ptr<Page> page) {
|
||||||
|
|||||||
@ -160,7 +160,12 @@ void onGoHome() {
|
|||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
t1 = millis();
|
t1 = millis();
|
||||||
Serial.begin(115200);
|
|
||||||
|
// Only start serial if USB connected
|
||||||
|
pinMode(UART0_RXD, INPUT);
|
||||||
|
if (digitalRead(UART0_RXD) == HIGH) {
|
||||||
|
Serial.begin(115200);
|
||||||
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION "\n", millis());
|
Serial.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION "\n", millis());
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
#include "CrossPointWebServer.h"
|
#include "CrossPointWebServer.h"
|
||||||
|
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
|
#include <FsHelpers.h>
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
|
|
||||||
@ -30,12 +31,22 @@ void CrossPointWebServer::begin() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (WiFi.status() != WL_CONNECTED) {
|
// Check if we have a valid network connection (either STA connected or AP mode)
|
||||||
Serial.printf("[%lu] [WEB] Cannot start webserver - WiFi not connected\n", millis());
|
const wifi_mode_t wifiMode = WiFi.getMode();
|
||||||
|
const bool isStaConnected = (wifiMode & WIFI_MODE_STA) && (WiFi.status() == WL_CONNECTED);
|
||||||
|
const bool isInApMode = (wifiMode & WIFI_MODE_AP) && (WiFi.softAPgetStationNum() >= 0); // AP is running
|
||||||
|
|
||||||
|
if (!isStaConnected && !isInApMode) {
|
||||||
|
Serial.printf("[%lu] [WEB] Cannot start webserver - no valid network (mode=%d, status=%d)\n", millis(), wifiMode,
|
||||||
|
WiFi.status());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store AP mode flag for later use (e.g., in handleStatus)
|
||||||
|
apMode = isInApMode;
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] [MEM] Free heap before begin: %d bytes\n", millis(), ESP.getFreeHeap());
|
Serial.printf("[%lu] [WEB] [MEM] Free heap before begin: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
Serial.printf("[%lu] [WEB] Network mode: %s\n", millis(), apMode ? "AP" : "STA");
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] Creating web server on port %d...\n", millis(), port);
|
Serial.printf("[%lu] [WEB] Creating web server on port %d...\n", millis(), port);
|
||||||
server.reset(new WebServer(port));
|
server.reset(new WebServer(port));
|
||||||
@ -70,7 +81,9 @@ void CrossPointWebServer::begin() {
|
|||||||
running = true;
|
running = true;
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] Web server started on port %d\n", millis(), port);
|
Serial.printf("[%lu] [WEB] Web server started on port %d\n", millis(), port);
|
||||||
Serial.printf("[%lu] [WEB] Access at http://%s/\n", millis(), WiFi.localIP().toString().c_str());
|
// Show the correct IP based on network mode
|
||||||
|
const String ipAddr = apMode ? WiFi.softAPIP().toString() : WiFi.localIP().toString();
|
||||||
|
Serial.printf("[%lu] [WEB] Access at http://%s/\n", millis(), ipAddr.c_str());
|
||||||
Serial.printf("[%lu] [WEB] [MEM] Free heap after server.begin(): %d bytes\n", millis(), ESP.getFreeHeap());
|
Serial.printf("[%lu] [WEB] [MEM] Free heap after server.begin(): %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,10 +154,14 @@ void CrossPointWebServer::handleNotFound() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CrossPointWebServer::handleStatus() const {
|
void CrossPointWebServer::handleStatus() const {
|
||||||
|
// Get correct IP based on AP vs STA mode
|
||||||
|
const String ipAddr = apMode ? WiFi.softAPIP().toString() : WiFi.localIP().toString();
|
||||||
|
|
||||||
String json = "{";
|
String json = "{";
|
||||||
json += "\"version\":\"" + String(CROSSPOINT_VERSION) + "\",";
|
json += "\"version\":\"" + String(CROSSPOINT_VERSION) + "\",";
|
||||||
json += "\"ip\":\"" + WiFi.localIP().toString() + "\",";
|
json += "\"ip\":\"" + ipAddr + "\",";
|
||||||
json += "\"rssi\":" + String(WiFi.RSSI()) + ",";
|
json += "\"mode\":\"" + String(apMode ? "AP" : "STA") + "\",";
|
||||||
|
json += "\"rssi\":" + String(apMode ? 0 : WiFi.RSSI()) + ","; // RSSI not applicable in AP mode
|
||||||
json += "\"freeHeap\":" + String(ESP.getFreeHeap()) + ",";
|
json += "\"freeHeap\":" + String(ESP.getFreeHeap()) + ",";
|
||||||
json += "\"uptime\":" + String(millis() / 1000);
|
json += "\"uptime\":" + String(millis() / 1000);
|
||||||
json += "}";
|
json += "}";
|
||||||
@ -323,8 +340,7 @@ void CrossPointWebServer::handleUpload() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Open file for writing
|
// Open file for writing
|
||||||
uploadFile = SD.open(filePath.c_str(), FILE_WRITE);
|
if (!FsHelpers::openFileForWrite("WEB", filePath, uploadFile)) {
|
||||||
if (!uploadFile) {
|
|
||||||
uploadError = "Failed to create file on SD card";
|
uploadError = "Failed to create file on SD card";
|
||||||
Serial.printf("[%lu] [WEB] [UPLOAD] FAILED to create file: %s\n", millis(), filePath.c_str());
|
Serial.printf("[%lu] [WEB] [UPLOAD] FAILED to create file: %s\n", millis(), filePath.c_str());
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -35,6 +35,7 @@ class CrossPointWebServer {
|
|||||||
private:
|
private:
|
||||||
std::unique_ptr<WebServer> server = nullptr;
|
std::unique_ptr<WebServer> server = nullptr;
|
||||||
bool running = false;
|
bool running = false;
|
||||||
|
bool apMode = false; // true when running in AP mode, false for STA mode
|
||||||
uint16_t port = 80;
|
uint16_t port = 80;
|
||||||
|
|
||||||
// File scanning
|
// File scanning
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user