diff --git a/components/control_plane/src/rest_server_print_image.c b/components/control_plane/src/rest_server_print_image.c index 938981c..f6f97fb 100644 --- a/components/control_plane/src/rest_server_print_image.c +++ b/components/control_plane/src/rest_server_print_image.c @@ -9,6 +9,7 @@ #include "domain.h" static const char *TAG = "rest_print"; +#define SCREEN_PREVIEW_TIMEOUT_MS 200 typedef struct { bool scale_to_width; @@ -355,6 +356,42 @@ static esp_err_t rest_server_send_image_generation_error(httpd_req_t *req, return rest_server_send_error(req, "502 Bad Gateway", message); } +static void rest_server_try_preview_raster(const char *source, + const uint8_t *raster, + size_t raster_len, + uint16_t width, + uint16_t height) { + if (raster == NULL || raster_len == 0 || width == 0 || height == 0) { + return; + } + + char preview_err[96] = {0}; + esp_err_t preview_rc = screen_preview_show_raster(raster, + raster_len, + width, + height, + SCREEN_PREVIEW_TIMEOUT_MS, + preview_err, + sizeof(preview_err)); + if (preview_rc == ESP_OK) { + ESP_LOGI(TAG, + "screen preview updated (%s), size=%ux%u", + source != NULL ? source : "unknown", + (unsigned)width, + (unsigned)height); + return; + } + if (preview_rc == ESP_ERR_NOT_SUPPORTED) { + return; + } + + ESP_LOGW(TAG, + "screen preview skipped (%s), rc=0x%x, msg=%s", + source != NULL ? source : "unknown", + (unsigned)preview_rc, + preview_err[0] != '\0' ? preview_err : "preview failed"); +} + static esp_err_t rest_server_print_image_binary(httpd_req_t *req) { bool scale_to_width = true; uint8_t threshold = 160; @@ -419,6 +456,7 @@ static esp_err_t rest_server_print_image_binary(httpd_req_t *req) { (unsigned)width, (unsigned)height, (unsigned)raster_len); + rest_server_try_preview_raster("upload", raster, raster_len, width, height); esp_err_t submit_rc = rest_server_print_submit_raster_job_and_reply(req, raster, @@ -562,6 +600,7 @@ static esp_err_t rest_server_print_image_generate(httpd_req_t *req) { (unsigned)width, (unsigned)height, (unsigned)raster_len); + rest_server_try_preview_raster("generated", raster, raster_len, width, height); char warning[160] = {0}; if (request_id[0] != '\0') { diff --git a/components/domain/CMakeLists.txt b/components/domain/CMakeLists.txt index 09f9cf5..a7761f2 100644 --- a/components/domain/CMakeLists.txt +++ b/components/domain/CMakeLists.txt @@ -7,6 +7,7 @@ idf_component_register( "src/raster_tools.c" "src/raster_tools_image_qr.c" "src/image_generation.c" + "src/screen_preview.c" "src/system_runtime.c" "src/voice_interaction.c" "src/voice_interaction_common.c" diff --git a/components/domain/include/domain.h b/components/domain/include/domain.h index ab9ae99..7c0bf83 100644 --- a/components/domain/include/domain.h +++ b/components/domain/include/domain.h @@ -100,6 +100,15 @@ esp_err_t image_generation_generate_png(const image_generation_request_t *req, char *err, size_t err_len); +// ---------- screen_preview ---------- +esp_err_t screen_preview_show_raster(const uint8_t *raster, + size_t raster_len, + uint16_t width, + uint16_t height, + uint32_t timeout_ms, + char *err, + size_t err_len); + // ---------- printer_protocol ---------- typedef enum { PRINT_JOB_STATE_NONE = 0, diff --git a/components/domain/src/screen_preview.c b/components/domain/src/screen_preview.c new file mode 100644 index 0000000..b7971ae --- /dev/null +++ b/components/domain/src/screen_preview.c @@ -0,0 +1,12 @@ +#include "domain.h" + +esp_err_t screen_preview_show_raster(const uint8_t *raster, + size_t raster_len, + uint16_t width, + uint16_t height, + uint32_t timeout_ms, + char *err, + size_t err_len) +{ + return platform_display_show_raster_1bpp(raster, raster_len, width, height, timeout_ms, err, err_len); +} diff --git a/components/platform/CMakeLists.txt b/components/platform/CMakeLists.txt index 1efdb67..a9d6d02 100644 --- a/components/platform/CMakeLists.txt +++ b/components/platform/CMakeLists.txt @@ -5,6 +5,9 @@ idf_component_register( "src/ble_printer_client.c" "src/direct_thermal_printer.c" "src/display_st7789.c" + "src/display_lcd_module.c" + "src/display_lcd_decode_image.c" + "src/display_st7789p3.c" "src/voice_audio.c" "src/runtime_policy.c" "src/runtime_diagnostics.c" @@ -21,8 +24,13 @@ idf_component_register( nvs_flash mbedtls esp_adc - esp_lcd + esp_driver_gpio + esp_driver_spi + lvgl + espressif__libpng esp_codec_dev esp_audio_codec driver + EMBED_FILES + "assets/lcd_demo.png" ) diff --git a/components/platform/assets/lcd_demo.png b/components/platform/assets/lcd_demo.png new file mode 100644 index 0000000..5f481b0 Binary files /dev/null and b/components/platform/assets/lcd_demo.png differ diff --git a/components/platform/include/platform.h b/components/platform/include/platform.h index 65baaa8..b60965f 100644 --- a/components/platform/include/platform.h +++ b/components/platform/include/platform.h @@ -92,6 +92,13 @@ esp_err_t platform_direct_printer_gap_move(uint32_t timeout_ms, char *err, size_ // ---------- display_st7789 ---------- esp_err_t platform_display_init(void); esp_err_t platform_display_show_test_pattern(void); +esp_err_t platform_display_show_raster_1bpp(const uint8_t *raster, + size_t raster_len, + uint16_t width, + uint16_t height, + uint32_t timeout_ms, + char *err, + size_t err_len); void platform_display_deinit(void); // ---------- voice_audio ---------- diff --git a/components/platform/internal/display_lcd_decode_image.h b/components/platform/internal/display_lcd_decode_image.h new file mode 100644 index 0000000..59fac70 --- /dev/null +++ b/components/platform/internal/display_lcd_decode_image.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +#include "esp_err.h" + +esp_err_t platform_lcd_decode_demo_png(uint16_t **out_pixels, + uint16_t *out_width, + uint16_t *out_height, + char *err, + size_t err_len); diff --git a/components/platform/internal/display_lcd_module.h b/components/platform/internal/display_lcd_module.h new file mode 100644 index 0000000..21cc19b --- /dev/null +++ b/components/platform/internal/display_lcd_module.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +esp_err_t lcd_module_init(void); +esp_err_t lcd_module_start_demo(void); +esp_err_t lcd_module_show_raster_1bpp(const uint8_t *raster, + size_t raster_len, + uint16_t width, + uint16_t height, + uint32_t timeout_ms, + char *err, + size_t err_len); +void lcd_module_deinit(void); + +#ifdef __cplusplus +} +#endif diff --git a/components/platform/internal/display_st7789p3.h b/components/platform/internal/display_st7789p3.h new file mode 100644 index 0000000..f0c13d7 --- /dev/null +++ b/components/platform/internal/display_st7789p3.h @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include + +#include "driver/spi_master.h" +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct st7789p3_panel_t *st7789p3_handle_t; + +#define ST7789P3_MADCTL_MY 0x80 +#define ST7789P3_MADCTL_MX 0x40 +#define ST7789P3_MADCTL_MV 0x20 +#define ST7789P3_MADCTL_ML 0x10 +#define ST7789P3_MADCTL_BGR 0x08 +#define ST7789P3_MADCTL_MH 0x04 + +typedef struct { + spi_host_device_t spi_host; + int pin_mosi; + int pin_miso; + int pin_sclk; + int pin_cs; + int pin_dc; + int pin_rst; + int pin_bckl; + uint32_t pclk_hz; + uint8_t spi_mode; + int queue_size; + size_t max_transfer_sz; + uint16_t h_res; + uint16_t v_res; + uint8_t madctl_val; + bool invert_color; + bool swap_color_bytes; + int backlight_on_level; + bool init_spi_bus; +} st7789p3_config_t; + +#define ST7789P3_CONFIG_DEFAULT() \ + { \ + .spi_host = SPI2_HOST, \ + .pin_mosi = -1, \ + .pin_miso = -1, \ + .pin_sclk = -1, \ + .pin_cs = -1, \ + .pin_dc = -1, \ + .pin_rst = -1, \ + .pin_bckl = -1, \ + .pclk_hz = 10 * 1000 * 1000, \ + .spi_mode = 0, \ + .queue_size = 7, \ + .max_transfer_sz = 0, \ + .h_res = 320, \ + .v_res = 240, \ + .madctl_val = ST7789P3_MADCTL_MX | ST7789P3_MADCTL_MV, \ + .invert_color = false, \ + .swap_color_bytes = false, \ + .backlight_on_level = 1, \ + .init_spi_bus = true, \ + } + +esp_err_t st7789p3_new_panel(const st7789p3_config_t *config, st7789p3_handle_t *ret_panel); +esp_err_t st7789p3_del(st7789p3_handle_t panel); + +esp_err_t st7789p3_reset(st7789p3_handle_t panel); +esp_err_t st7789p3_init(st7789p3_handle_t panel); + +esp_err_t st7789p3_set_backlight(st7789p3_handle_t panel, bool on); +esp_err_t st7789p3_set_madctl(st7789p3_handle_t panel, uint8_t madctl_val); +esp_err_t st7789p3_set_invert(st7789p3_handle_t panel, bool invert_color); +esp_err_t st7789p3_set_swap_color_bytes(st7789p3_handle_t panel, bool swap_color_bytes); + +esp_err_t st7789p3_set_window( + st7789p3_handle_t panel, uint16_t x_start, uint16_t y_start, uint16_t x_end, uint16_t y_end); +esp_err_t st7789p3_draw_bitmap(st7789p3_handle_t panel, + uint16_t x_start, + uint16_t y_start, + uint16_t x_end, + uint16_t y_end, + const uint16_t *color_data); + +esp_err_t st7789p3_fill_color(st7789p3_handle_t panel, uint16_t color); + +#ifdef __cplusplus +} +#endif diff --git a/components/platform/src/display_lcd_decode_image.c b/components/platform/src/display_lcd_decode_image.c new file mode 100644 index 0000000..5dfbfcd --- /dev/null +++ b/components/platform/src/display_lcd_decode_image.c @@ -0,0 +1,113 @@ +#include "display_lcd_decode_image.h" + +#include +#include +#include +#include + +#include "esp_check.h" +#include "esp_heap_caps.h" +#include "esp_log.h" +#include "lvgl.h" +#include "png.h" + +static const char *TAG = "lcd_png_dec"; + +extern const uint8_t lcd_demo_png_start[] asm("_binary_lcd_demo_png_start"); +extern const uint8_t lcd_demo_png_end[] asm("_binary_lcd_demo_png_end"); + +static void lcd_fill_err(char *err, size_t err_len, const char *msg) +{ + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "%s", msg != NULL ? msg : "unknown"); + } +} + +esp_err_t platform_lcd_decode_demo_png(uint16_t **out_pixels, + uint16_t *out_width, + uint16_t *out_height, + char *err, + size_t err_len) +{ + esp_err_t ret = ESP_OK; + + ESP_RETURN_ON_FALSE(out_pixels != NULL && out_width != NULL && out_height != NULL, + ESP_ERR_INVALID_ARG, + TAG, + "invalid output args"); + *out_pixels = NULL; + *out_width = 0; + *out_height = 0; + + const size_t png_len = (size_t)(lcd_demo_png_end - lcd_demo_png_start); + ESP_RETURN_ON_FALSE(png_len > 0, ESP_ERR_INVALID_SIZE, TAG, "empty png data"); + + png_image image; + memset(&image, 0, sizeof(image)); + image.version = PNG_IMAGE_VERSION; + + ESP_RETURN_ON_FALSE(png_image_begin_read_from_memory(&image, lcd_demo_png_start, png_len), + ESP_FAIL, + TAG, + "png header decode failed: %s", + image.message); + ESP_GOTO_ON_FALSE(image.width > 0 && image.height > 0, ESP_ERR_INVALID_SIZE, fail, TAG, "invalid png size"); + ESP_GOTO_ON_FALSE(image.width <= UINT16_MAX && image.height <= UINT16_MAX, + ESP_ERR_INVALID_SIZE, + fail, + TAG, + "png dimensions too large"); + ESP_GOTO_ON_FALSE(image.width <= SIZE_MAX / image.height, ESP_ERR_INVALID_SIZE, fail, TAG, "png too large"); + + image.format = PNG_FORMAT_RGB; + png_alloc_size_t rgb_size = PNG_IMAGE_SIZE(image); + ESP_GOTO_ON_FALSE(rgb_size > 0, ESP_ERR_INVALID_SIZE, fail, TAG, "png output size invalid"); + + uint8_t *rgb = (uint8_t *)heap_caps_malloc((size_t)rgb_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (rgb == NULL) { + rgb = (uint8_t *)heap_caps_malloc((size_t)rgb_size, MALLOC_CAP_8BIT); + } + ESP_GOTO_ON_FALSE(rgb != NULL, ESP_ERR_NO_MEM, fail, TAG, "alloc rgb buffer failed"); + + png_alloc_size_t stride = PNG_IMAGE_ROW_STRIDE(image); + if (!png_image_finish_read(&image, NULL, rgb, stride, NULL)) { + const char *png_err = image.message[0] != '\0' ? image.message : "png decode failed"; + lcd_fill_err(err, err_len, png_err); + heap_caps_free(rgb); + png_image_free(&image); + return ESP_FAIL; + } + png_image_free(&image); + + size_t pixel_count = (size_t)image.width * (size_t)image.height; + uint16_t *pixels = (uint16_t *)heap_caps_malloc(pixel_count * sizeof(uint16_t), + MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (pixels == NULL) { + pixels = (uint16_t *)heap_caps_malloc(pixel_count * sizeof(uint16_t), MALLOC_CAP_8BIT); + } + if (pixels == NULL) { + lcd_fill_err(err, err_len, "alloc rgb565 buffer failed"); + heap_caps_free(rgb); + return ESP_ERR_NO_MEM; + } + + for (size_t i = 0; i < pixel_count; ++i) { + const size_t base = i * 3u; + lv_color_t color = lv_color_make(rgb[base], rgb[base + 1u], rgb[base + 2u]); + pixels[i] = (uint16_t)color.full; + } + heap_caps_free(rgb); + + *out_pixels = pixels; + *out_width = (uint16_t)image.width; + *out_height = (uint16_t)image.height; + return ESP_OK; + +fail: + lcd_fill_err(err, err_len, "invalid png"); + png_image_free(&image); + if (ret == ESP_OK) { + ret = ESP_FAIL; + } + return ret; +} diff --git a/components/platform/src/display_lcd_module.c b/components/platform/src/display_lcd_module.c new file mode 100644 index 0000000..1207b31 --- /dev/null +++ b/components/platform/src/display_lcd_module.c @@ -0,0 +1,513 @@ +#include "display_lcd_module.h" + +#include +#include +#include +#include +#include + +#include "esp_check.h" +#include "esp_heap_caps.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "lvgl.h" +#include "sdkconfig.h" +#include "display_lcd_decode_image.h" +#include "display_st7789p3.h" + +static const char *TAG = "lcd_module"; + +#define ST7789_SPI_HOST SPI2_HOST +#define LCD_PANEL_H_RES 320 +#define LCD_PANEL_V_RES 240 +#define LCD_PREVIEW_TIMEOUT_MS_DEFAULT 200 +#define LCD_LOCK_TIMEOUT_MS 1000 +#define LCD_DRAW_LINES 20 + +#ifndef CONFIG_TQ_SCREEN_SPI_MODE +#define CONFIG_TQ_SCREEN_SPI_MODE 0 +#endif + +#ifndef CONFIG_TQ_SCREEN_DRAW_LINES +#define CONFIG_TQ_SCREEN_DRAW_LINES LCD_DRAW_LINES +#endif + +#ifndef CONFIG_TQ_SCREEN_BACKLIGHT_ACTIVE_HIGH +#define CONFIG_TQ_SCREEN_BACKLIGHT_ACTIVE_HIGH 1 +#endif + +#if CONFIG_TQ_SCREEN_BACKLIGHT_ACTIVE_HIGH +#define ST7789_BACKLIGHT_ON_LEVEL 1 +#else +#define ST7789_BACKLIGHT_ON_LEVEL 0 +#endif + +typedef struct { + bool bgr; + bool swap_bytes; + bool invert; +} lcd_color_mode_t; + +// Locked profile chosen from calibration (equivalent to MODE 6). +static const lcd_color_mode_t s_default_color_mode = { + .bgr = false, + .swap_bytes = true, + .invert = true, +}; + +typedef struct { + st7789p3_handle_t panel; + SemaphoreHandle_t lock; + lv_disp_draw_buf_t lv_draw_buf; + lv_disp_drv_t lv_drv; + lv_disp_t *lv_disp; + lv_obj_t *preview_img; + lv_color_t *draw_buf_1; + lv_color_t *draw_buf_2; + uint16_t *preview_pixels; + lv_img_dsc_t preview_dsc; + bool lvgl_inited; + bool lvgl_ready; + bool initialized; +} lcd_state_t; + +static lcd_state_t s_lcd; + +static int resolve_reset_pin(void) +{ + if (CONFIG_TQ_SCREEN_RESET_PIN >= 0) { + return CONFIG_TQ_SCREEN_RESET_PIN; + } + return CONFIG_TQ_SCREEN_BACKLIGHT_PIN; +} + +static void lcd_fill_err(char *err, size_t err_len, const char *msg) +{ + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "%s", msg != NULL ? msg : "unknown"); + } +} + +static bool lcd_lock_take(uint32_t timeout_ms) +{ + if (s_lcd.lock == NULL) { + return false; + } + TickType_t ticks = pdMS_TO_TICKS(timeout_ms); + if (timeout_ms > 0 && ticks == 0) { + ticks = 1; + } + return xSemaphoreTake(s_lcd.lock, ticks) == pdTRUE; +} + +static void lcd_lock_give(void) +{ + if (s_lcd.lock != NULL) { + xSemaphoreGive(s_lcd.lock); + } +} + +static void *lcd_alloc_fb(size_t bytes) +{ + void *buf = heap_caps_malloc(bytes, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); + if (buf == NULL) { + buf = heap_caps_malloc(bytes, MALLOC_CAP_8BIT); + } + return buf; +} + +static void lcd_free_lvgl_buffers(void) +{ + if (s_lcd.draw_buf_1 != NULL) { + heap_caps_free(s_lcd.draw_buf_1); + s_lcd.draw_buf_1 = NULL; + } + if (s_lcd.draw_buf_2 != NULL) { + heap_caps_free(s_lcd.draw_buf_2); + s_lcd.draw_buf_2 = NULL; + } +} + +static void lcd_lvgl_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_map) +{ + st7789p3_handle_t panel = (st7789p3_handle_t)disp_drv->user_data; + if (panel == NULL || area == NULL || color_map == NULL) { + lv_disp_flush_ready(disp_drv); + return; + } + if (area->x1 < 0 || area->y1 < 0 || area->x2 < area->x1 || area->y2 < area->y1) { + lv_disp_flush_ready(disp_drv); + return; + } + + esp_err_t err = st7789p3_draw_bitmap(panel, + (uint16_t)area->x1, + (uint16_t)area->y1, + (uint16_t)(area->x2 + 1), + (uint16_t)(area->y2 + 1), + (const uint16_t *)color_map); + if (err != ESP_OK) { + ESP_LOGW(TAG, "lvgl flush failed: %s", esp_err_to_name(err)); + } + lv_disp_flush_ready(disp_drv); +} + +static esp_err_t lcd_lvgl_init_locked(void) +{ + if (s_lcd.lvgl_ready) { + return ESP_OK; + } + + if (!s_lcd.lvgl_inited) { + lv_init(); + s_lcd.lvgl_inited = true; + } + + const size_t draw_buf_pixels = LCD_PANEL_H_RES * CONFIG_TQ_SCREEN_DRAW_LINES; + const size_t draw_buf_bytes = draw_buf_pixels * sizeof(lv_color_t); + s_lcd.draw_buf_1 = (lv_color_t *)lcd_alloc_fb(draw_buf_bytes); + if (s_lcd.draw_buf_1 == NULL) { + return ESP_ERR_NO_MEM; + } + s_lcd.draw_buf_2 = (lv_color_t *)lcd_alloc_fb(draw_buf_bytes); + if (s_lcd.draw_buf_2 == NULL) { + lcd_free_lvgl_buffers(); + return ESP_ERR_NO_MEM; + } + + lv_disp_draw_buf_init(&s_lcd.lv_draw_buf, s_lcd.draw_buf_1, s_lcd.draw_buf_2, draw_buf_pixels); + + lv_disp_drv_init(&s_lcd.lv_drv); + s_lcd.lv_drv.hor_res = LCD_PANEL_H_RES; + s_lcd.lv_drv.ver_res = LCD_PANEL_V_RES; + s_lcd.lv_drv.flush_cb = lcd_lvgl_flush_cb; + s_lcd.lv_drv.draw_buf = &s_lcd.lv_draw_buf; + s_lcd.lv_drv.user_data = s_lcd.panel; + s_lcd.lv_disp = lv_disp_drv_register(&s_lcd.lv_drv); + if (s_lcd.lv_disp == NULL) { + lcd_free_lvgl_buffers(); + ESP_LOGE(TAG, "lvgl display register failed"); + return ESP_FAIL; + } + + lv_obj_t *screen = lv_scr_act(); + lv_obj_set_style_bg_color(screen, lv_color_black(), LV_PART_MAIN); + lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_clear_flag(screen, LV_OBJ_FLAG_SCROLLABLE); + + s_lcd.preview_img = lv_img_create(screen); + lv_obj_add_flag(s_lcd.preview_img, LV_OBJ_FLAG_HIDDEN); + lv_obj_center(s_lcd.preview_img); + + s_lcd.lvgl_ready = true; + lv_refr_now(s_lcd.lv_disp); + return ESP_OK; +} + +static void lcd_calc_preview_size(uint16_t src_w, uint16_t src_h, uint16_t *out_w, uint16_t *out_h) +{ + if (src_w == 0 || src_h == 0 || out_w == NULL || out_h == NULL) { + return; + } + + if (src_w <= LCD_PANEL_H_RES && src_h <= LCD_PANEL_V_RES) { + *out_w = src_w; + *out_h = src_h; + return; + } + + if ((uint64_t)src_w * LCD_PANEL_V_RES > (uint64_t)src_h * LCD_PANEL_H_RES) { + *out_w = LCD_PANEL_H_RES; + *out_h = (uint16_t)(((uint32_t)src_h * LCD_PANEL_H_RES) / src_w); + } else { + *out_h = LCD_PANEL_V_RES; + *out_w = (uint16_t)(((uint32_t)src_w * LCD_PANEL_V_RES) / src_h); + } + if (*out_w == 0) { + *out_w = 1; + } + if (*out_h == 0) { + *out_h = 1; + } +} + +static esp_err_t lcd_render_raster_to_rgb565(const uint8_t *raster, + size_t raster_len, + uint16_t src_w, + uint16_t src_h, + uint16_t **out_pixels, + uint16_t *out_w, + uint16_t *out_h, + char *err, + size_t err_len) +{ + if (raster == NULL || src_w == 0 || src_h == 0 || out_pixels == NULL || out_w == NULL || out_h == NULL) { + lcd_fill_err(err, err_len, "invalid raster"); + return ESP_ERR_INVALID_ARG; + } + + size_t bytes_per_row = ((size_t)src_w + 7u) / 8u; + size_t needed_len = bytes_per_row * src_h; + if (raster_len < needed_len) { + lcd_fill_err(err, err_len, "raster length mismatch"); + return ESP_ERR_INVALID_SIZE; + } + + lcd_calc_preview_size(src_w, src_h, out_w, out_h); + size_t pixel_count = (size_t)(*out_w) * (size_t)(*out_h); + if (pixel_count == 0 || pixel_count > SIZE_MAX / sizeof(uint16_t)) { + lcd_fill_err(err, err_len, "preview size invalid"); + return ESP_ERR_INVALID_SIZE; + } + + uint16_t *pixels = (uint16_t *)heap_caps_malloc(pixel_count * sizeof(uint16_t), + MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (pixels == NULL) { + pixels = (uint16_t *)heap_caps_malloc(pixel_count * sizeof(uint16_t), MALLOC_CAP_8BIT); + } + if (pixels == NULL) { + lcd_fill_err(err, err_len, "no memory"); + return ESP_ERR_NO_MEM; + } + + for (uint16_t y = 0; y < *out_h; ++y) { + uint16_t src_y = (uint16_t)(((uint32_t)y * src_h) / (*out_h)); + size_t row_base = (size_t)src_y * bytes_per_row; + size_t out_row = (size_t)y * (*out_w); + for (uint16_t x = 0; x < *out_w; ++x) { + uint16_t src_x = (uint16_t)(((uint32_t)x * src_w) / (*out_w)); + size_t byte_idx = row_base + (src_x / 8u); + uint8_t bit = (uint8_t)(7u - (src_x % 8u)); + bool is_black = (raster[byte_idx] & (uint8_t)(1u << bit)) != 0; + pixels[out_row + x] = is_black ? 0x0000 : 0xFFFF; + } + } + + *out_pixels = pixels; + return ESP_OK; +} + +void lcd_module_deinit(void) +{ +#if CONFIG_TQ_SCREEN_ENABLE + if (s_lcd.lock != NULL) { + (void)xSemaphoreTake(s_lcd.lock, portMAX_DELAY); + } + + if (s_lcd.lv_disp != NULL) { + lv_disp_remove(s_lcd.lv_disp); + s_lcd.lv_disp = NULL; + } + if (s_lcd.preview_pixels != NULL) { + heap_caps_free(s_lcd.preview_pixels); + s_lcd.preview_pixels = NULL; + } + lcd_free_lvgl_buffers(); + s_lcd.preview_img = NULL; + s_lcd.lvgl_ready = false; + + if (s_lcd.lock != NULL) { + xSemaphoreGive(s_lcd.lock); + vSemaphoreDelete(s_lcd.lock); + s_lcd.lock = NULL; + } + + if (s_lcd.panel != NULL) { + (void)st7789p3_del(s_lcd.panel); + s_lcd.panel = NULL; + } + s_lcd.initialized = false; +#endif +} + +esp_err_t lcd_module_init(void) +{ +#if !CONFIG_TQ_SCREEN_ENABLE + return ESP_ERR_NOT_SUPPORTED; +#else + if (s_lcd.initialized) { + return ESP_OK; + } + + st7789p3_config_t lcd_cfg = ST7789P3_CONFIG_DEFAULT(); + lcd_cfg.spi_host = ST7789_SPI_HOST; + lcd_cfg.pin_miso = CONFIG_TQ_PRINTER_MISO_PIN; + lcd_cfg.pin_mosi = CONFIG_TQ_SPI_SHARED_MOSI_PIN; + lcd_cfg.pin_sclk = CONFIG_TQ_SPI_SHARED_CLK_PIN; + lcd_cfg.pin_cs = CONFIG_TQ_SCREEN_CS_PIN; + lcd_cfg.pin_dc = CONFIG_TQ_SCREEN_DC_PIN; + lcd_cfg.pin_rst = resolve_reset_pin(); + lcd_cfg.pin_bckl = CONFIG_TQ_SCREEN_BACKLIGHT_PIN; + lcd_cfg.backlight_on_level = ST7789_BACKLIGHT_ON_LEVEL; + lcd_cfg.pclk_hz = CONFIG_TQ_SCREEN_PIXEL_CLOCK_HZ; + lcd_cfg.spi_mode = CONFIG_TQ_SCREEN_SPI_MODE; + lcd_cfg.h_res = LCD_PANEL_H_RES; + lcd_cfg.v_res = LCD_PANEL_V_RES; + lcd_cfg.madctl_val = ST7789P3_MADCTL_MX | ST7789P3_MADCTL_MV; + if (s_default_color_mode.bgr) { + lcd_cfg.madctl_val |= ST7789P3_MADCTL_BGR; + } + lcd_cfg.invert_color = s_default_color_mode.invert; + lcd_cfg.swap_color_bytes = s_default_color_mode.swap_bytes; + lcd_cfg.max_transfer_sz = CONFIG_TQ_SCREEN_DRAW_LINES * LCD_PANEL_H_RES * sizeof(uint16_t) + 8; + + esp_err_t err = st7789p3_new_panel(&lcd_cfg, &s_lcd.panel); + if (err != ESP_OK) { + return err; + } + + err = st7789p3_init(s_lcd.panel); + if (err != ESP_OK) { + lcd_module_deinit(); + return err; + } + + s_lcd.lock = xSemaphoreCreateMutex(); + if (s_lcd.lock == NULL) { + lcd_module_deinit(); + return ESP_ERR_NO_MEM; + } + + if (!lcd_lock_take(LCD_LOCK_TIMEOUT_MS)) { + lcd_module_deinit(); + return ESP_ERR_TIMEOUT; + } + err = lcd_lvgl_init_locked(); + lcd_lock_give(); + if (err != ESP_OK) { + lcd_module_deinit(); + return err; + } + + s_lcd.initialized = true; + ESP_LOGI(TAG, + "st7789p3 ready: spi_host=%d mosi=%d clk=%d miso=%d dc=%d cs=%d rst=%d bk=%d pclk=%u", + ST7789_SPI_HOST, + CONFIG_TQ_SPI_SHARED_MOSI_PIN, + CONFIG_TQ_SPI_SHARED_CLK_PIN, + CONFIG_TQ_PRINTER_MISO_PIN, + CONFIG_TQ_SCREEN_DC_PIN, + CONFIG_TQ_SCREEN_CS_PIN, + lcd_cfg.pin_rst, + CONFIG_TQ_SCREEN_BACKLIGHT_PIN, + (unsigned)lcd_cfg.pclk_hz); + return ESP_OK; +#endif +} + +esp_err_t lcd_module_show_raster_1bpp(const uint8_t *raster, + size_t raster_len, + uint16_t width, + uint16_t height, + uint32_t timeout_ms, + char *err, + size_t err_len) +{ +#if !CONFIG_TQ_SCREEN_ENABLE + return ESP_ERR_NOT_SUPPORTED; +#else + ESP_RETURN_ON_ERROR(lcd_module_init(), TAG, "display init failed"); + + if (timeout_ms == 0) { + timeout_ms = LCD_PREVIEW_TIMEOUT_MS_DEFAULT; + } + + uint16_t *preview_pixels = NULL; + uint16_t preview_w = 0; + uint16_t preview_h = 0; + esp_err_t convert_err = lcd_render_raster_to_rgb565(raster, + raster_len, + width, + height, + &preview_pixels, + &preview_w, + &preview_h, + err, + err_len); + if (convert_err != ESP_OK) { + return convert_err; + } + + if (!lcd_lock_take(timeout_ms)) { + heap_caps_free(preview_pixels); + lcd_fill_err(err, err_len, "display lock timeout"); + return ESP_ERR_TIMEOUT; + } + + if (s_lcd.preview_pixels != NULL) { + heap_caps_free(s_lcd.preview_pixels); + } + s_lcd.preview_pixels = preview_pixels; + + memset(&s_lcd.preview_dsc, 0, sizeof(s_lcd.preview_dsc)); + s_lcd.preview_dsc.header.always_zero = 0; + s_lcd.preview_dsc.header.w = preview_w; + s_lcd.preview_dsc.header.h = preview_h; + s_lcd.preview_dsc.header.cf = LV_IMG_CF_TRUE_COLOR; + s_lcd.preview_dsc.data_size = (uint32_t)((size_t)preview_w * preview_h * sizeof(uint16_t)); + s_lcd.preview_dsc.data = (const uint8_t *)s_lcd.preview_pixels; + + lv_obj_t *screen = lv_scr_act(); + lv_obj_set_style_bg_color(screen, lv_color_white(), LV_PART_MAIN); + lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, LV_PART_MAIN); + lv_img_set_src(s_lcd.preview_img, &s_lcd.preview_dsc); + lv_obj_clear_flag(s_lcd.preview_img, LV_OBJ_FLAG_HIDDEN); + lv_obj_center(s_lcd.preview_img); + lv_refr_now(s_lcd.lv_disp); + + lcd_lock_give(); + return ESP_OK; +#endif +} + +esp_err_t lcd_module_start_demo(void) +{ +#if !CONFIG_TQ_SCREEN_ENABLE + return ESP_ERR_NOT_SUPPORTED; +#else + ESP_RETURN_ON_ERROR(lcd_module_init(), TAG, "display init failed"); + if (!lcd_lock_take(LCD_LOCK_TIMEOUT_MS)) { + return ESP_ERR_TIMEOUT; + } + + uint16_t *demo_pixels = NULL; + uint16_t demo_w = 0; + uint16_t demo_h = 0; + char decode_err[96] = {0}; + esp_err_t decode_rc = platform_lcd_decode_demo_png(&demo_pixels, + &demo_w, + &demo_h, + decode_err, + sizeof(decode_err)); + if (decode_rc != ESP_OK) { + ESP_LOGW(TAG, "demo png decode failed: rc=0x%x msg=%s", (unsigned)decode_rc, decode_err); + lcd_lock_give(); + return decode_rc; + } + + if (s_lcd.preview_pixels != NULL) { + heap_caps_free(s_lcd.preview_pixels); + } + s_lcd.preview_pixels = demo_pixels; + + memset(&s_lcd.preview_dsc, 0, sizeof(s_lcd.preview_dsc)); + s_lcd.preview_dsc.header.always_zero = 0; + s_lcd.preview_dsc.header.w = demo_w; + s_lcd.preview_dsc.header.h = demo_h; + s_lcd.preview_dsc.header.cf = LV_IMG_CF_TRUE_COLOR; + s_lcd.preview_dsc.data_size = (uint32_t)((size_t)demo_w * demo_h * sizeof(uint16_t)); + s_lcd.preview_dsc.data = (const uint8_t *)s_lcd.preview_pixels; + + lv_obj_t *screen = lv_scr_act(); + lv_obj_set_style_bg_color(screen, lv_color_black(), LV_PART_MAIN); + lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, LV_PART_MAIN); + + lv_img_set_src(s_lcd.preview_img, &s_lcd.preview_dsc); + lv_obj_clear_flag(s_lcd.preview_img, LV_OBJ_FLAG_HIDDEN); + lv_obj_center(s_lcd.preview_img); + lv_refr_now(s_lcd.lv_disp); + lcd_lock_give(); + return ESP_OK; +#endif +} diff --git a/components/platform/src/display_st7789.c b/components/platform/src/display_st7789.c index 6bf2f46..d4125f9 100644 --- a/components/platform/src/display_st7789.c +++ b/components/platform/src/display_st7789.c @@ -1,317 +1,29 @@ #include "platform.h" -#include -#include -#include +#include "display_lcd_module.h" -#include "driver/gpio.h" -#include "driver/spi_master.h" -#include "esp_check.h" -#include "esp_heap_caps.h" -#include "esp_lcd_panel_io.h" -#include "esp_lcd_panel_ops.h" -#include "esp_lcd_panel_vendor.h" -#include "esp_log.h" -#include "sdkconfig.h" - -static const char *TAG = "display_st7789"; - -#define ST7789_SPI_HOST SPI2_HOST -#define ST7789_COLOR_BYTES_PER_PIXEL 2 - -#ifndef CONFIG_TQ_SCREEN_SPI_MODE -#define CONFIG_TQ_SCREEN_SPI_MODE 0 -#endif - -#ifndef CONFIG_TQ_SCREEN_BACKLIGHT_ACTIVE_HIGH -#define CONFIG_TQ_SCREEN_BACKLIGHT_ACTIVE_HIGH 1 -#endif - -#if CONFIG_TQ_SCREEN_BACKLIGHT_ACTIVE_HIGH -#define ST7789_BACKLIGHT_ON_LEVEL 1 -#define ST7789_BACKLIGHT_OFF_LEVEL 0 -#else -#define ST7789_BACKLIGHT_ON_LEVEL 0 -#define ST7789_BACKLIGHT_OFF_LEVEL 1 -#endif - -typedef struct { - esp_lcd_panel_io_handle_t io_handle; - esp_lcd_panel_handle_t panel_handle; - bool bus_owned; - bool initialized; -} st7789_display_state_t; - -static st7789_display_state_t s_display; - -#if CONFIG_TQ_SCREEN_COLOR_ORDER_BGR -#define ST7789_COLOR_ORDER LCD_RGB_ELEMENT_ORDER_BGR -#else -#define ST7789_COLOR_ORDER LCD_RGB_ELEMENT_ORDER_RGB -#endif - -#if CONFIG_TQ_SCREEN_SWAP_XY -#define ST7789_SWAP_XY true -#else -#define ST7789_SWAP_XY false -#endif - -#if CONFIG_TQ_SCREEN_MIRROR_X -#define ST7789_MIRROR_X true -#else -#define ST7789_MIRROR_X false -#endif - -#if CONFIG_TQ_SCREEN_MIRROR_Y -#define ST7789_MIRROR_Y true -#else -#define ST7789_MIRROR_Y false -#endif - -static uint16_t rgb565(uint8_t r, uint8_t g, uint8_t b) { - return (uint16_t)(((uint16_t)(r & 0xF8) << 8) | - ((uint16_t)(g & 0xFC) << 3) | - ((uint16_t)(b & 0xF8) >> 3)); +void platform_display_deinit(void) +{ + lcd_module_deinit(); } -static uint16_t test_pattern_color(int x, int y, int h_res, int v_res) { - int top = v_res / 3; - int middle = (v_res * 2) / 3; - - if (y < top) { - uint8_t r = (uint8_t)((x * 255) / (h_res - 1)); - uint8_t g = (uint8_t)(255 - r); - return rgb565(r, g, 48); - } - - if (y < middle) { - uint8_t gray = (uint8_t)(((y - top) * 255) / ((middle - top) > 1 ? (middle - top - 1) : 1)); - return rgb565(gray, gray, gray); - } - - bool checker = (((x / 16) + (y / 16)) & 1) != 0; - return checker ? rgb565(250, 250, 250) : rgb565(8, 8, 8); +esp_err_t platform_display_init(void) +{ + return lcd_module_init(); } -static void fill_test_pattern_chunk(uint16_t *buf, - int y_start, - int lines, - int h_res, - int v_res) { - for (int row = 0; row < lines; ++row) { - int y = y_start + row; - for (int x = 0; x < h_res; ++x) { - uint16_t color = test_pattern_color(x, y, h_res, v_res); - - if (x == 0 || y == 0 || x == (h_res - 1) || y == (v_res - 1)) { - color = rgb565(255, 0, 0); - } else if (x == (h_res / 2) || y == (v_res / 2)) { - color = rgb565(255, 255, 255); - } - - buf[row * h_res + x] = color; - } - } +esp_err_t platform_display_show_test_pattern(void) +{ + return lcd_module_start_demo(); } -void platform_display_deinit(void) { -#if CONFIG_TQ_SCREEN_ENABLE - if (CONFIG_TQ_SCREEN_BACKLIGHT_PIN >= 0) { - (void)gpio_set_level(CONFIG_TQ_SCREEN_BACKLIGHT_PIN, ST7789_BACKLIGHT_OFF_LEVEL); - } - - if (s_display.panel_handle != NULL) { - (void)esp_lcd_panel_del(s_display.panel_handle); - s_display.panel_handle = NULL; - } - if (s_display.io_handle != NULL) { - (void)esp_lcd_panel_io_del(s_display.io_handle); - s_display.io_handle = NULL; - } - if (s_display.bus_owned) { - (void)spi_bus_free(ST7789_SPI_HOST); - s_display.bus_owned = false; - } - s_display.initialized = false; -#endif -} - -esp_err_t platform_display_init(void) { -#if !CONFIG_TQ_SCREEN_ENABLE - return ESP_ERR_NOT_SUPPORTED; -#else - if (s_display.initialized) { - return ESP_OK; - } - - if (CONFIG_TQ_SCREEN_BACKLIGHT_PIN >= 0) { - ESP_RETURN_ON_ERROR(gpio_set_level(CONFIG_TQ_SCREEN_BACKLIGHT_PIN, ST7789_BACKLIGHT_OFF_LEVEL), - TAG, - "set backlight off failed"); - } - - const size_t max_transfer_size = (size_t)CONFIG_TQ_SCREEN_H_RES * - (size_t)CONFIG_TQ_SCREEN_DRAW_LINES * - ST7789_COLOR_BYTES_PER_PIXEL + 8; - - spi_bus_config_t bus_cfg = { - .sclk_io_num = CONFIG_TQ_SPI_SHARED_CLK_PIN, - .mosi_io_num = CONFIG_TQ_SPI_SHARED_MOSI_PIN, - .miso_io_num = -1, - .quadwp_io_num = -1, - .quadhd_io_num = -1, - .max_transfer_sz = (int)max_transfer_size, - }; - - esp_err_t err = spi_bus_initialize(ST7789_SPI_HOST, &bus_cfg, SPI_DMA_CH_AUTO); - if (err == ESP_OK) { - s_display.bus_owned = true; - } else if (err == ESP_ERR_INVALID_STATE) { - s_display.bus_owned = false; - ESP_LOGW(TAG, "SPI host already initialized, reuse host=%d", ST7789_SPI_HOST); - } else { - return err; - } - - esp_lcd_panel_io_spi_config_t io_cfg = { - .cs_gpio_num = CONFIG_TQ_SCREEN_CS_PIN, - .dc_gpio_num = CONFIG_TQ_SCREEN_DC_PIN, - .spi_mode = CONFIG_TQ_SCREEN_SPI_MODE, - .pclk_hz = CONFIG_TQ_SCREEN_PIXEL_CLOCK_HZ, - .trans_queue_depth = 1, - .lcd_cmd_bits = 8, - .lcd_param_bits = 8, - }; - err = esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)ST7789_SPI_HOST, &io_cfg, &s_display.io_handle); - if (err != ESP_OK) { - platform_display_deinit(); - return err; - } - - esp_lcd_panel_dev_config_t panel_cfg = { - .reset_gpio_num = CONFIG_TQ_SCREEN_RESET_PIN, - .rgb_ele_order = ST7789_COLOR_ORDER, - .bits_per_pixel = 16, - }; - err = esp_lcd_new_panel_st7789(s_display.io_handle, &panel_cfg, &s_display.panel_handle); - if (err != ESP_OK) { - platform_display_deinit(); - return err; - } - - err = esp_lcd_panel_reset(s_display.panel_handle); - if (err != ESP_OK) { - ESP_LOGE(TAG, "panel reset failed: %s", esp_err_to_name(err)); - platform_display_deinit(); - return err; - } - err = esp_lcd_panel_init(s_display.panel_handle); - if (err != ESP_OK) { - ESP_LOGE(TAG, "panel init failed: %s", esp_err_to_name(err)); - platform_display_deinit(); - return err; - } - err = esp_lcd_panel_swap_xy(s_display.panel_handle, ST7789_SWAP_XY); - if (err != ESP_OK) { - ESP_LOGE(TAG, "swap xy failed: %s", esp_err_to_name(err)); - platform_display_deinit(); - return err; - } - err = esp_lcd_panel_mirror(s_display.panel_handle, ST7789_MIRROR_X, ST7789_MIRROR_Y); - if (err != ESP_OK) { - ESP_LOGE(TAG, "mirror failed: %s", esp_err_to_name(err)); - platform_display_deinit(); - return err; - } - err = esp_lcd_panel_set_gap(s_display.panel_handle, CONFIG_TQ_SCREEN_X_GAP, CONFIG_TQ_SCREEN_Y_GAP); - if (err != ESP_OK) { - ESP_LOGE(TAG, "set gap failed: %s", esp_err_to_name(err)); - platform_display_deinit(); - return err; - } - err = esp_lcd_panel_invert_color(s_display.panel_handle, false); - if (err != ESP_OK) { - ESP_LOGE(TAG, "invert control failed: %s", esp_err_to_name(err)); - platform_display_deinit(); - return err; - } - err = esp_lcd_panel_disp_on_off(s_display.panel_handle, true); - if (err != ESP_OK) { - ESP_LOGE(TAG, "display on failed: %s", esp_err_to_name(err)); - platform_display_deinit(); - return err; - } - - if (CONFIG_TQ_SCREEN_BACKLIGHT_PIN >= 0) { - err = gpio_set_level(CONFIG_TQ_SCREEN_BACKLIGHT_PIN, ST7789_BACKLIGHT_ON_LEVEL); - if (err != ESP_OK) { - ESP_LOGE(TAG, "set backlight on failed: %s", esp_err_to_name(err)); - platform_display_deinit(); - return err; - } - } - - s_display.initialized = true; - ESP_LOGI(TAG, - "st7789 ready: spi_host=%d sclk=%d mosi=%d dc=%d cs=%d rst=%d bk=%d bk_on=%d spi_mode=%d size=%dx%d pclk=%u", - ST7789_SPI_HOST, - CONFIG_TQ_SPI_SHARED_CLK_PIN, - CONFIG_TQ_SPI_SHARED_MOSI_PIN, - CONFIG_TQ_SCREEN_DC_PIN, - CONFIG_TQ_SCREEN_CS_PIN, - CONFIG_TQ_SCREEN_RESET_PIN, - CONFIG_TQ_SCREEN_BACKLIGHT_PIN, - ST7789_BACKLIGHT_ON_LEVEL, - CONFIG_TQ_SCREEN_SPI_MODE, - CONFIG_TQ_SCREEN_H_RES, - CONFIG_TQ_SCREEN_V_RES, - (unsigned)CONFIG_TQ_SCREEN_PIXEL_CLOCK_HZ); - return ESP_OK; -#endif -} - -esp_err_t platform_display_show_test_pattern(void) { -#if !CONFIG_TQ_SCREEN_ENABLE - return ESP_ERR_NOT_SUPPORTED; -#else - ESP_RETURN_ON_ERROR(platform_display_init(), TAG, "display init failed"); - - const int h_res = CONFIG_TQ_SCREEN_H_RES; - const int v_res = CONFIG_TQ_SCREEN_V_RES; - const int draw_lines = CONFIG_TQ_SCREEN_DRAW_LINES > 0 ? CONFIG_TQ_SCREEN_DRAW_LINES : 1; - const size_t buf_pixels = (size_t)h_res * (size_t)draw_lines; - const size_t buf_size = buf_pixels * ST7789_COLOR_BYTES_PER_PIXEL; - - uint16_t *draw_buf = (uint16_t *)heap_caps_malloc(buf_size, MALLOC_CAP_DMA | MALLOC_CAP_8BIT); - if (draw_buf == NULL) { - draw_buf = (uint16_t *)heap_caps_malloc(buf_size, MALLOC_CAP_8BIT); - } - ESP_RETURN_ON_FALSE(draw_buf != NULL, ESP_ERR_NO_MEM, TAG, "alloc draw buffer failed"); - - ESP_LOGI(TAG, "draw test pattern start"); - - esp_err_t err = ESP_OK; - for (int y = 0; y < v_res && err == ESP_OK; y += draw_lines) { - int chunk_lines = draw_lines; - if (y + chunk_lines > v_res) { - chunk_lines = v_res - y; - } - - fill_test_pattern_chunk(draw_buf, y, chunk_lines, h_res, v_res); - err = esp_lcd_panel_draw_bitmap(s_display.panel_handle, 0, y, h_res, y + chunk_lines, draw_buf); - if (err != ESP_OK) { - break; - } - - // Wait for queued color transfer to finish before reusing draw_buf. - err = esp_lcd_panel_io_tx_param(s_display.io_handle, -1, NULL, 0); - } - - heap_caps_free(draw_buf); - - if (err == ESP_OK) { - ESP_LOGI(TAG, "draw test pattern done"); - } - return err; -#endif +esp_err_t platform_display_show_raster_1bpp(const uint8_t *raster, + size_t raster_len, + uint16_t width, + uint16_t height, + uint32_t timeout_ms, + char *err, + size_t err_len) +{ + return lcd_module_show_raster_1bpp(raster, raster_len, width, height, timeout_ms, err, err_len); } diff --git a/components/platform/src/display_st7789p3.c b/components/platform/src/display_st7789p3.c new file mode 100644 index 0000000..5d5f47e --- /dev/null +++ b/components/platform/src/display_st7789p3.c @@ -0,0 +1,515 @@ +#include "display_st7789p3.h" + +#include +#include + +#include "driver/gpio.h" +#include "esp_attr.h" +#include "esp_heap_caps.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#define ST7789P3_CMD_DELAY_FLAG 0x80 +#define ST7789P3_CMD_DATA_LEN_MASK 0x1F +#define ST7789P3_TX_MAX_CHUNK_BYTES 1024 +#define ST7789P3_SWAP_BUF_PIXELS 256 + +typedef struct { + uint8_t cmd; + uint8_t data[16]; + uint8_t data_bytes; +} st7789p3_lcd_init_cmd_t; + +struct st7789p3_panel_t { + st7789p3_config_t cfg; + spi_device_handle_t spi; + bool bus_owned; +}; + +static const char *TAG = "st7789p3"; + +// Sequence verified on the LCD used by docs/lcd_verification. +DRAM_ATTR static const st7789p3_lcd_init_cmd_t s_st7789p3_init_cmds[] = { + {0x36, {0x00}, 1}, + {0x3A, {0x55}, 1}, + {0xB2, {0x0C, 0x0C, 0x00, 0x33, 0x33}, 5}, + {0xB7, {0x45}, 1}, + {0xBB, {0x2B}, 1}, + {0xC0, {0x2C}, 1}, + {0xC2, {0x01, 0xFF}, 2}, + {0xC3, {0x11}, 1}, + {0xC4, {0x20}, 1}, + {0xC6, {0x0F}, 1}, + {0xD0, {0xA4, 0xA1}, 2}, + {0xE0, {0xD0, 0x00, 0x05, 0x0E, 0x15, 0x0D, 0x37, 0x43, 0x47, 0x09, 0x15, 0x12, 0x16, 0x19}, 14}, + {0xE1, {0xD0, 0x00, 0x05, 0x0D, 0x0C, 0x06, 0x2D, 0x44, 0x40, 0x0E, 0x1C, 0x18, 0x16, 0x19}, 14}, + {0x11, {0}, ST7789P3_CMD_DELAY_FLAG}, + {0, {0}, 0xFF}, +}; + +static esp_err_t st7789p3_validate_config(const st7789p3_config_t *config) +{ + if (config == NULL) { + return ESP_ERR_INVALID_ARG; + } + if (config->pin_mosi < 0 || config->pin_sclk < 0 || config->pin_cs < 0 || config->pin_dc < 0) { + return ESP_ERR_INVALID_ARG; + } + if (config->h_res == 0 || config->v_res == 0) { + return ESP_ERR_INVALID_ARG; + } + if (config->spi_mode > 3) { + return ESP_ERR_INVALID_ARG; + } + return ESP_OK; +} + +static esp_err_t st7789p3_configure_gpio(st7789p3_handle_t panel) +{ + uint64_t pin_mask = 0; + + pin_mask |= (1ULL << panel->cfg.pin_dc); + if (panel->cfg.pin_rst >= 0) { + pin_mask |= (1ULL << panel->cfg.pin_rst); + } + if (panel->cfg.pin_bckl >= 0) { + pin_mask |= (1ULL << panel->cfg.pin_bckl); + } + + gpio_config_t io_conf = { + .pin_bit_mask = pin_mask, + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + + return gpio_config(&io_conf); +} + +static esp_err_t st7789p3_spi_write(st7789p3_handle_t panel, int dc_level, const void *data, size_t data_size) +{ + if (data_size == 0) { + return ESP_OK; + } + + esp_err_t ret = gpio_set_level(panel->cfg.pin_dc, dc_level); + if (ret != ESP_OK) { + return ret; + } + + spi_transaction_t t = {0}; + t.length = data_size * 8; + t.tx_buffer = data; + + return spi_device_transmit(panel->spi, &t); +} + +static esp_err_t st7789p3_send_cmd(st7789p3_handle_t panel, uint8_t cmd) +{ + return st7789p3_spi_write(panel, 0, &cmd, 1); +} + +static esp_err_t st7789p3_send_data(st7789p3_handle_t panel, const void *data, size_t data_size) +{ + const uint8_t *data_ptr = (const uint8_t *)data; + size_t remaining = data_size; + size_t tx_chunk_size = ST7789P3_TX_MAX_CHUNK_BYTES; + + if (panel->cfg.max_transfer_sz > 0 && panel->cfg.max_transfer_sz < tx_chunk_size) { + tx_chunk_size = panel->cfg.max_transfer_sz; + } + + while (remaining > 0) { + size_t chunk = (remaining > tx_chunk_size) ? tx_chunk_size : remaining; + esp_err_t ret = st7789p3_spi_write(panel, 1, data_ptr, chunk); + if (ret != ESP_OK) { + return ret; + } + data_ptr += chunk; + remaining -= chunk; + } + return ESP_OK; +} + +static esp_err_t st7789p3_send_color_data(st7789p3_handle_t panel, const uint16_t *color_data, size_t pixel_count) +{ + if (color_data == NULL || pixel_count == 0) { + return ESP_OK; + } + + if (!panel->cfg.swap_color_bytes) { + return st7789p3_send_data(panel, color_data, pixel_count * sizeof(uint16_t)); + } + + uint8_t swap_buf[ST7789P3_SWAP_BUF_PIXELS * sizeof(uint16_t)]; + size_t offset = 0; + while (offset < pixel_count) { + size_t chunk = pixel_count - offset; + if (chunk > ST7789P3_SWAP_BUF_PIXELS) { + chunk = ST7789P3_SWAP_BUF_PIXELS; + } + + for (size_t i = 0; i < chunk; ++i) { + uint16_t color = color_data[offset + i]; + swap_buf[i * 2] = (uint8_t)(color >> 8); + swap_buf[i * 2 + 1] = (uint8_t)(color & 0xFF); + } + + esp_err_t ret = st7789p3_spi_write(panel, 1, swap_buf, chunk * sizeof(uint16_t)); + if (ret != ESP_OK) { + return ret; + } + offset += chunk; + } + + return ESP_OK; +} + +static esp_err_t st7789p3_run_init_cmds(st7789p3_handle_t panel) +{ + for (size_t i = 0; s_st7789p3_init_cmds[i].data_bytes != 0xFF; i++) { + const st7789p3_lcd_init_cmd_t *init_cmd = &s_st7789p3_init_cmds[i]; + uint8_t data_bytes = init_cmd->data_bytes & ST7789P3_CMD_DATA_LEN_MASK; + uint8_t data[16] = {0}; + + if (data_bytes > 0) { + memcpy(data, init_cmd->data, data_bytes); + } + + if (init_cmd->cmd == 0x36 && data_bytes == 1) { + data[0] = panel->cfg.madctl_val; + } + + esp_err_t ret = st7789p3_send_cmd(panel, init_cmd->cmd); + if (ret != ESP_OK) { + return ret; + } + + ret = st7789p3_send_data(panel, data, data_bytes); + if (ret != ESP_OK) { + return ret; + } + + if (init_cmd->data_bytes & ST7789P3_CMD_DELAY_FLAG) { + vTaskDelay(pdMS_TO_TICKS(100)); + } + } + return ESP_OK; +} + +esp_err_t st7789p3_new_panel(const st7789p3_config_t *config, st7789p3_handle_t *ret_panel) +{ + if (ret_panel == NULL) { + return ESP_ERR_INVALID_ARG; + } + *ret_panel = NULL; + + esp_err_t ret = st7789p3_validate_config(config); + if (ret != ESP_OK) { + return ret; + } + + st7789p3_handle_t panel = calloc(1, sizeof(*panel)); + if (panel == NULL) { + return ESP_ERR_NO_MEM; + } + + panel->cfg = *config; + if (panel->cfg.pclk_hz == 0) { + panel->cfg.pclk_hz = 10 * 1000 * 1000; + } + if (panel->cfg.queue_size <= 0) { + panel->cfg.queue_size = 7; + } + if (panel->cfg.max_transfer_sz == 0) { + panel->cfg.max_transfer_sz = panel->cfg.h_res * 20 * sizeof(uint16_t) + 8; + } + + ret = st7789p3_configure_gpio(panel); + if (ret != ESP_OK) { + free(panel); + return ret; + } + + if (panel->cfg.init_spi_bus) { + spi_bus_config_t buscfg = { + .mosi_io_num = panel->cfg.pin_mosi, + .miso_io_num = panel->cfg.pin_miso, + .sclk_io_num = panel->cfg.pin_sclk, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + .max_transfer_sz = panel->cfg.max_transfer_sz, + }; + + ret = spi_bus_initialize(panel->cfg.spi_host, &buscfg, SPI_DMA_CH_AUTO); + if (ret == ESP_OK) { + panel->bus_owned = true; + } else if (ret == ESP_ERR_INVALID_STATE) { + ESP_LOGW(TAG, "SPI host already initialized, skip bus init"); + } else { + free(panel); + return ret; + } + } + + spi_device_interface_config_t devcfg = { + .clock_speed_hz = panel->cfg.pclk_hz, + .mode = panel->cfg.spi_mode, + .spics_io_num = panel->cfg.pin_cs, + .queue_size = panel->cfg.queue_size, + }; + + ret = spi_bus_add_device(panel->cfg.spi_host, &devcfg, &panel->spi); + if (ret != ESP_OK) { + if (panel->bus_owned) { + spi_bus_free(panel->cfg.spi_host); + } + free(panel); + return ret; + } + + *ret_panel = panel; + return ESP_OK; +} + +esp_err_t st7789p3_del(st7789p3_handle_t panel) +{ + if (panel == NULL) { + return ESP_ERR_INVALID_ARG; + } + + esp_err_t ret = ESP_OK; + esp_err_t local_ret = spi_bus_remove_device(panel->spi); + if (local_ret != ESP_OK) { + ret = local_ret; + } + + if (panel->bus_owned) { + local_ret = spi_bus_free(panel->cfg.spi_host); + if (ret == ESP_OK && local_ret != ESP_OK) { + ret = local_ret; + } + } + + free(panel); + return ret; +} + +esp_err_t st7789p3_set_backlight(st7789p3_handle_t panel, bool on) +{ + if (panel == NULL) { + return ESP_ERR_INVALID_ARG; + } + if (panel->cfg.pin_bckl < 0) { + return ESP_OK; + } + + int on_level = panel->cfg.backlight_on_level ? 1 : 0; + int off_level = on_level ? 0 : 1; + return gpio_set_level(panel->cfg.pin_bckl, on ? on_level : off_level); +} + +esp_err_t st7789p3_set_madctl(st7789p3_handle_t panel, uint8_t madctl_val) +{ + if (panel == NULL) { + return ESP_ERR_INVALID_ARG; + } + + panel->cfg.madctl_val = madctl_val; + esp_err_t ret = st7789p3_send_cmd(panel, 0x36); + if (ret != ESP_OK) { + return ret; + } + return st7789p3_send_data(panel, &madctl_val, 1); +} + +esp_err_t st7789p3_set_invert(st7789p3_handle_t panel, bool invert_color) +{ + if (panel == NULL) { + return ESP_ERR_INVALID_ARG; + } + + panel->cfg.invert_color = invert_color; + return st7789p3_send_cmd(panel, invert_color ? 0x21 : 0x20); +} + +esp_err_t st7789p3_set_swap_color_bytes(st7789p3_handle_t panel, bool swap_color_bytes) +{ + if (panel == NULL) { + return ESP_ERR_INVALID_ARG; + } + panel->cfg.swap_color_bytes = swap_color_bytes; + return ESP_OK; +} + +esp_err_t st7789p3_reset(st7789p3_handle_t panel) +{ + if (panel == NULL) { + return ESP_ERR_INVALID_ARG; + } + if (panel->cfg.pin_rst < 0) { + return ESP_OK; + } + + esp_err_t ret = gpio_set_level(panel->cfg.pin_rst, 0); + if (ret != ESP_OK) { + return ret; + } + vTaskDelay(pdMS_TO_TICKS(100)); + + ret = gpio_set_level(panel->cfg.pin_rst, 1); + if (ret != ESP_OK) { + return ret; + } + vTaskDelay(pdMS_TO_TICKS(100)); + return ESP_OK; +} + +esp_err_t st7789p3_init(st7789p3_handle_t panel) +{ + if (panel == NULL) { + return ESP_ERR_INVALID_ARG; + } + + esp_err_t ret = st7789p3_set_backlight(panel, false); + if (ret != ESP_OK) { + return ret; + } + + ret = st7789p3_reset(panel); + if (ret != ESP_OK) { + return ret; + } + + ret = st7789p3_run_init_cmds(panel); + if (ret != ESP_OK) { + return ret; + } + + // Re-assert MADCTL after init sequence while backlight is still off, + // to avoid a visible first-frame transition on some panels. + ret = st7789p3_set_madctl(panel, panel->cfg.madctl_val); + if (ret != ESP_OK) { + return ret; + } + + ret = st7789p3_set_invert(panel, panel->cfg.invert_color); + if (ret != ESP_OK) { + return ret; + } + + ret = st7789p3_send_cmd(panel, 0x29); + if (ret != ESP_OK) { + return ret; + } + vTaskDelay(pdMS_TO_TICKS(100)); + + return st7789p3_set_backlight(panel, true); +} + +esp_err_t st7789p3_set_window( + st7789p3_handle_t panel, uint16_t x_start, uint16_t y_start, uint16_t x_end, uint16_t y_end) +{ + if (panel == NULL || x_start >= x_end || y_start >= y_end) { + return ESP_ERR_INVALID_ARG; + } + if (x_end > panel->cfg.h_res || y_end > panel->cfg.v_res) { + return ESP_ERR_INVALID_ARG; + } + + uint8_t x_data[4] = { + (uint8_t)(x_start >> 8), + (uint8_t)(x_start & 0xFF), + (uint8_t)((x_end - 1) >> 8), + (uint8_t)((x_end - 1) & 0xFF), + }; + uint8_t y_data[4] = { + (uint8_t)(y_start >> 8), + (uint8_t)(y_start & 0xFF), + (uint8_t)((y_end - 1) >> 8), + (uint8_t)((y_end - 1) & 0xFF), + }; + + esp_err_t ret = st7789p3_send_cmd(panel, 0x2A); + if (ret != ESP_OK) { + return ret; + } + ret = st7789p3_send_data(panel, x_data, sizeof(x_data)); + if (ret != ESP_OK) { + return ret; + } + + ret = st7789p3_send_cmd(panel, 0x2B); + if (ret != ESP_OK) { + return ret; + } + ret = st7789p3_send_data(panel, y_data, sizeof(y_data)); + if (ret != ESP_OK) { + return ret; + } + + return st7789p3_send_cmd(panel, 0x2C); +} + +esp_err_t st7789p3_draw_bitmap(st7789p3_handle_t panel, + uint16_t x_start, + uint16_t y_start, + uint16_t x_end, + uint16_t y_end, + const uint16_t *color_data) +{ + if (panel == NULL || color_data == NULL) { + return ESP_ERR_INVALID_ARG; + } + + esp_err_t ret = st7789p3_set_window(panel, x_start, y_start, x_end, y_end); + if (ret != ESP_OK) { + return ret; + } + + size_t pixel_count = (size_t)(x_end - x_start) * (size_t)(y_end - y_start); + return st7789p3_send_color_data(panel, color_data, pixel_count); +} + +esp_err_t st7789p3_fill_color(st7789p3_handle_t panel, uint16_t color) +{ + if (panel == NULL) { + return ESP_ERR_INVALID_ARG; + } + + static const size_t fill_pixels = 256; + uint16_t *fill_buffer = heap_caps_malloc(fill_pixels * sizeof(uint16_t), MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); + if (fill_buffer == NULL) { + return ESP_ERR_NO_MEM; + } + + uint16_t fill_color = color; + if (panel->cfg.swap_color_bytes) { + fill_color = (uint16_t)((color >> 8) | (color << 8)); + } + for (size_t i = 0; i < fill_pixels; i++) { + fill_buffer[i] = fill_color; + } + + esp_err_t ret = st7789p3_set_window(panel, 0, 0, panel->cfg.h_res, panel->cfg.v_res); + if (ret != ESP_OK) { + free(fill_buffer); + return ret; + } + + size_t total_pixels = (size_t)panel->cfg.h_res * (size_t)panel->cfg.v_res; + while (total_pixels > 0) { + size_t pixels_to_send = (total_pixels > fill_pixels) ? fill_pixels : total_pixels; + ret = st7789p3_send_data(panel, fill_buffer, pixels_to_send * sizeof(uint16_t)); + if (ret != ESP_OK) { + free(fill_buffer); + return ret; + } + total_pixels -= pixels_to_send; + } + + free(fill_buffer); + return ESP_OK; +} diff --git a/dependencies.lock b/dependencies.lock index 6037eb8..1944d13 100644 --- a/dependencies.lock +++ b/dependencies.lock @@ -44,7 +44,7 @@ dependencies: type: service version: 1.6.55 espressif/zlib: - component_hash: d901723af51f13fc8e5824f39f32239c847956e8fd951a05266588dc5cfbb9ae + component_hash: 64ea2fd6754c5a05951f1ddb312ac835cd782e68cb5bd9dfcd1c3a107c8fbfbc dependencies: - name: idf require: private @@ -52,17 +52,25 @@ dependencies: source: registry_url: https://components.espressif.com type: service - version: 1.3.1 + version: 1.3.2 idf: source: type: idf version: 5.5.2 + lvgl/lvgl: + component_hash: d7c1ac037ae6e85d94897f807d6e7ba0946a83e720074fc95a4f6241da9f9f53 + dependencies: [] + source: + registry_url: https://components.espressif.com/ + type: service + version: 8.4.0 direct_dependencies: - espressif/esp_audio_codec - espressif/esp_codec_dev - espressif/esp_websocket_client - espressif/libpng - idf -manifest_hash: 3274b53ce89a58abbad0a03dfaff3410e35deb81305a21289606c4d2177a111b +- lvgl/lvgl +manifest_hash: d32a494b7c86c11954a7b0354da8967836fc6789173852455b8c0611e1898436 target: esp32s3 version: 2.0.0 diff --git a/main/idf_component.yml b/main/idf_component.yml index eb66955..b3308cf 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -18,3 +18,4 @@ dependencies: espressif/esp_codec_dev: ^1.5.4 espressif/esp_audio_codec: ^2.4.1 espressif/libpng: ^1.6.55 + lvgl/lvgl: ^8.3.11