fix: prevent Serial.printf from blocking when USB disconnected
All checks were successful
CI / build (push) Successful in 2m23s

On ESP32-C3 with USB CDC, Serial.printf() blocks indefinitely when USB
is not connected. This caused device freezes when booted without USB.

Solution: Call Serial.setTxTimeoutMs(0) after Serial.begin() to make
all Serial output non-blocking.

Also added if (Serial) guards to high-traffic logging paths in
EpubReaderActivity as belt-and-suspenders protection.

Includes documentation of the debugging process and Serial call inventory.

Also applies clang-format to fix pre-existing formatting issues.
This commit is contained in:
cottongin
2026-01-28 15:57:31 -05:00
parent f3075002c1
commit 4db384edb6
59 changed files with 989 additions and 621 deletions

View File

@@ -111,24 +111,25 @@ void EpubReaderActivity::onEnter() {
FsFile f;
if (SdMan.openFileForRead("ERS", epub->getCachePath() + "/progress.bin", f)) {
const size_t fileSize = f.size();
if (fileSize >= 9) {
// New format: version (1) + spineIndex (2) + pageNumber (2) + contentOffset (4) = 9 bytes
uint8_t version;
serialization::readPod(f, version);
if (version == EPUB_PROGRESS_VERSION) {
uint16_t spineIndex, pageNumber;
serialization::readPod(f, spineIndex);
serialization::readPod(f, pageNumber);
serialization::readPod(f, savedContentOffset);
currentSpineIndex = spineIndex;
nextPageNumber = pageNumber;
hasContentOffset = true;
Serial.printf("[%lu] [ERS] Loaded progress v1: spine %d, page %d, offset %u\n",
millis(), currentSpineIndex, nextPageNumber, savedContentOffset);
if (Serial)
Serial.printf("[%lu] [ERS] Loaded progress v1: spine %d, page %d, offset %u\n", millis(), currentSpineIndex,
nextPageNumber, savedContentOffset);
} else {
// Unknown version, try legacy format
f.seek(0);
@@ -137,8 +138,9 @@ void EpubReaderActivity::onEnter() {
currentSpineIndex = data[0] + (data[1] << 8);
nextPageNumber = data[2] + (data[3] << 8);
hasContentOffset = false;
Serial.printf("[%lu] [ERS] Loaded legacy progress (unknown version %d): spine %d, page %d\n",
millis(), version, currentSpineIndex, nextPageNumber);
if (Serial)
Serial.printf("[%lu] [ERS] Loaded legacy progress (unknown version %d): spine %d, page %d\n", millis(),
version, currentSpineIndex, nextPageNumber);
}
}
} else if (fileSize >= 4) {
@@ -148,20 +150,23 @@ void EpubReaderActivity::onEnter() {
currentSpineIndex = data[0] + (data[1] << 8);
nextPageNumber = data[2] + (data[3] << 8);
hasContentOffset = false;
Serial.printf("[%lu] [ERS] Loaded legacy progress: spine %d, page %d\n",
millis(), currentSpineIndex, nextPageNumber);
if (Serial)
Serial.printf("[%lu] [ERS] Loaded legacy progress: spine %d, page %d\n", millis(), currentSpineIndex,
nextPageNumber);
}
}
f.close();
}
// We may want a better condition to detect if we are opening for the first time.
// This will trigger if the book is re-opened at Chapter 0.
if (currentSpineIndex == 0) {
int textSpineIndex = epub->getSpineIndexForTextReference();
if (textSpineIndex != 0) {
currentSpineIndex = textSpineIndex;
Serial.printf("[%lu] [ERS] Opened for first time, navigating to text reference at index %d\n", millis(),
textSpineIndex);
if (Serial)
Serial.printf("[%lu] [ERS] Opened for first time, navigating to text reference at index %d\n", millis(),
textSpineIndex);
}
}
@@ -300,19 +305,20 @@ void EpubReaderActivity::loop() {
Section* cachedSection = section.get();
SemaphoreHandle_t cachedMutex = renderingMutex;
EpubReaderActivity* self = this;
// Handle dictionary mode selection - exitActivity deletes DictionaryMenuActivity
exitActivity();
if (mode == DictionaryMode::ENTER_WORD) {
// Enter word mode - show keyboard and search
self->enterNewActivity(new DictionarySearchActivity(cachedRenderer, cachedMappedInput,
[self]() {
// On back from dictionary
self->exitActivity();
self->updateRequired = true;
},
"")); // Empty string = show keyboard
self->enterNewActivity(new DictionarySearchActivity(
cachedRenderer, cachedMappedInput,
[self]() {
// On back from dictionary
self->exitActivity();
self->updateRequired = true;
},
"")); // Empty string = show keyboard
} else {
// Select from screen mode - show word selection on current page
if (cachedSection) {
@@ -322,7 +328,7 @@ void EpubReaderActivity::loop() {
// Get margins for word selection positioning
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
cachedRenderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom,
&orientedMarginLeft);
&orientedMarginLeft);
orientedMarginTop += SETTINGS.screenMargin;
orientedMarginLeft += SETTINGS.screenMargin;
@@ -335,12 +341,13 @@ void EpubReaderActivity::loop() {
[self](const std::string& selectedWord) {
// Word selected - look it up
self->exitActivity();
self->enterNewActivity(new DictionarySearchActivity(self->renderer, self->mappedInput,
[self]() {
self->exitActivity();
self->updateRequired = true;
},
selectedWord));
self->enterNewActivity(new DictionarySearchActivity(
self->renderer, self->mappedInput,
[self]() {
self->exitActivity();
self->updateRequired = true;
},
selectedWord));
},
[self]() {
// Cancelled word selection
@@ -372,14 +379,14 @@ void EpubReaderActivity::loop() {
if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::QUICK_MENU &&
mappedInput.wasReleased(MappedInputManager::Button::Power)) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
// Check if current page is bookmarked
bool isBookmarked = false;
if (section) {
const uint32_t contentOffset = section->getContentOffsetForPage(section->currentPage);
isBookmarked = BookmarkStore::isPageBookmarked(epub->getPath(), currentSpineIndex, contentOffset);
}
exitActivity();
enterNewActivity(new QuickMenuActivity(
renderer, mappedInput,
@@ -387,9 +394,9 @@ void EpubReaderActivity::loop() {
// Cache values before exitActivity
EpubReaderActivity* self = this;
SemaphoreHandle_t cachedMutex = renderingMutex;
exitActivity();
if (action == QuickMenuAction::DICTIONARY) {
// Open dictionary menu - cache renderer/input for this scope
GfxRenderer& cachedRenderer = self->renderer;
@@ -402,15 +409,17 @@ void EpubReaderActivity::loop() {
MappedInputManager& m = self->mappedInput;
Section* s = self->section.get();
SemaphoreHandle_t mtx = self->renderingMutex;
self->exitActivity();
if (mode == DictionaryMode::ENTER_WORD) {
self->enterNewActivity(new DictionarySearchActivity(r, m,
self->enterNewActivity(new DictionarySearchActivity(
r, m,
[self]() {
self->exitActivity();
self->updateRequired = true;
}, ""));
},
""));
} else if (s) {
xSemaphoreTake(mtx, portMAX_DELAY);
auto page = s->loadPageFromSectionFile();
@@ -420,7 +429,7 @@ void EpubReaderActivity::loop() {
mt += SETTINGS.screenMargin;
ml += SETTINGS.screenMargin;
const int fontId = SETTINGS.getReaderFontId();
self->enterNewActivity(new EpubWordSelectionActivity(
r, m, std::move(page), fontId, ml, mt,
[self](const std::string& word) {
@@ -430,7 +439,8 @@ void EpubReaderActivity::loop() {
[self]() {
self->exitActivity();
self->updateRequired = true;
}, word));
},
word));
},
[self]() {
self->exitActivity();
@@ -455,7 +465,7 @@ void EpubReaderActivity::loop() {
if (self->section) {
const uint32_t contentOffset = self->section->getContentOffsetForPage(self->section->currentPage);
const std::string& bookPath = self->epub->getPath();
if (BookmarkStore::isPageBookmarked(bookPath, self->currentSpineIndex, contentOffset)) {
// Remove bookmark
BookmarkStore::removeBookmark(bookPath, self->currentSpineIndex, contentOffset);
@@ -466,7 +476,7 @@ void EpubReaderActivity::loop() {
bm.contentOffset = contentOffset;
bm.pageNumber = self->section->currentPage;
bm.timestamp = millis() / 1000; // Approximate timestamp
// Generate name: "Chapter - Page X" or fallback
std::string chapterTitle;
const int tocIndex = self->epub->getTocIndexForSpineIndex(self->currentSpineIndex);
@@ -478,7 +488,7 @@ void EpubReaderActivity::loop() {
} else {
bm.name = "Page " + std::to_string(self->section->currentPage + 1);
}
BookmarkStore::addBookmark(bookPath, bm);
}
}
@@ -629,18 +639,19 @@ void EpubReaderActivity::renderScreen() {
if (!section) {
const auto filepath = epub->getSpineItem(currentSpineIndex).href;
Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex);
if (Serial)
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));
const uint16_t viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
const uint16_t viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom;
bool sectionWasReIndexed = false;
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled)) {
Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis());
if (Serial) Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis());
sectionWasReIndexed = true;
// Progress bar dimensions
@@ -683,15 +694,15 @@ void EpubReaderActivity::renderScreen() {
renderer.displayBuffer(EInkDisplay::FAST_REFRESH);
};
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, progressSetup, progressCallback)) {
Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis());
if (Serial) Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis());
section.reset();
return;
}
} else {
Serial.printf("[%lu] [ERS] Cache found, skipping build...\n", millis());
if (Serial) Serial.printf("[%lu] [ERS] Cache found, skipping build...\n", millis());
}
// Determine the correct page to display
@@ -703,8 +714,9 @@ void EpubReaderActivity::renderScreen() {
// Use the offset to find the correct page
const int restoredPage = section->findPageForContentOffset(savedContentOffset);
section->currentPage = restoredPage;
Serial.printf("[%lu] [ERS] Restored position via offset: %u -> page %d (was page %d)\n",
millis(), savedContentOffset, restoredPage, nextPageNumber);
if (Serial)
Serial.printf("[%lu] [ERS] Restored position via offset: %u -> page %d (was page %d)\n", millis(),
savedContentOffset, restoredPage, nextPageNumber);
// Clear the offset flag since we've used it
hasContentOffset = false;
} else {
@@ -716,7 +728,7 @@ void EpubReaderActivity::renderScreen() {
renderer.clearScreen();
if (section->pageCount == 0) {
Serial.printf("[%lu] [ERS] No pages to render\n", millis());
if (Serial) Serial.printf("[%lu] [ERS] No pages to render\n", millis());
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Empty chapter", true, EpdFontFamily::BOLD);
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
renderer.displayBuffer();
@@ -724,7 +736,9 @@ void EpubReaderActivity::renderScreen() {
}
if (section->currentPage < 0 || section->currentPage >= section->pageCount) {
Serial.printf("[%lu] [ERS] Page out of bounds: %d (max %d)\n", millis(), section->currentPage, section->pageCount);
if (Serial)
Serial.printf("[%lu] [ERS] Page out of bounds: %d (max %d)\n", millis(), section->currentPage,
section->pageCount);
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Out of bounds", true, EpdFontFamily::BOLD);
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
renderer.displayBuffer();
@@ -734,25 +748,25 @@ void EpubReaderActivity::renderScreen() {
{
auto p = section->loadPageFromSectionFile();
if (!p) {
Serial.printf("[%lu] [ERS] Failed to load page from SD - clearing section cache\n", millis());
if (Serial) Serial.printf("[%lu] [ERS] Failed to load page from SD - clearing section cache\n", millis());
section->clearCache();
section.reset();
return renderScreen();
}
// Handle empty pages (e.g., from malformed chapters that couldn't be parsed)
if (p->elements.empty()) {
Serial.printf("[%lu] [ERS] Page has no content (possibly malformed chapter)\n", millis());
if (Serial) Serial.printf("[%lu] [ERS] Page has no content (possibly malformed chapter)\n", millis());
renderer.drawCenteredText(UI_12_FONT_ID, 280, "Chapter content unavailable", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, "(File may be malformed)");
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
renderer.displayBuffer();
return;
}
const auto start = millis();
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start);
if (Serial) Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start);
}
// Save progress with content offset for position restoration after re-indexing
@@ -760,16 +774,17 @@ void EpubReaderActivity::renderScreen() {
if (SdMan.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) {
// Get content offset for current page
const uint32_t contentOffset = section->getContentOffsetForPage(section->currentPage);
// New format: version (1) + spineIndex (2) + pageNumber (2) + contentOffset (4) = 9 bytes
serialization::writePod(f, EPUB_PROGRESS_VERSION);
serialization::writePod(f, static_cast<uint16_t>(currentSpineIndex));
serialization::writePod(f, static_cast<uint16_t>(section->currentPage));
serialization::writePod(f, contentOffset);
f.close();
Serial.printf("[%lu] [ERS] Saved progress: spine %d, page %d, offset %u\n",
millis(), currentSpineIndex, section->currentPage, contentOffset);
if (Serial)
Serial.printf("[%lu] [ERS] Saved progress: spine %d, page %d, offset %u\n", millis(), currentSpineIndex,
section->currentPage, contentOffset);
}
}
@@ -777,7 +792,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
const int orientedMarginRight, const int orientedMarginBottom,
const int orientedMarginLeft) {
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
// Draw bookmark indicator (folded corner) if this page is bookmarked
if (section) {
const uint32_t contentOffset = section->getContentOffsetForPage(section->currentPage);
@@ -787,14 +802,14 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
constexpr int cornerSize = 20;
const int cornerX = screenWidth - orientedMarginRight - cornerSize;
const int cornerY = orientedMarginTop;
// Draw triangle (folded corner effect)
const int xPoints[3] = {cornerX, cornerX + cornerSize, cornerX + cornerSize};
const int yPoints[3] = {cornerY, cornerY, cornerY + cornerSize};
renderer.fillPolygon(xPoints, yPoints, 3, true); // Black triangle
}
}
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);

View File

@@ -24,7 +24,7 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
// End-of-book prompt state
bool showingEndOfBookPrompt = false;
int endOfBookSelection = 2; // 0=Archive, 1=Delete, 2=Keep (default to safe option)
// Content offset for position restoration after re-indexing
uint32_t savedContentOffset = 0;
bool hasContentOffset = false; // True if we have a valid content offset to use

View File

@@ -160,12 +160,13 @@ void EpubReaderChapterSelectionActivity::renderScreen() {
const int bezelLeft = renderer.getBezelOffsetLeft();
const int bezelRight = renderer.getBezelOffsetRight();
const std::string title =
renderer.truncatedText(UI_12_FONT_ID, epub->getTitle().c_str(), pageWidth - 40 - bezelLeft - bezelRight, EpdFontFamily::BOLD);
const std::string title = renderer.truncatedText(UI_12_FONT_ID, epub->getTitle().c_str(),
pageWidth - 40 - bezelLeft - bezelRight, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, title.c_str(), true, EpdFontFamily::BOLD);
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
renderer.fillRect(bezelLeft, 60 + bezelTop + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1 - bezelLeft - bezelRight, 30);
renderer.fillRect(bezelLeft, 60 + bezelTop + (selectorIndex % pageItems) * 30 - 2,
pageWidth - 1 - bezelLeft - bezelRight, 30);
for (int itemIndex = pageStartIndex; itemIndex < totalItems && itemIndex < pageStartIndex + pageItems; itemIndex++) {
const int displayY = 60 + bezelTop + (itemIndex % pageItems) * 30;
@@ -175,8 +176,8 @@ void EpubReaderChapterSelectionActivity::renderScreen() {
const int tocIndex = tocIndexFromItemIndex(itemIndex);
auto item = epub->getTocItem(tocIndex);
const int indentSize = 20 + bezelLeft + (item.level - 1) * 15;
const std::string chapterName =
renderer.truncatedText(UI_10_FONT_ID, item.title.c_str(), pageWidth - 40 - bezelLeft - bezelRight - indentSize + 20 + bezelLeft);
const std::string chapterName = renderer.truncatedText(
UI_10_FONT_ID, item.title.c_str(), pageWidth - 40 - bezelLeft - bezelRight - indentSize + 20 + bezelLeft);
renderer.drawText(UI_10_FONT_ID, indentSize, displayY, chapterName.c_str(), !isSelected);
}

View File

@@ -62,11 +62,8 @@ void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) {
currentBookPath = epubPath;
exitActivity();
enterNewActivity(new EpubReaderActivity(
renderer, mappedInput, std::move(epub),
[this, epubPath] { goToLibrary(epubPath); },
[this] { onGoBack(); },
onGoToClearCache,
onGoToSettings));
renderer, mappedInput, std::move(epub), [this, epubPath] { goToLibrary(epubPath); }, [this] { onGoBack(); },
onGoToClearCache, onGoToSettings));
}
void ReaderActivity::onGoToTxtReader(std::unique_ptr<Txt> txt) {

View File

@@ -658,15 +658,15 @@ void TxtReaderActivity::saveProgress() const {
if (SdMan.openFileForWrite("TRS", txt->getCachePath() + "/progress.bin", f)) {
// New format: version + byte offset + page number (for backwards compatibility debugging)
serialization::writePod(f, PROGRESS_VERSION);
// Store byte offset - this is stable across font/setting changes
const size_t byteOffset = (currentPage >= 0 && currentPage < static_cast<int>(pageOffsets.size()))
? pageOffsets[currentPage] : 0;
const size_t byteOffset =
(currentPage >= 0 && currentPage < static_cast<int>(pageOffsets.size())) ? pageOffsets[currentPage] : 0;
serialization::writePod(f, static_cast<uint32_t>(byteOffset));
// Also store page number for debugging/logging purposes
serialization::writePod(f, static_cast<uint16_t>(currentPage));
f.close();
Serial.printf("[%lu] [TRS] Saved progress: page %d, offset %zu\n", millis(), currentPage, byteOffset);
}
@@ -677,24 +677,24 @@ void TxtReaderActivity::loadProgress() {
if (SdMan.openFileForRead("TRS", txt->getCachePath() + "/progress.bin", f)) {
// Check file size to determine format
const size_t fileSize = f.size();
if (fileSize >= 7) {
// New format: version (1) + byte offset (4) + page number (2) = 7 bytes
uint8_t version;
serialization::readPod(f, version);
if (version == PROGRESS_VERSION) {
uint32_t savedOffset;
serialization::readPod(f, savedOffset);
uint16_t savedPage;
serialization::readPod(f, savedPage);
// Use byte offset to find the correct page (works even if re-indexed)
currentPage = findPageForOffset(savedOffset);
Serial.printf("[%lu] [TRS] Loaded progress: offset %u -> page %d/%d (was page %d)\n",
millis(), savedOffset, currentPage, totalPages, savedPage);
Serial.printf("[%lu] [TRS] Loaded progress: offset %u -> page %d/%d (was page %d)\n", millis(), savedOffset,
currentPage, totalPages, savedPage);
} else {
// Unknown version, fall back to legacy behavior
Serial.printf("[%lu] [TRS] Unknown progress version %d, ignoring\n", millis(), version);
@@ -708,7 +708,7 @@ void TxtReaderActivity::loadProgress() {
Serial.printf("[%lu] [TRS] Loaded legacy progress: page %d/%d\n", millis(), currentPage, totalPages);
}
}
// Bounds check
if (currentPage >= totalPages) {
currentPage = totalPages - 1;
@@ -716,7 +716,7 @@ void TxtReaderActivity::loadProgress() {
if (currentPage < 0) {
currentPage = 0;
}
f.close();
}
}
@@ -725,16 +725,16 @@ int TxtReaderActivity::findPageForOffset(size_t targetOffset) const {
if (pageOffsets.empty()) {
return 0;
}
// Binary search: find the largest offset that is <= targetOffset
// This finds the page that contains or starts at the target offset
auto it = std::upper_bound(pageOffsets.begin(), pageOffsets.end(), targetOffset);
if (it == pageOffsets.begin()) {
// Target is before the first page, return page 0
return 0;
}
// upper_bound returns iterator to first element > targetOffset
// So we need the element before it (which is <= targetOffset)
return static_cast<int>(std::distance(pageOffsets.begin(), it) - 1);