mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 14:47:37 +03:00
Merge b779378563 into 12c20bb09e
This commit is contained in:
commit
5078c490b1
2
.gitmodules
vendored
2
.gitmodules
vendored
@ -1,3 +1,3 @@
|
|||||||
[submodule "open-x4-sdk"]
|
[submodule "open-x4-sdk"]
|
||||||
path = open-x4-sdk
|
path = open-x4-sdk
|
||||||
url = https://github.com/open-x4-epaper/community-sdk.git
|
url = git@github.com:open-x4-epaper/community-sdk.git
|
||||||
|
|||||||
@ -73,6 +73,43 @@ class Xtc {
|
|||||||
uint16_t getPageHeight() const;
|
uint16_t getPageHeight() const;
|
||||||
uint8_t getBitDepth() const; // 1 = XTC (1-bit), 2 = XTCH (2-bit)
|
uint8_t getBitDepth() const; // 1 = XTC (1-bit), 2 = XTCH (2-bit)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态加载下一批页码
|
||||||
|
*/
|
||||||
|
xtc::XtcError loadNextPageBatch() const {
|
||||||
|
return parser ? parser->loadNextPageBatch() : xtc::XtcError::FILE_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
xtc::XtcError loadPageBatchByStart(uint16_t startPage) const {
|
||||||
|
return parser ? parser->loadPageBatchByStart(startPage): xtc::XtcError::FILE_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前已加载的最大页码
|
||||||
|
*/
|
||||||
|
uint16_t getLoadedMaxPage() const {
|
||||||
|
return parser ? parser->getLoadedMaxPage() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取每次加载的批次页数
|
||||||
|
*/
|
||||||
|
uint16_t getPageBatchSize() const {
|
||||||
|
return parser ? parser->getPageBatchSize() : 10;
|
||||||
|
}
|
||||||
|
xtc::XtcError readChapters_gd(uint16_t chapterStart) const {
|
||||||
|
return parser ? parser->readChapters_gd(chapterStart) : xtc::XtcError::FILE_NOT_FOUND;
|
||||||
|
}
|
||||||
|
uint32_t getChapterstartpage(int chapterIndex) {
|
||||||
|
return parser ? parser->getChapterstartpage(chapterIndex) : 0;
|
||||||
|
}
|
||||||
|
std::string getChapterTitleByIndex(int chapterIndex) {
|
||||||
|
return parser ? parser->getChapterTitleByIndex(chapterIndex) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load page bitmap data
|
* Load page bitmap data
|
||||||
* @param pageIndex Page index (0-based)
|
* @param pageIndex Page index (0-based)
|
||||||
|
|||||||
@ -21,7 +21,10 @@ XtcParser::XtcParser()
|
|||||||
m_defaultHeight(DISPLAY_HEIGHT),
|
m_defaultHeight(DISPLAY_HEIGHT),
|
||||||
m_bitDepth(1),
|
m_bitDepth(1),
|
||||||
m_hasChapters(false),
|
m_hasChapters(false),
|
||||||
m_lastError(XtcError::OK) {
|
m_lastError(XtcError::OK),
|
||||||
|
m_loadBatchSize(500), // 500 for one load
|
||||||
|
m_loadedMaxPage(0),
|
||||||
|
m_loadedStartPage(0) { // page_start
|
||||||
memset(&m_header, 0, sizeof(m_header));
|
memset(&m_header, 0, sizeof(m_header));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,21 +50,8 @@ XtcError XtcParser::open(const char* filepath) {
|
|||||||
return m_lastError;
|
return m_lastError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read title & author if available
|
// Read title if available
|
||||||
if (m_header.hasMetadata) {
|
readTitle();
|
||||||
m_lastError = readTitle();
|
|
||||||
if (m_lastError != XtcError::OK) {
|
|
||||||
Serial.printf("[%lu] [XTC] Failed to read title: %s\n", millis(), errorToString(m_lastError));
|
|
||||||
m_file.close();
|
|
||||||
return m_lastError;
|
|
||||||
}
|
|
||||||
m_lastError = readAuthor();
|
|
||||||
if (m_lastError != XtcError::OK) {
|
|
||||||
Serial.printf("[%lu] [XTC] Failed to read author: %s\n", millis(), errorToString(m_lastError));
|
|
||||||
m_file.close();
|
|
||||||
return m_lastError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read page table
|
// Read page table
|
||||||
m_lastError = readPageTable();
|
m_lastError = readPageTable();
|
||||||
@ -71,7 +61,7 @@ XtcError XtcParser::open(const char* filepath) {
|
|||||||
return m_lastError;
|
return m_lastError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read chapters if present
|
// Read chapters if present (to make it work, just keep the old readchapters)
|
||||||
m_lastError = readChapters();
|
m_lastError = readChapters();
|
||||||
if (m_lastError != XtcError::OK) {
|
if (m_lastError != XtcError::OK) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to read chapters: %s\n", millis(), errorToString(m_lastError));
|
Serial.printf("[%lu] [XTC] Failed to read chapters: %s\n", millis(), errorToString(m_lastError));
|
||||||
@ -80,8 +70,8 @@ XtcError XtcParser::open(const char* filepath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_isOpen = true;
|
m_isOpen = true;
|
||||||
Serial.printf("[%lu] [XTC] Opened file: %s (%u pages, %dx%d)\n", millis(), filepath, m_header.pageCount,
|
Serial.printf("[%lu] [XTC] Opened file: %s (total pages=%u, loaded pages=[0~%u], %dx%d)\n", millis(), filepath,
|
||||||
m_defaultWidth, m_defaultHeight);
|
m_header.pageCount, m_loadedMaxPage, m_defaultWidth, m_defaultHeight);
|
||||||
return XtcError::OK;
|
return XtcError::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,29 +84,24 @@ void XtcParser::close() {
|
|||||||
m_chapters.clear();
|
m_chapters.clear();
|
||||||
m_title.clear();
|
m_title.clear();
|
||||||
m_hasChapters = false;
|
m_hasChapters = false;
|
||||||
|
m_loadedMaxPage = 0;
|
||||||
memset(&m_header, 0, sizeof(m_header));
|
memset(&m_header, 0, sizeof(m_header));
|
||||||
}
|
}
|
||||||
|
|
||||||
XtcError XtcParser::readHeader() {
|
XtcError XtcParser::readHeader() {
|
||||||
// Read first 56 bytes of header
|
|
||||||
size_t bytesRead = m_file.read(reinterpret_cast<uint8_t*>(&m_header), sizeof(XtcHeader));
|
size_t bytesRead = m_file.read(reinterpret_cast<uint8_t*>(&m_header), sizeof(XtcHeader));
|
||||||
if (bytesRead != sizeof(XtcHeader)) {
|
if (bytesRead != sizeof(XtcHeader)) {
|
||||||
return XtcError::READ_ERROR;
|
return XtcError::READ_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify magic number (accept both XTC and XTCH)
|
|
||||||
if (m_header.magic != XTC_MAGIC && m_header.magic != XTCH_MAGIC) {
|
if (m_header.magic != XTC_MAGIC && m_header.magic != XTCH_MAGIC) {
|
||||||
Serial.printf("[%lu] [XTC] Invalid magic: 0x%08X (expected 0x%08X or 0x%08X)\n", millis(), m_header.magic,
|
Serial.printf("[%lu] [XTC] Invalid magic: 0x%08X (expected 0x%08X or 0x%08X)\n", millis(), m_header.magic,
|
||||||
XTC_MAGIC, XTCH_MAGIC);
|
XTC_MAGIC, XTCH_MAGIC);
|
||||||
return XtcError::INVALID_MAGIC;
|
return XtcError::INVALID_MAGIC;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine bit depth from file magic
|
|
||||||
m_bitDepth = (m_header.magic == XTCH_MAGIC) ? 2 : 1;
|
m_bitDepth = (m_header.magic == XTCH_MAGIC) ? 2 : 1;
|
||||||
|
|
||||||
// Check version
|
|
||||||
// Currently, version 1.0 is the only valid version, however some generators are swapping the bytes around, so we
|
|
||||||
// accept both 1.0 and 0.1 for compatibility
|
|
||||||
const bool validVersion = m_header.versionMajor == 1 && m_header.versionMinor == 0 ||
|
const bool validVersion = m_header.versionMajor == 1 && m_header.versionMinor == 0 ||
|
||||||
m_header.versionMajor == 0 && m_header.versionMinor == 1;
|
m_header.versionMajor == 0 && m_header.versionMinor == 1;
|
||||||
if (!validVersion) {
|
if (!validVersion) {
|
||||||
@ -124,12 +109,11 @@ XtcError XtcParser::readHeader() {
|
|||||||
return XtcError::INVALID_VERSION;
|
return XtcError::INVALID_VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Basic validation
|
|
||||||
if (m_header.pageCount == 0) {
|
if (m_header.pageCount == 0) {
|
||||||
return XtcError::CORRUPTED_HEADER;
|
return XtcError::CORRUPTED_HEADER;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTC] Header: magic=0x%08X (%s), ver=%u.%u, pages=%u, bitDepth=%u\n", millis(), m_header.magic,
|
Serial.printf("[%lu] [XTC] Header: magic=0x%08X (%s), ver=%u.%u, total pages=%u, bitDepth=%u\n", millis(), m_header.magic,
|
||||||
(m_header.magic == XTCH_MAGIC) ? "XTCH" : "XTC", m_header.versionMajor, m_header.versionMinor,
|
(m_header.magic == XTCH_MAGIC) ? "XTCH" : "XTC", m_header.versionMajor, m_header.versionMinor,
|
||||||
m_header.pageCount, m_bitDepth);
|
m_header.pageCount, m_bitDepth);
|
||||||
|
|
||||||
@ -150,37 +134,29 @@ XtcError XtcParser::readTitle() {
|
|||||||
return XtcError::OK;
|
return XtcError::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
XtcError XtcParser::readAuthor() {
|
//load the next pagetable (for XtcReadActivity.cpp)
|
||||||
// Read author as null-terminated UTF-8 string with max length 64, directly following title
|
|
||||||
constexpr auto authorOffset = 0xB8;
|
|
||||||
if (!m_file.seek(authorOffset)) {
|
|
||||||
return XtcError::READ_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
char authorBuf[64] = {0};
|
|
||||||
m_file.read(authorBuf, sizeof(authorBuf) - 1);
|
|
||||||
m_author = authorBuf;
|
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTC] Author: %s\n", millis(), m_author.c_str());
|
|
||||||
return XtcError::OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
XtcError XtcParser::readPageTable() {
|
XtcError XtcParser::readPageTable() {
|
||||||
|
m_pageTable.clear();
|
||||||
|
m_pageTable.shrink_to_fit();
|
||||||
if (m_header.pageTableOffset == 0) {
|
if (m_header.pageTableOffset == 0) {
|
||||||
Serial.printf("[%lu] [XTC] Page table offset is 0, cannot read\n", millis());
|
Serial.printf("[%lu] [XTC] Page table offset is 0, cannot read\n", millis());
|
||||||
return XtcError::CORRUPTED_HEADER;
|
return XtcError::CORRUPTED_HEADER;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seek to page table
|
|
||||||
if (!m_file.seek(m_header.pageTableOffset)) {
|
if (!m_file.seek(m_header.pageTableOffset)) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to seek to page table at %llu\n", millis(), m_header.pageTableOffset);
|
Serial.printf("[%lu] [XTC] Failed to seek to page table at %llu\n", millis(), m_header.pageTableOffset);
|
||||||
return XtcError::READ_ERROR;
|
return XtcError::READ_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_pageTable.resize(m_header.pageCount);
|
// for the first
|
||||||
|
uint16_t startPage = 0;
|
||||||
|
uint16_t endPage = startPage + m_loadBatchSize - 1;
|
||||||
|
if(endPage >= m_header.pageCount) endPage = m_header.pageCount - 1;
|
||||||
|
uint16_t loadCount = endPage - startPage + 1;
|
||||||
|
|
||||||
// Read page table entries
|
m_pageTable.resize(endPage + 1);
|
||||||
for (uint16_t i = 0; i < m_header.pageCount; i++) {
|
|
||||||
|
for (uint16_t i = startPage; i <= endPage; i++) {
|
||||||
PageTableEntry entry;
|
PageTableEntry entry;
|
||||||
size_t bytesRead = m_file.read(reinterpret_cast<uint8_t*>(&entry), sizeof(PageTableEntry));
|
size_t bytesRead = m_file.read(reinterpret_cast<uint8_t*>(&entry), sizeof(PageTableEntry));
|
||||||
if (bytesRead != sizeof(PageTableEntry)) {
|
if (bytesRead != sizeof(PageTableEntry)) {
|
||||||
@ -194,17 +170,18 @@ XtcError XtcParser::readPageTable() {
|
|||||||
m_pageTable[i].height = entry.height;
|
m_pageTable[i].height = entry.height;
|
||||||
m_pageTable[i].bitDepth = m_bitDepth;
|
m_pageTable[i].bitDepth = m_bitDepth;
|
||||||
|
|
||||||
// Update default dimensions from first page
|
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
m_defaultWidth = entry.width;
|
m_defaultWidth = entry.width;
|
||||||
m_defaultHeight = entry.height;
|
m_defaultHeight = entry.height;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTC] Read %u page table entries\n", millis(), m_header.pageCount);
|
m_loadedMaxPage = endPage;
|
||||||
|
Serial.printf("[%lu] [XTC] 初始化加载页表: 成功加载 [0~%u] 共%u页\n", millis(), m_loadedMaxPage, loadCount);
|
||||||
return XtcError::OK;
|
return XtcError::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
XtcError XtcParser::readChapters() {
|
XtcError XtcParser::readChapters() {
|
||||||
m_hasChapters = false;
|
m_hasChapters = false;
|
||||||
m_chapters.clear();
|
m_chapters.clear();
|
||||||
@ -217,129 +194,97 @@ XtcError XtcParser::readChapters() {
|
|||||||
return XtcError::READ_ERROR;
|
return XtcError::READ_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasChaptersFlag != 1) {
|
if (hasChaptersFlag != 1) {}
|
||||||
return XtcError::OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint64_t chapterOffset = 0;
|
uint64_t chapterOffset = 0;
|
||||||
if (!m_file.seek(0x30)) {
|
if (!m_file.seek(0x30)) {return XtcError::READ_ERROR;}
|
||||||
return XtcError::READ_ERROR;
|
if (m_file.read(reinterpret_cast<uint8_t*>(&chapterOffset), sizeof(chapterOffset)) != sizeof(chapterOffset)) {return XtcError::READ_ERROR;}
|
||||||
}
|
if (chapterOffset == 0) {}
|
||||||
if (m_file.read(reinterpret_cast<uint8_t*>(&chapterOffset), sizeof(chapterOffset)) != sizeof(chapterOffset)) {
|
|
||||||
return XtcError::READ_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chapterOffset == 0) {
|
|
||||||
return XtcError::OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uint64_t fileSize = m_file.size();
|
const uint64_t fileSize = m_file.size();
|
||||||
if (chapterOffset < sizeof(XtcHeader) || chapterOffset >= fileSize || chapterOffset + 96 > fileSize) {
|
if (chapterOffset < sizeof(XtcHeader) || chapterOffset >= fileSize || chapterOffset + 96 > fileSize) {}
|
||||||
return XtcError::OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint64_t maxOffset = 0;
|
uint64_t maxOffset = 0;
|
||||||
if (m_header.pageTableOffset > chapterOffset) {
|
if (m_header.pageTableOffset > chapterOffset) {maxOffset = m_header.pageTableOffset;}
|
||||||
maxOffset = m_header.pageTableOffset;
|
else if (m_header.dataOffset > chapterOffset) {maxOffset = m_header.dataOffset;}
|
||||||
} else if (m_header.dataOffset > chapterOffset) {
|
else {maxOffset = fileSize;}
|
||||||
maxOffset = m_header.dataOffset;
|
if (maxOffset <= chapterOffset) {}
|
||||||
} else {
|
|
||||||
maxOffset = fileSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxOffset <= chapterOffset) {
|
|
||||||
return XtcError::OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr size_t chapterSize = 96;
|
constexpr size_t chapterSize = 96;
|
||||||
const uint64_t available = maxOffset - chapterOffset;
|
const uint64_t available = maxOffset - chapterOffset;
|
||||||
const size_t chapterCount = static_cast<size_t>(available / chapterSize);
|
const size_t chapterCount = static_cast<size_t>(available / chapterSize);
|
||||||
if (chapterCount == 0) {
|
if (chapterCount == 0) {}
|
||||||
return XtcError::OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!m_file.seek(chapterOffset)) {
|
|
||||||
return XtcError::READ_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (!m_file.seek(chapterOffset)) {return XtcError::READ_ERROR;}
|
||||||
std::vector<uint8_t> chapterBuf(chapterSize);
|
std::vector<uint8_t> chapterBuf(chapterSize);
|
||||||
for (size_t i = 0; i < chapterCount; i++) {
|
for (size_t i = 0; i < chapterCount; i++) {
|
||||||
if (m_file.read(chapterBuf.data(), chapterSize) != chapterSize) {
|
if (m_file.read(chapterBuf.data(), chapterSize) != chapterSize) {return XtcError::READ_ERROR;}
|
||||||
return XtcError::READ_ERROR;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
char nameBuf[81];
|
|
||||||
memcpy(nameBuf, chapterBuf.data(), 80);
|
|
||||||
nameBuf[80] = '\0';
|
|
||||||
const size_t nameLen = strnlen(nameBuf, 80);
|
|
||||||
std::string name(nameBuf, nameLen);
|
|
||||||
|
|
||||||
uint16_t startPage = 0;
|
|
||||||
uint16_t endPage = 0;
|
|
||||||
memcpy(&startPage, chapterBuf.data() + 0x50, sizeof(startPage));
|
|
||||||
memcpy(&endPage, chapterBuf.data() + 0x52, sizeof(endPage));
|
|
||||||
|
|
||||||
if (name.empty() && startPage == 0 && endPage == 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startPage > 0) {
|
|
||||||
startPage--;
|
|
||||||
}
|
|
||||||
if (endPage > 0) {
|
|
||||||
endPage--;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startPage >= m_header.pageCount) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (endPage >= m_header.pageCount) {
|
|
||||||
endPage = m_header.pageCount - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startPage > endPage) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChapterInfo chapter{std::move(name), startPage, endPage};
|
|
||||||
m_chapters.push_back(std::move(chapter));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
std::string chapterName = m_title.empty() ? "全书" : m_title;
|
||||||
|
ChapterInfo singleChapter{std::move(chapterName), 0, m_header.pageCount - 1};
|
||||||
|
m_chapters.push_back(std::move(singleChapter));
|
||||||
m_hasChapters = !m_chapters.empty();
|
m_hasChapters = !m_chapters.empty();
|
||||||
Serial.printf("[%lu] [XTC] Chapters: %u\n", millis(), static_cast<unsigned int>(m_chapters.size()));
|
|
||||||
|
Serial.printf("[%lu] [XTC] 解析章节 #01 : 名称=[%s] | 包含全书共%u页\n", millis(), singleChapter.name.c_str(), m_header.pageCount);
|
||||||
|
Serial.printf("[%lu] [XTC] 解析完成 ✔️ 共加载有效章节数: %u\n", millis(), static_cast<unsigned int>(m_chapters.size()));
|
||||||
return XtcError::OK;
|
return XtcError::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool XtcParser::getPageInfo(uint32_t pageIndex, PageInfo& info) const {
|
// for the next pagetable
|
||||||
if (pageIndex >= m_pageTable.size()) {
|
XtcError XtcParser::loadNextPageBatch() {
|
||||||
return false;
|
if(!m_isOpen) return XtcError::FILE_NOT_FOUND;
|
||||||
|
if(m_loadedMaxPage >= m_header.pageCount - 1) {
|
||||||
|
Serial.printf("[XTC] 已加载全部%u页\n", m_header.pageCount);
|
||||||
|
return XtcError::PAGE_OUT_OF_RANGE;
|
||||||
}
|
}
|
||||||
info = m_pageTable[pageIndex];
|
|
||||||
|
return loadPageBatchByStart(m_loadedMaxPage + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
uint16_t XtcParser::getLoadedMaxPage() const {
|
||||||
|
return m_loadedMaxPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
uint16_t XtcParser::getPageBatchSize() const {
|
||||||
|
return m_loadBatchSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool XtcParser::getPageInfo(uint32_t pageIndex, PageInfo& info) const {
|
||||||
|
if (pageIndex >= m_header.pageCount) return false;
|
||||||
|
uint16_t targetStart = (pageIndex / m_loadBatchSize) * m_loadBatchSize;
|
||||||
|
if (pageIndex < m_loadedStartPage || pageIndex > m_loadedMaxPage) {
|
||||||
|
auto* self = const_cast<XtcParser*>(this);
|
||||||
|
self->loadPageBatchByStart(targetStart);
|
||||||
|
}
|
||||||
|
uint16_t idx = pageIndex - m_loadedStartPage;
|
||||||
|
if(idx >= m_pageTable.size()) return false;
|
||||||
|
info = m_pageTable[idx];
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//change:to get page
|
||||||
size_t XtcParser::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSize) {
|
size_t XtcParser::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSize) {
|
||||||
if (!m_isOpen) {
|
if (!m_isOpen || pageIndex >= m_header.pageCount) {
|
||||||
m_lastError = XtcError::FILE_NOT_FOUND;
|
m_lastError = (pageIndex >= m_header.pageCount) ? XtcError::PAGE_OUT_OF_RANGE : XtcError::FILE_NOT_FOUND;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pageIndex >= m_header.pageCount) {
|
if (pageIndex < m_loadedStartPage || pageIndex > m_loadedMaxPage) {
|
||||||
m_lastError = XtcError::PAGE_OUT_OF_RANGE;
|
loadNextPageBatch();
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PageInfo& page = m_pageTable[pageIndex];
|
uint16_t idx = pageIndex - m_loadedStartPage;
|
||||||
|
const PageInfo& page = m_pageTable[idx];
|
||||||
// Seek to page data
|
|
||||||
if (!m_file.seek(page.offset)) {
|
if (!m_file.seek(page.offset)) {
|
||||||
Serial.printf("[%lu] [XTC] Failed to seek to page %u at offset %lu\n", millis(), pageIndex, page.offset);
|
Serial.printf("[%lu] [XTC] Failed to seek to page %u at offset %lu\n", millis(), pageIndex, page.offset);
|
||||||
m_lastError = XtcError::READ_ERROR;
|
m_lastError = XtcError::READ_ERROR;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read page header (XTG for 1-bit, XTH for 2-bit - same structure)
|
|
||||||
XtgPageHeader pageHeader;
|
XtgPageHeader pageHeader;
|
||||||
size_t headerRead = m_file.read(reinterpret_cast<uint8_t*>(&pageHeader), sizeof(XtgPageHeader));
|
size_t headerRead = m_file.read(reinterpret_cast<uint8_t*>(&pageHeader), sizeof(XtgPageHeader));
|
||||||
if (headerRead != sizeof(XtgPageHeader)) {
|
if (headerRead != sizeof(XtgPageHeader)) {
|
||||||
@ -348,7 +293,6 @@ size_t XtcParser::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSiz
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify page magic (XTG for 1-bit, XTH for 2-bit)
|
|
||||||
const uint32_t expectedMagic = (m_bitDepth == 2) ? XTH_MAGIC : XTG_MAGIC;
|
const uint32_t expectedMagic = (m_bitDepth == 2) ? XTH_MAGIC : XTG_MAGIC;
|
||||||
if (pageHeader.magic != expectedMagic) {
|
if (pageHeader.magic != expectedMagic) {
|
||||||
Serial.printf("[%lu] [XTC] Invalid page magic for page %u: 0x%08X (expected 0x%08X)\n", millis(), pageIndex,
|
Serial.printf("[%lu] [XTC] Invalid page magic for page %u: 0x%08X (expected 0x%08X)\n", millis(), pageIndex,
|
||||||
@ -357,25 +301,19 @@ size_t XtcParser::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSiz
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate bitmap size based on bit depth
|
|
||||||
// XTG (1-bit): Row-major, ((width+7)/8) * height bytes
|
|
||||||
// XTH (2-bit): Two bit planes, column-major, ((width * height + 7) / 8) * 2 bytes
|
|
||||||
size_t bitmapSize;
|
size_t bitmapSize;
|
||||||
if (m_bitDepth == 2) {
|
if (m_bitDepth == 2) {
|
||||||
// XTH: two bit planes, each containing (width * height) bits rounded up to bytes
|
|
||||||
bitmapSize = ((static_cast<size_t>(pageHeader.width) * pageHeader.height + 7) / 8) * 2;
|
bitmapSize = ((static_cast<size_t>(pageHeader.width) * pageHeader.height + 7) / 8) * 2;
|
||||||
} else {
|
} else {
|
||||||
bitmapSize = ((pageHeader.width + 7) / 8) * pageHeader.height;
|
bitmapSize = ((pageHeader.width + 7) / 8) * pageHeader.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check buffer size
|
|
||||||
if (bufferSize < bitmapSize) {
|
if (bufferSize < bitmapSize) {
|
||||||
Serial.printf("[%lu] [XTC] Buffer too small: need %u, have %u\n", millis(), bitmapSize, bufferSize);
|
Serial.printf("[%lu] [XTC] Buffer too small: need %u, have %u\n", millis(), bitmapSize, bufferSize);
|
||||||
m_lastError = XtcError::MEMORY_ERROR;
|
m_lastError = XtcError::MEMORY_ERROR;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read bitmap data
|
|
||||||
size_t bytesRead = m_file.read(buffer, bitmapSize);
|
size_t bytesRead = m_file.read(buffer, bitmapSize);
|
||||||
if (bytesRead != bitmapSize) {
|
if (bytesRead != bitmapSize) {
|
||||||
Serial.printf("[%lu] [XTC] Page read error: expected %u, got %u\n", millis(), bitmapSize, bytesRead);
|
Serial.printf("[%lu] [XTC] Page read error: expected %u, got %u\n", millis(), bitmapSize, bytesRead);
|
||||||
@ -390,32 +328,18 @@ size_t XtcParser::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSiz
|
|||||||
XtcError XtcParser::loadPageStreaming(uint32_t pageIndex,
|
XtcError XtcParser::loadPageStreaming(uint32_t pageIndex,
|
||||||
std::function<void(const uint8_t* data, size_t size, size_t offset)> callback,
|
std::function<void(const uint8_t* data, size_t size, size_t offset)> callback,
|
||||||
size_t chunkSize) {
|
size_t chunkSize) {
|
||||||
if (!m_isOpen) {
|
if (!m_isOpen || pageIndex > m_loadedMaxPage || pageIndex >= m_header.pageCount) {
|
||||||
return XtcError::FILE_NOT_FOUND;
|
return (pageIndex >= m_header.pageCount) ? XtcError::PAGE_OUT_OF_RANGE : XtcError::FILE_NOT_FOUND;
|
||||||
}
|
|
||||||
|
|
||||||
if (pageIndex >= m_header.pageCount) {
|
|
||||||
return XtcError::PAGE_OUT_OF_RANGE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PageInfo& page = m_pageTable[pageIndex];
|
const PageInfo& page = m_pageTable[pageIndex];
|
||||||
|
if (!m_file.seek(page.offset)) {return XtcError::READ_ERROR;}
|
||||||
|
|
||||||
// Seek to page data
|
|
||||||
if (!m_file.seek(page.offset)) {
|
|
||||||
return XtcError::READ_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read and skip page header (XTG for 1-bit, XTH for 2-bit)
|
|
||||||
XtgPageHeader pageHeader;
|
XtgPageHeader pageHeader;
|
||||||
size_t headerRead = m_file.read(reinterpret_cast<uint8_t*>(&pageHeader), sizeof(XtgPageHeader));
|
size_t headerRead = m_file.read(reinterpret_cast<uint8_t*>(&pageHeader), sizeof(XtgPageHeader));
|
||||||
const uint32_t expectedMagic = (m_bitDepth == 2) ? XTH_MAGIC : XTG_MAGIC;
|
const uint32_t expectedMagic = (m_bitDepth == 2) ? XTH_MAGIC : XTG_MAGIC;
|
||||||
if (headerRead != sizeof(XtgPageHeader) || pageHeader.magic != expectedMagic) {
|
if (headerRead != sizeof(XtgPageHeader) || pageHeader.magic != expectedMagic) {return XtcError::READ_ERROR;}
|
||||||
return XtcError::READ_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate bitmap size based on bit depth
|
|
||||||
// XTG (1-bit): Row-major, ((width+7)/8) * height bytes
|
|
||||||
// XTH (2-bit): Two bit planes, ((width * height + 7) / 8) * 2 bytes
|
|
||||||
size_t bitmapSize;
|
size_t bitmapSize;
|
||||||
if (m_bitDepth == 2) {
|
if (m_bitDepth == 2) {
|
||||||
bitmapSize = ((static_cast<size_t>(pageHeader.width) * pageHeader.height + 7) / 8) * 2;
|
bitmapSize = ((static_cast<size_t>(pageHeader.width) * pageHeader.height + 7) / 8) * 2;
|
||||||
@ -423,40 +347,177 @@ XtcError XtcParser::loadPageStreaming(uint32_t pageIndex,
|
|||||||
bitmapSize = ((pageHeader.width + 7) / 8) * pageHeader.height;
|
bitmapSize = ((pageHeader.width + 7) / 8) * pageHeader.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read in chunks
|
|
||||||
std::vector<uint8_t> chunk(chunkSize);
|
std::vector<uint8_t> chunk(chunkSize);
|
||||||
size_t totalRead = 0;
|
size_t totalRead = 0;
|
||||||
|
|
||||||
while (totalRead < bitmapSize) {
|
while (totalRead < bitmapSize) {
|
||||||
size_t toRead = std::min(chunkSize, bitmapSize - totalRead);
|
size_t toRead = std::min(chunkSize, bitmapSize - totalRead);
|
||||||
size_t bytesRead = m_file.read(chunk.data(), toRead);
|
size_t bytesRead = m_file.read(chunk.data(), toRead);
|
||||||
|
if (bytesRead == 0) return XtcError::READ_ERROR;
|
||||||
if (bytesRead == 0) {
|
|
||||||
return XtcError::READ_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(chunk.data(), bytesRead, totalRead);
|
callback(chunk.data(), bytesRead, totalRead);
|
||||||
totalRead += bytesRead;
|
totalRead += bytesRead;
|
||||||
}
|
}
|
||||||
|
|
||||||
return XtcError::OK;
|
return XtcError::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool XtcParser::isValidXtcFile(const char* filepath) {
|
bool XtcParser::isValidXtcFile(const char* filepath) {
|
||||||
FsFile file;
|
FsFile file;
|
||||||
if (!SdMan.openFileForRead("XTC", filepath, file)) {
|
if (!SdMan.openFileForRead("XTC", filepath, file)) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t magic = 0;
|
uint32_t magic = 0;
|
||||||
size_t bytesRead = file.read(reinterpret_cast<uint8_t*>(&magic), sizeof(magic));
|
size_t bytesRead = file.read(reinterpret_cast<uint8_t*>(&magic), sizeof(magic));
|
||||||
file.close();
|
file.close();
|
||||||
|
return (bytesRead == sizeof(magic)) && (magic == XTC_MAGIC || magic == XTCH_MAGIC);
|
||||||
|
}
|
||||||
|
//charge to get chapters separately
|
||||||
|
XtcError XtcParser::readChapters_gd(uint16_t chapterStart) {
|
||||||
|
chapterActualCount = 0;
|
||||||
|
memset(ChapterList, 0, sizeof(ChapterList));
|
||||||
|
Serial.printf("[Memory] memset memory \n");
|
||||||
|
|
||||||
if (bytesRead != sizeof(magic)) {
|
uint8_t hasChaptersFlag = 0;
|
||||||
return false;
|
if (!m_file.seek(0x0B)) {
|
||||||
|
return XtcError::READ_ERROR;
|
||||||
|
}
|
||||||
|
if (m_file.read(&hasChaptersFlag, sizeof(hasChaptersFlag)) != sizeof(hasChaptersFlag)) {
|
||||||
|
return XtcError::READ_ERROR;
|
||||||
|
}
|
||||||
|
if (hasChaptersFlag != 1) {
|
||||||
|
return XtcError::OK;
|
||||||
|
}
|
||||||
|
// Serial.printf("[%lu] [XTC] 位置1");//for debug
|
||||||
|
|
||||||
|
uint64_t chapterOffset = 0;
|
||||||
|
if (!m_file.seek(0x30)) {
|
||||||
|
return XtcError::READ_ERROR;
|
||||||
|
}
|
||||||
|
if (m_file.read(reinterpret_cast<uint8_t*>(&chapterOffset), sizeof(chapterOffset)) != sizeof(chapterOffset)) {
|
||||||
|
return XtcError::READ_ERROR;
|
||||||
|
}
|
||||||
|
if (chapterOffset == 0) {
|
||||||
|
return XtcError::OK;
|
||||||
|
}
|
||||||
|
// Serial.printf("[%lu] [XTC] 位置2");//for debug
|
||||||
|
|
||||||
|
const uint64_t fileSize = m_file.size();
|
||||||
|
if (chapterOffset < sizeof(XtcHeader) || chapterOffset >= fileSize || chapterOffset + 96 > fileSize) {
|
||||||
|
return XtcError::OK;
|
||||||
|
}
|
||||||
|
uint64_t maxOffset = 0;
|
||||||
|
if (m_header.pageTableOffset > chapterOffset) {
|
||||||
|
maxOffset = m_header.pageTableOffset;
|
||||||
|
} else if (m_header.dataOffset > chapterOffset) {
|
||||||
|
maxOffset = m_header.dataOffset;
|
||||||
|
} else {
|
||||||
|
maxOffset = fileSize;
|
||||||
|
}
|
||||||
|
if (maxOffset <= chapterOffset) {
|
||||||
|
return XtcError::OK;
|
||||||
|
}
|
||||||
|
constexpr size_t chapterSize = 96;
|
||||||
|
const uint64_t available = maxOffset - chapterOffset;
|
||||||
|
const size_t chapterCount = static_cast<size_t>(available / chapterSize);
|
||||||
|
if (chapterCount == 0) {
|
||||||
|
return XtcError::OK;
|
||||||
|
}
|
||||||
|
// Serial.printf("[%lu] [XTC] 位置3"); //for debug
|
||||||
|
// find the start offset
|
||||||
|
uint64_t startReadOffset = chapterOffset + (chapterStart * chapterSize);
|
||||||
|
if (!m_file.seek(startReadOffset)) {
|
||||||
|
return XtcError::READ_ERROR;
|
||||||
|
}
|
||||||
|
Serial.printf("[%lu] [XTC] 位置4");
|
||||||
|
|
||||||
|
std::vector<uint8_t> chapterBuf(chapterSize);
|
||||||
|
int readCount = 0;
|
||||||
|
size_t currentChapterIdx = chapterStart;
|
||||||
|
|
||||||
|
// 25 chapters once
|
||||||
|
Serial.printf("[%lu] [XTC] readCount:%d,currentChapterIdx:%d, chapterCount %u\n", millis(), readCount, currentChapterIdx,chapterCount);
|
||||||
|
while (readCount < 25 && currentChapterIdx < chapterCount) {
|
||||||
|
if (m_file.read(chapterBuf.data(), chapterSize) != chapterSize) {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (magic == XTC_MAGIC || magic == XTCH_MAGIC);
|
//no changes
|
||||||
}
|
char nameBuf[81];
|
||||||
|
memcpy(nameBuf, chapterBuf.data(), 80);
|
||||||
|
nameBuf[80] = '\0';
|
||||||
|
const size_t nameLen = strnlen(nameBuf, 80);
|
||||||
|
std::string name(nameBuf, nameLen);
|
||||||
|
|
||||||
|
|
||||||
|
uint16_t startPage = 0;
|
||||||
|
uint16_t endPage = 0;
|
||||||
|
memcpy(&startPage, chapterBuf.data() + 0x50, sizeof(startPage));
|
||||||
|
memcpy(&endPage, chapterBuf.data() + 0x52, sizeof(endPage));
|
||||||
|
|
||||||
|
if (name.empty() && startPage == 0 && endPage == 0) {
|
||||||
|
currentChapterIdx++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (startPage > 0) {
|
||||||
|
startPage--;
|
||||||
|
}
|
||||||
|
if (endPage > 0) {
|
||||||
|
endPage--;
|
||||||
|
}
|
||||||
|
if (startPage >= m_header.pageCount || startPage > endPage) {
|
||||||
|
currentChapterIdx++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (endPage >= m_header.pageCount) {
|
||||||
|
endPage = m_header.pageCount - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
strncpy(ChapterList[readCount].shortTitle, name.c_str(), 63);
|
||||||
|
ChapterList[readCount].shortTitle[63] = '\0';
|
||||||
|
ChapterList[readCount].startPage = startPage;
|
||||||
|
ChapterList[readCount].chapterIndex = currentChapterIdx;
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [XTC] 第%d章,名字为:%s %u\n", millis(), readCount, ChapterList[readCount].shortTitle);
|
||||||
|
readCount++; // getpages
|
||||||
|
currentChapterIdx++;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
m_hasChapters = readCount > 0;
|
||||||
|
Serial.printf("[%lu] [XTC] 翻页读取章节:起始=%d,有效数=%u\n", millis(), chapterStart, (unsigned int)readCount);
|
||||||
|
return XtcError::OK;
|
||||||
|
}
|
||||||
|
XtcError XtcParser::loadPageBatchByStart(uint16_t startPage) {
|
||||||
|
if(!m_isOpen) return XtcError::FILE_NOT_FOUND;
|
||||||
|
if(startPage >= m_header.pageCount) return XtcError::PAGE_OUT_OF_RANGE;
|
||||||
|
|
||||||
|
|
||||||
|
m_pageTable.clear();
|
||||||
|
m_pageTable.shrink_to_fit();
|
||||||
|
|
||||||
|
|
||||||
|
m_loadedStartPage = startPage;
|
||||||
|
uint16_t endPage = startPage + m_loadBatchSize - 1;
|
||||||
|
if(endPage >= m_header.pageCount) endPage = m_header.pageCount - 1;
|
||||||
|
uint16_t loadCount = endPage - startPage + 1;
|
||||||
|
|
||||||
|
// find the offset for new table
|
||||||
|
uint64_t seekOffset = m_header.pageTableOffset + (startPage * sizeof(PageTableEntry));
|
||||||
|
if(!m_file.seek(seekOffset)) return XtcError::READ_ERROR;
|
||||||
|
|
||||||
|
// load
|
||||||
|
m_pageTable.resize(loadCount);
|
||||||
|
for(uint16_t i = startPage; i <= endPage; i++) {
|
||||||
|
PageTableEntry entry;
|
||||||
|
if(m_file.read(reinterpret_cast<uint8_t*>(&entry), sizeof(PageTableEntry)) != sizeof(PageTableEntry)) {
|
||||||
|
return XtcError::READ_ERROR;
|
||||||
|
}
|
||||||
|
m_pageTable[i - startPage].offset = static_cast<uint32_t>(entry.dataOffset);
|
||||||
|
m_pageTable[i - startPage].size = entry.dataSize;
|
||||||
|
m_pageTable[i - startPage].width = entry.width;
|
||||||
|
m_pageTable[i - startPage].height = entry.height;
|
||||||
|
m_pageTable[i - startPage].bitDepth = m_bitDepth;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_loadedMaxPage = endPage;
|
||||||
|
Serial.printf("[XTC] 强制加载批次 : 清空旧表 → 加载 [%u~%u] | 内存占用恒定\n", startPage, endPage);
|
||||||
|
return XtcError::OK;
|
||||||
|
}
|
||||||
} // namespace xtc
|
} // namespace xtc
|
||||||
|
|||||||
@ -29,6 +29,11 @@ class XtcParser {
|
|||||||
XtcParser();
|
XtcParser();
|
||||||
~XtcParser();
|
~XtcParser();
|
||||||
|
|
||||||
|
#define MAX_SAVE_CHAPTER 30
|
||||||
|
#define TITLE_KEEP_LENGTH 20
|
||||||
|
#define TITLE_BUF_SIZE 64
|
||||||
|
|
||||||
|
|
||||||
// File open/close
|
// File open/close
|
||||||
XtcError open(const char* filepath);
|
XtcError open(const char* filepath);
|
||||||
void close();
|
void close();
|
||||||
@ -54,6 +59,46 @@ class XtcParser {
|
|||||||
*/
|
*/
|
||||||
size_t loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSize);
|
size_t loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSize);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief
|
||||||
|
* @return XtcError Loading status: OK = success, PAGE_OUT_OF_RANGE = no more pages to load, others = loading failed.
|
||||||
|
*/
|
||||||
|
XtcError loadNextPageBatch();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the maximum page number that has been loaded currently.
|
||||||
|
* @return uint16_t The maximum valid page number loaded currently.
|
||||||
|
*/
|
||||||
|
uint16_t getLoadedMaxPage() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the number of pages loaded dynamically each time (batch size).
|
||||||
|
* @return uint16_t Page batch size, default is 10.
|
||||||
|
*/
|
||||||
|
uint16_t getPageBatchSize() const;
|
||||||
|
|
||||||
|
uint32_t getChapterstartpage(int chapterIndex) {
|
||||||
|
for(int i = 0; i < 25; i++) {
|
||||||
|
if(ChapterList[i].chapterIndex == chapterIndex) {
|
||||||
|
return ChapterList[i].startPage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0; // Return 0 if the chapter does not exist.
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string getChapterTitleByIndex(int chapterIndex) {
|
||||||
|
Serial.printf("[%lu] [XTC] Entered getChapterTitleByIndex,chapterActualCount=%d\n", millis(),chapterActualCount);
|
||||||
|
for(int i = 0; i < 25; i++) {
|
||||||
|
if(ChapterList[i].chapterIndex == chapterIndex) {
|
||||||
|
return std::string(ChapterList[i].shortTitle);
|
||||||
|
Serial.printf("[%lu] [XTC] In getChapterTitleByIndex, the title of chapter %d is: %s %u\n", millis(), i, ChapterList[i].shortTitle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""; // Return empty string if the chapter does not exist.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Streaming page load
|
* Streaming page load
|
||||||
* Memory-efficient method that reads page data in chunks.
|
* Memory-efficient method that reads page data in chunks.
|
||||||
@ -74,6 +119,11 @@ class XtcParser {
|
|||||||
bool hasChapters() const { return m_hasChapters; }
|
bool hasChapters() const { return m_hasChapters; }
|
||||||
const std::vector<ChapterInfo>& getChapters() const { return m_chapters; }
|
const std::vector<ChapterInfo>& getChapters() const { return m_chapters; }
|
||||||
|
|
||||||
|
XtcError readChapters_gd(uint16_t chapterStart);
|
||||||
|
ChapterData ChapterList[MAX_SAVE_CHAPTER];
|
||||||
|
int chapterActualCount = 0;
|
||||||
|
XtcError loadPageBatchByStart(uint16_t startPage);
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
static bool isValidXtcFile(const char* filepath);
|
static bool isValidXtcFile(const char* filepath);
|
||||||
|
|
||||||
@ -93,6 +143,7 @@ class XtcParser {
|
|||||||
uint8_t m_bitDepth; // 1 = XTC/XTG (1-bit), 2 = XTCH/XTH (2-bit)
|
uint8_t m_bitDepth; // 1 = XTC/XTG (1-bit), 2 = XTCH/XTH (2-bit)
|
||||||
bool m_hasChapters;
|
bool m_hasChapters;
|
||||||
XtcError m_lastError;
|
XtcError m_lastError;
|
||||||
|
uint16_t m_loadedStartPage = 0;
|
||||||
|
|
||||||
// Internal helper functions
|
// Internal helper functions
|
||||||
XtcError readHeader();
|
XtcError readHeader();
|
||||||
@ -100,6 +151,8 @@ class XtcParser {
|
|||||||
XtcError readTitle();
|
XtcError readTitle();
|
||||||
XtcError readAuthor();
|
XtcError readAuthor();
|
||||||
XtcError readChapters();
|
XtcError readChapters();
|
||||||
|
uint16_t m_loadBatchSize = 10; // pages for once load
|
||||||
|
uint16_t m_loadedMaxPage = 0; // Record the maximum page currently loaded
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace xtc
|
} // namespace xtc
|
||||||
|
|||||||
@ -101,6 +101,12 @@ struct ChapterInfo {
|
|||||||
uint16_t startPage;
|
uint16_t startPage;
|
||||||
uint16_t endPage;
|
uint16_t endPage;
|
||||||
};
|
};
|
||||||
|
//new struct
|
||||||
|
struct ChapterData {
|
||||||
|
int chapterIndex;
|
||||||
|
uint16_t startPage;
|
||||||
|
char shortTitle[64];
|
||||||
|
};
|
||||||
|
|
||||||
// Error codes
|
// Error codes
|
||||||
enum class XtcError {
|
enum class XtcError {
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
Subproject commit bd4e6707503ab9c97d13ee0d8f8c69e9ff03cd12
|
Subproject commit c39f253a7dabbc193a8d7d310fb8777dca0ab8f1
|
||||||
@ -21,6 +21,7 @@
|
|||||||
namespace {
|
namespace {
|
||||||
constexpr unsigned long skipPageMs = 700;
|
constexpr unsigned long skipPageMs = 700;
|
||||||
constexpr unsigned long goHomeMs = 1000;
|
constexpr unsigned long goHomeMs = 1000;
|
||||||
|
constexpr int loadedMaxPage_per= 500;
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void XtcReaderActivity::taskTrampoline(void* param) {
|
void XtcReaderActivity::taskTrampoline(void* param) {
|
||||||
@ -92,6 +93,7 @@ void XtcReaderActivity::loop() {
|
|||||||
},
|
},
|
||||||
[this](const uint32_t newPage) {
|
[this](const uint32_t newPage) {
|
||||||
currentPage = newPage;
|
currentPage = newPage;
|
||||||
|
this->gotoPage(newPage);
|
||||||
exitActivity();
|
exitActivity();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}));
|
}));
|
||||||
@ -200,48 +202,30 @@ void XtcReaderActivity::renderPage() {
|
|||||||
pageBufferSize = ((pageWidth + 7) / 8) * pageHeight;
|
pageBufferSize = ((pageWidth + 7) / 8) * pageHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allocate page buffer
|
// This part defines global variables to reduce memory usage, but the effect seems to be mediocre.
|
||||||
uint8_t* pageBuffer = static_cast<uint8_t*>(malloc(pageBufferSize));
|
uint8_t* pageBuffer = s_pageBuffer;
|
||||||
if (!pageBuffer) {
|
|
||||||
Serial.printf("[%lu] [XTR] Failed to allocate page buffer (%lu bytes)\n", millis(), pageBufferSize);
|
|
||||||
renderer.clearScreen();
|
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Memory error", true, EpdFontFamily::BOLD);
|
|
||||||
renderer.displayBuffer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load page data
|
// load new page
|
||||||
size_t bytesRead = xtc->loadPage(currentPage, pageBuffer, pageBufferSize);
|
size_t bytesRead = xtc->loadPage(currentPage, pageBuffer, pageBufferSize);
|
||||||
if (bytesRead == 0) {
|
if (bytesRead == 0) {
|
||||||
Serial.printf("[%lu] [XTR] Failed to load page %lu\n", millis(), currentPage);
|
Serial.printf("[%lu] [提示] 页码%lu加载中...\n", millis(), currentPage);
|
||||||
free(pageBuffer);
|
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Page load error", true, EpdFontFamily::BOLD);
|
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Loading...", true, EpdFontFamily::BOLD);
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
|
updateRequired = true; // for some bugs
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear screen first
|
// keep
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
|
|
||||||
// Copy page bitmap using GfxRenderer's drawPixel
|
|
||||||
// XTC/XTCH pages are pre-rendered with status bar included, so render full page
|
|
||||||
const uint16_t maxSrcY = pageHeight;
|
const uint16_t maxSrcY = pageHeight;
|
||||||
|
|
||||||
if (bitDepth == 2) {
|
if (bitDepth == 2) {
|
||||||
// XTH 2-bit mode: Two bit planes, column-major order
|
|
||||||
// - Columns scanned right to left (x = width-1 down to 0)
|
|
||||||
// - 8 vertical pixels per byte (MSB = topmost pixel in group)
|
|
||||||
// - First plane: Bit1, Second plane: Bit2
|
|
||||||
// - Pixel value = (bit1 << 1) | bit2
|
|
||||||
// - Grayscale: 0=White, 1=Dark Grey, 2=Light Grey, 3=Black
|
|
||||||
|
|
||||||
const size_t planeSize = (static_cast<size_t>(pageWidth) * pageHeight + 7) / 8;
|
const size_t planeSize = (static_cast<size_t>(pageWidth) * pageHeight + 7) / 8;
|
||||||
const uint8_t* plane1 = pageBuffer; // Bit1 plane
|
const uint8_t* plane1 = pageBuffer;
|
||||||
const uint8_t* plane2 = pageBuffer + planeSize; // Bit2 plane
|
const uint8_t* plane2 = pageBuffer + planeSize;
|
||||||
const size_t colBytes = (pageHeight + 7) / 8; // Bytes per column (100 for 800 height)
|
const size_t colBytes = (pageHeight + 7) / 8;
|
||||||
|
|
||||||
// Lambda to get pixel value at (x, y)
|
|
||||||
auto getPixelValue = [&](uint16_t x, uint16_t y) -> uint8_t {
|
auto getPixelValue = [&](uint16_t x, uint16_t y) -> uint8_t {
|
||||||
const size_t colIndex = pageWidth - 1 - x;
|
const size_t colIndex = pageWidth - 1 - x;
|
||||||
const size_t byteInCol = y / 8;
|
const size_t byteInCol = y / 8;
|
||||||
@ -252,20 +236,6 @@ void XtcReaderActivity::renderPage() {
|
|||||||
return (bit1 << 1) | bit2;
|
return (bit1 << 1) | bit2;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Optimized grayscale rendering without storeBwBuffer (saves 48KB peak memory)
|
|
||||||
// Flow: BW display → LSB/MSB passes → grayscale display → re-render BW for next frame
|
|
||||||
|
|
||||||
// Count pixel distribution for debugging
|
|
||||||
uint32_t pixelCounts[4] = {0, 0, 0, 0};
|
|
||||||
for (uint16_t y = 0; y < pageHeight; y++) {
|
|
||||||
for (uint16_t x = 0; x < pageWidth; x++) {
|
|
||||||
pixelCounts[getPixelValue(x, y)]++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Serial.printf("[%lu] [XTR] Pixel distribution: White=%lu, DarkGrey=%lu, LightGrey=%lu, Black=%lu\n", millis(),
|
|
||||||
pixelCounts[0], pixelCounts[1], pixelCounts[2], pixelCounts[3]);
|
|
||||||
|
|
||||||
// Pass 1: BW buffer - draw all non-white pixels as black
|
|
||||||
for (uint16_t y = 0; y < pageHeight; y++) {
|
for (uint16_t y = 0; y < pageHeight; y++) {
|
||||||
for (uint16_t x = 0; x < pageWidth; x++) {
|
for (uint16_t x = 0; x < pageWidth; x++) {
|
||||||
if (getPixelValue(x, y) >= 1) {
|
if (getPixelValue(x, y) >= 1) {
|
||||||
@ -274,7 +244,6 @@ void XtcReaderActivity::renderPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display BW with conditional refresh based on pagesUntilFullRefresh
|
|
||||||
if (pagesUntilFullRefresh <= 1) {
|
if (pagesUntilFullRefresh <= 1) {
|
||||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||||
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
||||||
@ -283,35 +252,28 @@ void XtcReaderActivity::renderPage() {
|
|||||||
pagesUntilFullRefresh--;
|
pagesUntilFullRefresh--;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass 2: LSB buffer - mark DARK gray only (XTH value 1)
|
|
||||||
// In LUT: 0 bit = apply gray effect, 1 bit = untouched
|
|
||||||
renderer.clearScreen(0x00);
|
renderer.clearScreen(0x00);
|
||||||
for (uint16_t y = 0; y < pageHeight; y++) {
|
for (uint16_t y = 0; y < pageHeight; y++) {
|
||||||
for (uint16_t x = 0; x < pageWidth; x++) {
|
for (uint16_t x = 0; x < pageWidth; x++) {
|
||||||
if (getPixelValue(x, y) == 1) { // Dark grey only
|
if (getPixelValue(x, y) == 1) {
|
||||||
renderer.drawPixel(x, y, false);
|
renderer.drawPixel(x, y, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
renderer.copyGrayscaleLsbBuffers();
|
renderer.copyGrayscaleLsbBuffers();
|
||||||
|
|
||||||
// Pass 3: MSB buffer - mark LIGHT AND DARK gray (XTH value 1 or 2)
|
|
||||||
// In LUT: 0 bit = apply gray effect, 1 bit = untouched
|
|
||||||
renderer.clearScreen(0x00);
|
renderer.clearScreen(0x00);
|
||||||
for (uint16_t y = 0; y < pageHeight; y++) {
|
for (uint16_t y = 0; y < pageHeight; y++) {
|
||||||
for (uint16_t x = 0; x < pageWidth; x++) {
|
for (uint16_t x = 0; x < pageWidth; x++) {
|
||||||
const uint8_t pv = getPixelValue(x, y);
|
const uint8_t pv = getPixelValue(x, y);
|
||||||
if (pv == 1 || pv == 2) { // Dark grey or Light grey
|
if (pv == 1 || pv == 2) {
|
||||||
renderer.drawPixel(x, y, false);
|
renderer.drawPixel(x, y, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
renderer.copyGrayscaleMsbBuffers();
|
renderer.copyGrayscaleMsbBuffers();
|
||||||
|
|
||||||
// Display grayscale overlay
|
|
||||||
renderer.displayGrayBuffer();
|
renderer.displayGrayBuffer();
|
||||||
|
|
||||||
// Pass 4: Re-render BW to framebuffer (restore for next frame, instead of restoreBwBuffer)
|
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
for (uint16_t y = 0; y < pageHeight; y++) {
|
for (uint16_t y = 0; y < pageHeight; y++) {
|
||||||
for (uint16_t x = 0; x < pageWidth; x++) {
|
for (uint16_t x = 0; x < pageWidth; x++) {
|
||||||
@ -320,41 +282,20 @@ void XtcReaderActivity::renderPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup grayscale buffers with current frame buffer
|
|
||||||
renderer.cleanupGrayscaleWithFrameBuffer();
|
renderer.cleanupGrayscaleWithFrameBuffer();
|
||||||
|
|
||||||
free(pageBuffer);
|
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTR] Rendered page %lu/%lu (2-bit grayscale)\n", millis(), currentPage + 1,
|
|
||||||
xtc->getPageCount());
|
|
||||||
return;
|
|
||||||
} else {
|
} else {
|
||||||
// 1-bit mode: 8 pixels per byte, MSB first
|
const size_t srcRowBytes = (pageWidth + 7) / 8;
|
||||||
const size_t srcRowBytes = (pageWidth + 7) / 8; // 60 bytes for 480 width
|
|
||||||
|
|
||||||
for (uint16_t srcY = 0; srcY < maxSrcY; srcY++) {
|
for (uint16_t srcY = 0; srcY < maxSrcY; srcY++) {
|
||||||
const size_t srcRowStart = srcY * srcRowBytes;
|
const size_t srcRowStart = srcY * srcRowBytes;
|
||||||
|
|
||||||
for (uint16_t srcX = 0; srcX < pageWidth; srcX++) {
|
for (uint16_t srcX = 0; srcX < pageWidth; srcX++) {
|
||||||
// Read source pixel (MSB first, bit 7 = leftmost pixel)
|
|
||||||
const size_t srcByte = srcRowStart + srcX / 8;
|
const size_t srcByte = srcRowStart + srcX / 8;
|
||||||
const size_t srcBit = 7 - (srcX % 8);
|
const size_t srcBit = 7 - (srcX % 8);
|
||||||
const bool isBlack = !((pageBuffer[srcByte] >> srcBit) & 1); // XTC: 0 = black, 1 = white
|
const bool isBlack = !((pageBuffer[srcByte] >> srcBit) & 1);
|
||||||
|
|
||||||
if (isBlack) {
|
if (isBlack) {
|
||||||
renderer.drawPixel(srcX, srcY, true);
|
renderer.drawPixel(srcX, srcY, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// White pixels are already cleared by clearScreen()
|
|
||||||
|
|
||||||
free(pageBuffer);
|
|
||||||
|
|
||||||
// XTC pages already have status bar pre-rendered, no need to add our own
|
|
||||||
|
|
||||||
// Display with appropriate refresh
|
|
||||||
if (pagesUntilFullRefresh <= 1) {
|
if (pagesUntilFullRefresh <= 1) {
|
||||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||||
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
||||||
@ -362,37 +303,88 @@ void XtcReaderActivity::renderPage() {
|
|||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
pagesUntilFullRefresh--;
|
pagesUntilFullRefresh--;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [XTR] Rendered page %lu/%lu (%u-bit)\n", millis(), currentPage + 1, xtc->getPageCount(),
|
Serial.printf("[%lu] [成功] 显示页码: %lu/%lu\n", millis(), currentPage+1, xtc->getPageCount());
|
||||||
bitDepth);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void XtcReaderActivity::gotoPage(uint32_t targetPage) {
|
||||||
|
const uint32_t totalPages = xtc->getPageCount();
|
||||||
|
if (targetPage >= totalPages) targetPage = totalPages - 1;
|
||||||
|
if (targetPage < 0) targetPage = 0;
|
||||||
|
|
||||||
|
uint32_t targetBatchStart = (targetPage / loadedMaxPage_per) * loadedMaxPage_per;
|
||||||
|
|
||||||
|
xtc->loadPageBatchByStart(targetBatchStart);
|
||||||
|
|
||||||
|
m_loadedMax = targetBatchStart + loadedMaxPage_per - 1; // Activity的最大值
|
||||||
|
if(m_loadedMax >= totalPages) m_loadedMax = totalPages - 1;
|
||||||
|
|
||||||
|
|
||||||
|
currentPage = targetPage;
|
||||||
|
updateRequired = true;
|
||||||
|
Serial.printf("[跳转] 目标页%lu → 加载批次[%lu~%lu] | 内存已释放\n", targetPage, targetBatchStart, m_loadedMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void XtcReaderActivity::saveProgress() const {
|
void XtcReaderActivity::saveProgress() const {
|
||||||
FsFile f;
|
FsFile f;
|
||||||
if (SdMan.openFileForWrite("XTR", xtc->getCachePath() + "/progress.bin", f)) {
|
if (SdMan.openFileForWrite("XTR", xtc->getCachePath() + "/progress.bin", f)) {
|
||||||
uint8_t data[4];
|
uint8_t data[8]; // for 2 data:currentPage and m_loadedMax
|
||||||
|
// currentPage
|
||||||
data[0] = currentPage & 0xFF;
|
data[0] = currentPage & 0xFF;
|
||||||
data[1] = (currentPage >> 8) & 0xFF;
|
data[1] = (currentPage >> 8) & 0xFF;
|
||||||
data[2] = (currentPage >> 16) & 0xFF;
|
data[2] = (currentPage >> 16) & 0xFF;
|
||||||
data[3] = (currentPage >> 24) & 0xFF;
|
data[3] = (currentPage >> 24) & 0xFF;
|
||||||
f.write(data, 4);
|
// m_loadedMax
|
||||||
|
data[4] = m_loadedMax & 0xFF;
|
||||||
|
data[5] = (m_loadedMax >> 8) & 0xFF;
|
||||||
|
data[6] = (m_loadedMax >> 16) & 0xFF;
|
||||||
|
data[7] = (m_loadedMax >> 24) & 0xFF;
|
||||||
|
|
||||||
|
f.write(data, 8);
|
||||||
f.close();
|
f.close();
|
||||||
|
Serial.printf("[%lu] [进度] 保存成功 → 页码: %lu | 页表上限: %lu\n", millis(), currentPage, m_loadedMax);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//2data to load
|
||||||
|
|
||||||
void XtcReaderActivity::loadProgress() {
|
void XtcReaderActivity::loadProgress() {
|
||||||
FsFile f;
|
FsFile f;
|
||||||
if (SdMan.openFileForRead("XTR", xtc->getCachePath() + "/progress.bin", f)) {
|
if (SdMan.openFileForRead("XTR", xtc->getCachePath() + "/progress.bin", f)) {
|
||||||
uint8_t data[4];
|
uint8_t data[8];
|
||||||
if (f.read(data, 4) == 4) {
|
if (f.read(data, 8) == 8) {
|
||||||
currentPage = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
|
|
||||||
Serial.printf("[%lu] [XTR] Loaded progress: page %lu\n", millis(), currentPage);
|
|
||||||
|
|
||||||
// Validate page number
|
currentPage = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
|
||||||
if (currentPage >= xtc->getPageCount()) {
|
uint32_t savedLoadedMax = data[4] | (data[5] << 8) | (data[6] << 16) | (data[7] << 24);
|
||||||
currentPage = 0;
|
|
||||||
}
|
Serial.printf("[%lu] [进度] 恢复成功 → 页码: %lu | 保存的页表上限: %lu\n", millis(), currentPage, savedLoadedMax);
|
||||||
|
|
||||||
|
|
||||||
|
const uint32_t totalPages = xtc->getPageCount();
|
||||||
|
if (currentPage >= totalPages) currentPage = totalPages - 1;
|
||||||
|
if (currentPage < 0) currentPage = 0;
|
||||||
|
|
||||||
|
// Determine whether loading is required and which batch of tables to load.
|
||||||
|
uint32_t targetBatchStart = (currentPage / loadedMaxPage_per) * loadedMaxPage_per;
|
||||||
|
xtc->loadPageBatchByStart(targetBatchStart);
|
||||||
|
|
||||||
|
|
||||||
|
m_loadedMax = targetBatchStart + loadedMaxPage_per - 1;
|
||||||
|
if(m_loadedMax >= totalPages) m_loadedMax = totalPages - 1;
|
||||||
|
|
||||||
|
Serial.printf("[进度] 恢复进度后加载批次 → 页码%lu → 批次[%lu~%lu]\n", currentPage, targetBatchStart, m_loadedMax);
|
||||||
}
|
}
|
||||||
f.close();
|
f.close();
|
||||||
|
} else {
|
||||||
|
|
||||||
|
const uint32_t totalPages = xtc->getPageCount();
|
||||||
|
currentPage = 0;
|
||||||
|
m_loadedMax = loadedMaxPage_per - 1;
|
||||||
|
if(m_loadedMax >= totalPages) m_loadedMax = totalPages - 1;
|
||||||
|
Serial.printf("[%lu] [进度] 无进度文件 → 初始化页码: 0 | 页表上限: %lu\n", millis(), m_loadedMax);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,10 @@
|
|||||||
#include <freertos/task.h>
|
#include <freertos/task.h>
|
||||||
|
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
namespace {
|
||||||
|
constexpr size_t MAX_PAGE_BUFFER_SIZE = (480 * 800 + 7) / 8 * 2;
|
||||||
|
static uint8_t s_pageBuffer[MAX_PAGE_BUFFER_SIZE] = {0};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
class XtcReaderActivity final : public ActivityWithSubactivity {
|
class XtcReaderActivity final : public ActivityWithSubactivity {
|
||||||
std::shared_ptr<Xtc> xtc;
|
std::shared_ptr<Xtc> xtc;
|
||||||
@ -23,6 +27,8 @@ class XtcReaderActivity final : public ActivityWithSubactivity {
|
|||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
const std::function<void()> onGoBack;
|
const std::function<void()> onGoBack;
|
||||||
const std::function<void()> onGoHome;
|
const std::function<void()> onGoHome;
|
||||||
|
//pages once load
|
||||||
|
uint32_t m_loadedMax = 499;
|
||||||
|
|
||||||
static void taskTrampoline(void* param);
|
static void taskTrampoline(void* param);
|
||||||
[[noreturn]] void displayTaskLoop();
|
[[noreturn]] void displayTaskLoop();
|
||||||
@ -30,6 +36,8 @@ class XtcReaderActivity final : public ActivityWithSubactivity {
|
|||||||
void renderPage();
|
void renderPage();
|
||||||
void saveProgress() const;
|
void saveProgress() const;
|
||||||
void loadProgress();
|
void loadProgress();
|
||||||
|
//new
|
||||||
|
void gotoPage(uint32_t targetPage);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit XtcReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Xtc> xtc,
|
explicit XtcReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Xtc> xtc,
|
||||||
|
|||||||
@ -4,38 +4,15 @@
|
|||||||
|
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
#include "Xtc.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int SKIP_PAGE_MS = 700;
|
constexpr int SKIP_PAGE_MS = 700;
|
||||||
|
int page = 1;
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int XtcReaderChapterSelectionActivity::getPageItems() const {
|
int XtcReaderChapterSelectionActivity::getPageItems() const {
|
||||||
constexpr int startY = 60;
|
return 25; // 25 for one page
|
||||||
constexpr int lineHeight = 30;
|
|
||||||
|
|
||||||
const int screenHeight = renderer.getScreenHeight();
|
|
||||||
const int endY = screenHeight - lineHeight;
|
|
||||||
|
|
||||||
const int availableHeight = endY - startY;
|
|
||||||
int items = availableHeight / lineHeight;
|
|
||||||
if (items < 1) {
|
|
||||||
items = 1;
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
int XtcReaderChapterSelectionActivity::findChapterIndexForPage(uint32_t page) const {
|
|
||||||
if (!xtc) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto& chapters = xtc->getChapters();
|
|
||||||
for (size_t i = 0; i < chapters.size(); i++) {
|
|
||||||
if (page >= chapters[i].startPage && page <= chapters[i].endPage) {
|
|
||||||
return static_cast<int>(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void XtcReaderChapterSelectionActivity::taskTrampoline(void* param) {
|
void XtcReaderChapterSelectionActivity::taskTrampoline(void* param) {
|
||||||
@ -44,34 +21,27 @@ void XtcReaderChapterSelectionActivity::taskTrampoline(void* param) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void XtcReaderChapterSelectionActivity::onEnter() {
|
void XtcReaderChapterSelectionActivity::onEnter() {
|
||||||
|
renderer.clearScreen();
|
||||||
Activity::onEnter();
|
Activity::onEnter();
|
||||||
|
|
||||||
if (!xtc) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderingMutex = xSemaphoreCreateMutex();
|
|
||||||
selectorIndex = findChapterIndexForPage(currentPage);
|
|
||||||
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
xTaskCreate(&XtcReaderChapterSelectionActivity::taskTrampoline, "XtcReaderChapterSelectionActivityTask",
|
selectorIndex = 0;
|
||||||
4096, // Stack size
|
page = 1;
|
||||||
this, // Parameters
|
xTaskCreate(&XtcReaderChapterSelectionActivity::taskTrampoline, "XtcReaderChapterSelectionTask",
|
||||||
1, // Priority
|
4096,
|
||||||
&displayTaskHandle // Task handle
|
this,
|
||||||
|
1,
|
||||||
|
&displayTaskHandle
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void XtcReaderChapterSelectionActivity::onExit() {
|
void XtcReaderChapterSelectionActivity::onExit() {
|
||||||
Activity::onExit();
|
Activity::onExit();
|
||||||
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
if (displayTaskHandle) {
|
if (displayTaskHandle) {
|
||||||
vTaskDelete(displayTaskHandle);
|
vTaskDelete(displayTaskHandle);
|
||||||
displayTaskHandle = nullptr;
|
displayTaskHandle = nullptr;
|
||||||
}
|
}
|
||||||
vSemaphoreDelete(renderingMutex);
|
|
||||||
renderingMutex = nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void XtcReaderChapterSelectionActivity::loop() {
|
void XtcReaderChapterSelectionActivity::loop() {
|
||||||
@ -84,32 +54,34 @@ void XtcReaderChapterSelectionActivity::loop() {
|
|||||||
const int pageItems = getPageItems();
|
const int pageItems = getPageItems();
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
const auto& chapters = xtc->getChapters();
|
const int pagebegin=(page-1)*25;
|
||||||
if (!chapters.empty() && selectorIndex >= 0 && selectorIndex < static_cast<int>(chapters.size())) {
|
xtc->readChapters_gd(pagebegin);
|
||||||
onSelectPage(chapters[selectorIndex].startPage);
|
//to get the page for the select chapter
|
||||||
}
|
uint32_t chapterpage = this->xtc->getChapterstartpage(selectorIndex);
|
||||||
|
Serial.printf("[%lu] [XTC] 跳转章节:%d,跳转页数:%d\n", millis(), selectorIndex, chapterpage);
|
||||||
|
|
||||||
|
onSelectPage(chapterpage);
|
||||||
|
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
onGoBack();
|
onGoBack();
|
||||||
} else if (prevReleased) {
|
} else if (prevReleased) {
|
||||||
const int total = static_cast<int>(xtc->getChapters().size());
|
bool isUpKey = mappedInput.wasReleased(MappedInputManager::Button::Up);
|
||||||
if (total == 0) {
|
if (skipPage || isUpKey) {
|
||||||
return;
|
page -= 1;
|
||||||
}
|
if(page < 1) page = 1;
|
||||||
if (skipPage) {
|
selectorIndex = (page-1)*25;
|
||||||
selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + total) % total;
|
|
||||||
} else {
|
} else {
|
||||||
selectorIndex = (selectorIndex + total - 1) % total;
|
selectorIndex--;
|
||||||
|
if(selectorIndex < 0) selectorIndex = 0;
|
||||||
}
|
}
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (nextReleased) {
|
} else if (nextReleased) {
|
||||||
const int total = static_cast<int>(xtc->getChapters().size());
|
bool isDownKey = mappedInput.wasReleased(MappedInputManager::Button::Down);
|
||||||
if (total == 0) {
|
if (skipPage || isDownKey) {
|
||||||
return;
|
page += 1;
|
||||||
}
|
selectorIndex = (page-1)*25;
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % total;
|
|
||||||
} else {
|
} else {
|
||||||
selectorIndex = (selectorIndex + 1) % total;
|
selectorIndex++;
|
||||||
}
|
}
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
}
|
||||||
@ -119,9 +91,7 @@ void XtcReaderChapterSelectionActivity::displayTaskLoop() {
|
|||||||
while (true) {
|
while (true) {
|
||||||
if (updateRequired) {
|
if (updateRequired) {
|
||||||
updateRequired = false;
|
updateRequired = false;
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
renderScreen();
|
renderScreen();
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
}
|
}
|
||||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
}
|
}
|
||||||
@ -129,28 +99,38 @@ void XtcReaderChapterSelectionActivity::displayTaskLoop() {
|
|||||||
|
|
||||||
void XtcReaderChapterSelectionActivity::renderScreen() {
|
void XtcReaderChapterSelectionActivity::renderScreen() {
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
|
const int pagebegin=(page-1)*25;
|
||||||
|
int page_chapter=25;
|
||||||
|
static int parsedPage = -1;
|
||||||
|
|
||||||
|
if (parsedPage != page) {
|
||||||
|
xtc->readChapters_gd(pagebegin);
|
||||||
|
parsedPage = page;
|
||||||
|
}
|
||||||
|
|
||||||
const auto pageWidth = renderer.getScreenWidth();
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
const int pageItems = getPageItems();
|
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Select Chapter", true, EpdFontFamily::BOLD);
|
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Select Chapter", true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
const auto& chapters = xtc->getChapters();
|
const int FIX_LINE_HEIGHT = 29;
|
||||||
if (chapters.empty()) {
|
const int BASE_Y = 60;
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, 120, "No chapters");
|
|
||||||
renderer.displayBuffer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
|
|
||||||
renderer.fillRect(0, 60 + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1, 30);
|
|
||||||
for (int i = pageStartIndex; i < static_cast<int>(chapters.size()) && i < pageStartIndex + pageItems; i++) {
|
|
||||||
const auto& chapter = chapters[i];
|
|
||||||
const char* title = chapter.name.empty() ? "Unnamed" : chapter.name.c_str();
|
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % pageItems) * 30, title, i != selectorIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down");
|
for (int i = pagebegin; i <= pagebegin + page_chapter - 1; i++) {
|
||||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
int localIdx = i - pagebegin;
|
||||||
|
|
||||||
|
uint32_t currOffset = this->xtc->getChapterstartpage(i);
|
||||||
|
std::string dirTitle = this->xtc->getChapterTitleByIndex(i);
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [XTC_CHAPTER] 第%d章,名字为:%s,页码为%d\n", millis(), i, dirTitle.c_str(),currOffset);
|
||||||
|
static char title[64];
|
||||||
|
strncpy(title, dirTitle.c_str(), sizeof(title)-1);
|
||||||
|
title[sizeof(title)-1] = '\0';
|
||||||
|
|
||||||
|
int drawY = BASE_Y + localIdx * FIX_LINE_HEIGHT;
|
||||||
|
|
||||||
|
Serial.printf("选中的选项是:%d\n",selectorIndex);
|
||||||
|
renderer.drawText(UI_10_FONT_ID, 20, drawY, title, i!= selectorIndex);
|
||||||
|
}
|
||||||
|
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user