diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index d620c314..2b0c5202 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -343,10 +343,13 @@ void setup() { if (SD.begin(SDCARD_CS, displaySpi, 4000000)) { MESH_DEBUG_PRINTLN("setup() - SD card initialized"); - // Tell the text reader that SD is ready + // Tell the text reader that SD is ready, then pre-index books at boot TextReaderScreen* reader = (TextReaderScreen*)ui_task.getTextReaderScreen(); if (reader) { reader->setSDReady(true); + if (disp) { + reader->bootIndex(*disp); + } } } else { MESH_DEBUG_PRINTLN("setup() - SD card initialization failed!"); diff --git a/examples/companion_radio/ui-new/Textreaderscreen.h b/examples/companion_radio/ui-new/Textreaderscreen.h index a5669a57..01b55b2f 100644 --- a/examples/companion_radio/ui-new/Textreaderscreen.h +++ b/examples/companion_radio/ui-new/Textreaderscreen.h @@ -163,7 +163,9 @@ private: UITask* _task; Mode _mode; bool _sdReady; - bool _initialized; // Layout metrics calculated + bool _initialized; // Layout metrics calculated + bool _bootIndexed; // Boot-time pre-indexing done + DisplayDriver* _display; // Stored reference for splash screens // Display layout (calculated once from display metrics) int _charsPerLine; @@ -190,6 +192,138 @@ private: int _pageBufLen; bool _contentDirty; // Need to re-read from SD + // ---- Splash Screen Drawing ---- + // Draw directly to display outside the normal render cycle. + // Matches the style of the standalone text reader firmware splash. + + // Word-wrapping splash for opening a large book. + // Shows: "Indexing / Pages..." (large), word-wrapped filename, "Please wait. / Loading shortly..." + void drawIndexingSplash(const String& filename) { + if (!_display) return; + _display->startFrame(); + + // "Indexing" / "Pages..." in large text + // Original: textSize(2) at real (20, 40) and (20, 65) + // Virtual: (10, 11) and (10, 21) + _display->setTextSize(2); + _display->setColor(DisplayDriver::GREEN); + _display->setCursor(10, 11); + _display->print("Indexing"); + _display->setCursor(10, 21); + _display->print("Pages..."); + + // Word-wrapped filename in normal text + _display->setTextSize(1); + _display->setColor(DisplayDriver::LIGHT); + int y = 39; + int leftMargin = 10; + // Calculate max chars that fit: (display_width - margin) / char_width + uint16_t charW = _display->getTextWidth("M"); + int maxChars = charW > 0 ? (_display->width() - leftMargin) / charW : 20; + if (maxChars < 10) maxChars = 10; + if (maxChars > 40) maxChars = 40; + String remaining = filename; + + while (remaining.length() > 0 && y < 80) { + String line; + if ((int)remaining.length() <= maxChars) { + line = remaining; + remaining = ""; + } else { + int breakPoint = maxChars; + for (int i = maxChars; i > 0; i--) { + if (remaining[i] == ' ' || remaining[i] == '-' || remaining[i] == '_') { + breakPoint = i; + break; + } + } + line = remaining.substring(0, breakPoint); + remaining = remaining.substring(breakPoint); + remaining.trim(); + } + _display->setCursor(10, y); + _display->print(line.c_str()); + y += 5; + } + + // "Please wait." / "Loading shortly..." + // Original: textSize(1) at real (20, 230) and (20, 245) + // Virtual: y=87 and y=93 + _display->setColor(DisplayDriver::LIGHT); + _display->setCursor(10, 87); + _display->print("Please wait."); + _display->setCursor(10, 93); + _display->print("Loading shortly..."); + + _display->endFrame(); + } + + // Boot-time progress splash with file counter. + // Shows: "Indexing / Pages..." (large), "(2/10)", word-wrapped filename, "Please wait." + // If current==0 and total==0, skips the progress counter (used for initial scan splash). + void drawBootSplash(int current, int total, const String& filename) { + if (!_display) return; + _display->startFrame(); + + // "Indexing" / "Pages..." in large text + _display->setTextSize(2); + _display->setColor(DisplayDriver::GREEN); + _display->setCursor(10, 11); + _display->print("Indexing"); + _display->setCursor(10, 21); + _display->print("Pages..."); + + _display->setTextSize(1); + _display->setColor(DisplayDriver::LIGHT); + + int y = 35; + + // Progress counter (skip if both zero) + if (current > 0 || total > 0) { + char progress[20]; + sprintf(progress, "(%d/%d)", current, total); + _display->setCursor(10, y); + _display->print(progress); + y += 8; + } + + // Word-wrapped filename + int leftMargin = 10; + uint16_t charW = _display->getTextWidth("M"); + int maxChars = charW > 0 ? (_display->width() - leftMargin) / charW : 20; + if (maxChars < 10) maxChars = 10; + if (maxChars > 40) maxChars = 40; + String remaining = filename; + + while (remaining.length() > 0 && y < 80) { + String line; + if ((int)remaining.length() <= maxChars) { + line = remaining; + remaining = ""; + } else { + int breakPoint = maxChars; + for (int i = maxChars; i > 0; i--) { + if (remaining[i] == ' ' || remaining[i] == '-' || remaining[i] == '_') { + breakPoint = i; + break; + } + } + line = remaining.substring(0, breakPoint); + remaining = remaining.substring(breakPoint); + remaining.trim(); + } + _display->setCursor(10, y); + _display->print(line.c_str()); + y += 5; + } + + // "Please wait." + _display->setCursor(10, 87); + _display->print("Please wait."); + + _display->endFrame(); + } + // ---- SD Index I/O ---- String getIndexPath(const String& filename) { @@ -333,45 +467,6 @@ private: Serial.printf("TextReader: Found %d files\n", _fileList.size()); } - void buildIndexes() { - _fileCache.clear(); - for (int i = 0; i < (int)_fileList.size(); i++) { - FileCache cache; - if (loadIndex(_fileList[i], cache)) { - Serial.printf("TextReader: %s - loaded %d pages (resume pg %d)\n", - _fileList[i].c_str(), cache.pagePositions.size(), - cache.lastReadPage + 1); - _fileCache.push_back(cache); - continue; - } - - // Build new index - String fullPath = String(BOOKS_FOLDER) + "/" + _fileList[i]; - File file = SD.open(fullPath.c_str(), FILE_READ); - if (!file) continue; - - cache.filename = _fileList[i]; - cache.fileSize = file.size(); - cache.fullyIndexed = false; - cache.lastReadPage = 0; - cache.pagePositions.push_back(0); - - int added = indexPagesWordWrap(file, 0, cache.pagePositions, - _linesPerPage, _charsPerLine, - PREINDEX_PAGES - 1); - cache.fullyIndexed = !file.available(); - file.close(); - - saveIndex(cache.filename, cache.pagePositions, cache.fileSize, - cache.fullyIndexed, 0); - _fileCache.push_back(cache); - - Serial.printf("TextReader: %s - indexed %d pages%s\n", - _fileList[i].c_str(), (int)cache.pagePositions.size(), - cache.fullyIndexed ? " (complete)" : ""); - } - } - // ---- Book Open/Close ---- void openBook(const String& filename) { @@ -406,18 +501,31 @@ private: _currentPage = cache->lastReadPage; } + // Already fully indexed - open immediately if (cache->fullyIndexed) { _totalPages = _pagePositions.size(); _mode = READING; loadPageContent(); + Serial.printf("TextReader: Opened %s, %d pages, resume pg %d\n", + filename.c_str(), _totalPages, _currentPage + 1); return; } - // Continue indexing from cache + // Partially indexed - show splash and finish indexing + Serial.printf("TextReader: Finishing index for %s (have %d pages so far)\n", + filename.c_str(), (int)_pagePositions.size()); + + drawIndexingSplash(filename); + long lastPos = cache->pagePositions.back(); indexPagesWordWrap(_file, lastPos, _pagePositions, _linesPerPage, _charsPerLine, 0); } else { + // No cache at all - full index from scratch with splash + Serial.printf("TextReader: Full index for %s\n", filename.c_str()); + + drawIndexingSplash(filename); + _pagePositions.push_back(0); indexPagesWordWrap(_file, 0, _pagePositions, _linesPerPage, _charsPerLine, 0); @@ -465,8 +573,8 @@ private: } // ---- Page Content Loading ---- - // FIX: Read exact span between indexed page positions instead of guessing. - // This ensures the renderer gets exactly the bytes the indexer counted. + // Read exact span between indexed page positions so renderer gets + // exactly the bytes the indexer counted for this page. void loadPageContent() { if (!_fileOpen || _currentPage >= _totalPages) { @@ -491,9 +599,6 @@ private: _pageBuf[_pageBufLen] = '\0'; _contentDirty = false; - Serial.printf("TextReader: Page %d/%d, filePos=%ld, span=%ld, read=%d bytes\n", - _currentPage + 1, _totalPages, pageStart, pageSpan, _pageBufLen); - // Deselect SD to free SPI bus for display digitalWrite(SDCARD_CS, HIGH); } @@ -541,8 +646,8 @@ private: if (selected) { display.setColor(DisplayDriver::LIGHT); - // FIX: setCursor adds +5 to y internally, but fillRect does not. - // So we offset fillRect by +5 to align the highlight bar with the text. + // setCursor adds +5 to y internally, but fillRect does not. + // Offset fillRect by +5 to align highlight bar with text. display.fillRect(0, y + 5, display.width(), listLineH); display.setColor(DisplayDriver::DARK); } else { @@ -649,6 +754,7 @@ private: public: TextReaderScreen(UITask* task) : _task(task), _mode(FILE_LIST), _sdReady(false), _initialized(false), + _bootIndexed(false), _display(nullptr), _charsPerLine(38), _linesPerPage(22), _lineHeight(5), _headerHeight(14), _footerHeight(14), _selectedFile(0), _fileOpen(false), _currentPage(0), _totalPages(0), @@ -659,6 +765,9 @@ public: void initLayout(DisplayDriver& display) { if (_initialized) return; + // Store display reference for splash screens during openBook + _display = &display; + // Measure tiny font metrics using the display driver display.setTextSize(0); @@ -672,13 +781,12 @@ public: // Line height for built-in 6x8 font: // setCursor adds +5 to y, so effective text top = (y+5)*scale_y - // The font is ~8px tall in real coords. In virtual coords: 8/scale_y ≈ 3.2 units - // We need inter-line spacing, so use measured char width to estimate: - // Built-in font: 6px wide, 8px tall → height ≈ width * 8/6 + // The font is ~8px tall in real coords. In virtual coords: 8/scale_y ~ 3.2 units + // We derive from measured char width since we can't measure height directly. + // Built-in font: 6px wide, 8px tall -> height ~ width * 7/6 // Then add ~20% for spacing uint16_t mWidth = display.getTextWidth("M"); if (mWidth > 0) { - // Use a 1.2x multiplier on estimated character height for line spacing _lineHeight = max(3, (int)((mWidth * 7 * 12) / (6 * 10))); } else { _lineHeight = 5; // Safe fallback @@ -698,22 +806,96 @@ public: _charsPerLine, _linesPerPage, _lineHeight, display.width(), display.height()); } - // Initialize SD card access. Call from main.cpp setup(). - // Returns true if SD is ready. + // ---- Boot-time Indexing ---- + // Called from setup() after SD card init. Scans files, pre-indexes first + // 100 pages of each, and shows progress on the e-ink display. + + void bootIndex(DisplayDriver& display) { + if (!_sdReady) return; + + // Calculate layout metrics first (needed for indexing) + initLayout(display); + + // Show initial splash + drawBootSplash(0, 0, "Scanning..."); + Serial.println("TextReader: Boot indexing started"); + + // Scan for files + scanFiles(); + + if (_fileList.size() == 0) { + Serial.println("TextReader: No files to index"); + _bootIndexed = true; + return; + } + + // Pre-index each file, showing progress + _fileCache.clear(); + for (int i = 0; i < (int)_fileList.size(); i++) { + // Draw progress splash + drawBootSplash(i + 1, (int)_fileList.size(), _fileList[i]); + + FileCache cache; + if (loadIndex(_fileList[i], cache)) { + Serial.printf("TextReader: %s - loaded %d pages (resume pg %d)\n", + _fileList[i].c_str(), cache.pagePositions.size(), + cache.lastReadPage + 1); + _fileCache.push_back(cache); + continue; + } + + // Build new index (first 100 pages only) + String fullPath = String(BOOKS_FOLDER) + "/" + _fileList[i]; + File file = SD.open(fullPath.c_str(), FILE_READ); + if (!file) continue; + + cache.filename = _fileList[i]; + cache.fileSize = file.size(); + cache.fullyIndexed = false; + cache.lastReadPage = 0; + cache.pagePositions.push_back(0); + + int added = indexPagesWordWrap(file, 0, cache.pagePositions, + _linesPerPage, _charsPerLine, + PREINDEX_PAGES - 1); + cache.fullyIndexed = !file.available(); + file.close(); + + saveIndex(cache.filename, cache.pagePositions, cache.fileSize, + cache.fullyIndexed, 0); + _fileCache.push_back(cache); + + Serial.printf("TextReader: %s - indexed %d pages%s\n", + _fileList[i].c_str(), (int)cache.pagePositions.size(), + cache.fullyIndexed ? " (complete)" : ""); + } + + // Deselect SD to free SPI bus + digitalWrite(SDCARD_CS, HIGH); + + _bootIndexed = true; + Serial.printf("TextReader: Boot indexing complete, %d files\n", (int)_fileList.size()); + } + + // ---- Public Interface ---- + void setSDReady(bool ready) { _sdReady = ready; } bool isSDReady() const { return _sdReady; } - // Called when entering the reader screen + // Called when entering the reader screen (press R). + // If boot indexing already ran, this is lightweight. void enter(DisplayDriver& display) { initLayout(display); - if (_sdReady && !_fileOpen) { - scanFiles(); - buildIndexes(); - // Deselect SD to free SPI bus for display rendering - digitalWrite(SDCARD_CS, HIGH); + + if (_sdReady && !_bootIndexed) { + // Boot indexing didn't run (shouldn't happen, but safety fallback) + bootIndex(display); + } + + if (!_fileOpen) { _selectedFile = 0; _mode = FILE_LIST; - } else if (_fileOpen) { + } else { _mode = READING; loadPageContent(); }