diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index b8a5d88d..9e33d5f0 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -268,10 +268,18 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no if (file.read((uint8_t *)&_prefs.auto_lock_minutes, sizeof(_prefs.auto_lock_minutes)) != sizeof(_prefs.auto_lock_minutes)) { _prefs.auto_lock_minutes = 0; // default: disabled } + if (file.read((uint8_t *)&_prefs.hint_shown, sizeof(_prefs.hint_shown)) != sizeof(_prefs.hint_shown)) { + _prefs.hint_shown = 0; // default: show boot hint + } + if (file.read((uint8_t *)&_prefs.large_font, sizeof(_prefs.large_font)) != sizeof(_prefs.large_font)) { + _prefs.large_font = 0; // default: tiny font + } // Clamp to valid ranges if (_prefs.dark_mode > 1) _prefs.dark_mode = 0; if (_prefs.portrait_mode > 1) _prefs.portrait_mode = 0; + if (_prefs.hint_shown > 1) _prefs.hint_shown = 0; + if (_prefs.large_font > 1) _prefs.large_font = 0; // auto_lock_minutes: only accept known options (0, 2, 5, 10, 15, 30) { uint8_t alm = _prefs.auto_lock_minutes; @@ -324,6 +332,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.dark_mode, sizeof(_prefs.dark_mode)); // 98 file.write((uint8_t *)&_prefs.portrait_mode, sizeof(_prefs.portrait_mode)); // 99 file.write((uint8_t *)&_prefs.auto_lock_minutes, sizeof(_prefs.auto_lock_minutes)); // 100 + file.write((uint8_t *)&_prefs.hint_shown, sizeof(_prefs.hint_shown)); // 101 + file.write((uint8_t *)&_prefs.large_font, sizeof(_prefs.large_font)); // 102 file.close(); } diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index a985f439..55ae1f8f 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -39,4 +39,39 @@ struct NodePrefs { // persisted to file uint8_t portrait_mode; // 0=landscape, 1=portrait — T5S3 only uint8_t auto_lock_minutes; // 0=disabled, 2/5/10/15/30=auto-lock after idle uint8_t hint_shown; // 0=show nav hint on boot, 1=already shown (dismiss permanently) + uint8_t large_font; // 0=tiny (built-in 6x8), 1=larger (FreeSans9pt) — T-Deck Pro only + + // --- Font helpers (inline, no overhead) --- + // Returns the DisplayDriver text-size index for "small/body" text. + // T-Deck Pro: 0 = built-in 6×8, 1 = FreeSans9pt. + // T5S3: both 0 and 1 are 12pt fonts (regular vs bold) with identical line + // height, so large_font has no layout effect there. + inline uint8_t smallTextSize() const { + return large_font ? 1 : 0; + } + + // Returns the virtual-coordinate line height matching smallTextSize(). + // T-Deck Pro size 0 → 9 (6×8 + 1px gap), size 1 → 11 (9pt ascent+descent). + // T5S3 size 0/1 → same 12pt height → always 9 in virtual coords. + inline int smallLineH() const { +#if defined(LilyGo_T5S3_EPaper_Pro) + return 9; +#else + return large_font ? 11 : 9; +#endif + } + + // Returns the Y offset for selection highlight fillRect (T-Deck Pro only). + // Size 0 (built-in font): cursor positions at top-left, +5 offset in + // setCursor places text below → fillRect at y+5 aligns with text. + // Size 1 (FreeSans9pt): cursor positions at baseline, ascenders render + // upward → fillRect must start above baseline to cover ascenders. + // T5S3: always 0 (both sizes use baseline fonts with highlight at y). + inline int smallHighlightOff() const { +#if defined(LilyGo_T5S3_EPaper_Pro) + return 0; +#else + return large_font ? -2 : 5; +#endif + } }; \ No newline at end of file diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index c8acd4e7..b3c3753d 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -2701,9 +2701,28 @@ void handleKeyboardInput() { // A/D keys switch channels (only when buffer is empty, not in DM mode) if ((key == 'a') && composePos == 0 && !composeDM) { - // Previous channel + // Previous channel — skip gaps if (composeChannelIdx > 0) { - composeChannelIdx--; + bool found = false; + for (uint8_t prev = composeChannelIdx - 1; ; prev--) { + ChannelDetails ch; + if (the_mesh.getChannel(prev, ch) && ch.name[0] != '\0') { + composeChannelIdx = prev; + found = true; + break; + } + if (prev == 0) break; + } + if (!found) { + // Wrap to last valid channel + for (uint8_t i = MAX_GROUP_CHANNELS - 1; i > 0; i--) { + ChannelDetails ch; + if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') { + composeChannelIdx = i; + break; + } + } + } } else { // Wrap to last valid channel for (uint8_t i = MAX_GROUP_CHANNELS - 1; i > 0; i--) { @@ -2720,12 +2739,17 @@ void handleKeyboardInput() { } if ((key == 'd') && composePos == 0 && !composeDM) { - // Next channel - ChannelDetails ch; - uint8_t nextIdx = composeChannelIdx + 1; - if (the_mesh.getChannel(nextIdx, ch) && ch.name[0] != '\0') { - composeChannelIdx = nextIdx; - } else { + // Next channel — skip gaps + bool found = false; + for (uint8_t next = composeChannelIdx + 1; next < MAX_GROUP_CHANNELS; next++) { + ChannelDetails ch; + if (the_mesh.getChannel(next, ch) && ch.name[0] != '\0') { + composeChannelIdx = next; + found = true; + break; + } + } + if (!found) { composeChannelIdx = 0; // Wrap to first channel } Serial.printf("Compose: Channel switched to %d\n", composeChannelIdx); @@ -3173,7 +3197,7 @@ void handleKeyboardInput() { Serial.printf("Audiobook: lazy init - free heap: %d, largest block: %d\n", ESP.getFreeHeap(), ESP.getMaxAllocHeap()); audio = new Audio(); - AudiobookPlayerScreen* abScreen = new AudiobookPlayerScreen(&ui_task, audio); + AudiobookPlayerScreen* abScreen = new AudiobookPlayerScreen(&ui_task, audio, the_mesh.getNodePrefs()); abScreen->setSDReady(sdCardReady); ui_task.setAudiobookScreen(abScreen); Serial.printf("Audiobook: init complete - free heap: %d\n", ESP.getFreeHeap()); diff --git a/examples/companion_radio/ui-new/Audiobookplayerscreen.h b/examples/companion_radio/ui-new/Audiobookplayerscreen.h index f34513cf..7fe2d05b 100644 --- a/examples/companion_radio/ui-new/Audiobookplayerscreen.h +++ b/examples/companion_radio/ui-new/Audiobookplayerscreen.h @@ -43,6 +43,8 @@ // JPEG decoder for cover art — JPEGDEC by bitbank2 #include +#include "../NodePrefs.h" + // Forward declarations class UITask; @@ -151,6 +153,7 @@ public: private: UITask* _task; + NodePrefs* _prefs; Audio* _audio; Mode _mode; bool _sdReady; @@ -1193,10 +1196,10 @@ private: } // Switch to tiny font for file list (6x8 built-in) - display.setTextSize(0); + display.setTextSize(_prefs ? _prefs->smallTextSize() : 0); - // Calculate visible items — tiny font uses ~8 virtual units per line - int itemHeight = 8; + // Calculate visible items + int itemHeight = (_prefs ? _prefs->smallLineH() : 9) - 1; int listTop = 13; int listBottom = display.height() - 14; // Reserve footer space int visibleItems = (listBottom - listTop) / itemHeight; @@ -1208,7 +1211,7 @@ private: _scrollOffset = _selectedFile - visibleItems + 1; } - // Approx chars that fit in tiny font (~36 on 128 virtual width) + // Approx chars for suffix/type tag sizing (still needed for type tag assembly) const int charsPerLine = 36; // Draw file list @@ -1218,9 +1221,7 @@ private: if (fileIdx == _selectedFile) { display.setColor(DisplayDriver::LIGHT); - // 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(), itemHeight - 1); + display.fillRect(0, y + (_prefs ? _prefs->smallHighlightOff() : 5), display.width(), itemHeight - 1); display.setColor(DisplayDriver::DARK); } else { display.setColor(DisplayDriver::LIGHT); @@ -1231,29 +1232,15 @@ private: char fullLine[96]; if (fe.isDir) { - // Directory entry: show as "/ FolderName" or just ".." if (fe.name == "..") { snprintf(fullLine, sizeof(fullLine), ".. (up)"); } else { snprintf(fullLine, sizeof(fullLine), "/%s", fe.name.c_str()); - // Truncate if needed - if ((int)strlen(fullLine) > charsPerLine - 1) { - fullLine[charsPerLine - 4] = '.'; - fullLine[charsPerLine - 3] = '.'; - fullLine[charsPerLine - 2] = '.'; - fullLine[charsPerLine - 1] = '\0'; - } } } else { // Audio file: "Title - Author [TYPE]" char lineBuf[80]; - // Reserve space for type tag and bookmark indicator - int suffixLen = fe.fileType.length() + 3; // " [M4B]" or " [MP3]" - int bmkLen = fe.hasBookmark ? 2 : 0; // " >" - int availChars = charsPerLine - suffixLen - bmkLen; - if (availChars < 10) availChars = 10; - if (fe.displayAuthor.length() > 0) { snprintf(lineBuf, sizeof(lineBuf), "%s - %s", fe.displayTitle.c_str(), fe.displayAuthor.c_str()); @@ -1261,24 +1248,13 @@ private: snprintf(lineBuf, sizeof(lineBuf), "%s", fe.displayTitle.c_str()); } - // Truncate with ellipsis if needed - if ((int)strlen(lineBuf) > availChars) { - if (availChars > 3) { - lineBuf[availChars - 3] = '.'; - lineBuf[availChars - 2] = '.'; - lineBuf[availChars - 1] = '.'; - lineBuf[availChars] = '\0'; - } else { - lineBuf[availChars] = '\0'; - } - } - // Append file type tag snprintf(fullLine, sizeof(fullLine), "%s [%s]", lineBuf, fe.fileType.c_str()); } - display.setCursor(2, y); - display.print(fullLine); + // Pixel-aware ellipsis — reserve space for bookmark indicator + int reserveRight = (!fe.isDir && fe.hasBookmark) ? 10 : 2; + display.drawTextEllipsized(2, y, display.width() - reserveRight, fullLine); // Bookmark indicator (right-aligned, files only) if (!fe.isDir && fe.hasBookmark) { @@ -1464,8 +1440,8 @@ private: } public: - AudiobookPlayerScreen(UITask* task, Audio* audio) - : _task(task), _audio(audio), _mode(FILE_LIST), + AudiobookPlayerScreen(UITask* task, Audio* audio, NodePrefs* prefs = nullptr) + : _task(task), _prefs(prefs), _audio(audio), _mode(FILE_LIST), _sdReady(false), _i2sInitialized(false), _dacPowered(false), _displayRef(nullptr), _selectedFile(0), _scrollOffset(0), diff --git a/examples/companion_radio/ui-new/ChannelScreen.h b/examples/companion_radio/ui-new/ChannelScreen.h index 9465978b..5772e64e 100644 --- a/examples/companion_radio/ui-new/ChannelScreen.h +++ b/examples/companion_radio/ui-new/ChannelScreen.h @@ -637,8 +637,8 @@ public: } // Render inbox list - display.setTextSize(0); - int lineH = 9; + display.setTextSize(the_mesh.getNodePrefs()->smallTextSize()); + int lineH = the_mesh.getNodePrefs()->smallLineH(); int headerH = 14; int footerH = 14; int maxY = display.height() - footerH; @@ -672,7 +672,7 @@ public: #if defined(LilyGo_T5S3_EPaper_Pro) display.fillRect(0, y, display.width(), lineH); #else - display.fillRect(0, y + 5, display.width(), lineH); + display.fillRect(0, y + the_mesh.getNodePrefs()->smallHighlightOff(), display.width(), lineH); #endif display.setColor(DisplayDriver::DARK); } else { @@ -745,8 +745,8 @@ public: // --- Path detail overlay --- if (_showPathOverlay) { - display.setTextSize(0); - int lineH = 9; + display.setTextSize(the_mesh.getNodePrefs()->smallTextSize()); + int lineH = the_mesh.getNodePrefs()->smallLineH(); int y = 14; ChannelMessage* msg = getNewestReceivedMsg(); @@ -942,7 +942,7 @@ public: } if (channelMsgCount == 0) { - display.setTextSize(0); // Tiny font for body text + display.setTextSize(the_mesh.getNodePrefs()->smallTextSize()); // Tiny font for body text display.setCursor(0, 20); display.setColor(DisplayDriver::LIGHT); if (_viewChannelIdx == 0xFF) { @@ -975,8 +975,8 @@ public: // ================================================================= // DM Inbox: list of contacts/rooms you have DM history with // ================================================================= - display.setTextSize(0); - int lineHeight = 9; + display.setTextSize(the_mesh.getNodePrefs()->smallTextSize()); + int lineHeight = the_mesh.getNodePrefs()->smallLineH(); int headerHeight = 14; int footerHeight = 14; int maxY = display.height() - footerHeight; @@ -1056,7 +1056,7 @@ public: #if defined(LilyGo_T5S3_EPaper_Pro) display.fillRect(0, y, display.width(), lineHeight); #else - display.fillRect(0, y + 5, display.width(), lineHeight); + display.fillRect(0, y + the_mesh.getNodePrefs()->smallHighlightOff(), display.width(), lineHeight); #endif display.setColor(DisplayDriver::DARK); } else { @@ -1094,8 +1094,8 @@ public: } display.setTextSize(1); } else { - display.setTextSize(0); // Tiny font for message body - int lineHeight = 9; // 8px font + 1px spacing + display.setTextSize(the_mesh.getNodePrefs()->smallTextSize()); // Tiny font for message body + int lineHeight = the_mesh.getNodePrefs()->smallLineH(); // 8px font + 1px spacing int headerHeight = 14; int footerHeight = 14; int scrollBarW = 4; // Width of scroll indicator on right edge @@ -1163,7 +1163,7 @@ public: #if defined(LilyGo_T5S3_EPaper_Pro) display.fillRect(0, y, contentW, maxFillH); #else - display.fillRect(0, y + 5, contentW, maxFillH); + display.fillRect(0, y + the_mesh.getNodePrefs()->smallHighlightOff(), contentW, maxFillH); #endif } @@ -1324,7 +1324,7 @@ public: #if defined(LilyGo_T5S3_EPaper_Pro) display.fillRect(0, y, contentW, maxFillH - usedH); #else - display.fillRect(0, y + 5, contentW, maxFillH - usedH); + display.fillRect(0, y + the_mesh.getNodePrefs()->smallHighlightOff(), contentW, maxFillH - usedH); #endif } } @@ -1646,7 +1646,26 @@ public: } } } else if (_viewChannelIdx > 0) { - _viewChannelIdx--; + // Skip backwards over any empty/gap slots + uint8_t prev = _viewChannelIdx - 1; + bool found = false; + while (true) { + ChannelDetails ch; + if (the_mesh.getChannel(prev, ch) && ch.name[0] != '\0') { + _viewChannelIdx = prev; + found = true; + break; + } + if (prev == 0) break; + prev--; + } + if (!found) { + // No valid channel below → wrap to DM tab + _viewChannelIdx = 0xFF; + _dmInboxMode = true; + _dmInboxScroll = 0; + _dmFilterName[0] = '\0'; + } } else { // Channel 0 → wrap to DM tab _viewChannelIdx = 0xFF; @@ -1667,11 +1686,17 @@ public: // DM tab → wrap to channel 0 _viewChannelIdx = 0; } else { - ChannelDetails ch; - uint8_t nextIdx = _viewChannelIdx + 1; - if (the_mesh.getChannel(nextIdx, ch) && ch.name[0] != '\0') { - _viewChannelIdx = nextIdx; - } else { + // Skip forward over any empty/gap slots + bool found = false; + for (uint8_t next = _viewChannelIdx + 1; next < MAX_GROUP_CHANNELS; next++) { + ChannelDetails ch; + if (the_mesh.getChannel(next, ch) && ch.name[0] != '\0') { + _viewChannelIdx = next; + found = true; + break; + } + } + if (!found) { // Past last channel → go to DM tab _viewChannelIdx = 0xFF; _dmInboxMode = true; diff --git a/examples/companion_radio/ui-new/Contactsscreen.h b/examples/companion_radio/ui-new/Contactsscreen.h index 6b791470..50b62f78 100644 --- a/examples/companion_radio/ui-new/Contactsscreen.h +++ b/examples/companion_radio/ui-new/Contactsscreen.h @@ -162,11 +162,11 @@ public: // Returns: 0=miss, 1=moved, 2=tapped current row. int selectRowAtVY(int vy) { if (_filteredCount == 0) return 0; - const int headerH = 14, footerH = 14, lineH = 9; + const int headerH = 14, footerH = 14, lineH = the_mesh.getNodePrefs()->smallLineH(); #if defined(LilyGo_T5S3_EPaper_Pro) const int bodyTop = headerH; #else - const int bodyTop = headerH + 5; + const int bodyTop = headerH + the_mesh.getNodePrefs()->smallHighlightOff(); #endif if (vy < bodyTop || vy >= 128 - footerH) return 0; @@ -235,8 +235,8 @@ public: display.drawRect(0, 11, display.width(), 1); // === Body - contact rows === - display.setTextSize(0); // tiny font for compact rows - int lineHeight = 9; // 8px font + 1px gap + display.setTextSize(the_mesh.getNodePrefs()->smallTextSize()); // tiny font for compact rows + int lineHeight = the_mesh.getNodePrefs()->smallLineH(); // 8px font + 1px gap int headerHeight = 14; int footerHeight = 14; int maxY = display.height() - footerHeight; @@ -275,7 +275,7 @@ public: #if defined(LilyGo_T5S3_EPaper_Pro) display.fillRect(0, y, display.width(), lineHeight); #else - display.fillRect(0, y + 5, display.width(), lineHeight); + display.fillRect(0, y + the_mesh.getNodePrefs()->smallHighlightOff(), display.width(), lineHeight); #endif display.setColor(DisplayDriver::DARK); } else { diff --git a/examples/companion_radio/ui-new/Discoveryscreen.h b/examples/companion_radio/ui-new/Discoveryscreen.h index 1fc87e8d..b7c37e77 100644 --- a/examples/companion_radio/ui-new/Discoveryscreen.h +++ b/examples/companion_radio/ui-new/Discoveryscreen.h @@ -49,11 +49,11 @@ public: int selectRowAtVY(int vy) { int count = the_mesh.getDiscoveredCount(); if (count == 0) return 0; - const int headerH = 14, footerH = 14, lineH = 9; + const int headerH = 14, footerH = 14, lineH = the_mesh.getNodePrefs()->smallLineH(); #if defined(LilyGo_T5S3_EPaper_Pro) const int bodyTop = headerH; #else - const int bodyTop = headerH + 5; + const int bodyTop = headerH + the_mesh.getNodePrefs()->smallHighlightOff(); #endif if (vy < bodyTop || vy >= 128 - footerH) return 0; @@ -91,8 +91,8 @@ public: display.drawRect(0, 11, display.width(), 1); // === Body — discovered node rows === - display.setTextSize(0); // tiny font for compact rows - int lineHeight = 9; + display.setTextSize(the_mesh.getNodePrefs()->smallTextSize()); // tiny font for compact rows + int lineHeight = the_mesh.getNodePrefs()->smallLineH(); int headerHeight = 14; int footerHeight = 14; int maxY = display.height() - footerHeight; @@ -129,7 +129,7 @@ public: #if defined(LilyGo_T5S3_EPaper_Pro) display.fillRect(0, y, display.width(), lineHeight); #else - display.fillRect(0, y + 5, display.width(), lineHeight); + display.fillRect(0, y + the_mesh.getNodePrefs()->smallHighlightOff(), display.width(), lineHeight); #endif display.setColor(DisplayDriver::DARK); } else { diff --git a/examples/companion_radio/ui-new/Lastheardscreen.h b/examples/companion_radio/ui-new/Lastheardscreen.h index c6902931..1977e423 100644 --- a/examples/companion_radio/ui-new/Lastheardscreen.h +++ b/examples/companion_radio/ui-new/Lastheardscreen.h @@ -68,11 +68,11 @@ public: // Returns: 0=miss, 1=moved, 2=tapped current row. int selectRowAtVY(int vy) { if (_count == 0) return 0; - const int headerH = 14, footerH = 14, lineH = 9; + const int headerH = 14, footerH = 14, lineH = the_mesh.getNodePrefs()->smallLineH(); #if defined(LilyGo_T5S3_EPaper_Pro) const int bodyTop = headerH; #else - const int bodyTop = headerH + 5; + const int bodyTop = headerH + the_mesh.getNodePrefs()->smallHighlightOff(); #endif if (vy < bodyTop || vy >= 128 - footerH) return 0; @@ -117,8 +117,8 @@ public: display.drawRect(0, 11, display.width(), 1); // === Body — node rows === - display.setTextSize(0); - int lineHeight = 9; + display.setTextSize(the_mesh.getNodePrefs()->smallTextSize()); + int lineHeight = the_mesh.getNodePrefs()->smallLineH(); int headerHeight = 14; int footerHeight = 14; int maxY = display.height() - footerHeight; @@ -147,7 +147,7 @@ public: #if defined(LilyGo_T5S3_EPaper_Pro) display.fillRect(0, y, display.width(), lineHeight); #else - display.fillRect(0, y + 5, display.width(), lineHeight); + display.fillRect(0, y + the_mesh.getNodePrefs()->smallHighlightOff(), display.width(), lineHeight); #endif display.setColor(DisplayDriver::DARK); } else { diff --git a/examples/companion_radio/ui-new/Notesscreen.h b/examples/companion_radio/ui-new/Notesscreen.h index 05a62972..15e5d903 100644 --- a/examples/companion_radio/ui-new/Notesscreen.h +++ b/examples/companion_radio/ui-new/Notesscreen.h @@ -5,6 +5,7 @@ #include #include #include "Utf8CP437.h" +#include "../NodePrefs.h" // Forward declarations class UITask; @@ -52,9 +53,11 @@ public: private: UITask* _task; + NodePrefs* _prefs; Mode _mode; bool _sdReady; bool _initialized; + uint8_t _lastFontPref; DisplayDriver* _display; // Display layout (calculated once from display metrics) @@ -518,8 +521,8 @@ private: display.drawRect(0, 11, display.width(), 1); // File list with "+ New Note" at index 0 - display.setTextSize(0); - int listLineH = 9; // Match contacts/discovery for consistent selection highlight + display.setTextSize(_prefs->smallTextSize()); + int listLineH = _prefs->smallLineH(); int startY = 14; int totalItems = 1 + (int)_fileList.size(); int maxVisible = (display.height() - startY - _footerHeight) / listLineH; @@ -539,27 +542,21 @@ private: #if defined(LilyGo_T5S3_EPaper_Pro) display.fillRect(0, y, display.width(), listLineH); #else - display.fillRect(0, y + 5, display.width(), listLineH); + display.fillRect(0, y + _prefs->smallHighlightOff(), display.width(), listLineH); #endif display.setColor(DisplayDriver::DARK); } else { display.setColor(DisplayDriver::LIGHT); } - display.setCursor(0, y); - if (i == 0) { display.setColor(selected ? DisplayDriver::DARK : DisplayDriver::GREEN); - display.print(selected ? "> + New Note" : " + New Note"); + display.drawTextEllipsized(0, y, display.width() - 4, + selected ? "> + New Note" : " + New Note"); } else { String line = selected ? "> " : " "; - String name = _fileList[i - 1]; - int maxLen = _charsPerLine - 4; - if ((int)name.length() > maxLen) { - name = name.substring(0, maxLen - 3) + "..."; - } - line += name; - display.print(line.c_str()); + line += _fileList[i - 1]; + display.drawTextEllipsized(0, y, display.width() - 4, line.c_str()); } y += listLineH; } @@ -605,7 +602,7 @@ private: } // Render current page using tiny font - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.setColor(DisplayDriver::LIGHT); int pageStart = _pageOffsets[_currentPage]; @@ -722,7 +719,7 @@ private: int textAreaTop = 14; int textAreaBottom = display.height() - 16; - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); // Find cursor line int cursorLine = lineForPos(_cursorPos); @@ -771,7 +768,7 @@ private: // If buffer is empty, show cursor at top if (_bufLen == 0) { - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.setColor(DisplayDriver::GREEN); display.setCursor(0, textAreaTop); display.print("|"); @@ -829,7 +826,7 @@ private: display.setCursor(0, 20); display.setColor(DisplayDriver::LIGHT); display.print("From: "); - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); String origDisplay = _renameOriginal; if (origDisplay.length() > 30) origDisplay = origDisplay.substring(0, 27) + "..."; display.print(origDisplay.c_str()); @@ -840,7 +837,7 @@ private: display.setColor(DisplayDriver::LIGHT); display.print("To: "); - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.setColor(DisplayDriver::GREEN); char displayName[NOTES_RENAME_MAX + 2]; snprintf(displayName, sizeof(displayName), "%s|", _renameBuf); @@ -880,7 +877,7 @@ private: display.setCursor(0, 25); display.print("File:"); - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.setCursor(0, 38); String nameDisplay = _deleteTarget; if (nameDisplay.length() > 35) nameDisplay = nameDisplay.substring(0, 32) + "..."; @@ -1096,9 +1093,9 @@ private: } public: - NotesScreen(UITask* task) - : _task(task), _mode(FILE_LIST), - _sdReady(false), _initialized(false), _display(nullptr), + NotesScreen(UITask* task, NodePrefs* prefs = nullptr) + : _task(task), _prefs(prefs), _mode(FILE_LIST), + _sdReady(false), _initialized(false), _lastFontPref(0), _display(nullptr), _charsPerLine(38), _linesPerPage(22), _lineHeight(5), _footerHeight(14), _editCharsPerLine(20), _editLineHeight(12), _editMaxLines(8), _selectedFile(0), _buf(nullptr), _bufLen(0), _cursorPos(0), @@ -1133,15 +1130,31 @@ public: // ---- Layout Init ---- void initLayout(DisplayDriver& display) { + // Re-init if font preference changed since last layout + uint8_t curFont = _prefs ? _prefs->large_font : 0; + if (_initialized && curFont != _lastFontPref) { + _initialized = false; + Serial.println("Notes: font changed, recalculating layout"); + } if (_initialized) return; + _lastFontPref = curFont; _display = &display; - // Tiny font metrics (for read mode) - display.setTextSize(0); + // Font metrics (for read mode) + display.setTextSize(_prefs->smallTextSize()); uint16_t tenCharsW = display.getTextWidth("MMMMMMMMMM"); if (tenCharsW > 0) { _charsPerLine = (display.width() * 10) / tenCharsW; } + // Proportional font: use average-width measurement instead of M-width + if (_prefs && _prefs->large_font) { + const char* sample = "the quick brown fox jumps over lazy dog"; + uint16_t sampleW = display.getTextWidth(sample); + int sampleLen = strlen(sample); + if (sampleW > 0 && sampleLen > 0) { + _charsPerLine = (display.width() * sampleLen * 70) / ((int)sampleW * 100); + } + } if (_charsPerLine < 15) _charsPerLine = 15; if (_charsPerLine > 60) _charsPerLine = 60; @@ -1151,6 +1164,10 @@ public: } else { _lineHeight = 5; } + // Large font: formula above assumes built-in 6x8 ratio — too small for 9pt + if (_prefs && _prefs->large_font) { + _lineHeight = _prefs->smallLineH(); + } _footerHeight = 14; int textAreaHeight = display.height() - _footerHeight; diff --git a/examples/companion_radio/ui-new/Repeateradminscreen.h b/examples/companion_radio/ui-new/Repeateradminscreen.h index 41c37ef4..f652110b 100644 --- a/examples/companion_radio/ui-new/Repeateradminscreen.h +++ b/examples/companion_radio/ui-new/Repeateradminscreen.h @@ -777,8 +777,8 @@ private: // ===================================================================== void renderCategoryMenu(DisplayDriver& display, int y, int bodyHeight) { - display.setTextSize(0); - int lineHeight = 9; + display.setTextSize(the_mesh.getNodePrefs()->smallTextSize()); + int lineHeight = the_mesh.getNodePrefs()->smallLineH(); // Clock drift info line if (_serverTime > 0) { @@ -862,8 +862,8 @@ private: // ===================================================================== void renderCommandMenu(DisplayDriver& display, int y, int bodyHeight) { - display.setTextSize(0); - int lineHeight = 9; + display.setTextSize(the_mesh.getNodePrefs()->smallTextSize()); + int lineHeight = the_mesh.getNodePrefs()->smallLineH(); const AdminCategoryDef& cat = CATEGORIES[_catSel]; // Category title @@ -1025,7 +1025,7 @@ private: if (_pendingCmd) display.print(_pendingCmd->label); y += 14; - display.setTextSize(0); + display.setTextSize(the_mesh.getNodePrefs()->smallTextSize()); display.setCursor(0, y); // Show the param value if one was collected @@ -1033,7 +1033,7 @@ private: char preview[80]; snprintf(preview, sizeof(preview), "Value: %s", _paramBuf); display.print(preview); - y += 10; + y += the_mesh.getNodePrefs()->smallLineH() + 1; display.setCursor(0, y); } @@ -1071,8 +1071,8 @@ private: // ===================================================================== void renderResponse(DisplayDriver& display, int y, int bodyHeight) { - display.setTextSize(0); - int lineHeight = 9; + display.setTextSize(the_mesh.getNodePrefs()->smallTextSize()); + int lineHeight = the_mesh.getNodePrefs()->smallLineH(); display.setColor((_state == STATE_ERROR) ? DisplayDriver::YELLOW : DisplayDriver::LIGHT); @@ -1166,7 +1166,7 @@ private: #if defined(LilyGo_T5S3_EPaper_Pro) display.fillRect(0, y, display.width(), lineHeight); #else - display.fillRect(0, y + 5, display.width(), lineHeight); + display.fillRect(0, y + the_mesh.getNodePrefs()->smallHighlightOff(), display.width(), lineHeight); #endif display.setColor(DisplayDriver::DARK); } else if (warn) { diff --git a/examples/companion_radio/ui-new/SMSScreen.h b/examples/companion_radio/ui-new/SMSScreen.h index e69b3c7b..6c0440f9 100644 --- a/examples/companion_radio/ui-new/SMSScreen.h +++ b/examples/companion_radio/ui-new/SMSScreen.h @@ -36,6 +36,7 @@ #include "ModemManager.h" #include "SMSStore.h" #include "SMSContacts.h" +#include "../NodePrefs.h" // Limits #define SMS_INBOX_PAGE_SIZE 4 @@ -51,6 +52,7 @@ public: private: UITask* _task; + NodePrefs* _prefs; SubView _view; // App menu state @@ -117,8 +119,8 @@ private: } public: - SMSScreen(UITask* task) - : _task(task), _view(APP_MENU) + SMSScreen(UITask* task, NodePrefs* prefs = nullptr) + : _task(task), _prefs(prefs), _view(APP_MENU) , _menuCursor(0) , _convCount(0), _inboxCursor(0), _inboxScrollTop(0) , _msgCount(0), _msgScrollPos(0) @@ -276,7 +278,7 @@ public: // Show modem state text if not ready if (ms != ModemState::READY && ms != ModemState::SENDING_SMS) { - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.setColor(DisplayDriver::YELLOW); const char* label = ModemManager::stateToString(ms); uint16_t labelW = display.getTextWidth(label); @@ -356,7 +358,7 @@ public: // Modem status indicator ModemState ms = modemManager.getState(); - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.setCursor(4, y + lineHeight + 8); if (ms == ModemState::OFF || ms == ModemState::POWERING_ON || ms == ModemState::INITIALIZING) { @@ -483,7 +485,7 @@ public: bool isAction = (row == 4); // Bottom row has action buttons if (isAction) { - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); if (col == 2 && _phoneInputPos > 0) { display.setColor(DisplayDriver::GREEN); // CALL } else if (col == 1) { @@ -544,7 +546,7 @@ public: display.drawRect(0, 11, display.width(), 1); if (_convCount == 0) { - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.setColor(DisplayDriver::LIGHT); display.setCursor(0, 20); display.print("No conversations"); @@ -560,8 +562,8 @@ public: } display.setTextSize(1); } else { - display.setTextSize(0); - int lineHeight = 10; + display.setTextSize(_prefs->smallTextSize()); + int lineHeight = _prefs->smallLineH() + 1; int y = 14; int visibleCount = (display.height() - 14 - 14) / (lineHeight * 2 + 2); @@ -643,14 +645,14 @@ public: display.drawRect(0, 11, display.width(), 1); if (_msgCount == 0) { - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.setColor(DisplayDriver::LIGHT); display.setCursor(0, 25); display.print("No messages"); display.setTextSize(1); } else { - display.setTextSize(0); - int lineHeight = 10; + display.setTextSize(_prefs->smallTextSize()); + int lineHeight = _prefs->smallLineH() + 1; int headerHeight = 14; int footerHeight = 14; @@ -764,12 +766,13 @@ public: // Message body display.setCursor(0, 14); display.setColor(DisplayDriver::LIGHT); - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); uint16_t testWidth = display.getTextWidth("MMMMMMMMMM"); int charsPerLine = (testWidth > 0) ? (display.width() * 10) / testWidth : 20; if (charsPerLine < 12) charsPerLine = 12; + int composeLH = _prefs->smallLineH() + 1; int y = 14; int x = 0; char cs[2] = {0, 0}; @@ -780,7 +783,7 @@ public: x++; if (x >= charsPerLine) { x = 0; - y += 10; + y += composeLH; } } @@ -827,7 +830,7 @@ public: int cnt = smsContacts.count(); if (cnt == 0) { - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.setColor(DisplayDriver::LIGHT); display.setCursor(0, 25); display.print("No contacts saved"); @@ -837,8 +840,8 @@ public: display.print("and press A to add"); display.setTextSize(1); } else { - display.setTextSize(0); - int lineHeight = 10; + display.setTextSize(_prefs->smallTextSize()); + int lineHeight = _prefs->smallLineH() + 1; int y = 14; int visibleCount = (display.height() - 14 - 14) / (lineHeight * 2 + 2); @@ -900,7 +903,7 @@ public: display.drawRect(0, 11, display.width(), 1); // Phone number (read-only) - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.setColor(DisplayDriver::LIGHT); display.setCursor(0, 16); display.print("Phone: "); @@ -956,7 +959,7 @@ public: display.print(dispName); // Phone number below name (smaller, dimmer) - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.setColor(DisplayDriver::LIGHT); display.setCursor(4, 36); display.print(_callPhone); @@ -1011,7 +1014,7 @@ public: display.print(dispName); // Phone number below name (smaller, dimmer) - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.setColor(DisplayDriver::LIGHT); display.setCursor(4, 36); display.print(_callPhone); @@ -1070,7 +1073,7 @@ public: display.print(dispName); // Phone number below name (smaller, dimmer) - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.setColor(DisplayDriver::LIGHT); display.setCursor(4, 36); display.print(_callPhone); @@ -1090,7 +1093,7 @@ public: display.print(timeBuf); // Volume (left-aligned) - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.setColor(DisplayDriver::LIGHT); char volLabel[12]; snprintf(volLabel, sizeof(volLabel), "Vol: %d/5", _callVolume); diff --git a/examples/companion_radio/ui-new/Settingsscreen.h b/examples/companion_radio/ui-new/Settingsscreen.h index 14327cbd..7acc3d69 100644 --- a/examples/companion_radio/ui-new/Settingsscreen.h +++ b/examples/companion_radio/ui-new/Settingsscreen.h @@ -112,6 +112,7 @@ enum SettingsRowType : uint8_t { ROW_UTC_OFFSET, // UTC offset (-12 to +14) ROW_MSG_NOTIFY, // Keyboard flash on new msg toggle ROW_DARK_MODE, // Dark mode toggle (inverted display) + ROW_LARGE_FONT, // Font size toggle: 0=tiny (default), 1=larger #if defined(LilyGo_T5S3_EPaper_Pro) ROW_PORTRAIT_MODE, // Portrait orientation toggle #endif @@ -352,12 +353,12 @@ private: } } else if (_subScreen == SUB_CHANNELS) { // --- Channels sub-screen: only channel-related rows --- + // Scan ALL slots — companion app may write non-contiguously, and + // gaps can appear after channel deletion if compaction is incomplete. for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) { ChannelDetails ch; if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') { addRow(ROW_CHANNEL, i); - } else { - break; } } addRow(ROW_ADD_CHANNEL); @@ -375,6 +376,7 @@ private: addRow(ROW_GPS_BAUD); addRow(ROW_PATH_HASH_SIZE); addRow(ROW_DARK_MODE); + addRow(ROW_LARGE_FONT); #if defined(LilyGo_T5S3_EPaper_Pro) addRow(ROW_PORTRAIT_MODE); #endif @@ -504,14 +506,12 @@ private: ChannelDetails empty; memset(&empty, 0, sizeof(empty)); - // Find total channel count + // Find highest used channel slot (scan all — gaps may exist) int total = 0; for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) { ChannelDetails ch; if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') { total = i + 1; - } else { - break; } } @@ -606,13 +606,13 @@ public: // and move cursor there. Returns: 0=miss, 1=moved to new row, 2=tapped current row. int selectRowAtVY(int vy) { if (_editMode != EDIT_NONE) return 0; // Don't change cursor while editing - const int headerH = 14, footerH = 14, lineH = 9; - // T-Deck Pro render offsets fillRect by +5 (GxEPD baseline compensation), - // so visual rows start 5 units below headerH. T5S3 renders at y directly. + const int headerH = 14, footerH = 14, lineH = _prefs->smallLineH(); + // bodyTop must match where the visual rows start (highlight bar position). + // T5S3 renders highlight at y directly. T-Deck Pro offsets by smallHighlightOff(). #if defined(LilyGo_T5S3_EPaper_Pro) const int bodyTop = headerH; #else - const int bodyTop = headerH + 5; + const int bodyTop = headerH + _prefs->smallHighlightOff(); #endif if (vy < bodyTop || vy >= 128 - footerH) return 0; // Outside body area @@ -979,7 +979,7 @@ public: display.fillRect(2, 14, display.width() - 4, display.height() - 28); display.setColor(DisplayDriver::LIGHT); display.drawRect(2, 14, display.width() - 4, display.height() - 28); - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.drawTextCentered(display.width() / 2, 22, "Flashing Firmware"); snprintf(tmp, sizeof(tmp), "%d / %d KB", (int)(totalWritten / 1024), (int)(fileSize / 1024)); display.drawTextCentered(display.width() / 2, 42, tmp); @@ -1053,7 +1053,7 @@ public: display.fillRect(2, 14, display.width() - 4, display.height() - 28); display.setColor(DisplayDriver::LIGHT); display.drawRect(2, 14, display.width() - 4, display.height() - 28); - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.setColor(DisplayDriver::GREEN); display.drawTextCentered(display.width() / 2, 30, "Update Complete!"); display.setColor(DisplayDriver::LIGHT); @@ -1133,8 +1133,8 @@ public: display.drawRect(0, 11, display.width(), 1); // === Body === - display.setTextSize(0); // tiny font - int lineHeight = 9; + display.setTextSize(_prefs->smallTextSize()); // tiny font + int lineHeight = _prefs->smallLineH(); int headerH = 14; int footerH = 14; int maxY = display.height() - footerH; @@ -1159,7 +1159,7 @@ public: // Highlight needs to start above the baseline to cover ascenders. display.fillRect(0, y, display.width(), lineHeight); #else - display.fillRect(0, y + 5, display.width(), lineHeight); + display.fillRect(0, y + _prefs->smallHighlightOff(), display.width(), lineHeight); #endif display.setColor(DisplayDriver::DARK); } else { @@ -1252,7 +1252,7 @@ public: break; case ROW_MSG_NOTIFY: - snprintf(tmp, sizeof(tmp), "Msg Rcvd LED Light Pulse: %s", + snprintf(tmp, sizeof(tmp), "Msg LED Flash: %s", _prefs->kb_flash_notify ? "ON" : "OFF"); display.print(tmp); break; @@ -1285,6 +1285,12 @@ public: display.print(tmp); break; + case ROW_LARGE_FONT: + snprintf(tmp, sizeof(tmp), "Font Size: %s", + _prefs->large_font ? "LARGER" : "TINY"); + display.print(tmp); + break; + #if defined(LilyGo_T5S3_EPaper_Pro) case ROW_PORTRAIT_MODE: snprintf(tmp, sizeof(tmp), "Portrait Mode: %s", @@ -1525,7 +1531,7 @@ public: display.setColor(DisplayDriver::LIGHT); display.drawRect(bx, by, bw, bh); - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); if (_confirmAction == 1) { uint8_t chIdx = _rows[_cursor].param; ChannelDetails ch; @@ -1553,7 +1559,7 @@ public: display.setColor(DisplayDriver::LIGHT); display.drawRect(bx, by, bw, bh); - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); int wy = by + 4; if (_wifiPhase == WIFI_PHASE_SCANNING) { @@ -1639,7 +1645,7 @@ public: display.setColor(DisplayDriver::LIGHT); display.drawRect(bx, by, bw, bh); - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); int oy = by + 4; if (_otaPhase == OTA_PHASE_CONFIRM) { @@ -2330,6 +2336,12 @@ public: Serial.printf("Settings: Dark mode = %s\n", _prefs->dark_mode ? "ON" : "OFF"); break; + case ROW_LARGE_FONT: + _prefs->large_font = _prefs->large_font ? 0 : 1; + the_mesh.savePrefs(); + Serial.printf("Settings: Font size = %s\n", + _prefs->large_font ? "LARGER" : "TINY"); + break; #if defined(LilyGo_T5S3_EPaper_Pro) case ROW_PORTRAIT_MODE: _prefs->portrait_mode = _prefs->portrait_mode ? 0 : 1; diff --git a/examples/companion_radio/ui-new/Textreaderscreen.h b/examples/companion_radio/ui-new/Textreaderscreen.h index 69902e92..1b0fbd66 100644 --- a/examples/companion_radio/ui-new/Textreaderscreen.h +++ b/examples/companion_radio/ui-new/Textreaderscreen.h @@ -6,6 +6,7 @@ #include #include "Utf8CP437.h" #include "EpubProcessor.h" +#include "../NodePrefs.h" // Forward declarations class UITask; @@ -327,12 +328,13 @@ inline int indexPagesWordWrap(File& file, long startPos, inline int indexPagesWordWrapPixel(File& file, long startPos, std::vector& pagePositions, int linesPerPage, int maxChars, - DisplayDriver* display, int maxPages) { + DisplayDriver* display, int maxPages, + NodePrefs* prefs = nullptr) { const int BUF_SIZE = READER_BUF_SIZE; // Match page buffer to avoid chunk boundary wrap mismatches char buffer[BUF_SIZE]; // Ensure body font is active for pixel measurement - display->setTextSize(0); + display->setTextSize(prefs ? prefs->smallTextSize() : 0); file.seek(startPos); int pagesAdded = 0; @@ -396,9 +398,11 @@ public: private: UITask* _task; + NodePrefs* _prefs; Mode _mode; bool _sdReady; bool _initialized; // Layout metrics calculated + uint8_t _lastFontPref; // Font preference at last layout init (detect changes) bool _bootIndexed; // Boot-time pre-indexing done DisplayDriver* _display; // Stored reference for splash screens @@ -1084,8 +1088,8 @@ private: display.setCursor(0, 42); display.print("/books/ on SD card"); } else { - display.setTextSize(0); // Tiny font for file list - int listLineH = 8; // Approximate tiny font line height in virtual coords + display.setTextSize(_prefs->smallTextSize()); // Tiny font for file list + int listLineH = _prefs->smallLineH(); int startY = 14; int maxVisible = (display.height() - startY - _footerHeight) / listLineH; if (maxVisible < 3) maxVisible = 3; @@ -1106,7 +1110,7 @@ private: #else // 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.fillRect(0, y + _prefs->smallHighlightOff(), display.width(), listLineH); #endif display.setColor(DisplayDriver::DARK); } else { @@ -1114,8 +1118,6 @@ private: } // Set cursor AFTER fillRect so text draws on top of highlight - display.setCursor(0, y); - int type = itemTypeAt(i); String line = selected ? "> " : " "; @@ -1125,10 +1127,6 @@ private: } else if (type == 1) { // Subdirectory line += "/" + dirNameAt(i); - // Truncate if needed - if ((int)line.length() > _charsPerLine) { - line = line.substring(0, _charsPerLine - 3) + "..."; - } } else { // File int fi = fileIndexAt(i); @@ -1141,16 +1139,11 @@ private: suffix = " *"; } } - - // Truncate if needed - int maxLen = _charsPerLine - 4 - suffix.length(); - if ((int)name.length() > maxLen) { - name = name.substring(0, maxLen - 3) + "..."; - } line += name + suffix; } - display.print(line.c_str()); + // Pixel-aware ellipsis — small margin prevents GxEPD edge wrapping + display.drawTextEllipsized(0, y, display.width() - 4, line.c_str()); y += listLineH; } display.setTextSize(1); // Restore @@ -1163,7 +1156,7 @@ private: display.setColor(DisplayDriver::YELLOW); #if defined(LilyGo_T5S3_EPaper_Pro) - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.drawTextCentered(display.width() / 2, footerY, "Swipe: Scroll Tap: Open Boot: home"); #else display.setCursor(0, footerY); @@ -1177,7 +1170,7 @@ private: void renderPage(DisplayDriver& display) { // Use tiny font for maximum text density - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.setColor(DisplayDriver::LIGHT); int y = 0; @@ -1270,7 +1263,7 @@ private: } #if defined(LilyGo_T5S3_EPaper_Pro) - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.setCursor(0, footerY); display.print(status); const char* right = "Swipe:Page Tap:GoTo Hold:Close"; @@ -1287,8 +1280,8 @@ private: } public: - TextReaderScreen(UITask* task) - : _task(task), _mode(FILE_LIST), _sdReady(false), _initialized(false), + TextReaderScreen(UITask* task, NodePrefs* prefs = nullptr) + : _task(task), _prefs(prefs), _mode(FILE_LIST), _sdReady(false), _initialized(false), _lastFontPref(0), _bootIndexed(false), _display(nullptr), _charsPerLine(38), _linesPerPage(22), _lineHeight(5), _textAreaHeight(100), _headerHeight(14), _footerHeight(14), @@ -1313,16 +1306,24 @@ public: // Call once after display is available to calculate layout metrics void initLayout(DisplayDriver& display) { + // Re-init if font preference changed since last layout + uint8_t curFont = _prefs ? _prefs->large_font : 0; + if (_initialized && curFont != _lastFontPref) { + _initialized = false; + Serial.println("TextReader: font changed, recalculating layout"); + } if (_initialized) return; + _lastFontPref = curFont; // Store display reference for splash screens during openBook _display = &display; // Measure tiny font metrics using the display driver - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); - // Measure character width: use 10 M's for monospace (T-Deck Pro). - // T5S3 overrides this below with average-width measurement. + // Measure character width: use 10 M's for monospace (T-Deck Pro tiny font). + // Proportional fonts (T5S3 and T-Deck Pro large_font) override below with + // average-width measurement since M is the widest glyph (~40% wider than average). uint16_t tenCharsW = display.getTextWidth("MMMMMMMMMM"); if (tenCharsW > 0) { _charsPerLine = (display.width() * 10) / tenCharsW; @@ -1343,6 +1344,15 @@ public: if (_charsPerLine < 15) _charsPerLine = 15; if (_charsPerLine > 80) _charsPerLine = 80; #else + // T-Deck Pro: large_font uses FreeSans9pt (proportional) — same fix + if (_prefs && _prefs->large_font) { + const char* sample = "the quick brown fox jumps over lazy dog"; + uint16_t sampleW = display.getTextWidth(sample); + int sampleLen = strlen(sample); + if (sampleW > 0 && sampleLen > 0) { + _charsPerLine = (display.width() * sampleLen * 70) / ((int)sampleW * 100); + } + } if (_charsPerLine < 15) _charsPerLine = 15; if (_charsPerLine > 60) _charsPerLine = 60; #endif @@ -1362,13 +1372,17 @@ public: #if defined(LilyGo_T5S3_EPaper_Pro) // T5S3 uses FreeSans12pt/FreeSerif12pt for size 0 (yAdvance=29px). - // Line height in virtual coords depends on orientation: - // Landscape: 29px / scale_y(4.22) ≈ 7 + 1 spacing = 8 - // Portrait: 29px / scale_y(7.50) ≈ 4 + 1 spacing = 5 { extern DISPLAY_CLASS display; _lineHeight = display.isPortraitMode() ? 5 : 8; } +#else + // T-Deck Pro large_font uses FreeSans9pt (yAdvance=22px at scale 1.5625×). + // The 6x8 formula above gives ~5-7 which is way too small — lines overlap. + // Use smallLineH() which is already tuned for this font. + if (_prefs && _prefs->large_font) { + _lineHeight = _prefs->smallLineH(); + } #endif _headerHeight = 0; // No header in reading mode (maximize text area) @@ -1574,11 +1588,12 @@ public: // Returns: 0=miss, 1=moved, 2=tapped current row. int selectRowAtVY(int vy) { if (_mode != FILE_LIST) return 0; - const int startY = 14, footerH = 14, listLineH = 8; + const int startY = 14, footerH = 14; + const int listLineH = _prefs ? _prefs->smallLineH() : 9; #if defined(LilyGo_T5S3_EPaper_Pro) const int bodyTop = startY; #else - const int bodyTop = startY + 5; // GxEPD baseline offset + const int bodyTop = startY + (_prefs ? _prefs->smallHighlightOff() : 5); #endif if (vy < bodyTop || vy >= 128 - footerH) return 0; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 2aa0abb6..e58948d7 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -159,7 +159,7 @@ void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts, } display.setColor(DisplayDriver::GREEN); - display.setTextSize(0); + display.setTextSize(_node_prefs->smallTextSize()); #if defined(LilyGo_T5S3_EPaper_Pro) // T5S3: text-only battery indicator — "Batt 99% 4.1v" @@ -173,7 +173,7 @@ void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts, display.print(battStr); display.setTextSize(1); // restore default text size #else - // T-Deck Pro: icon + percentage text + // T-Deck Pro: icon + percentage text (icon hidden in large font) int iconWidth = 16; int iconHeight = 6; int iconY = 0; @@ -184,26 +184,35 @@ void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts, sprintf(pctStr, "%d%%", batteryPercentage); uint16_t textWidth = display.getTextWidth(pctStr); - // layout: [icon][cap 2px][gap 2px][text][margin 2px] - int totalWidth = iconWidth + 2 + 2 + textWidth + 2; - int iconX = display.width() - totalWidth; + if (_node_prefs->large_font) { + // Large font: text only — no room for icon in header + int textX = display.width() - textWidth - 2; + if (outIconX) *outIconX = textX; + display.setCursor(textX, textY); + display.print(pctStr); + } else { + // Tiny font: icon + text + // layout: [icon][cap 2px][gap 2px][text][margin 2px] + int totalWidth = iconWidth + 2 + 2 + textWidth + 2; + int iconX = display.width() - totalWidth; - if (outIconX) *outIconX = iconX; + if (outIconX) *outIconX = iconX; - // battery outline - display.drawRect(iconX, iconY, iconWidth, iconHeight); + // battery outline + display.drawRect(iconX, iconY, iconWidth, iconHeight); - // battery "cap" - display.fillRect(iconX + iconWidth, iconY + (iconHeight / 4), 2, iconHeight / 2); + // battery "cap" + display.fillRect(iconX + iconWidth, iconY + (iconHeight / 4), 2, iconHeight / 2); - // fill the battery based on the percentage - int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100; - display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4); + // fill the battery based on the percentage + int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100; + display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4); - // draw percentage text after the battery cap - int textX = iconX + iconWidth + 2 + 2; // after cap + gap - display.setCursor(textX, textY); - display.print(pctStr); + // draw percentage text after the battery cap + int textX = iconX + iconWidth + 2 + 2; // after cap + gap + display.setCursor(textX, textY); + display.print(pctStr); + } display.setTextSize(1); // restore default text size #endif } @@ -218,7 +227,7 @@ void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts, if (!_task->isAudioPlayingInBackground()) return; display.setColor(DisplayDriver::GREEN); - display.setTextSize(0); // tiny font (same as clock & battery %) + display.setTextSize(_node_prefs->smallTextSize()); // tiny font (same as clock & battery %) int x = batteryLeftX - display.getTextWidth(">>") - 2; display.setCursor(x, -3); // align vertically with battery text display.print(">>"); @@ -235,7 +244,7 @@ void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts, // Calculate X: shift left past audio indicator if it's showing int rightEdge = batteryLeftX; if (_task->isAudioPlayingInBackground()) { - display.setTextSize(0); + display.setTextSize(_node_prefs->smallTextSize()); rightEdge = rightEdge - display.getTextWidth(">>") - 2; } @@ -298,7 +307,7 @@ public: _task->setHomeShowingTiles(false); // Reset — only set true on FIRST page #endif // node name (tinyfont to avoid overlapping clock) - display.setTextSize(0); + display.setTextSize(_node_prefs->smallTextSize()); display.setColor(DisplayDriver::GREEN); char filtered_name[sizeof(_node_prefs->node_name)]; display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name)); @@ -312,7 +321,7 @@ public: display.setCursor(0, HOME_HDR_Y); display.print(filtered_name); - // battery voltage + // battery voltage + status icons #ifdef MECK_AUDIO_VARIANT int battLeftX = display.width(); // default if battery doesn't render renderBatteryIndicator(display, _task->getBattMilliVolts(), &battLeftX); @@ -326,7 +335,7 @@ public: renderBatteryIndicator(display, _task->getBattMilliVolts()); #endif - // centered clock (tinyfont) - only show when time is valid + // centered clock — only show when time is valid { uint32_t now = _rtc->getCurrentTime(); if (now > 1700000000) { // valid timestamp (after ~Nov 2023) @@ -340,11 +349,14 @@ public: char timeBuf[6]; sprintf(timeBuf, "%02d:%02d", hrs, mins); - display.setTextSize(0); // tinyfont + display.setTextSize(_node_prefs->smallTextSize()); display.setColor(DisplayDriver::LIGHT); uint16_t tw = display.getTextWidth(timeBuf); int clockX = (display.width() - tw) / 2; - display.setCursor(clockX, HOME_HDR_Y); // align with node name Y + // Ensure clock doesn't overlap the node name + int nameRight = display.getTextWidth(filtered_name) + 4; + if (clockX < nameRight) clockX = nameRight; + display.setCursor(clockX, HOME_HDR_Y); display.print(timeBuf); display.setTextSize(1); // restore } @@ -387,17 +399,17 @@ public: IPAddress ip = WiFi.localIP(); if (ip != IPAddress(0,0,0,0)) { snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d:%d", ip[0], ip[1], ip[2], ip[3], TCP_PORT); - display.setTextSize(0); // Tiny font for IP + display.setTextSize(_node_prefs->smallTextSize()); // Tiny font for IP display.drawTextCentered(display.width() / 2, y, tmp); - y += 8; + y += _node_prefs->smallLineH() - 1; } #endif #if defined(BLE_PIN_CODE) || defined(WIFI_SSID) || defined(MECK_WIFI_COMPANION) if (_task->hasConnection()) { display.setColor(DisplayDriver::GREEN); - display.setTextSize(0); // Tiny font for Connected + display.setTextSize(_node_prefs->smallTextSize()); // Tiny font for Connected display.drawTextCentered(display.width() / 2, y, "< Connected >"); - y += 8; // Reduced from 12 + y += _node_prefs->smallLineH() - 1; #ifdef BLE_PIN_CODE } else if (_task->isSerialEnabled() && the_mesh.getBLEPin() != 0) { display.setColor(DisplayDriver::RED); @@ -448,7 +460,7 @@ public: display.drawXbm(iconX, iconY, tiles[row][col].icon, HOME_ICON_W, HOME_ICON_H); // Label centered below icon - display.setTextSize(0); + display.setTextSize(_node_prefs->smallTextSize()); display.drawTextCentered(tx + tileW / 2, ty + 18, tiles[row][col].label); } } @@ -456,51 +468,99 @@ public: // Nav hint below grid y = gridY + 2 * tileH + gapY + 2; display.setColor(DisplayDriver::GREEN); - display.setTextSize(0); + display.setTextSize(_node_prefs->smallTextSize()); display.drawTextCentered(display.width() / 2, y, "Tap tile to open"); } display.setTextSize(1); #else // ----- T-Deck Pro: Keyboard shortcut text menu ----- - // Menu shortcuts - tinyfont monospaced grid - y += 6; display.setColor(DisplayDriver::LIGHT); - display.setTextSize(0); // tinyfont 6x8 monospaced - display.drawTextCentered(display.width() / 2, y, "Press:"); - y += 12; - display.drawTextCentered(display.width() / 2, y, "[M] Messages [C] Contacts "); - y += 10; - display.drawTextCentered(display.width() / 2, y, "[N] Notes [S] Settings "); - y += 10; + display.setTextSize(_node_prefs->smallTextSize()); + int menuLH = _node_prefs->smallLineH(); + + if (_node_prefs->large_font) { + // Proportional font: two-column layout with fixed X positions + y += 2; + int col1 = 2; + int col2 = display.width() / 2; + + display.setCursor(col1, y); display.print("[M] Messages"); + display.setCursor(col2, y); display.print("[C] Contacts"); + y += menuLH; + display.setCursor(col1, y); display.print("[N] Notes"); + display.setCursor(col2, y); display.print("[S] Settings"); + y += menuLH; #if HAS_GPS - display.drawTextCentered(display.width() / 2, y, "[E] Reader [G] Maps "); + display.setCursor(col1, y); display.print("[E] Reader"); + display.setCursor(col2, y); display.print("[G] Maps"); #else - display.drawTextCentered(display.width() / 2, y, "[E] Reader "); + display.setCursor(col1, y); display.print("[E] Reader"); #endif - y += 10; + y += menuLH; #if defined(HAS_4G_MODEM) && defined(MECK_WEB_READER) - display.drawTextCentered(display.width() / 2, y, "[T] Phone [B] Browser "); + display.setCursor(col1, y); display.print("[T] Phone"); + display.setCursor(col2, y); display.print("[B] Browser"); #elif defined(HAS_4G_MODEM) - display.drawTextCentered(display.width() / 2, y, "[T] Phone [F] Discover "); + display.setCursor(col1, y); display.print("[T] Phone"); + display.setCursor(col2, y); display.print("[F] Discover"); #elif defined(MECK_AUDIO_VARIANT) - display.drawTextCentered(display.width() / 2, y, "[P] Audiobooks [K] Alarm "); - y += 10; + display.setCursor(col1, y); display.print("[P] Audio"); + display.setCursor(col2, y); display.print("[K] Alarm"); + y += menuLH; #ifdef MECK_WEB_READER - display.drawTextCentered(display.width() / 2, y, "[B] Browser [F] Discover "); + display.setCursor(col1, y); display.print("[B] Browser"); + display.setCursor(col2, y); display.print("[F] Discover"); #else - display.drawTextCentered(display.width() / 2, y, "[F] Discover "); + display.setCursor(col1, y); display.print("[F] Discover"); #endif #elif defined(MECK_WEB_READER) - display.drawTextCentered(display.width() / 2, y, "[B] Browser "); + display.setCursor(col1, y); display.print("[B] Browser"); #else - display.drawTextCentered(display.width() / 2, y, "[F] Discover "); + display.setCursor(col1, y); display.print("[F] Discover"); #endif - y += 14; + y += menuLH + 2; + } else { + // Monospaced built-in font: centered space-padded strings + y += 6; + display.drawTextCentered(display.width() / 2, y, "Press:"); + y += 12; + display.drawTextCentered(display.width() / 2, y, "[M] Messages [C] Contacts "); + y += 10; + display.drawTextCentered(display.width() / 2, y, "[N] Notes [S] Settings "); + y += 10; +#if HAS_GPS + display.drawTextCentered(display.width() / 2, y, "[E] Reader [G] Maps "); +#else + display.drawTextCentered(display.width() / 2, y, "[E] Reader "); +#endif + y += 10; +#if defined(HAS_4G_MODEM) && defined(MECK_WEB_READER) + display.drawTextCentered(display.width() / 2, y, "[T] Phone [B] Browser "); +#elif defined(HAS_4G_MODEM) + display.drawTextCentered(display.width() / 2, y, "[T] Phone [F] Discover "); +#elif defined(MECK_AUDIO_VARIANT) + display.drawTextCentered(display.width() / 2, y, "[P] Audiobooks [K] Alarm "); + y += 10; + #ifdef MECK_WEB_READER + display.drawTextCentered(display.width() / 2, y, "[B] Browser [F] Discover "); + #else + display.drawTextCentered(display.width() / 2, y, "[F] Discover "); + #endif +#elif defined(MECK_WEB_READER) + display.drawTextCentered(display.width() / 2, y, "[B] Browser "); +#else + display.drawTextCentered(display.width() / 2, y, "[F] Discover "); +#endif + y += 14; + } - // Nav hint - display.setColor(DisplayDriver::GREEN); - display.drawTextCentered(display.width() / 2, y, "Press A/D to cycle home views"); + // Nav hint (only if room) + if (y < display.height() - 14) { + display.setColor(DisplayDriver::GREEN); + display.drawTextCentered(display.width() / 2, y, + _node_prefs->large_font ? "A/D: cycle views" : "Press A/D to cycle home views"); + } display.setTextSize(1); // restore #endif } else if (_page == HomePage::RECENT) { @@ -530,7 +590,7 @@ public: } // Hint for full Last Heard screen display.setColor(DisplayDriver::LIGHT); - display.setTextSize(0); + display.setTextSize(_node_prefs->smallTextSize()); #if defined(LilyGo_T5S3_EPaper_Pro) display.drawTextCentered(display.width() / 2, display.height() - 24, "Tap here for full Last Heard list"); @@ -600,19 +660,20 @@ public: display.drawTextCentered(display.width() / 2, 18, "WiFi Companion"); int wy = 36; - display.setTextSize(0); + display.setTextSize(_node_prefs->smallTextSize()); + int wLH = _node_prefs->smallLineH() + 1; if (WiFi.status() == WL_CONNECTED) { display.setColor(DisplayDriver::GREEN); snprintf(tmp, sizeof(tmp), "SSID: %s", WiFi.SSID().c_str()); display.drawTextCentered(display.width() / 2, wy, tmp); - wy += 10; + wy += wLH; IPAddress ip = WiFi.localIP(); snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); display.drawTextCentered(display.width() / 2, wy, tmp); - wy += 10; + wy += wLH; snprintf(tmp, sizeof(tmp), "Port: %d", TCP_PORT); display.drawTextCentered(display.width() / 2, wy, tmp); - wy += 12; + wy += wLH + 2; if (_task->hasConnection()) { display.setColor(DisplayDriver::GREEN); display.setTextSize(1); @@ -625,7 +686,7 @@ public: } else { display.setColor(DisplayDriver::RED); display.drawTextCentered(display.width() / 2, wy, "Not connected"); - wy += 12; + wy += wLH + 2; display.setColor(DisplayDriver::LIGHT); display.drawTextCentered(display.width() / 2, wy, "Configure in Settings"); } @@ -726,7 +787,7 @@ public: display.drawTextCentered(display.width() / 2, by + 4, buf); // Show controls hint - display.setTextSize(0); + display.setTextSize(_node_prefs->smallTextSize()); display.drawTextCentered(display.width() / 2, by + bh - 10, "W/S:adj Enter:ok Q:cancel"); display.setTextSize(1); } @@ -1136,12 +1197,10 @@ public: } // ---- Unlock hint ---- - display.setTextSize(0); - display.setColor(DisplayDriver::LIGHT); #if defined(LilyGo_T5S3_EPaper_Pro) + display.setTextSize(_node_prefs->smallTextSize()); + display.setColor(DisplayDriver::LIGHT); display.drawTextCentered(display.width() / 2, 120, "Hold button to unlock"); -#else - display.drawTextCentered(display.width() / 2, 120, "Dbl-press to unlock"); #endif return 30000; @@ -1227,8 +1286,8 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no ((ChannelScreen*)channel_screen)->setDMUnreadPtr(_dmUnread); contacts_screen = new ContactsScreen(this, &rtc_clock); ((ContactsScreen*)contacts_screen)->setDMUnreadPtr(_dmUnread); - text_reader = new TextReaderScreen(this); - notes_screen = new NotesScreen(this); + text_reader = new TextReaderScreen(this, node_prefs); + notes_screen = new NotesScreen(this, node_prefs); settings_screen = new SettingsScreen(this, &rtc_clock, node_prefs); repeater_admin = nullptr; // Lazy-initialized on first use to preserve heap for audio discovery_screen = new DiscoveryScreen(this, &rtc_clock); @@ -1241,7 +1300,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no alarm_screen = nullptr; // Created and assigned from main.cpp if audio hardware present #endif #ifdef HAS_4G_MODEM - sms_screen = new SMSScreen(this); + sms_screen = new SMSScreen(this, node_prefs); #endif #if HAS_GPS map_screen = new MapScreen(this); @@ -2727,7 +2786,7 @@ void UITask::gotoWebReader() { if (web_reader == nullptr) { Serial.printf("WebReader: lazy init - free heap: %d, largest block: %d\n", ESP.getFreeHeap(), ESP.getMaxAllocHeap()); - web_reader = new WebReaderScreen(this); + web_reader = new WebReaderScreen(this, _node_prefs); Serial.printf("WebReader: init complete - free heap: %d\n", ESP.getFreeHeap()); } WebReaderScreen* wr = (WebReaderScreen*)web_reader; diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index e4973212..876c852e 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -306,6 +306,7 @@ public: UIScreen* getContactsScreen() const { return contacts_screen; } UIScreen* getChannelScreen() const { return channel_screen; } UIScreen* getSettingsScreen() const { return settings_screen; } + NodePrefs* getNodePrefs() const { return _node_prefs; } UIScreen* getAudiobookScreen() const { return audiobook_screen; } void setAudiobookScreen(UIScreen* s) { audiobook_screen = s; } #ifdef MECK_AUDIO_VARIANT diff --git a/examples/companion_radio/ui-new/Webreaderscreen.h b/examples/companion_radio/ui-new/Webreaderscreen.h index 1b30c772..0f3fcf47 100644 --- a/examples/companion_radio/ui-new/Webreaderscreen.h +++ b/examples/companion_radio/ui-new/Webreaderscreen.h @@ -39,6 +39,7 @@ #include "ModemManager.h" #endif #include "Utf8CP437.h" +#include "../NodePrefs.h" // Forward declarations class UITask; @@ -1030,8 +1031,10 @@ public: private: UITask* _task; + NodePrefs* _prefs; Mode _mode; bool _initialized; + uint8_t _lastFontPref; DisplayDriver* _display; // Display layout (calculated once) @@ -1424,7 +1427,7 @@ private: _display->print("WiFi Setup"); _display->drawRect(0, 11, _display->width(), 1); _display->setColor(DisplayDriver::LIGHT); - _display->setTextSize(0); + _display->setTextSize(_prefs->smallTextSize()); _display->setCursor(0, 18); _display->print("Scanning for networks..."); _display->endFrame(); @@ -1524,7 +1527,7 @@ private: _display->print("Web Reader"); _display->drawRect(0, 11, _display->width(), 1); - _display->setTextSize(0); + _display->setTextSize(_prefs->smallTextSize()); _display->setCursor(0, 18); _display->print("Connected!"); _display->setCursor(0, 30); @@ -2306,7 +2309,7 @@ private: _display->print("Web Reader"); _display->drawRect(0, 11, _display->width(), 1); _display->setColor(DisplayDriver::YELLOW); - _display->setTextSize(0); + _display->setTextSize(_prefs->smallTextSize()); _display->setCursor(0, 18); _display->print("Fetch failed:"); _display->setColor(DisplayDriver::LIGHT); @@ -2442,7 +2445,7 @@ private: _display->setTextSize(2); _display->setCursor(10, 20); _display->print("Logging in..."); - _display->setTextSize(0); + _display->setTextSize(_prefs->smallTextSize()); _display->setColor(DisplayDriver::LIGHT); _display->setCursor(10, 45); _display->print("Refreshing session..."); @@ -2656,14 +2659,14 @@ private: display.drawRect(0, 11, display.width(), 1); display.setColor(DisplayDriver::LIGHT); - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); if (_wifiState == WIFI_SCANNING) { display.setCursor(0, 18); display.print("Scanning for networks..."); } else if (_wifiState == WIFI_SCAN_DONE) { int y = 14; - int listLineH = 8; + int listLineH = _prefs ? _prefs->smallLineH() : 9; for (int i = 0; i < _ssidCount && y < display.height() - 24; i++) { bool selected = (i == _selectedSSID); if (selected) { @@ -2671,7 +2674,7 @@ private: #if defined(LilyGo_T5S3_EPaper_Pro) display.fillRect(0, y, display.width(), listLineH); #else - display.fillRect(0, y + 5, display.width(), listLineH); + display.fillRect(0, y + _prefs->smallHighlightOff(), display.width(), listLineH); #endif display.setColor(DisplayDriver::DARK); } else { @@ -2695,7 +2698,7 @@ private: y += 12; display.setCursor(0, y); display.print("Password:"); - y += 10; + y += _prefs->smallLineH() + 1; display.setCursor(0, y); // Show masked password with brief reveal of last char char passBuf[WEB_WIFI_PASS_LEN + 2]; @@ -2771,7 +2774,7 @@ private: if (isNetworkAvailable()) { display.print("Web Reader"); - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.setColor(DisplayDriver::GREEN); if (isWiFiConnected()) { IPAddress ip = WiFi.localIP(); @@ -2797,7 +2800,7 @@ private: const int footerY = display.height() - 12; const int viewportH = display.height() - headerY - footerH; const int scrollbarW = 4; - const int listLineH = 8; + const int listLineH = _prefs ? _prefs->smallLineH() : 9; const int sepH = 8; // Separator between IRC and web sections const int sectionH = listLineH; // Section header height int maxChars = _charsPerLine - 2; // Account for "> " prefix @@ -2875,7 +2878,7 @@ private: if (totalContentH <= viewportH) _homeScrollY = 0; // ---- Render pass (with scroll offset) ---- - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); int y = headerY - _homeScrollY; // Start Y in screen coords itemIdx = 0; bool needsScroll = (totalContentH > viewportH); @@ -2895,7 +2898,7 @@ private: #if defined(LilyGo_T5S3_EPaper_Pro) display.fillRect(0, y, display.width() - (needsScroll ? scrollbarW + 1 : 0), listLineH); #else - display.fillRect(0, y + 5, display.width() - (needsScroll ? scrollbarW + 1 : 0), listLineH); + display.fillRect(0, y + _prefs->smallHighlightOff(), display.width() - (needsScroll ? scrollbarW + 1 : 0), listLineH); #endif display.setColor(DisplayDriver::DARK); } else { @@ -2934,7 +2937,7 @@ private: #if defined(LilyGo_T5S3_EPaper_Pro) display.fillRect(0, y, display.width() - (needsScroll ? scrollbarW + 1 : 0), listLineH); #else - display.fillRect(0, y + 5, display.width() - (needsScroll ? scrollbarW + 1 : 0), listLineH); + display.fillRect(0, y + _prefs->smallHighlightOff(), display.width() - (needsScroll ? scrollbarW + 1 : 0), listLineH); #endif display.setColor(DisplayDriver::DARK); } else { @@ -2971,7 +2974,7 @@ private: #if defined(LilyGo_T5S3_EPaper_Pro) display.fillRect(0, y, display.width() - (needsScroll ? scrollbarW + 1 : 0), listLineH); #else - display.fillRect(0, y + 5, display.width() - (needsScroll ? scrollbarW + 1 : 0), listLineH); + display.fillRect(0, y + _prefs->smallHighlightOff(), display.width() - (needsScroll ? scrollbarW + 1 : 0), listLineH); #endif display.setColor(DisplayDriver::DARK); } else { @@ -3024,7 +3027,7 @@ private: #if defined(LilyGo_T5S3_EPaper_Pro) display.fillRect(0, y, contentW, itemH); #else - display.fillRect(0, y + 5, contentW, itemH); + display.fillRect(0, y + _prefs->smallHighlightOff(), contentW, itemH); #endif display.setColor(DisplayDriver::DARK); } else { @@ -3076,7 +3079,7 @@ private: #if defined(LilyGo_T5S3_EPaper_Pro) display.fillRect(0, y, contentW, itemH); #else - display.fillRect(0, y + 5, contentW, itemH); + display.fillRect(0, y + _prefs->smallHighlightOff(), contentW, itemH); #endif display.setColor(DisplayDriver::DARK); } else { @@ -3198,7 +3201,7 @@ private: display.setCursor(10, 20); display.print("Loading..."); - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.setColor(DisplayDriver::LIGHT); // Word-wrap the URL across multiple lines @@ -3243,7 +3246,7 @@ private: display.print("Download Complete"); display.drawRect(0, 11, display.width(), 1); - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.setColor(DisplayDriver::LIGHT); display.setCursor(0, 16); display.print("Saved to /books/:"); @@ -3277,7 +3280,7 @@ private: display.print("Download Failed"); display.drawRect(0, 11, display.width(), 1); - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.setColor(DisplayDriver::LIGHT); display.setCursor(0, 18); display.print(_fetchError.c_str()); @@ -3314,7 +3317,7 @@ private: return; } - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.setColor(DisplayDriver::LIGHT); // Determine page bounds @@ -3476,9 +3479,16 @@ private: // ---- Layout Initialization ---- void initLayout(DisplayDriver& display) { + // Re-init if font preference changed since last layout + uint8_t curFont = _prefs ? _prefs->large_font : 0; + if (_initialized && curFont != _lastFontPref) { + _initialized = false; + Serial.println("WebReader: font changed, recalculating layout"); + } if (_initialized) return; + _lastFontPref = curFont; - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); uint16_t mWidth = display.getTextWidth("M"); if (mWidth > 0) { _charsPerLine = display.width() / mWidth; @@ -3487,6 +3497,19 @@ private: _charsPerLine = 40; _lineHeight = 5; } + // Proportional font: use average-width measurement instead of M-width + if (_prefs && _prefs->large_font && mWidth > 0) { + const char* sample = "the quick brown fox jumps over lazy dog"; + uint16_t sampleW = display.getTextWidth(sample); + int sampleLen = strlen(sample); + if (sampleW > 0 && sampleLen > 0) { + _charsPerLine = (display.width() * sampleLen * 70) / ((int)sampleW * 100); + } + } + // Large font: formula above assumes built-in 6x8 ratio — too small for 9pt + if (_prefs && _prefs->large_font) { + _lineHeight = _prefs->smallLineH(); + } _footerHeight = 14; int textAreaHeight = display.height() - _footerHeight; @@ -3931,7 +3954,7 @@ private: if (_activeForm < 0 || _activeForm >= _formCount) return; WebForm& form = _forms[_activeForm]; - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); // Header display.setColor(DisplayDriver::GREEN); @@ -3954,7 +3977,7 @@ private: display.drawRect(0, 9, display.width(), 1); int y = 12; - int lineH = 10; // Taller lines for form fields + int lineH = _prefs->smallLineH() + 1; // Taller lines for form fields int visCount = getVisibleFieldCount(form); // Render each visible field @@ -4662,9 +4685,9 @@ private: display.print("IRC Setup"); display.drawRect(0, 11, display.width(), 1); - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); int y = 16; - int lineH = 10; + int lineH = _prefs->smallLineH() + 1; const char* labels[] = {"Server:", "Port:", "Nick:", "Channel:", "[ Connect ]"}; const char* chanDisp = (_ircChannel[0] != '\0') ? _ircChannel : "(none)"; @@ -4822,7 +4845,7 @@ private: display.print(header); // Connection indicator on right - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); if (!_ircConnected) { display.setColor(DisplayDriver::YELLOW); display.setCursor(display.width() - 42, -3); @@ -4848,7 +4871,7 @@ private: if (_ircComposing) { // Compose text just above separator (tiny font to match messages) - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); display.setColor(DisplayDriver::LIGHT); display.setCursor(0, footerY - 12); char compDisp[IRC_COMPOSE_MAX + 4]; @@ -4878,10 +4901,10 @@ private: } // Message area - display.setTextSize(0); + display.setTextSize(_prefs->smallTextSize()); int msgAreaTop = 14; int msgAreaBottom = _ircComposing ? footerY - 16 : footerY - 4; - int lineH = 8; + int lineH = _prefs->smallLineH() - 1; int scrollBarW = 4; int lineW = _charsPerLine - 1; // Reserve space for scroll bar _ircLinesPerPage = (msgAreaBottom - msgAreaTop) / lineH; @@ -5065,8 +5088,8 @@ private: } public: - WebReaderScreen(UITask* task) - : _task(task), _mode(HOME), _initialized(false), _display(nullptr), + WebReaderScreen(UITask* task, NodePrefs* prefs = nullptr) + : _task(task), _prefs(prefs), _mode(HOME), _initialized(false), _lastFontPref(0), _display(nullptr), _charsPerLine(40), _linesPerPage(15), _lineHeight(5), _footerHeight(14), _wifiState(WIFI_IDLE), _ssidCount(0), _selectedSSID(0), _wifiPassLen(0), _urlLen(0), _urlCursor(0), @@ -5150,7 +5173,7 @@ public: _display->print("Web Reader"); _display->drawRect(0, 11, _display->width(), 1); _display->setColor(DisplayDriver::LIGHT); - _display->setTextSize(0); + _display->setTextSize(_prefs->smallTextSize()); _display->setCursor(0, 18); _display->print("Connecting to WiFi..."); _display->endFrame(); diff --git a/variants/lilygo_t5s3_epaper_pro/T5S3Board.cpp b/variants/lilygo_t5s3_epaper_pro/T5S3Board.cpp index 4d60ace7..7704d8e0 100644 --- a/variants/lilygo_t5s3_epaper_pro/T5S3Board.cpp +++ b/variants/lilygo_t5s3_epaper_pro/T5S3Board.cpp @@ -188,9 +188,15 @@ int16_t T5S3Board::getBattTemperature() { } // ---- BQ27220 Design Capacity configuration ---- -// Identical procedure to TDeckBoard — sets 1500 mAh for T5S3's larger cell. -// The BQ27220 ships with 3000 mAh default. This writes once on first boot -// and persists in battery-backed RAM. +// The BQ27220 ships with a 3000 mAh default. T5S3 uses a 1500 mAh cell. +// This function checks on boot and writes the correct value via the +// MAC Data Memory interface if needed. The value persists in battery-backed +// RAM, so this typically only writes once (or after a full battery disconnect). +// +// When DC and DE are already correct but FCC is stuck (common after initial +// flash), the root cause is Qmax Cell 0 (0x9106) and stored FCC (0x929D) +// retaining factory 3000 mAh defaults. This function detects and fixes all +// three layers: DC/DE, Qmax, and stored FCC. bool T5S3Board::configureFuelGauge(uint16_t designCapacity_mAh) { #if HAS_BQ27220 @@ -198,23 +204,169 @@ bool T5S3Board::configureFuelGauge(uint16_t designCapacity_mAh) { Serial.printf("BQ27220: Design Capacity = %d mAh (target %d)\n", currentDC, designCapacity_mAh); if (currentDC == designCapacity_mAh) { + // Design Capacity correct, but check if Full Charge Capacity is sane. uint16_t fcc = bq27220_read16(BQ27220_REG_FULL_CAP); - Serial.printf("BQ27220: Design Capacity correct, FCC=%d mAh\n", fcc); - if (fcc < designCapacity_mAh * 3 / 2) { - return true; // FCC is sane, nothing to do + Serial.printf("BQ27220: Design Capacity already correct, FCC=%d mAh\n", fcc); + if (fcc >= designCapacity_mAh * 3 / 2) { + // FCC is >=150% of design — stale from factory defaults (typically 3000 mAh). + uint16_t designEnergy = (uint16_t)((uint32_t)designCapacity_mAh * 37 / 10); + Serial.printf("BQ27220: FCC %d >> DC %d, checking Design Energy (target %d mWh)\n", + fcc, designCapacity_mAh, designEnergy); + + // Unseal to read data memory and issue RESET + bq27220_writeControl(0x0414); delay(2); + bq27220_writeControl(0x3672); delay(2); + // Full Access + bq27220_writeControl(0xFFFF); delay(2); + bq27220_writeControl(0xFFFF); delay(2); + + // Enter CFG_UPDATE to access data memory + bq27220_writeControl(0x0090); + bool ready = false; + for (int i = 0; i < 50; i++) { + delay(20); + uint16_t opSt = bq27220_read16(BQ27220_REG_OP_STATUS); + if (opSt & 0x0400) { ready = true; break; } + } + if (ready) { + // Read Design Energy at data memory address 0x92A1 + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x3E); Wire.write(0xA1); Wire.write(0x92); + Wire.endTransmission(); + delay(10); + uint8_t oldMSB = bq27220_read8(0x40); + uint8_t oldLSB = bq27220_read8(0x41); + uint16_t currentDE = (oldMSB << 8) | oldLSB; + + if (currentDE != designEnergy) { + // Design Energy actually needs updating — write it + uint8_t oldChk = bq27220_read8(0x60); + uint8_t dLen = bq27220_read8(0x61); + uint8_t newMSB = (designEnergy >> 8) & 0xFF; + uint8_t newLSB = designEnergy & 0xFF; + uint8_t temp = (255 - oldChk - oldMSB - oldLSB); + uint8_t newChk = 255 - ((temp + newMSB + newLSB) & 0xFF); + + Serial.printf("BQ27220: DE old=%d new=%d mWh, writing\n", currentDE, designEnergy); + + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x3E); Wire.write(0xA1); Wire.write(0x92); + Wire.write(newMSB); Wire.write(newLSB); + Wire.endTransmission(); + delay(5); + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x60); Wire.write(newChk); Wire.write(dLen); + Wire.endTransmission(); + delay(10); + + // Exit with reinit since we actually changed data + bq27220_writeControl(0x0091); // EXIT_CFG_UPDATE_REINIT + delay(200); + Serial.println("BQ27220: Design Energy written, exited CFG_UPDATE"); + } else { + // DC and DE are both correct, but FCC is stuck. + // Root cause: Qmax Cell 0 (0x9106) and stored FCC (0x929D) retain + // factory 3000 mAh defaults. Overwrite both with designCapacity_mAh. + Serial.printf("BQ27220: DE correct (%d mWh) — fixing Qmax + stored FCC\n", currentDE); + + // --- Helper lambda for MAC data memory 2-byte write --- + // Reads old value + checksum, computes differential checksum, writes new value. + auto writeDM16 = [](uint16_t addr, uint16_t newVal) -> bool { + // Select address + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x3E); + Wire.write(addr & 0xFF); + Wire.write((addr >> 8) & 0xFF); + Wire.endTransmission(); + delay(10); + + uint8_t oldMSB = bq27220_read8(0x40); + uint8_t oldLSB = bq27220_read8(0x41); + uint8_t oldChk = bq27220_read8(0x60); + uint8_t dLen = bq27220_read8(0x61); + uint16_t oldVal = (oldMSB << 8) | oldLSB; + + if (oldVal == newVal) { + Serial.printf("BQ27220: [0x%04X] already %d, skip\n", addr, newVal); + return true; // already correct + } + + uint8_t newMSB = (newVal >> 8) & 0xFF; + uint8_t newLSB = newVal & 0xFF; + uint8_t temp = (255 - oldChk - oldMSB - oldLSB); + uint8_t newChk = 255 - ((temp + newMSB + newLSB) & 0xFF); + + Serial.printf("BQ27220: [0x%04X] %d -> %d\n", addr, oldVal, newVal); + + // Write new value + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x3E); + Wire.write(addr & 0xFF); + Wire.write((addr >> 8) & 0xFF); + Wire.write(newMSB); + Wire.write(newLSB); + Wire.endTransmission(); + delay(5); + + // Write checksum + Wire.beginTransmission(BQ27220_I2C_ADDR); + Wire.write(0x60); + Wire.write(newChk); + Wire.write(dLen); + Wire.endTransmission(); + delay(10); + return true; + }; + + // Overwrite Qmax Cell 0 (IT Cfg) — this is what FCC is derived from + writeDM16(0x9106, designCapacity_mAh); + + // Overwrite stored FCC reference (Gas Gauging, 2 bytes before DC) + writeDM16(0x929D, designCapacity_mAh); + + // Exit with reinit to apply the new values + bq27220_writeControl(0x0091); // EXIT_CFG_UPDATE_REINIT + delay(200); + Serial.println("BQ27220: Qmax + stored FCC updated, exited CFG_UPDATE"); + } + } else { + Serial.println("BQ27220: Failed to enter CFG_UPDATE for DE check"); + } + + // Seal first, then issue RESET. + // RESET forces the gauge to fully reinitialize its Impedance Track + // algorithm and recalculate FCC from the current DC/DE values. + bq27220_writeControl(0x0030); // SEAL + delay(5); + Serial.println("BQ27220: Issuing RESET to force FCC recalculation..."); + bq27220_writeControl(0x0041); // RESET + delay(2000); // Full reset needs generous settle time + + fcc = bq27220_read16(BQ27220_REG_FULL_CAP); + Serial.printf("BQ27220: FCC after RESET: %d mAh (target <= %d)\n", fcc, designCapacity_mAh); + + if (fcc > designCapacity_mAh * 3 / 2) { + // RESET didn't fix FCC — the gauge IT algorithm is stubbornly + // retaining its learned value. This typically resolves after one + // full charge/discharge cycle. Software clamp in + // getFullChargeCapacity() ensures correct display regardless. + Serial.printf("BQ27220: FCC still stale at %d — software clamp active\n", fcc); + } } - // FCC is stale from factory — fall through to reconfigure - Serial.printf("BQ27220: FCC %d >> DC %d, reconfiguring\n", fcc, designCapacity_mAh); + return true; } - // Unseal + Serial.printf("BQ27220: Updating Design Capacity from %d to %d mAh\n", currentDC, designCapacity_mAh); + + // Step 1: Unseal (default unseal keys) bq27220_writeControl(0x0414); delay(2); bq27220_writeControl(0x3672); delay(2); - // Full Access + + // Step 2: Full Access bq27220_writeControl(0xFFFF); delay(2); bq27220_writeControl(0xFFFF); delay(2); - // Enter CFG_UPDATE + // Step 3: Enter CFG_UPDATE bq27220_writeControl(0x0090); bool cfgReady = false; for (int i = 0; i < 50; i++) { @@ -229,7 +381,7 @@ bool T5S3Board::configureFuelGauge(uint16_t designCapacity_mAh) { return false; } - // Write Design Capacity at 0x929F + // Step 4: Write Design Capacity at 0x929F Wire.beginTransmission(BQ27220_I2C_ADDR); Wire.write(0x3E); Wire.write(0x9F); Wire.write(0x92); Wire.endTransmission(); @@ -255,7 +407,7 @@ bool T5S3Board::configureFuelGauge(uint16_t designCapacity_mAh) { Wire.endTransmission(); delay(10); - // Write Design Energy at 0x92A1 + // Step 4a: Write Design Energy at 0x92A1 { uint16_t designEnergy = (uint16_t)((uint32_t)designCapacity_mAh * 37 / 10); Wire.beginTransmission(BQ27220_I2C_ADDR); @@ -271,6 +423,9 @@ bool T5S3Board::configureFuelGauge(uint16_t designCapacity_mAh) { uint8_t deTemp = (255 - deOldChk - deOldMSB - deOldLSB); uint8_t deNewChk = 255 - ((deTemp + deNewMSB + deNewLSB) & 0xFF); + Serial.printf("BQ27220: Design Energy: old=%d new=%d mWh\n", + (deOldMSB << 8) | deOldLSB, designEnergy); + Wire.beginTransmission(BQ27220_I2C_ADDR); Wire.write(0x3E); Wire.write(0xA1); Wire.write(0x92); Wire.write(deNewMSB); Wire.write(deNewLSB); @@ -282,16 +437,17 @@ bool T5S3Board::configureFuelGauge(uint16_t designCapacity_mAh) { delay(10); } - // Exit CFG_UPDATE with reinit + // Step 5: Exit CFG_UPDATE with reinit bq27220_writeControl(0x0091); + Serial.println("BQ27220: Sent EXIT_CFG_UPDATE_REINIT, waiting..."); delay(200); - // Seal + // Step 6: Seal bq27220_writeControl(0x0030); delay(5); - // Force RESET to reinitialize FCC - bq27220_writeControl(0x0041); + // Step 7: Force RESET to reinitialize FCC from new DC/DE + bq27220_writeControl(0x0041); // RESET delay(1000); uint16_t verifyDC = bq27220_read16(BQ27220_REG_DESIGN_CAP); @@ -302,4 +458,4 @@ bool T5S3Board::configureFuelGauge(uint16_t designCapacity_mAh) { #else return false; #endif -} +} \ No newline at end of file