From b55892431dd9f37c333fe583c81f81fc8d3b2d82 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Thu, 12 Mar 2026 23:05:30 +1100 Subject: [PATCH] t5s3 portrait mode and dark mode --- examples/companion_radio/NodePrefs.h | 2 + examples/companion_radio/main.cpp | 45 +++++++++++++++---- .../companion_radio/ui-new/Settingsscreen.h | 36 +++++++++++++++ examples/companion_radio/ui-new/UITask.cpp | 23 ++++++++++ src/helpers/ui/FastEPDDisplay.cpp | 41 ++++++++++++++++- src/helpers/ui/FastEPDDisplay.h | 22 +++++++-- 6 files changed, 155 insertions(+), 14 deletions(-) diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index aca50425..8227043e 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -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 }; \ No newline at end of file diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 95d57384..9bc0cee5 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -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(); } diff --git a/examples/companion_radio/ui-new/Settingsscreen.h b/examples/companion_radio/ui-new/Settingsscreen.h index 51c325d7..d536977c 100644 --- a/examples/companion_radio/ui-new/Settingsscreen.h +++ b/examples/companion_radio/ui-new/Settingsscreen.h @@ -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 diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index ac6eada3..08f0d941 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -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"); } diff --git a/src/helpers/ui/FastEPDDisplay.cpp b/src/helpers/ui/FastEPDDisplay.cpp index a930a249..f3d7f272 100644 --- a/src/helpers/ui/FastEPDDisplay.cpp +++ b/src/helpers/ui/FastEPDDisplay.cpp @@ -115,6 +115,8 @@ void FastEPDDisplay::startFrame(Color bkg) { _canvas->setTextColor(0); // Black text _curr_color = GxEPD_BLACK; _frameCRC.reset(); + _frameCRC.update(_darkMode); + _frameCRC.update(_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 } \ No newline at end of file diff --git a/src/helpers/ui/FastEPDDisplay.h b/src/helpers/ui/FastEPDDisplay.h index 84f9f3e6..9bf57fa0 100644 --- a/src/helpers/ui/FastEPDDisplay.h +++ b/src/helpers/ui/FastEPDDisplay.h @@ -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; } }; \ No newline at end of file