Compare commits

...
1 Commits
Author SHA1 Message Date
Kaj SchittecatandClaude Opus 4.8 4e6a9e0108 touch: beta_49 - T-Display P4 fixes (exit traps, screen timeout, UI scale, GPS, control center)
P4-focused beta. Non-P4 boards only pick up minor shared exit-trap code, nothing user-facing.

- Snake + full-screen apps (Monitor/Spectrum/Discover/Web mirror/Advert) exit on status-bar tap
- Screen timeout / standby / burn-in dimming wired for the AMOLED (setBrightness path)
- UI scale pinned to 100% on the P4 (text overflow above 100%)
- No hard-lock on screen-off (P4 has no lock screen yet)
- GPS: L76K detect at 9600 baud, skip boot-detect race, 4096 RX buffer
- Control center dropdown taller on tall screens so the GPS line clears the toggle buttons
- P4 LCD SKU: HI8561 backlight PWM on GPIO51 (fixes the black screen)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 20:34:17 +02:00
6 changed files with 122 additions and 20 deletions

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
# beta_49 - T-Display P4 only
# One user-facing note per non-blank, non-# line; # lines are section comments.
This beta only has T-Display P4 fixes. If you run any other board there is nothing new for you here, stay on beta_48.
# Fixed (T-Display P4)
The Snake game no longer traps you. Tap the top bar to leave it. Before, the only way out was a reset.
The full-screen tools (Monitor, Spectrum, Discover, Web mirror, Advert) can now be left by tapping the top bar, so they no longer trap you either.
The screen timeout now works. The display turns off after your idle time and wakes on touch. This also fixes standby and burn-in dimming.
Text no longer overflows its boxes. The interface is pinned to the 100% size that fits the panel.
GPS: the L76K module is now found reliably and read at 9600 baud. You still need the GPS antenna connected and a clear view of the sky to get a fix.
The control center dropdown is taller so the GPS status line sits below the buttons instead of behind them.
P4 LCD model only: fixed the black screen. The backlight now turns on. Please test and report.
+12
View File
@@ -37,6 +37,18 @@ void SnakeGame::launch() {
if (!g->open()) { s_active = nullptr; delete g; }
}
void SnakeGame::dismiss() {
// Close from outside the game (a status-bar tap). The in-game X can sit UNDER a taller status
// bar on big-screen boards (the P4's bar is set taller at boot + raised over the game), leaving
// no reachable exit — this is the guaranteed way out. Mirrors closeCb's teardown. Safe to del
// synchronously here: the triggering event target is the status bar, not a Snake object.
if (!s_active) return;
SnakeGame* g = s_active;
s_active = nullptr;
g->close();
delete g;
}
void SnakeGame::placeFood() {
const int cells = cols_ * rows_;
for (int tries = 0; tries < 4 * cells + 16; ++tries) {
+1
View File
@@ -17,6 +17,7 @@ public:
static void launch(); // open (no-op if already open)
static bool isOpen(); // UITask: gate the trackball + tab bar
static void steer(int dx, int dy); // UITask: trackball motion -> direction
static void dismiss(); // close from outside (status-bar tap) — guaranteed exit
private:
// The playfield grid is sized to the screen at open() (cols_ x rows_), capped
+51 -13
View File
@@ -528,7 +528,15 @@ static void initTouchFontFallbacks() {
// upscaling a low-res frame). g_font_12/14/16 are what the whole UI draws with, so this scales
// every screen at once. The colour-emoji + non-Latin fallbacks stay their baked sizes (they don't
// grow), which is fine for Latin text. g_font_tab is pinned to 16 px so the bottom bar never grows.
#if defined(HAS_TDISPLAY_P4)
// The P4 is a NARROW 284 px portrait: bigger fonts overflow the fixed-height/width chrome (the
// status bar, home grid, and list/settings rows all use unscaled dims), so 100% is the only size
// that fits. Ignore any Large/Huge pref (a unit can inherit one from adopted T-Deck prefs). The
// Tanmatsu is 800 px landscape and keeps the option. GH: P4 text overflow above 100%.
s_ui_fscale = 100;
#else
switch (touchPrefsGetUiScale()) { case 1: s_ui_fscale = 140; break; case 2: s_ui_fscale = 170; break; default: s_ui_fscale = 100; break; }
#endif
switch (s_ui_fscale) {
case 140: g_font_12 = lv_font_montserrat_16; g_font_14 = lv_font_montserrat_20; g_font_16 = lv_font_montserrat_24; break; // Large ~1.4x
case 170: g_font_12 = lv_font_montserrat_20; g_font_14 = lv_font_montserrat_24; g_font_16 = lv_font_montserrat_28; break; // Huge ~1.7x
@@ -11823,9 +11831,11 @@ static void buildDeviceSettings(int sec) {
}
#endif // HAS_TDECK_GT911 || HAS_THINKNODE_M9
/* Auto-lock on screen-off: when the idle timeout dims the screen, also hard-
lock so the touchscreen is inert (Tim: pocket-taps while dark). Both boards;
unlock with a trackball hold (T-Deck) or the button (V4). */
// Auto-lock on screen-off: when the idle timeout dims the screen, also hard-lock so the
// touchscreen is inert (Tim: pocket-taps while dark). NOT on the P4 — it has no unlock path
// (no button/trackball and no touch-unlock overlay yet), so a hard lock there is a power-cycle
// trap. The P4 still dims on idle (screen-off wakes on touch); a proper P4 lock is a follow-up.
#if !defined(HAS_TDISPLAY_P4)
{
int h = settingsRowLabel(body, y, 6, TR("Lock when screen off"), COLOR_SUB, nullptr, 56);
lv_obj_t* sw = lv_switch_create(body);
@@ -11836,6 +11846,7 @@ static void buildDeviceSettings(int sec) {
lv_obj_add_event_cb(sw, lockOnScreenOffToggleCb, LV_EVENT_VALUE_CHANGED, nullptr);
y += LV_MAX(40, h + 12);
}
#endif
}
@@ -35159,9 +35170,9 @@ static void openControlCenter() {
lv_obj_t* card = lv_obj_create(s_cc_root);
lv_obj_remove_style_all(card);
#if defined(HAS_TANMATSU)
lv_obj_set_size(card, card_w, 384); // bigger: header + 3 roomier sliders + toggle grid + sysinfo
const int card_h = 384; // bigger: header + 3 roomier sliders + toggle grid + sysinfo
#elif defined(HAS_TDECK_GT911) || defined(HAS_THINKNODE_M9)
lv_obj_set_size(card, card_w, 200); // sysinfo + thin brightness slider + 2-row toggle grid (fits 240−22 screen)
const int card_h = 200; // sysinfo + thin brightness slider + 2-row toggle grid (fits 240−22 screen)
#elif defined(TLORA_PAGER)
// 222-px-tall screen: the shared V4/T-Deck-landscape 212px card below is
// tuned for a 240px-tall screen (212 + the card's own 4px y-offset = 216,
@@ -35169,12 +35180,20 @@ static void openControlCenter() {
// same card overflows the 200px root by 16px, pushing the toggle row/sysinfo
// text off the bottom of the physical display. Size from the actual
// available height instead of the shared constant, with a small margin.
lv_obj_set_size(card, card_w, sh - STATUSBAR_H - 4 - 6);
const int card_h = sh - STATUSBAR_H - 4 - 6;
#elif defined(HAS_TDISPLAY_P4)
// Tall panel (1232 px): the shared V4 236px card is far too short here. The P4's
// GPS line sits lower (it clears two 30px sliders, so gps_y ~124 vs the V4's ~70),
// and the bottom-anchored 2-row chip grid then climbs up over the GPS text ("the
// acquiring text is under the buttons"). Make the dropdown taller so GPS + chips +
// sysinfo each get their own band — the chip-row math below derives off card_h.
const int card_h = 320;
#else
// Portrait has headroom on the 320-tall screen; make the card taller so the
// brightness slider + toggles + sysinfo all get their own rows.
lv_obj_set_size(card, card_w, portrait ? 236 : 212);
const int card_h = portrait ? 236 : 212;
#endif
lv_obj_set_size(card, card_w, card_h);
lv_obj_align(card, LV_ALIGN_TOP_MID, 0, 4);
lv_obj_set_style_bg_color(card, lv_color_hex(COLOR_PANEL), LV_PART_MAIN);
// Frosted-glass panel: translucent fill so the (dimmed) screen behind shows through.
@@ -35454,7 +35473,7 @@ static void openControlCenter() {
const int per_row = (chip_count + 1) / 2; // 6 -> 3 per row, 5 -> 3 then 2
tw = (card_w - 20 - 5 * (per_row - 1)) / per_row;
if (tw > 92) tw = 92;
int rowh = 196 - (gps_y + 38); // row bottom (216 content − 20) up to just below GPS/env
int rowh = (card_h - 40) - (gps_y + 38); // card bottom (minus margin) up to just below GPS/env (card_h-40 == the old 196 on the 236px V4 card)
if (rowh > 116) rowh = 116;
th = (rowh - 8) / 2; // two rows + an 8px inter-row gap
if (th > 52) th = 52;
@@ -36207,9 +36226,10 @@ static void statusBarHoldCb(lv_event_t* e) {
static void statusBarTapCb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_CLICKED) return;
// While Snake is up the bar is raised to the FRONT of lv_layer_top (see the
// edge-trigger in updateGlobalStatusBar), so it stays tappable over the game.
// Swallow the tap so it can't open the control centre over the game.
if (SnakeGame::isOpen()) return;
// edge-trigger in updateGlobalStatusBar), so it stays tappable over the game. The
// game's own X can sit UNDER a taller status bar on big-screen boards (the P4),
// leaving no reachable exit — so a bar tap closes Snake here (the guaranteed way out).
if (SnakeGame::isOpen()) { SnakeGame::dismiss(); return; }
if (s_sb_shot_done) { s_sb_shot_done = false; return; } // this press was a screenshot hold
// A full-screen tool page (RF Monitor / Spectrum) is up: the bar carries its Back
// chevron + title, so a tap goes back (closes the page) just like settings.
@@ -36241,7 +36261,12 @@ static void statusBarTapCb(lv_event_t* e) {
// nothing". This closes the reader on touch-down whenever it's the open page.
static void statusBarReaderBackCb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_PRESSED) return;
if (s_reader_page_open && s_apppage_close) s_apppage_close();
// Every full-screen tool page (Reader, Monitor, Spectrum, Discover, VNC, Remote, Advert, …) is
// exited only by the bar's CLICKED -> s_apppage_close. On cap-touch the swipe detector's
// click-abort (lvglTouchRead) can drop that lone CLICKED, trapping the user on a touch-only board
// (the P4). Close on PRESSED too — the proven Reader fallback, now generalised to every app page.
// close() clears s_apppage_close, so the later CLICKED in statusBarTapCb is then a no-op.
if (s_apppage_close) s_apppage_close();
}
// Capture the composited screen to a 16-bit BMP on the SD card. BMP (no
@@ -43302,6 +43327,13 @@ static inline void touchScreenBacklight(bool on) {
// the base = MORE conduction = brighter. applyBrightness() below already accounts for this.
if (on) applyBrightness(s_brightness_pct);
else ledcWrite(kM9BlPwmChannel, 255); // inverted: 255 = 0% conduction = off
#elif defined(HAS_TDISPLAY_P4)
// T-Display P4: the RM69A10 AMOLED has no backlight pin — "brightness" is the panel's own DCS
// 0x51 register. Off = 0 (blanks the AMOLED), on = restore the saved brightness. Without this
// branch touchScreenBacklight was the (void)on no-op, so screen-timeout, wake, lock, sleep and
// the burn-in guard all silently did nothing on the P4 (reported: "screen timeout not working").
if (on) applyBrightness(s_brightness_pct);
else display.setBrightness(0);
#else
(void)on;
#endif
@@ -44067,6 +44099,10 @@ void UITask::loop() {
// every click. Signed keeps a slightly-ahead stamp negative (= "just had input").
if (_screen_timeout_ms > 0 && !_screen_off &&
(int32_t)(now - _last_input_ms) >= (int32_t)_screen_timeout_ms) {
#if !defined(HAS_TDISPLAY_P4)
// The P4 has no unlock path (no button/overlay), so it must NEVER hard-lock, even if an old
// pref left the flag set — it always takes the plain screen-off branch below, which wakes on
// touch. (Guards the trap; the "Lock when screen off" toggle is also hidden on the P4.)
if (s_lock_on_screen_off && !_manual_lock) {
// "Lock when screen off": idle dim also hard-locks, so the touchscreen is
// inert until a deliberate unlock (trackball hold on the T-Deck, BOOT
@@ -44074,7 +44110,9 @@ void UITask::loop() {
// !_manual_lock guard stops the Tanmatsu's lit lock screen from re-firing
// lockScreen() (and re-lighting) on every idle tick.
lockScreen();
} else {
} else
#endif
{
touchScreenBacklight(false);
setCpuForScreen(false); // idle dim (no lock) -> drop to 80 MHz too
_screen_off = true;
+6 -2
View File
@@ -69,8 +69,12 @@ set(WADA_DEFS
# PIN_GPS_TX = the GPIO wired to the MODULE'S TX pin; PIN_GPS_RX = wired to the module's RX.
# Matches LilyGo's t_display_p4_config.h "GPS_TX 22 / GPS_RX 23" naming. The first cut had them
# swapped (silent: UART opens fine, just never hears NMEA). Wake: XL9535 GPS_WAKE must be HIGH
# (powerOnSequence) or the L76K sleeps. Baud: L76K power-on default 9600 = the core default. ---
ENV_INCLUDE_GPS=1 PIN_GPS_TX=22 PIN_GPS_RX=23
# (powerOnSequence) or the L76K sleeps. Baud: L76K power-on default 9600 = the core default.
# ENV_SKIP_GPS_DETECT: like the T-Deck, force gps_detected=true — the P4's heavy boot can miss the
# 1 s "any NMEA byte" detect window, and a false result then HIDES the GPS toggle and no-ops the
# saved-pref resume, so GPS stays dead with no recovery until a lucky reboot. GPS_BAUD_RATE=9600
# pins the correct L76K baud (a stale 38400 NVS override will still need clearing on-device). ---
ENV_INCLUDE_GPS=1 PIN_GPS_TX=22 PIN_GPS_RX=23 ENV_SKIP_GPS_DETECT=1 GPS_BAUD_RATE=9600
# --- microSD on SDMMC slot 0 (CLK43/CMD44/D0-3=39-42); C6 esp-hosted is slot 1 ---
BOARD_HAS_SDMMC=1 BOARD_SDMMC_SLOT=0
+39 -5
View File
@@ -19,9 +19,19 @@
#include "esp_lcd_panel_io.h" // esp_lcd_panel_io_tx_param (runtime brightness 0x51)
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_hi8561.h"
#include "driver/ledc.h" // GPIO 51 PWM backlight — the TFT-LCD has a real LED backlight
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
// The HI8561 TFT-LCD backlight is a hardware LED driven by PWM on GPIO 51 (LilyGo's
// hi8561::kScreenBacklight). Unlike the AMOLED (RM69A10, self-emissive, brightness via DCS 0x51),
// the LCD panel renders but stays dark until this PWM is driven — the "no backlight, works under a
// flashlight" report on the P4 LCD. Set up LEDC and drive it from setBrightness().
static constexpr int kBacklightGpio = 51;
static constexpr ledc_timer_t kBlTimer = LEDC_TIMER_0;
static constexpr ledc_channel_t kBlChan = LEDC_CHANNEL_0;
static bool s_hi8561_bl_ready = false;
// The DPI panel copies each draw_bitmap band into the internal frame buffer ASYNCHRONOUSLY
// (DMA2D) and fires on_color_trans_done when the copy is complete. writePixelsRGB565 reuses a
// single upscale scratch buffer (and LVGL reuses its draw buffer) on the very next flush, so
@@ -119,9 +129,28 @@ bool HI8561Display::begin() {
if (esp_lcd_panel_init(_panel) != ESP_OK) { Serial.println("[HI8561] panel_init fail"); return false; }
esp_lcd_panel_disp_on_off(_panel, true);
// Brightness: the HI8561's default init does SLPOUT/DISPON but leaves brightness unset; drive it at
// runtime via DCS 0x51 (the TDDI's integrated backlight — no GPIO). Keep the DBI IO handle so
// setBrightness() can drive it live (CC slider); start at max so the LCD actually lights.
// Backlight: bring up the LEDC PWM on GPIO 51 (the LCD's real LED backlight). Without this the
// panel renders but is dark. 20 kHz avoids audible/visible flicker; 8-bit duty maps 0..255 directly.
{
ledc_timer_config_t bl_tmr = {};
bl_tmr.speed_mode = LEDC_LOW_SPEED_MODE;
bl_tmr.timer_num = kBlTimer;
bl_tmr.duty_resolution = LEDC_TIMER_8_BIT;
bl_tmr.freq_hz = 20000;
bl_tmr.clk_cfg = LEDC_AUTO_CLK;
ledc_channel_config_t bl_ch = {};
bl_ch.gpio_num = kBacklightGpio;
bl_ch.speed_mode = LEDC_LOW_SPEED_MODE;
bl_ch.channel = kBlChan;
bl_ch.timer_sel = kBlTimer;
bl_ch.duty = 0;
bl_ch.hpoint = 0;
s_hi8561_bl_ready = (ledc_timer_config(&bl_tmr) == ESP_OK) &&
(ledc_channel_config(&bl_ch) == ESP_OK);
if (!s_hi8561_bl_ready) Serial.println("[HI8561] backlight PWM init FAILED");
}
// Keep the DBI IO handle so setBrightness() can also send DCS 0x51 (harmless if the panel
// ignores it); the real light is the PWM above. Start at max so the LCD actually lights.
_dbi_io = dbi_io;
setBrightness(255);
_on = true;
@@ -182,8 +211,13 @@ void HI8561Display::writePixelsRGB565(int x, int y, int w, int h, const uint16_t
}
void HI8561Display::setBrightness(uint8_t b) {
if (!_dbi_io) return;
esp_lcd_panel_io_tx_param(_dbi_io, 0x51, &b, 1); // DCS SET_DISPLAY_BRIGHTNESS
// The real backlight is the LEDC PWM on GPIO 51 (drives the LCD's LED); the DCS 0x51 is a
// best-effort no-op the AMOLED path used. Drive both so the CC slider dims the LCD.
if (s_hi8561_bl_ready) {
ledc_set_duty(LEDC_LOW_SPEED_MODE, kBlChan, b);
ledc_update_duty(LEDC_LOW_SPEED_MODE, kBlChan);
}
if (_dbi_io) esp_lcd_panel_io_tx_param(_dbi_io, 0x51, &b, 1); // DCS SET_DISPLAY_BRIGHTNESS
}
void HI8561Display::turnOn() { if (_panel) esp_lcd_panel_disp_on_off(_panel, true); _on = true; }