mirror of
https://github.com/pelgraine/Meck.git
synced 2026-08-04 15:52:42 +02:00
lower brightness to 4 for best darkroom reading; first prelim touch implementation; ui improvements
This commit is contained in:
@@ -337,6 +337,58 @@
|
||||
}
|
||||
#endif
|
||||
|
||||
// =============================================================================
|
||||
// T5S3 E-Paper Pro — GT911 Touch Input
|
||||
// =============================================================================
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
#include "TouchDrvGT911.hpp"
|
||||
#include <SD.h>
|
||||
#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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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<int>(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<int>(x);
|
||||
_frameCRC.update<int>(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)
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#include <Fonts/FreeSansBold12pt7b.h>
|
||||
#include <Fonts/FreeSansBold18pt7b.h>
|
||||
#include <Fonts/FreeSansBold24pt7b.h>
|
||||
#include <Fonts/FreeSerif12pt7b.h>
|
||||
#include <Fonts/FreeSerif18pt7b.h>
|
||||
|
||||
#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);
|
||||
|
||||
@@ -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}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user