mirror of
https://github.com/pelgraine/Meck.git
synced 2026-05-01 11:02:31 +02:00
tdpro large font pref option; various large font ui fixes; fix fcc recognition in t5s3 to match 1500
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
@@ -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());
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
// JPEG decoder for cover art — JPEGDEC by bitbank2
|
||||
#include <JPEGDEC.h>
|
||||
|
||||
#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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <SD.h>
|
||||
#include <vector>
|
||||
#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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <vector>
|
||||
#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<long>& 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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user