t5s3 portrait mode and dark mode

This commit is contained in:
pelgraine
2026-03-12 23:05:30 +11:00
parent dc5331702d
commit b55892431d
6 changed files with 155 additions and 14 deletions
+2
View File
@@ -35,4 +35,6 @@ struct NodePrefs { // persisted to file
uint8_t autoadd_max_hops; // 0=no limit, N=up to N-1 hops (max 64)
uint32_t gps_baudrate; // GPS baud rate (0 = use compile-time GPS_BAUDRATE default)
uint8_t interference_threshold; // Interference threshold in dB (0=disabled, 14+=enabled)
uint8_t dark_mode; // 0=off (white bg), 1=on (black bg) — T5S3 only
uint8_t portrait_mode; // 0=landscape, 1=portrait — T5S3 only
};
+36 -9
View File
@@ -387,6 +387,30 @@
}
return false;
}
// Read GT911 in portrait orientation (540×960, rotation 3)
// Maps GT911 native coords to portrait logical space
// Read GT911 in portrait orientation (540×960, canvas rotation 3)
// Rotation 3 maps logical(lx,ly) → physical(ly, 539-lx).
// Inverting: logical_x = raw_x, logical_y = raw_y (GT911 native = portrait).
static bool readTouchPortrait(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_x;
*outY = raw_y;
return true;
}
return false;
}
// Unified touch reader — picks landscape or portrait based on display mode
static bool readTouch(int16_t* outX, int16_t* outY) {
if (display.isPortraitMode()) {
return readTouchPortrait(outX, outY);
}
return readTouchLandscape(outX, outY);
}
#endif
// Board-agnostic: CPU frequency scaling and AGC reset
@@ -507,9 +531,12 @@ MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store
#if defined(LilyGo_T5S3_EPaper_Pro)
// Map a single tap based on current screen context
static char mapTouchTap(int16_t x, int16_t y) {
// Convert physical (960×540) to virtual (128×128) coordinates
int vx = (int)(x / 7.5f);
int vy = (int)(y / 4.21875f);
// Convert physical screen coords to virtual (128×128) using current scale
// Scale factors change between landscape (7.5, 4.22) and portrait (4.22, 7.5)
float sx = display.isPortraitMode() ? ((float)EPD_HEIGHT / 128.0f) : ((float)EPD_WIDTH / 128.0f);
float sy = display.isPortraitMode() ? ((float)EPD_WIDTH / 128.0f) : ((float)EPD_HEIGHT / 128.0f);
int vx = (int)(x / sx);
int vy = (int)(y / sy);
// --- Status bar tap (top ~18 virtual units) → go home from any non-home screen ---
// Exception: text reader reading mode uses full screen for content (no header)
@@ -556,7 +583,6 @@ MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store
// Home screen (non-tile pages): left half taps backward, right half forward
if (ui_task.isOnHomeScreen()) {
int vx = (int)(x / 7.5f);
return (vx < 64) ? (char)KEY_PREV : (char)KEY_NEXT;
}
@@ -580,7 +606,6 @@ MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store
// Channel screen: tap footer area → hop path, tap elsewhere → no action
if (ui_task.isOnChannelScreen()) {
int vy = (int)(y / 4.21875f);
ChannelScreen* chScr = (ChannelScreen*)ui_task.getChannelScreen();
if (chScr && chScr->isShowingPathOverlay()) {
return 'q'; // Dismiss overlay on any tap
@@ -1488,7 +1513,7 @@ void loop() {
if (!ui_task.isLocked() && !ui_task.isVKBActive())
{
int16_t tx, ty;
bool gotPoint = readTouchLandscape(&tx, &ty);
bool gotPoint = readTouch(&tx, &ty);
unsigned long now = millis();
if (gotPoint) {
@@ -1580,7 +1605,7 @@ void loop() {
if (ui_task.isVKBActive()) {
int16_t tx, ty;
bool gotPt = readTouchLandscape(&tx, &ty);
bool gotPt = readTouch(&tx, &ty);
if (!gotPt) {
vkbNeedLift = false; // Finger lifted
@@ -1589,8 +1614,10 @@ void loop() {
bool cooldownOk = (millis() - ui_task.vkbOpenedAt()) > 2000;
if (gotPt && !vkbNeedLift && cooldownOk) {
int vx = (int)(tx / 7.5f);
int vy = (int)(ty / 4.21875f);
float sx = display.isPortraitMode() ? ((float)EPD_HEIGHT / 128.0f) : ((float)EPD_WIDTH / 128.0f);
float sy = display.isPortraitMode() ? ((float)EPD_WIDTH / 128.0f) : ((float)EPD_HEIGHT / 128.0f);
int vx = (int)(tx / sx);
int vy = (int)(ty / sy);
if (ui_task.getVKB().handleTap(vx, vy)) {
ui_task.forceRefresh();
}
@@ -64,6 +64,10 @@ enum SettingsRowType : uint8_t {
ROW_TX_POWER, // TX power (1-20 dBm)
ROW_UTC_OFFSET, // UTC offset (-12 to +14)
ROW_MSG_NOTIFY, // Keyboard flash on new msg toggle
#if defined(LilyGo_T5S3_EPaper_Pro)
ROW_DARK_MODE, // Dark mode toggle (inverted display)
ROW_PORTRAIT_MODE, // Portrait orientation toggle
#endif
ROW_PATH_HASH_SIZE, // Path hash size (1, 2, or 3 bytes per hop)
#ifdef MECK_WIFI_COMPANION
ROW_WIFI_SETUP, // WiFi SSID/password configuration
@@ -246,6 +250,10 @@ private:
addRow(ROW_UTC_OFFSET);
addRow(ROW_MSG_NOTIFY);
addRow(ROW_PATH_HASH_SIZE);
#if defined(LilyGo_T5S3_EPaper_Pro)
addRow(ROW_DARK_MODE);
addRow(ROW_PORTRAIT_MODE);
#endif
#ifdef MECK_WIFI_COMPANION
addRow(ROW_WIFI_SETUP);
addRow(ROW_WIFI_TOGGLE);
@@ -765,6 +773,20 @@ public:
display.print(tmp);
break;
#if defined(LilyGo_T5S3_EPaper_Pro)
case ROW_DARK_MODE:
snprintf(tmp, sizeof(tmp), "Dark Mode: %s",
_prefs->dark_mode ? "ON" : "OFF");
display.print(tmp);
break;
case ROW_PORTRAIT_MODE:
snprintf(tmp, sizeof(tmp), "Portrait Mode: %s",
_prefs->portrait_mode ? "ON" : "OFF");
display.print(tmp);
break;
#endif
#ifdef MECK_WIFI_COMPANION
case ROW_WIFI_SETUP:
if (WiFi.status() == WL_CONNECTED) {
@@ -1561,6 +1583,20 @@ public:
case ROW_PATH_HASH_SIZE:
startEditInt(_prefs->path_hash_mode + 1); // display as 1-3
break;
#if defined(LilyGo_T5S3_EPaper_Pro)
case ROW_DARK_MODE:
_prefs->dark_mode = _prefs->dark_mode ? 0 : 1;
the_mesh.savePrefs();
Serial.printf("Settings: Dark mode = %s\n",
_prefs->dark_mode ? "ON" : "OFF");
break;
case ROW_PORTRAIT_MODE:
_prefs->portrait_mode = _prefs->portrait_mode ? 0 : 1;
the_mesh.savePrefs();
Serial.printf("Settings: Portrait mode = %s\n",
_prefs->portrait_mode ? "ON" : "OFF");
break;
#endif
#ifdef MECK_WIFI_COMPANION
case ROW_WIFI_SETUP: {
// Launch WiFi scan → select → password → connect flow
@@ -1167,6 +1167,17 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
#else
map_screen = nullptr;
#endif
#if defined(LilyGo_T5S3_EPaper_Pro)
// Apply saved display preferences before first render
if (_node_prefs->dark_mode) {
::display.setDarkMode(true);
}
if (_node_prefs->portrait_mode) {
::display.setPortraitMode(true);
}
#endif
setCurrScreen(splash);
}
@@ -1523,9 +1534,19 @@ if (curr) curr->poll();
if (_display != NULL && _display->isOn()) {
if (millis() >= _next_refresh && curr) {
#if defined(LilyGo_T5S3_EPaper_Pro)
// Sync display modes with prefs (settings toggles take effect here)
if (_node_prefs && display.isDarkMode() != (_node_prefs->dark_mode != 0)) {
display.setDarkMode(_node_prefs->dark_mode != 0);
}
if (_node_prefs && display.isPortraitMode() != (_node_prefs->portrait_mode != 0)) {
display.setPortraitMode(_node_prefs->portrait_mode != 0);
}
#endif
_display->startFrame();
#if defined(LilyGo_T5S3_EPaper_Pro)
if (_vkbActive) {
display.setForcePartial(true); // No flash while typing
_vkb.render(*_display);
_next_refresh = millis() + 500; // Moderate refresh for cursor blink
// Check if keyboard was submitted or cancelled during render cycle
@@ -1822,6 +1843,7 @@ void UITask::onVKBSubmit() {
}
_screenBeforeVKB = nullptr;
_next_refresh = 0;
display.setForcePartial(false); // Next frame does full refresh to clear VKB ghosts
display.invalidateFrameCRC();
}
@@ -1830,6 +1852,7 @@ void UITask::onVKBCancel() {
if (_screenBeforeVKB) setCurrScreen(_screenBeforeVKB);
_screenBeforeVKB = nullptr;
_next_refresh = 0;
display.setForcePartial(false); // Next frame does full refresh to clear VKB ghosts
display.invalidateFrameCRC();
Serial.println("[UI] VKB cancelled");
}
+40 -1
View File
@@ -115,6 +115,8 @@ void FastEPDDisplay::startFrame(Color bkg) {
_canvas->setTextColor(0); // Black text
_curr_color = GxEPD_BLACK;
_frameCRC.reset();
_frameCRC.update<bool>(_darkMode);
_frameCRC.update<bool>(_portraitMode);
}
void FastEPDDisplay::setTextSize(int sz) {
@@ -290,6 +292,11 @@ void FastEPDDisplay::endFrame() {
memcpy(dst, src, bufSize);
// Dark mode: invert every byte in the buffer (white↔black)
if (_darkMode) {
for (size_t i = 0; i < bufSize; i++) dst[i] = ~dst[i];
}
// Refresh strategy:
// partialUpdate(true) — no flash, differential, keeps previous buffer
// fullUpdate(false) — brief flash, clears ghosting (CLEAR_FAST)
@@ -298,11 +305,43 @@ void FastEPDDisplay::endFrame() {
// Use partial for most frames. Periodic full refresh every N frames
// to clear accumulated ghosting artifacts.
_fullRefreshCount++;
if (_fullRefreshCount >= FULL_SLOW_PERIOD) {
if (_forcePartial) {
// VKB typing mode — no flash, fast differential update
_epd->partialUpdate(true);
_fullRefreshCount = 0; // Reset so next non-partial frame does full refresh
} else if (_fullRefreshCount >= FULL_SLOW_PERIOD) {
_fullRefreshCount = 0;
_epd->fullUpdate(true); // Full clean refresh — clears all ghosting
} else {
_epd->partialUpdate(true); // No flash — differential
}
_epd->backupPlane();
}
void FastEPDDisplay::setDarkMode(bool dark) {
_darkMode = dark;
_lastCRC = 0; // Force redraw
Serial.printf("[FastEPD] Dark mode: %s\n", dark ? "ON" : "OFF");
}
void FastEPDDisplay::setPortraitMode(bool portrait) {
if (_portraitMode == portrait) return;
_portraitMode = portrait;
if (!_canvas) return;
if (portrait) {
_canvas->setRotation(3); // 270° CW — USB-C on right when held portrait
scale_x = (float)EPD_HEIGHT / 128.0f; // 540 / 128 = 4.21875
scale_y = (float)EPD_WIDTH / 128.0f; // 960 / 128 = 7.5
Serial.printf("[FastEPD] Portrait mode: ON (logical %dx%d, scale %.2f x %.2f)\n",
EPD_HEIGHT, EPD_WIDTH, scale_x, scale_y);
} else {
_canvas->setRotation(0); // Normal landscape
scale_x = (float)EPD_WIDTH / 128.0f; // 960 / 128 = 7.5
scale_y = (float)EPD_HEIGHT / 128.0f; // 540 / 128 = 4.21875
Serial.printf("[FastEPD] Portrait mode: OFF (logical %dx%d, scale %.2f x %.2f)\n",
EPD_WIDTH, EPD_HEIGHT, scale_x, scale_y);
}
_lastCRC = 0; // Force redraw
}
+18 -4
View File
@@ -73,11 +73,13 @@ class FastEPDDisplay : public DisplayDriver {
uint32_t _lastCRC = 0;
int _fullRefreshCount = 0; // Track for periodic slow refresh
uint32_t _lastUpdateMs = 0; // Rate limiting — minimum interval between refreshes
bool _forcePartial = false; // When true, use partial updates (VKB typing)
bool _darkMode = false; // Invert all pixels (black bg, white text)
bool _portraitMode = false; // Rotated 90° (540×960 logical)
// Virtual 128×128 → physical 960×540 mapping
// Non-square scaling (1.78:1 aspect stretch) — acceptable for initial bringup
static constexpr float scale_x = 7.5f; // 960 / 128
static constexpr float scale_y = 4.21875f; // 540 / 128
// Virtual 128×128 → physical canvas mapping (runtime, changes with portrait)
float scale_x = 7.5f; // 960 / 128 (landscape default)
float scale_y = 4.21875f; // 540 / 128 (landscape default)
static constexpr float offset_x = 0.0f;
static constexpr float offset_y = 0.0f;
@@ -119,4 +121,16 @@ public:
}
void invalidateFrameCRC() { _lastCRC = 0; }
// Temporarily force partial (no-flash) updates — use during VKB typing
void setForcePartial(bool partial) { _forcePartial = partial; }
bool isForcePartial() const { return _forcePartial; }
// Dark mode — invert all pixels in endFrame (black bg, white text)
void setDarkMode(bool dark);
bool isDarkMode() const { return _darkMode; }
// Portrait mode — rotate canvas 90° (540×960 logical), swap scale factors
void setPortraitMode(bool portrait);
bool isPortraitMode() const { return _portraitMode; }
};