@@ -1,4 +1,4 @@
// display_ui.cpp — ST7796 status screen via LovyanGFX , Phase 1 .
// display_ui.cpp — ST7796 + LVGL status screen, Phase 2a .
//
// Panel / bus configuration faithfully replicates the original Freenove
// Arduino firmware (ui_freenove_allinone/src/drivers/display/display_hal_lgfx.cpp
@@ -15,33 +15,49 @@
// dlen_16bit false, bus_shared true
// - Rotation: 1 (landscape)
//
// BACKLIGHT (Phase 1 ):
// The reference firmware uses Light_PWM (LEDC channel 7, 44100 Hz) on
// GPIO 2. Phase 1 uses plain gpio_set_level(TFT_PIN_BL, 1) instead ,
// because qr_puzzle already claims LEDC_TIMER_0 / LEDC_CHANNEL_0 for the
// camera XCLK — calling Light_PWM::init() on any channel would attempt to
// reconfigure the LEDC timer and may conflict. Phase 2 will switch to
// Light_PWM on a free timer/channel pair once the XCLK LEDC allocation is
// moved to the esp32-camera driver's own management.
// LVGL (Phase 2a ):
// LVGL 8.4 (same line as the original: platformio.ini:35 lvgl/lvgl@8.4.0),
// configured via the managed component's Kconfig (sdkconfig: depth 16 ,
// swap 0, mem 160 KB, Montserrat 24/28 — mirroring the original lv_conf.h).
// Flush callback is the canonical LovyanGFX↔LVGL8 glue (startWrite /
// setAddrWindow / writePixels rgb565 / endWrite), matching the effective
// behaviour of the original ui_manager_display.cpp glue (its DMA path is
// disabled at runtime in the original HAL, so the synchronous path is the
// faithful one). Draw buffer: width × 40 lines, single, INTERNAL ram —
// exactly like the original (buffer_cfg_.draw_in_psram = false, lines = 40).
// Tick: esp_timer periodic 2 ms → lv_tick_inc(2) (the original calls
// lv_tick_inc(elapsed) from its UI task; LV_TICK_CUSTOM is off both sides).
//
// BACKLIGHT (Phase 2a):
// LEDC PWM on LEDC_TIMER_1 / LEDC_CHANNEL_1 (low-speed), ~44.1 kHz like the
// original Light_PWM. LEDC_TIMER_0 / LEDC_CHANNEL_0 stay reserved for the
// camera XCLK (qr_puzzle). display_ui_set_brightness() exposes duty 0-100%.
//
// Thread model:
// All LovyanGFX rendering runs exclusively on the display_task (prio 3,
// stack 4096). display_ui_set_status() copies the status under a mutex
// and sets a dirty flag; the task pick s it up within 2 50 ms.
// ALL LVGL and LovyanGFX calls run exclusively on the display_task.
// display_ui_set_status() copies the status under a mutex and sets a dirty
// flag; the task fold s it into LVGL labels within ~ 50 ms. lv_tick_inc is
// called from the esp_timer task (atomic counter increment — the standard
// espressif pattern).
# include "display_ui.h"
# include "board_pins_mediakit.h"
# include <string.h>
# include <stdio.h>
# include "driver/gpio.h"
# include "driver/ledc.h"
# include "esp_heap_caps.h"
# include "esp_log.h"
# include "esp_timer.h"
# include "freertos/FreeRTOS.h"
# include "freertos/semphr.h"
# include "freertos/task.h"
# define LGFX_USE_V1
# include <LovyanGFX.hpp>
# include "lvgl.h"
static const char * TAG = " display_ui " ;
@@ -93,7 +109,7 @@ class ZacusDisplay final : public lgfx::LGFX_Device {
_panel_instance . config ( cfg ) ;
}
// Backlight handled externally (plain GPIO , see file header).
// Backlight handled externally (LEDC PWM , see file header).
setPanel ( & _panel_instance ) ;
}
@@ -113,7 +129,6 @@ static volatile bool s_dirty = false;
static bool s_running = false ;
// ─── Palette (Amiga Workbench-ish) ────────────────────────────────────────────
// Keep tasteful — this is a status screen, not a demo.
static const uint32_t COL_BG = 0x0055AA ; // Workbench blue
static const uint32_t COL_TITLEBAR = 0x0000AA ; // darker blue
static const uint32_t COL_TITLE_TXT = 0xFFFFFF ; // white
@@ -121,143 +136,267 @@ static const uint32_t COL_LABEL = 0xAAAAAA; // mid grey
static const uint32_t COL_VALUE = 0xFFFFFF ; // white
static const uint32_t COL_CODE = 0xFF8800 ; // Workbench orange
// ─── Rendering (called only from display_task) ────────────────────────────────
// ─── LVGL plumbing ──────────────────────────── ────────────────────────────────
static void render_status ( const display_status_t * s ) {
const int W = s_lcd . width ( ) ; // 480 in rotation 1
const int H = s_lcd . height ( ) ; // 320 in rotation 1
# define DUI_HOR_RES 480 // rotation 1 (landscape)
# define DUI_VER_RES 320
# define DUI_BUF_LINES 40 // like the original (buffer_cfg_.lines)
static lv_disp_draw_buf_t s_draw_buf ;
static lv_color_t * s_buf1 ;
static lv_disp_drv_t s_disp_drv ;
static esp_timer_handle_t s_tick_timer ;
// Canonical LovyanGFX↔LVGL8 glue (synchronous — matches the original's
// effective path; its DMA branch is disabled at runtime).
static void lvgl_flush_cb ( lv_disp_drv_t * drv , const lv_area_t * area ,
lv_color_t * color_p ) {
const uint32_t w = ( uint32_t ) ( area - > x2 - area - > x1 + 1 ) ;
const uint32_t h = ( uint32_t ) ( area - > y2 - area - > y1 + 1 ) ;
s_lcd . startWrite ( ) ;
s_lcd . setAddrWindow ( area - > x1 , area - > y1 , w , h ) ;
s_lcd . writePixels ( ( lgfx : : rgb565_t * ) & color_p - > full , w * h ) ;
s_lcd . endWrite ( ) ;
lv_disp_flush_ready ( drv ) ;
}
static void lvgl_tick_cb ( void * arg ) {
( void ) arg ;
lv_tick_inc ( 2 ) ;
}
// ─── LVGL status screen objects (display task only) ───────────────────────────
typedef struct {
lv_obj_t * value ; // value label of a row
} dui_row_t ;
static lv_obj_t * s_lbl_title ;
static dui_row_t s_row_ip , s_row_wake , s_row_step , s_row_armed , s_row_solved ;
static lv_obj_t * s_lbl_code ;
static lv_obj_t * make_row ( lv_obj_t * parent , int y , const char * label_txt ,
dui_row_t * out ) {
lv_obj_t * lbl = lv_label_create ( parent ) ;
lv_obj_set_style_text_color ( lbl , lv_color_hex ( COL_LABEL ) , 0 ) ;
lv_obj_set_style_text_font ( lbl , & lv_font_montserrat_14 , 0 ) ;
lv_label_set_text ( lbl , label_txt ) ;
lv_obj_set_pos ( lbl , 8 , y ) ;
lv_obj_t * val = lv_label_create ( parent ) ;
lv_obj_set_style_text_color ( val , lv_color_hex ( COL_VALUE ) , 0 ) ;
lv_obj_set_style_text_font ( val , & lv_font_montserrat_14 , 0 ) ;
lv_label_set_text ( val , " ... " ) ;
lv_obj_set_pos ( val , 160 , y ) ;
lv_label_set_long_mode ( val , LV_LABEL_LONG_DOT ) ;
lv_obj_set_width ( val , DUI_HOR_RES - 168 ) ;
out - > value = val ;
return val ;
}
static void build_status_screen ( void ) {
lv_obj_t * scr = lv_scr_act ( ) ;
lv_obj_set_style_bg_color ( scr , lv_color_hex ( COL_BG ) , 0 ) ;
lv_obj_set_style_bg_opa ( scr , LV_OPA_COVER , 0 ) ;
// Title bar
s_lcd . fillRect ( 0 , 0 , W , 28 , COL_TITLEBAR ) ;
s_lcd . setTextColor ( COL_TITLE_TXT ) ;
s_lcd . setFont ( & fonts : : Font4 ) ;
s_lcd . setCursor ( 8 , 5 ) ;
s_lcd . print ( " ZACUS MASTER v1.0 " ) ;
lv_obj_t * bar = lv_obj_create ( scr ) ;
lv_obj_remove_style_all ( bar ) ;
lv_obj_set_size ( bar , DUI_HOR_RES , 36 ) ;
lv_obj_set_pos ( bar , 0 , 0 ) ;
lv_obj_set_style_bg_color ( bar , lv_color_hex ( COL_TITLEBAR ) , 0 ) ;
lv_obj_set_style_bg_opa ( bar , LV_OPA_COVER , 0 ) ;
// Background for content area
s_lcd . fillRect ( 0 , 28 , W , H - 28 , COL_BG ) ;
s_lbl_title = lv_label_create ( bar ) ;
lv_obj_set_style_text_color ( s_lbl_title , lv_color_hex ( COL_TITLE_TXT ) , 0 ) ;
lv_obj_set_style_text_font ( s_lbl_title , & lv_font_montserrat_24 , 0 ) ;
lv_label_set_text ( s_lbl_title , " ZACUS MASTER " ) ;
lv_obj_align ( s_lbl_title , LV_ALIGN_LEFT_MID , 8 , 0 ) ;
// Helper lambda: label + value pair on one line
int y = 36 ;
const int LINE_H = 28 ;
// Rows
int y = 52 ;
const int LINE_H = 30 ;
make_row ( scr , y , " IP " , & s_row_ip ) ; y + = LINE_H ;
make_row ( scr , y , " Wake " , & s_row_wake ) ; y + = LINE_H ;
make_row ( scr , y , " Etape " , & s_row_step ) ; y + = LINE_H ;
make_row ( scr , y , " Enigme " , & s_row_armed ) ; y + = LINE_H ;
make_row ( scr , y , " Resolues " , & s_row_solved ) ; y + = LINE_H ;
auto draw_row = [ & ] ( const char * label , const char * value , uint32_t vcol ) {
s_lcd . setFont ( & fonts : : Font2 ) ;
s_lcd . setTextColor ( COL_LABEL ) ;
s_lcd . setCursor ( 8 , y ) ;
s_lcd . print ( label ) ;
s_lcd . setTextColor ( vcol ) ;
s_lcd . setCursor ( 160 , y ) ;
s_lcd . print ( value ) ;
y + = LINE_H ;
} ;
// IP
draw_row ( " IP : " , s [ 0 ] . ip [ 0 ] ? s [ 0 ] . ip : " (aucune) " , COL_VALUE ) ;
// Wake detector
draw_row ( " Wake : " , s [ 0 ] . wake_active ? " oui " : " non " , COL_VALUE ) ;
// Current step
draw_row ( " Etape : " , s [ 0 ] . step_id [ 0 ] ? s [ 0 ] . step_id : " (aucune) " , COL_VALUE ) ;
// Armed puzzle type
draw_row ( " Enigme : " , s [ 0 ] . armed [ 0 ] ? s [ 0 ] . armed : " none " , COL_VALUE ) ;
// Solved count
char solved_str [ 8 ] ;
snprintf ( solved_str , sizeof ( solved_str ) , " %u " , ( unsigned ) s [ 0 ] . solved_count ) ;
draw_row ( " Resolues : " , solved_str , COL_VALUE ) ;
// Assembled code — big font, orange
s_lcd . setFont ( & fonts : : Font6 ) ;
s_lcd . setTextColor ( COL_CODE ) ;
s_lcd . setCursor ( 8 , y + 4 ) ;
s_lcd . print ( " Code: " ) ;
s_lcd . print ( s [ 0 ] . code [ 0 ] ? s [ 0 ] . code : " --- " ) ;
// Assembled code — big, orange
s_lbl_code = lv_label_create ( scr ) ;
lv_obj_set_style_text_color ( s_lbl_code , lv_color_hex ( COL_CODE ) , 0 ) ;
lv_obj_set_style_text_font ( s_lbl_code , & lv_font_montserrat_28 , 0 ) ;
lv_label_set_text ( s_lbl_code , " Code: --- " ) ;
lv_obj_set_pos ( s_lbl_code , 8 , y + 12 ) ;
}
static void apply_status ( const display_status_t * s ) {
lv_label_set_text ( s_row_ip . value , s - > ip [ 0 ] ? s - > ip : " (aucune) " ) ;
lv_label_set_text ( s_row_wake . value , s - > wake_active ? " oui " : " non " ) ;
lv_label_set_text ( s_row_step . value , s - > step_id [ 0 ] ? s - > step_id : " (aucune) " ) ;
lv_label_set_text ( s_row_armed . value , s - > armed [ 0 ] ? s - > armed : " none " ) ;
char tmp [ 24 ] ;
snprintf ( tmp , sizeof ( tmp ) , " %u " , ( unsigned ) s - > solved_count ) ;
lv_label_set_text ( s_row_solved . value , tmp ) ;
char code [ 32 ] ;
snprintf ( code , sizeof ( code ) , " Code: %s " , s - > code [ 0 ] ? s - > code : " --- " ) ;
lv_label_set_text ( s_lbl_code , code ) ;
}
// ─── Raw splash (pre-LVGL, visible during the rest of boot) ───────────────────
static void render_splash ( void ) {
const int W = s_lcd . width ( ) ;
const int H = s_lcd . height ( ) ;
s_lcd . fillScreen ( COL_BG ) ;
// Title bar
s_lcd . fillRect ( 0 , 0 , W , 36 , COL_TITLEBAR ) ;
s_lcd . setTextColor ( COL_TITLE_TXT ) ;
s_lcd . setFont ( & fonts : : Font4 ) ;
s_lcd . setCursor ( 8 , 8 ) ;
s_lcd . print ( " ZACUS MASTER " ) ;
// Sub-title
s_lcd . setFont ( & fonts : : Font2 ) ;
s_lcd . setTextColor ( COL_VALUE ) ;
s_lcd . setCursor ( 8 , 50 ) ;
s_lcd . print ( " Demarrage... " ) ;
// Version line
s_lcd . setTextColor ( COL_LABEL ) ;
s_lcd . setCursor ( 8 , H - 20 ) ;
s_lcd . print ( " FNK0102H ESP-IDF 5.4 Phase 1 " ) ;
s_lcd . print ( " FNK0102H ESP-IDF 5.4 Phase 2a " ) ;
}
// ─── Backlight (LEDC PWM — free pair; camera XCLK owns TIMER_0/CHANNEL_0) ─────
# define DUI_BL_TIMER LEDC_TIMER_1
# define DUI_BL_CHANNEL LEDC_CHANNEL_1
# define DUI_BL_FREQ_HZ 44100 // like the original Light_PWM
# define DUI_BL_RES LEDC_TIMER_8_BIT
static esp_err_t backlight_init ( void ) {
ledc_timer_config_t tcfg = { } ;
tcfg . speed_mode = LEDC_LOW_SPEED_MODE ;
tcfg . timer_num = DUI_BL_TIMER ;
tcfg . duty_resolution = DUI_BL_RES ;
tcfg . freq_hz = DUI_BL_FREQ_HZ ;
tcfg . clk_cfg = LEDC_AUTO_CLK ;
esp_err_t err = ledc_timer_config ( & tcfg ) ;
if ( err ! = ESP_OK ) return err ;
ledc_channel_config_t ccfg = { } ;
ccfg . gpio_num = TFT_PIN_BL ;
ccfg . speed_mode = LEDC_LOW_SPEED_MODE ;
ccfg . channel = DUI_BL_CHANNEL ;
ccfg . timer_sel = DUI_BL_TIMER ;
ccfg . duty = 255 ; // full on by default
ccfg . hpoint = 0 ;
return ledc_channel_config ( & ccfg ) ;
}
extern " C " void display_ui_set_brightness ( uint8_t pct ) {
if ( pct > 100 ) pct = 100 ;
const uint32_t duty = ( 255u * pct ) / 100u ;
ledc_set_duty ( LEDC_LOW_SPEED_MODE , DUI_BL_CHANNEL , duty ) ;
ledc_update_duty ( LEDC_LOW_SPEED_MODE , DUI_BL_CHANNEL ) ;
}
// ─── Display task ─────────────────────────────────────────────────────────────
static void display_task ( void * arg ) {
( void ) arg ;
// LVGL lives entirely on this task.
build_status_screen ( ) ;
display_status_t local_status ;
memset ( & local_status , 0 , sizeof ( local_status ) ) ;
for ( ; ; ) {
vTaskDelay ( pdMS_TO_TICKS ( 250 ) ) ;
if ( ! s_dirty ) {
continue ;
}
// Snapshot under mutex
if ( xSemaphoreTake ( s_mutex , pdMS_TO_TICKS ( 50 ) ) = = pdTRUE ) {
if ( s_dirty & &
xSemaphoreTake ( s_mutex , pdMS_TO_TICKS ( 20 ) ) = = pdTRUE ) {
memcpy ( & local_status , & s_status , sizeof ( local_status ) ) ;
s_dirty = false ;
xSemaphoreGive ( s_mutex ) ;
} else {
continue ;
apply_status ( & local_status ) ;
}
render_status ( & local_status ) ;
lv_timer_handler ( ) ;
vTaskDelay ( pdMS_TO_TICKS ( 10 ) ) ;
}
}
// ─── Public API ──────────────────────────────────────────────────────────────
extern " C " esp_err_t display_ui_init ( void ) {
// Backlight on (plain GPIO — Phase 1; see header for LEDC Phase 2 plan ).
gpio_config_t bl_cfg = { } ;
bl_cfg . pin_bit_mask = ( 1ULL < < TFT_PIN_BL ) ;
bl_cfg . mode = GPIO_MODE_OUTPUT ;
bl_cfg . pull_up_en = GPIO_PULLUP_DISABLE ;
bl_cfg . pull_down_en = GPIO_PULLDOWN_DISABLE ;
bl_cfg . intr_type = GPIO_INTR_DISABLE ;
gpio_config ( & bl_cfg ) ;
gpio_set_level ( ( gpio_num_t ) TFT_PIN_BL , 1 ) ;
// Backlight PWM up first (panel garbage stays dark otherwise ).
esp_err_t err = backlight_init ( ) ;
if ( err ! = ESP_OK ) {
ESP_LOGW ( TAG , " backlight LEDC init: %s — falling back to plain GPIO " ,
esp_err_to_name ( err ) ) ;
gpio_config_t bl_cfg = { } ;
bl_cfg . pin_bit_mask = ( 1ULL < < TFT_PIN_BL ) ;
bl_cfg . mode = GPIO_MODE_OUTPUT ;
gpio_config ( & bl_cfg ) ;
gpio_set_level ( ( gpio_num_t ) TFT_PIN_BL , 1 ) ;
}
// Initialise panel
if ( ! s_lcd . init ( ) ) {
ESP_LOGE ( TAG , " LovyanGFX panel init failed " ) ;
return ESP_FAIL ;
}
s_lcd . setRotation ( 1 ) ; // landscape, matches FREENOVE_LCD_ROTATION 1
s_lcd . setBrightness ( 255 ) ;
ESP_LOGI ( TAG , " ST7796 panel up (%dx%d) " ,
( int ) s_lcd . width ( ) , ( int ) s_lcd . height ( ) ) ;
render_splash ( ) ;
// ── LVGL ──────────────────────────────────────────────────────────────
lv_init ( ) ;
// Draw buffer: width × 40 lines, single, INTERNAL ram (like the original).
const size_t buf_px = ( size_t ) DUI_HOR_RES * DUI_BUF_LINES ;
s_buf1 = ( lv_color_t * ) heap_caps_malloc ( buf_px * sizeof ( lv_color_t ) ,
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT ) ;
if ( ! s_buf1 ) {
ESP_LOGE ( TAG , " LVGL draw buffer alloc failed (%u B) " ,
( unsigned ) ( buf_px * sizeof ( lv_color_t ) ) ) ;
return ESP_ERR_NO_MEM ;
}
lv_disp_draw_buf_init ( & s_draw_buf , s_buf1 , NULL , buf_px ) ;
lv_disp_drv_init ( & s_disp_drv ) ;
s_disp_drv . hor_res = DUI_HOR_RES ;
s_disp_drv . ver_res = DUI_VER_RES ;
s_disp_drv . flush_cb = lvgl_flush_cb ;
s_disp_drv . draw_buf = & s_draw_buf ;
if ( ! lv_disp_drv_register ( & s_disp_drv ) ) {
ESP_LOGE ( TAG , " lv_disp_drv_register failed " ) ;
return ESP_FAIL ;
}
// Tick: 2 ms periodic (LV_TICK_CUSTOM off, like the original).
const esp_timer_create_args_t targs = {
. callback = lvgl_tick_cb ,
. arg = NULL ,
. dispatch_method = ESP_TIMER_TASK ,
. name = " lv_tick " ,
. skip_unhandled_events = true ,
} ;
err = esp_timer_create ( & targs , & s_tick_timer ) ;
if ( err = = ESP_OK ) err = esp_timer_start_periodic ( s_tick_timer , 2000 ) ;
if ( err ! = ESP_OK ) {
ESP_LOGE ( TAG , " LVGL tick timer: %s " , esp_err_to_name ( err ) ) ;
return err ;
}
ESP_LOGI ( TAG , " LVGL %d.%d up (buf %u lines, internal ram) " ,
lv_version_major ( ) , lv_version_minor ( ) , ( unsigned ) DUI_BUF_LINES ) ;
// State
s_mutex = xSemaphoreCreateMutex ( ) ;
if ( ! s_mutex ) {
@@ -268,14 +407,14 @@ extern "C" esp_err_t display_ui_init(void) {
s_dirty = false ;
s_running = true ;
// Spawn refresh task (all rendering lives here)
BaseType_t rc = xTaskCreate ( display_task , " display_ui " , 4096 , NULL , 3 , NULL ) ;
// Spawn refresh task (ALL LVGL + panel access lives here).
BaseType_t rc = xTaskCreate ( display_task , " display_ui " , 8192 , NULL , 3 , NULL ) ;
if ( rc ! = pdPASS ) {
ESP_LOGE ( TAG , " xTaskCreate failed " ) ;
return ESP_ERR_NO_MEM ;
}
ESP_LOGI ( TAG , " display_ui_init OK — splash visibl e " ) ;
ESP_LOGI ( TAG , " display_ui_init OK — LVGL status screen activ e " ) ;
return ESP_OK ;
}