diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index b4e7de88..9b7617b8 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -337,6 +337,58 @@ } #endif +// ============================================================================= +// T5S3 E-Paper Pro — GT911 Touch Input +// ============================================================================= +#if defined(LilyGo_T5S3_EPaper_Pro) + #include "TouchDrvGT911.hpp" + #include + #include "TextReaderScreen.h" + #include "NotesScreen.h" + #include "ContactsScreen.h" + #include "ChannelScreen.h" + #include "SettingsScreen.h" + #include "RepeaterAdminScreen.h" + #include "DiscoveryScreen.h" + + static TouchDrvGT911 gt911Touch; + static bool gt911Ready = false; + static bool sdCardReady = false; // T5S3 SD card state + + // Touch state machine — supports tap, long press, and swipe + static bool touchDown = false; + static unsigned long touchDownTime = 0; + static int16_t touchDownX = 0; + static int16_t touchDownY = 0; + static int16_t touchLastX = 0; + static int16_t touchLastY = 0; + static unsigned long lastTouchSeenMs = 0; // Last time getPoint() returned true + #define TOUCH_LONG_PRESS_MS 500 + #define TOUCH_SWIPE_THRESHOLD 60 // Min pixels to count as a swipe (physical) + #define TOUCH_LIFT_DEBOUNCE_MS 150 // No-touch duration before "finger lifted" + #define TOUCH_MIN_INTERVAL_MS 300 // Min ms between accepted events + static bool longPressHandled = false; + static bool swipeHandled = false; + static bool touchCooldown = false; + static unsigned long lastTouchEventMs = 0; + + // Read GT911 in landscape orientation (960×540) + // GT911 reports portrait (540×960), rotate: x=raw_y, y=540-1-raw_x + // Note: Do NOT gate on GT911_PIN_INT — it pulses briefly per event + // and goes high between reports, causing drags to look like taps. + // Polling getPoint() directly works for continuous touch tracking. + static bool readTouchLandscape(int16_t* outX, int16_t* outY) { + if (!gt911Ready) return false; + int16_t raw_x, raw_y; + if (gt911Touch.getPoint(&raw_x, &raw_y)) { + *outX = raw_y; + *outY = EPD_HEIGHT - 1 - raw_x; + return true; + } + return false; + } +#endif + // Board-agnostic: CPU frequency scaling and AGC reset CPUPowerManager cpuPower; #define AGC_RESET_INTERVAL_MS 500 @@ -451,6 +503,122 @@ MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store /* END GLOBAL OBJECTS */ +// T5S3 touch mapping — must be after ui_task declaration +#if defined(LilyGo_T5S3_EPaper_Pro) + // Home screen tile grid — physical touch zones (960×540) + #define TILE_Y_START 150 + #define TILE_Y_MID 300 + #define TILE_Y_END 460 + #define TILE_COL1 325 + #define TILE_COL2 640 + + // Map a single tap based on current screen context + static char mapTouchTap(int16_t x, int16_t y) { + // --- Status bar tap (top ~80px) → go home from any non-home screen --- + if (y < 80 && !ui_task.isOnHomeScreen()) { + ui_task.gotoHomeScreen(); + return 0; + } + + // Home screen FIRST page: tile taps + if (ui_task.isOnHomeScreen() && ui_task.isHomeShowingTiles()) { + if (y >= TILE_Y_START && y < TILE_Y_END) { + int col = (x < TILE_COL1) ? 0 : (x < TILE_COL2) ? 1 : 2; + int row = (y < TILE_Y_MID) ? 0 : 1; + if (row == 0 && col == 0) { ui_task.gotoChannelScreen(); return 0; } + if (row == 0 && col == 1) { ui_task.gotoContactsScreen(); return 0; } + if (row == 0 && col == 2) { ui_task.gotoSettingsScreen(); return 0; } + if (row == 1 && col == 0) { ui_task.gotoTextReader(); return 0; } + if (row == 1 && col == 1) { ui_task.gotoNotesScreen(); return 0; } + if (row == 1 && col == 2) { ui_task.gotoDiscoveryScreen(); return 0; } + } + // Tap outside tiles — cycle home pages + return (char)KEY_NEXT; + } + + // Home screen (non-tile pages): tap cycles pages + if (ui_task.isOnHomeScreen()) { + return (char)KEY_NEXT; + } + + // Reader (reading mode): tap = next page + if (ui_task.isOnTextReader()) { + TextReaderScreen* reader = (TextReaderScreen*)ui_task.getTextReaderScreen(); + if (reader && reader->isReading()) { + return 'd'; // next page + } + return KEY_ENTER; // file list: open selected + } + + // All other screens: tap = select + return KEY_ENTER; + } + + // Map a swipe direction to a key + static char mapTouchSwipe(int16_t dx, int16_t dy) { + bool horizontal = abs(dx) > abs(dy); + + // Reader (reading mode): swipe left/right for page turn + if (ui_task.isOnTextReader()) { + TextReaderScreen* reader = (TextReaderScreen*)ui_task.getTextReaderScreen(); + if (reader && reader->isReading()) { + if (horizontal) { + return (dx < 0) ? 'd' : 'a'; // swipe left=next, right=prev + } + // Vertical swipe in reader: also page turn (natural scroll) + return (dy > 0) ? 'd' : 'a'; // swipe down=next, up=prev + } + } + + // Home screen: horizontal swipe cycles pages + if (ui_task.isOnHomeScreen()) { + return (char)KEY_NEXT; + } + + // Settings: horizontal swipe → a/d for picker/number editing + if (ui_task.isOnSettingsScreen() && horizontal) { + return (dx < 0) ? 'd' : 'a'; // swipe left=next option, right=prev + } + + // Channel screen: horizontal swipe → a/d to switch channels + if (ui_task.isOnChannelScreen() && horizontal) { + return (dx < 0) ? 'd' : 'a'; // swipe left=next channel, right=prev + } + + // Contacts screen: horizontal swipe → a/d to change filter + if (ui_task.isOnContactsScreen() && horizontal) { + return (dx < 0) ? 'd' : 'a'; // swipe left=next filter, right=prev + } + + // All other screens: vertical swipe scrolls + if (!horizontal) { + return (dy > 0) ? 's' : 'w'; // swipe down=scroll down, up=scroll up + } + + return 0; // ignore horizontal swipes on non-applicable screens + } + + // Map a long press to a key + static char mapTouchLongPress(int16_t x, int16_t y) { + // Home screen: long press cycles pages + if (ui_task.isOnHomeScreen()) { + return (char)KEY_NEXT; + } + + // Reader reading: long press = close book + if (ui_task.isOnTextReader()) { + TextReaderScreen* reader = (TextReaderScreen*)ui_task.getTextReaderScreen(); + if (reader && reader->isReading()) { + return 'q'; + } + return KEY_ENTER; // file list: open + } + + // Default: enter/select (settings toggle, etc.) + return KEY_ENTER; + } +#endif + void halt() { while (1) ; } @@ -649,6 +817,37 @@ void setup() { MESH_DEBUG_PRINTLN("setup() - SD card not available after 3 attempts"); } } + #elif defined(LilyGo_T5S3_EPaper_Pro) && defined(HAS_SDCARD) + { + // T5S3: SD card shares LoRa SPI bus (SCK=14, MOSI=13, MISO=21) + // LoRa SPI already initialized by target.cpp. Create a local HSPI + // reference for SD init (same hardware peripheral, different CS). + static SPIClass sdSpi(HSPI); + sdSpi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI, SDCARD_CS); + + pinMode(SDCARD_CS, OUTPUT); + digitalWrite(SDCARD_CS, HIGH); + pinMode(P_LORA_NSS, OUTPUT); + digitalWrite(P_LORA_NSS, HIGH); + delay(100); + + bool mounted = false; + for (int attempt = 0; attempt < 3 && !mounted; attempt++) { + if (attempt > 0) { + digitalWrite(SDCARD_CS, HIGH); + delay(250); + Serial.printf("setup() - SD card retry %d/3\n", attempt + 1); + } + mounted = SD.begin(SDCARD_CS, sdSpi, 4000000); + } + + if (mounted) { + sdCardReady = true; + Serial.println("setup() - SD card initialized"); + } else { + Serial.println("setup() - SD card not available"); + } + } #endif MESH_DEBUG_PRINTLN("setup() - about to call store.begin()"); @@ -759,6 +958,18 @@ void setup() { } #endif + // Initialize GT911 touch (T5S3 E-Paper Pro) + #if defined(LilyGo_T5S3_EPaper_Pro) + gt911Touch.setPins(GT911_PIN_RST, GT911_PIN_INT); + pinMode(GT911_PIN_INT, INPUT_PULLUP); // Ensure INT pin has pullup for clean transitions + if (gt911Touch.begin(Wire, GT911_SLAVE_ADDRESS_L, GT911_PIN_SDA, GT911_PIN_SCL)) { + gt911Ready = true; + Serial.println("setup() - GT911 touch initialized"); + } else { + Serial.println("setup() - GT911 touch FAILED"); + } + #endif + // --------------------------------------------------------------------------- // SD card is already initialized (early init above). // Now set up SD-dependent features: message history + text reader. @@ -826,8 +1037,36 @@ void setup() { } #endif - // --------------------------------------------------------------------------- - // First-boot onboarding detection + // T5S3 SD-dependent features + #if defined(LilyGo_T5S3_EPaper_Pro) && defined(HAS_SDCARD) + if (sdCardReady) { + // Channel message history + ChannelScreen* chanScr = (ChannelScreen*)ui_task.getChannelScreen(); + if (chanScr) { + chanScr->setSDReady(true); + if (chanScr->loadFromSD()) { + Serial.println("setup() - Message history loaded from SD"); + } + } + + // Text reader — set SD ready and pre-index books + TextReaderScreen* reader = (TextReaderScreen*)ui_task.getTextReaderScreen(); + if (reader) { + reader->setSDReady(true); + if (disp) { + cpuPower.setBoost(); + reader->bootIndex(*disp); + } + } + + // Notes screen + NotesScreen* notesScr = (NotesScreen*)ui_task.getNotesScreen(); + if (notesScr) { + notesScr->setSDReady(true); + } + Serial.println("setup() - SD features initialized"); + } + #endif // Check if node name is still the default hex prefix (first 4 bytes of pub key) // If so, launch onboarding wizard to set name and radio preset // --------------------------------------------------------------------------- @@ -1120,6 +1359,102 @@ void loop() { handleKeyboardInput(); #endif + // --------------------------------------------------------------------------- + // T5S3 GT911 Touch Input — tap/swipe/long-press state machine + // Gestures: + // Tap = finger down + up with minimal movement → select/open + // Swipe = finger drag > threshold → scroll/page turn + // Long press = finger held > 500ms without moving → edit/enter + // After processing an event, cooldown waits for finger lift before next event. + // --------------------------------------------------------------------------- + #if defined(LilyGo_T5S3_EPaper_Pro) + { + int16_t tx, ty; + bool gotPoint = readTouchLandscape(&tx, &ty); + unsigned long now = millis(); + + if (gotPoint) { + lastTouchSeenMs = now; // Track when we last saw a valid touch report + } + + // Determine if finger is "present" — GT911 getPoint() only returns true + // once per report cycle (~10ms), then returns false until the next report. + // During a blocking e-ink refresh (~1s), many cycles are missed. + // So "finger lifted" = no valid report for TOUCH_LIFT_DEBOUNCE_MS. + bool fingerPresent = (now - lastTouchSeenMs) < TOUCH_LIFT_DEBOUNCE_MS; + + // Rate limit — after processing an event, wait for finger lift + cooldown + if (touchCooldown) { + if (!fingerPresent && (now - lastTouchEventMs) >= TOUCH_MIN_INTERVAL_MS) { + touchCooldown = false; + touchDown = false; + } + } + else if (gotPoint && !touchDown) { + // Finger just touched down (first valid report) + touchDown = true; + touchDownTime = now; + touchDownX = tx; + touchDownY = ty; + touchLastX = tx; + touchLastY = ty; + longPressHandled = false; + swipeHandled = false; + } + else if (touchDown && fingerPresent) { + // Finger still down — update position if we got a new point + if (gotPoint) { + touchLastX = tx; + touchLastY = ty; + } + + int16_t dx = touchLastX - touchDownX; + int16_t dy = touchLastY - touchDownY; + int16_t dist = abs(dx) > abs(dy) ? abs(dx) : abs(dy); + + // Swipe detection — fire once when threshold exceeded + if (!swipeHandled && !longPressHandled && dist >= TOUCH_SWIPE_THRESHOLD) { + swipeHandled = true; + Serial.printf("[Touch] SWIPE dx=%d dy=%d\n", dx, dy); + char c = mapTouchSwipe(dx, dy); + if (c) { + ui_task.injectKey(c); + cpuPower.setBoost(); + } + lastTouchEventMs = now; + touchCooldown = true; + } + // Long press — only if finger hasn't moved much + else if (!longPressHandled && !swipeHandled && dist < TOUCH_SWIPE_THRESHOLD && + (now - touchDownTime) >= TOUCH_LONG_PRESS_MS) { + longPressHandled = true; + Serial.printf("[Touch] LONG PRESS at (%d,%d)\n", touchDownX, touchDownY); + char c = mapTouchLongPress(touchDownX, touchDownY); + if (c) { + ui_task.injectKey(c); + cpuPower.setBoost(); + } + lastTouchEventMs = now; + touchCooldown = true; + } + } + else if (touchDown && !fingerPresent) { + // Finger lifted (no report for TOUCH_LIFT_DEBOUNCE_MS) + touchDown = false; + if (!longPressHandled && !swipeHandled) { + Serial.printf("[Touch] TAP at (%d,%d)\n", touchDownX, touchDownY); + char c = mapTouchTap(touchDownX, touchDownY); + if (c) { + ui_task.injectKey(c); + } + cpuPower.setBoost(); + lastTouchEventMs = now; + touchCooldown = true; + } + } + } + #endif + // Poll touch input for phone dialer numpad // Hybrid debounce: finger-up detection + 150ms minimum between accepted taps. // The CST328 INT pin is pulse-based (not level), so getPoint() can return diff --git a/examples/companion_radio/ui-new/ChannelScreen.h b/examples/companion_radio/ui-new/ChannelScreen.h index 151fb9c1..3de22f87 100644 --- a/examples/companion_radio/ui-new/ChannelScreen.h +++ b/examples/companion_radio/ui-new/ChannelScreen.h @@ -651,7 +651,7 @@ public: display.setCursor(display.width() - display.getTextWidth(copyHint) - 2, footerY); display.print(copyHint); -#if AUTO_OFF_MILLIS == 0 +#ifdef USE_EINK return 5000; #else return 1000; @@ -735,7 +735,11 @@ public: int availH = maxY - y; if (maxFillH > availH) maxFillH = availH; display.setColor(DisplayDriver::LIGHT); + #if defined(LilyGo_T5S3_EPaper_Pro) + display.fillRect(0, y, contentW, maxFillH); +#else display.fillRect(0, y + 5, contentW, maxFillH); +#endif } // Time indicator with hop count - inline on same line as message start @@ -888,7 +892,11 @@ public: if (maxFillH > availH) maxFillH = availH; if (usedH < maxFillH) { display.setColor(DisplayDriver::DARK); +#if defined(LilyGo_T5S3_EPaper_Pro) + display.fillRect(0, y, contentW, maxFillH - usedH); +#else display.fillRect(0, y + 5, contentW, maxFillH - usedH); +#endif } } @@ -943,6 +951,10 @@ public: display.setCursor(0, footerY); display.setColor(DisplayDriver::YELLOW); +#if defined(LilyGo_T5S3_EPaper_Pro) + display.setTextSize(0); + display.drawTextCentered(display.width() / 2, footerY, "Swipe: Scroll Tap: Select Boot: Home"); +#else // Left side: abbreviated controls if (_replySelectMode) { display.print("W/S:Sel V:Pth Q:X"); @@ -955,8 +967,9 @@ public: display.setCursor(display.width() - display.getTextWidth(rightText) - 2, footerY); display.print(rightText); } +#endif -#if AUTO_OFF_MILLIS == 0 // e-ink +#ifdef USE_EINK return 5000; #else return 1000; diff --git a/examples/companion_radio/ui-new/Contactsscreen.h b/examples/companion_radio/ui-new/Contactsscreen.h index 1107fd96..7e71dac8 100644 --- a/examples/companion_radio/ui-new/Contactsscreen.h +++ b/examples/companion_radio/ui-new/Contactsscreen.h @@ -237,7 +237,11 @@ public: // Highlight: fill LIGHT rect first, then draw DARK text on top if (selected) { display.setColor(DisplayDriver::LIGHT); +#if defined(LilyGo_T5S3_EPaper_Pro) + display.fillRect(0, y, display.width(), lineHeight); +#else display.fillRect(0, y + 5, display.width(), lineHeight); +#endif display.setColor(DisplayDriver::DARK); } else { display.setColor(DisplayDriver::LIGHT); @@ -297,6 +301,10 @@ public: display.drawRect(0, footerY - 2, display.width(), 1); display.setColor(DisplayDriver::YELLOW); +#if defined(LilyGo_T5S3_EPaper_Pro) + display.setTextSize(0); + display.drawTextCentered(display.width() / 2, footerY, "Swipe: Scroll Tap: Select Boot: Home"); +#else // Left: Q:Bk display.setCursor(0, footerY); display.print("Q:Bk"); @@ -310,6 +318,7 @@ public: const char* right = "F:Dscvr"; display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); display.print(right); +#endif return 5000; // e-ink: next render after 5s } diff --git a/examples/companion_radio/ui-new/Settingsscreen.h b/examples/companion_radio/ui-new/Settingsscreen.h index a454c46c..9b4a53d9 100644 --- a/examples/companion_radio/ui-new/Settingsscreen.h +++ b/examples/companion_radio/ui-new/Settingsscreen.h @@ -586,7 +586,13 @@ public: // Selection highlight if (selected) { display.setColor(DisplayDriver::LIGHT); +#if defined(LilyGo_T5S3_EPaper_Pro) + // FreeSans12pt: baseline at (y+5)*scale_y, ascent ~17px above. + // 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); +#endif display.setColor(DisplayDriver::DARK); } else { display.setColor(DisplayDriver::LIGHT); @@ -992,6 +998,14 @@ public: display.setColor(DisplayDriver::YELLOW); display.setCursor(0, footerY); +#if defined(LilyGo_T5S3_EPaper_Pro) + display.setTextSize(0); + if (_editMode == EDIT_NONE) { + display.drawTextCentered(display.width() / 2, footerY, "Swipe: Scroll Tap: Select Hold: Edit Boot: Home"); + } else { + display.print("Editing..."); + } +#else if (_editMode == EDIT_TEXT) { display.print("Type, Enter:Ok Q:Cancel"); #ifdef MECK_WIFI_COMPANION @@ -1020,6 +1034,7 @@ public: display.setCursor(display.width() - display.getTextWidth(r) - 2, footerY); display.print(r); } +#endif return _editMode != EDIT_NONE ? 700 : 1000; } diff --git a/examples/companion_radio/ui-new/Textreaderscreen.h b/examples/companion_radio/ui-new/Textreaderscreen.h index 01f58c1b..656d17e2 100644 --- a/examples/companion_radio/ui-new/Textreaderscreen.h +++ b/examples/companion_radio/ui-new/Textreaderscreen.h @@ -863,9 +863,13 @@ private: if (selected) { display.setColor(DisplayDriver::LIGHT); +#if defined(LilyGo_T5S3_EPaper_Pro) + display.fillRect(0, y, display.width(), listLineH); +#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); +#endif display.setColor(DisplayDriver::DARK); } else { display.setColor(DisplayDriver::LIGHT); @@ -918,13 +922,19 @@ private: display.setTextSize(1); int footerY = display.height() - 12; display.drawRect(0, footerY - 2, display.width(), 1); - display.setCursor(0, footerY); display.setColor(DisplayDriver::YELLOW); + +#if defined(LilyGo_T5S3_EPaper_Pro) + display.setTextSize(0); + display.drawTextCentered(display.width() / 2, footerY, "Swipe: Scroll Tap: Open Boot: Home"); +#else + display.setCursor(0, footerY); display.print("Q:Back W/S:Nav"); const char* right = "Ent:Open"; display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); display.print(right); +#endif } void renderPage(DisplayDriver& display) { @@ -1002,12 +1012,22 @@ private: char status[30]; int pct = _totalPages > 1 ? (_currentPage * 100) / (_totalPages - 1) : 100; sprintf(status, "%d/%d %d%%", _currentPage + 1, _totalPages, pct); + +#if defined(LilyGo_T5S3_EPaper_Pro) + display.setTextSize(0); + display.setCursor(0, footerY); + display.print(status); + const char* right = "Swipe: Page Tap: Next Hold: Close"; + display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); + display.print(right); +#else display.setCursor(0, footerY); display.print(status); const char* right = "W/S:Nav Q:Back"; display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); display.print(right); +#endif } public: @@ -1036,8 +1056,22 @@ public: if (tenCharsW > 0) { _charsPerLine = (display.width() * 10) / tenCharsW; } +#if defined(LilyGo_T5S3_EPaper_Pro) + // FreeSans12pt is proportional — "M" is the widest character. + // Using M-width gives ~56 chars/line but actual text only fills 60% of screen. + // Re-measure with representative lowercase text for realistic average width. + { + uint16_t sampleW = display.getTextWidth("abcdefghijklmno"); // 15 chars + if (sampleW > 0) { + _charsPerLine = (display.width() * 15) / sampleW; + } + } + if (_charsPerLine < 15) _charsPerLine = 15; + if (_charsPerLine > 120) _charsPerLine = 120; // Proportional fonts can fit many chars +#else if (_charsPerLine < 15) _charsPerLine = 15; if (_charsPerLine > 60) _charsPerLine = 60; +#endif // Line height for built-in 6x8 font: // setCursor adds +5 to y, so effective text top = (y+5)*scale_y @@ -1052,6 +1086,13 @@ public: _lineHeight = 5; // Safe fallback } +#if defined(LilyGo_T5S3_EPaper_Pro) + // T5S3 uses FreeSans12pt/FreeSerif12pt for size 0 (yAdvance=29px). + // Line height in virtual coords: 29px / scale_y(4.22) ≈ 7 units. + // Add 1 unit for comfortable spacing. + _lineHeight = 8; +#endif + _headerHeight = 0; // No header in reading mode (maximize text area) _footerHeight = 14; int textAreaHeight = display.height() - _headerHeight - _footerHeight; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 0ca59326..86cacdc9 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -245,6 +245,9 @@ public: int render(DisplayDriver& display) override { char tmp[80]; +#if defined(LilyGo_T5S3_EPaper_Pro) + _task->setHomeShowingTiles(false); // Reset — only set true on FIRST page +#endif // node name (tinyfont to avoid overlapping clock) display.setTextSize(0); display.setColor(DisplayDriver::GREEN); @@ -299,6 +302,9 @@ public: } if (_page == HomePage::FIRST) { +#if defined(LilyGo_T5S3_EPaper_Pro) + _task->setHomeShowingTiles(true); +#endif int y = 20; display.setColor(DisplayDriver::YELLOW); display.setTextSize(2); @@ -332,6 +338,54 @@ public: } #endif + // ----- T5S3: Tappable tile grid (touch-friendly home screen) ----- +#if defined(LilyGo_T5S3_EPaper_Pro) + // 3×2 grid of tiles below MSG count + // Virtual coords (128×128), scaled by DisplayDriver + { + struct Tile { const char* letter; const char* label; }; + const Tile tiles[2][3] = { + { {"M", "Messages"}, {"C", "Contacts"}, {"S", "Settings"} }, + { {"E", "Reader"}, {"N", "Notes"}, {"D", "Discover"} } + }; + + const int tileW = 40; + const int tileH = 32; + const int gapX = 1; + const int gapY = 2; + const int gridW = tileW * 3 + gapX * 2; + const int gridX = (display.width() - gridW) / 2; + const int gridY = y + 2; + + for (int row = 0; row < 2; row++) { + for (int col = 0; col < 3; col++) { + int tx = gridX + col * (tileW + gapX); + int ty = gridY + row * (tileH + gapY); + + // Tile border + display.setColor(DisplayDriver::LIGHT); + display.drawRect(tx, ty, tileW, tileH); + + // Letter centered in tile (pushed down for vertical centering) + display.setTextSize(2); + display.drawTextCentered(tx + tileW / 2, ty + 10, tiles[row][col].letter); + + // Label centered below letter + display.setTextSize(0); + display.drawTextCentered(tx + tileW / 2, ty + 20, tiles[row][col].label); + } + } + + // Nav hint below grid + y = gridY + 2 * tileH + gapY + 2; + display.setColor(DisplayDriver::GREEN); + display.setTextSize(0); + 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); @@ -365,12 +419,9 @@ public: // Nav hint display.setColor(DisplayDriver::GREEN); -#if defined(LilyGo_T5S3_EPaper_Pro) - display.drawTextCentered(display.width() / 2, y, "Tap screen to cycle home views"); -#else display.drawTextCentered(display.width() / 2, y, "Press A/D to cycle home views"); -#endif display.setTextSize(1); // restore +#endif } else if (_page == HomePage::RECENT) { the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE); display.setColor(DisplayDriver::GREEN); @@ -1188,7 +1239,28 @@ void UITask::loop() { #elif defined(PIN_USER_BTN) int ev = user_btn.check(); if (ev == BUTTON_EVENT_CLICK) { +#if defined(LilyGo_T5S3_EPaper_Pro) + // T5S3: single click = cycle pages on home, go back to home from elsewhere + if (curr == home) { + c = checkDisplayOn(KEY_NEXT); + } else { + // Navigate back: reader reading→file list, file list→home, others→home + if (isOnTextReader()) { + TextReaderScreen* reader = (TextReaderScreen*)text_reader; + if (reader && reader->isReading()) { + c = checkDisplayOn('q'); // reading mode: close book → file list + } else { + gotoHomeScreen(); // file list: go home + c = 0; + } + } else { + gotoHomeScreen(); + c = 0; // consumed + } + } +#else c = checkDisplayOn(KEY_NEXT); +#endif } else if (ev == BUTTON_EVENT_LONG_PRESS) { c = handleLongPress(KEY_ENTER); } else if (ev == BUTTON_EVENT_DOUBLE_CLICK) { @@ -1398,7 +1470,7 @@ char UITask::handleTripleClick(char c) { if (board.isBacklightOn()) { board.setBacklight(false); // If already on, turn off } else { - board.setBacklightBrightness(80); + board.setBacklightBrightness(4); board.setBacklight(true); } #else diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 5a02627b..0ea83d12 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -85,6 +85,7 @@ class UITask : public AbstractUITask { #endif UIScreen* map_screen; // Map tile screen (GPS + SD card tiles) UIScreen* curr; + bool _homeShowingTiles = false; // Set by HomeScreen render when tile grid is visible void userLedHandler(); @@ -147,6 +148,9 @@ public: bool isOnChannelScreen() const { return curr == channel_screen; } bool isOnContactsScreen() const { return curr == contacts_screen; } bool isOnTextReader() const { return curr == text_reader; } // *** NEW *** + bool isOnHomeScreen() const { return curr == home; } + bool isHomeShowingTiles() const { return _homeShowingTiles; } + void setHomeShowingTiles(bool v) { _homeShowingTiles = v; } bool isOnNotesScreen() const { return curr == notes_screen; } bool isOnSettingsScreen() const { return curr == settings_screen; } bool isOnAudiobookPlayer() const { return curr == audiobook_screen; } diff --git a/src/helpers/ui/FastEPDDisplay.cpp b/src/helpers/ui/FastEPDDisplay.cpp index 187dcc51..335e54ce 100644 --- a/src/helpers/ui/FastEPDDisplay.cpp +++ b/src/helpers/ui/FastEPDDisplay.cpp @@ -78,7 +78,11 @@ bool FastEPDDisplay::begin() { // Set canvas defaults _canvas->fillScreen(1); // White background (bit=1 → white in FastEPD) _canvas->setTextColor(0); // Black text (bit=0 → black in FastEPD) - _canvas->setFont(&FreeSans24pt7b); +#ifdef MECK_SERIF_FONT + _canvas->setFont(&FreeSerif12pt7b); +#else + _canvas->setFont(&FreeSans12pt7b); +#endif _canvas->setTextWrap(false); _curr_color = GxEPD_BLACK; @@ -118,28 +122,34 @@ void FastEPDDisplay::setTextSize(int sz) { _frameCRC.update(sz); // Font mapping for 960×540 display at ~234 DPI - // The T-Deck Pro at 240×320 (~140 DPI) uses FreeSans9pt for body text. - // At 234 DPI we need roughly 2.5× larger fonts for equivalent physical size. - // Built-in 5×7 font scaled 4× = 20×28px — readable for status bar items. + // Toggle between font families via -D MECK_SERIF_FONT build flag switch(sz) { - case 0: // Tiny — node name, clock, battery %, menu shortcuts - _canvas->setFont(NULL); - _canvas->setTextSize(4); // 5×7 × 4 = 20×28 physical pixels - break; - case 1: // Small/normal — body text, contact list items - _canvas->setFont(&FreeSans24pt7b); + case 0: // Body text — reader content, settings rows, messages, footers +#ifdef MECK_SERIF_FONT + _canvas->setFont(&FreeSerif12pt7b); +#else + _canvas->setFont(&FreeSans12pt7b); +#endif _canvas->setTextSize(1); break; - case 2: // Medium bold — MSG count, headings, labels - _canvas->setFont(&FreeSansBold24pt7b); + case 1: // Headings — screen titles, channel names (bold, same height as body) + _canvas->setFont(&FreeSansBold12pt7b); _canvas->setTextSize(1); break; - case 3: // Large — splash screen title, onboarding + case 2: // Large bold — MSG count, tile letters + _canvas->setFont(&FreeSansBold18pt7b); + _canvas->setTextSize(1); + break; + case 3: // Extra large — splash screen title _canvas->setFont(&FreeSansBold24pt7b); _canvas->setTextSize(1); break; default: - _canvas->setFont(&FreeSans24pt7b); +#ifdef MECK_SERIF_FONT + _canvas->setFont(&FreeSerif12pt7b); +#else + _canvas->setFont(&FreeSans12pt7b); +#endif _canvas->setTextSize(1); break; } @@ -166,9 +176,8 @@ void FastEPDDisplay::setCursor(int x, int y) { _frameCRC.update(x); _frameCRC.update(y); - // Scale virtual coordinates to physical, with baseline offset - // The +5 pushes text baseline down so ascenders don't overlap elements above - // (Same convention as GxEPDDisplay for T-Deck Pro) + // Scale virtual coordinates to physical, with baseline offset. + // The +5 pushes text baseline down so ascenders at y=0 are visible. _canvas->setCursor( (int)((x + offset_x) * scale_x), (int)((y + offset_y + 5) * scale_y) diff --git a/src/helpers/ui/FastEPDDisplay.h b/src/helpers/ui/FastEPDDisplay.h index 23f2eb93..84f9f3e6 100644 --- a/src/helpers/ui/FastEPDDisplay.h +++ b/src/helpers/ui/FastEPDDisplay.h @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include "DisplayDriver.h" @@ -110,7 +112,7 @@ public: void drawTextRaw(int16_t x, int16_t y, const char* text, uint16_t color) { if (!_canvas) return; _canvas->setFont(NULL); - _canvas->setTextSize(4); // 4× built-in 5×7 = 20×28, readable on 960×540 + _canvas->setTextSize(3); // 3× built-in 5×7 = 15×21, readable on 960×540 _canvas->setTextColor(color ? 1 : 0); _canvas->setCursor(x, y); _canvas->print(text); diff --git a/variants/lilygo_t5s3_epaper_pro/platformio.ini b/variants/lilygo_t5s3_epaper_pro/platformio.ini index c63e65ac..95e3277e 100644 --- a/variants/lilygo_t5s3_epaper_pro/platformio.ini +++ b/variants/lilygo_t5s3_epaper_pro/platformio.ini @@ -77,6 +77,9 @@ build_flags = -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=FastEPDDisplay -D USE_EINK + ; Font family: comment/uncomment to toggle (delete .indexes on SD after switching) + ; -D MECK_SERIF_FONT ; FreeSerif (Times New Roman-like) + ; ; Default (no flag): FreeSans (Arial-like) build_src_filter = ${LilyGo_T5S3_EPaper_Pro.build_src_filter} + + @@ -88,6 +91,7 @@ lib_deps = densaugeo/base64 @ ~1.4.0 adafruit/Adafruit GFX Library@^1.11.0 https://github.com/mverch67/FastEPD/archive/0df1bff329b6fc782e062f611758880762340647.zip + https://github.com/lewisxhe/SensorLib/archive/refs/tags/v0.3.4.zip ; --------------------------------------------------------------------------- ; Phase 3+ variants (uncomment when touch input is implemented)