mirror of
https://github.com/pelgraine/Meck.git
synced 2026-08-07 17:22:44 +02:00
t5s3 portrait mode and dark mode
This commit is contained in:
@@ -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
|
||||
};
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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; }
|
||||
};
|
||||
Reference in New Issue
Block a user