/* * Direct ST7789 LCD backend. * * This backend owns the low-level panel initialization, a serialized render * loop, and a minimal presentation queue. */ #include "lcd_direct.h" #include "lcd_direct_assets.h" #include #include #include #include #include #include #include #include "driver/gpio.h" #include "driver/spi_master.h" #include "esp_check.h" #include "esp_lcd_panel_io.h" #include "esp_lcd_panel_ops.h" #include "esp_lcd_panel_vendor.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "freertos/task.h" #include "sdkconfig.h" #define LCD_DIRECT_SPI_HOST SPI2_HOST #define LCD_DIRECT_WIDTH 320U #define LCD_DIRECT_HEIGHT 240U #define LCD_DIRECT_ROWS_PER_TRANSFER 8U #define LCD_DIRECT_PIXELS_PER_TRANSFER (LCD_DIRECT_WIDTH * LCD_DIRECT_ROWS_PER_TRANSFER) #define LCD_DIRECT_PRESENTATION_WAIT_POLL_MS 20U #define LCD_CMD_MADCTL 0x36 #define LCD_CMD_RAMCTRL 0xB0 static const char *TAG = "lcd_direct"; typedef struct { platform_lcd_presentation_kind_t kind; uint8_t priority; bool interrupt_current; bool return_to_idle; char asset_dir[PLATFORM_LCD_PRESENTATION_ASSET_DIR_MAX]; uint8_t frame_order[PLATFORM_LCD_PRESENTATION_FRAME_ORDER_MAX]; size_t frame_order_count; uint16_t repeat_count; uint32_t frame_ms; uint32_t duration_ms; platform_lcd_presentation_step_t steps[PLATFORM_LCD_PRESENTATION_STEP_MAX]; size_t step_count; } lcd_direct_request_t; typedef struct { uint8_t cmd; uint8_t data[16]; uint8_t databytes; } lcd_direct_init_cmd_t; static const lcd_direct_init_cmd_t s_st7789_init_cmds[] = { {0x36, {(1 << 5) | (1 << 6)}, 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}, {0, {0}, 0xFF}, }; static esp_lcd_panel_io_handle_t s_panel_io; static esp_lcd_panel_handle_t s_panel; static SemaphoreHandle_t s_lock; static SemaphoreHandle_t s_completion_sem; static TaskHandle_t s_render_task; static bool s_initialized; static bool s_render_loop_running; static bool s_stop_requested; static bool s_pending_valid; static lcd_direct_request_t s_pending; static uint32_t s_completion_count; static portMUX_TYPE s_state_lock = portMUX_INITIALIZER_UNLOCKED; static uint16_t s_line_buffer[LCD_DIRECT_PIXELS_PER_TRANSFER]; static lcd_direct_rle_run_t s_rle_file_chunk[128]; static void lcd_direct_signal_completion(void); static uint16_t lcd_direct_swap_color(uint16_t color) { #if CONFIG_TQ_SCREEN_SWAP_COLOR_BYTES return (uint16_t)((color >> 8) | (color << 8)); #else return color; #endif } static esp_err_t lcd_direct_draw_rows(uint16_t row_start, uint16_t row_count) { ESP_RETURN_ON_FALSE(s_panel != NULL, ESP_ERR_INVALID_STATE, TAG, "panel is not ready"); return esp_lcd_panel_draw_bitmap(s_panel, 0, row_start, LCD_DIRECT_WIDTH, row_start + row_count, s_line_buffer); } static esp_err_t lcd_direct_gpio_init(void) { uint64_t pin_mask = (1ULL << CONFIG_TQ_SCREEN_BACKLIGHT_GPIO) | (1ULL << CONFIG_TQ_SCREEN_DC_GPIO); if (CONFIG_TQ_SCREEN_RESET_PIN >= 0) { pin_mask |= (1ULL << CONFIG_TQ_SCREEN_RESET_PIN); } gpio_config_t gpio_config_data = { .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, }; ESP_RETURN_ON_ERROR(gpio_config(&gpio_config_data), TAG, "gpio config failed"); gpio_set_level(CONFIG_TQ_SCREEN_BACKLIGHT_GPIO, CONFIG_TQ_SCREEN_BACKLIGHT_ACTIVE_HIGH ? 1 : 0); gpio_set_level(CONFIG_TQ_SCREEN_DC_GPIO, 0); if (CONFIG_TQ_SCREEN_RESET_PIN >= 0) { gpio_set_level(CONFIG_TQ_SCREEN_RESET_PIN, 1); } return ESP_OK; } static esp_err_t lcd_direct_panel_init(void) { spi_bus_config_t bus_config = { .mosi_io_num = CONFIG_TQ_SCREEN_SPI_MOSI_GPIO, .miso_io_num = CONFIG_TQ_SCREEN_SPI_MISO_GPIO, .sclk_io_num = CONFIG_TQ_SCREEN_SPI_CLK_GPIO, .quadwp_io_num = -1, .quadhd_io_num = -1, .max_transfer_sz = LCD_DIRECT_PIXELS_PER_TRANSFER * sizeof(uint16_t), }; ESP_RETURN_ON_ERROR(spi_bus_initialize(LCD_DIRECT_SPI_HOST, &bus_config, SPI_DMA_CH_AUTO), TAG, "esp_lcd spi bus init failed"); esp_lcd_panel_io_spi_config_t io_config = { .dc_gpio_num = CONFIG_TQ_SCREEN_DC_GPIO, .cs_gpio_num = CONFIG_TQ_SCREEN_SPI_CS_GPIO, .pclk_hz = CONFIG_TQ_SCREEN_SPI_PIXEL_CLOCK_HZ, .lcd_cmd_bits = 8, .lcd_param_bits = 8, .spi_mode = 0, .trans_queue_depth = 10, }; ESP_RETURN_ON_ERROR(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_DIRECT_SPI_HOST, &io_config, &s_panel_io), TAG, "new panel io failed"); esp_lcd_panel_dev_config_t panel_config = { .reset_gpio_num = CONFIG_TQ_SCREEN_RESET_PIN, #if CONFIG_TQ_SCREEN_COLOR_ORDER_BGR .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR, #else .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, #endif .data_endian = LCD_RGB_DATA_ENDIAN_BIG, .bits_per_pixel = 16, }; ESP_RETURN_ON_ERROR(esp_lcd_new_panel_st7789(s_panel_io, &panel_config, &s_panel), TAG, "new st7789 panel failed"); ESP_RETURN_ON_ERROR(esp_lcd_panel_reset(s_panel), TAG, "panel reset failed"); ESP_RETURN_ON_ERROR(esp_lcd_panel_init(s_panel), TAG, "panel init failed"); for (size_t i = 0; s_st7789_init_cmds[i].databytes != 0xFFU; ++i) { const lcd_direct_init_cmd_t *cmd = &s_st7789_init_cmds[i]; ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(s_panel_io, cmd->cmd, cmd->databytes > 0U ? cmd->data : NULL, cmd->databytes), TAG, "custom init cmd failed"); } ESP_RETURN_ON_ERROR(esp_lcd_panel_swap_xy(s_panel, CONFIG_TQ_SCREEN_SWAP_XY), TAG, "panel swap xy failed"); ESP_RETURN_ON_ERROR(esp_lcd_panel_mirror(s_panel, CONFIG_TQ_SCREEN_MIRROR_X, false), TAG, "panel mirror failed"); uint8_t madctl = 0; if (CONFIG_TQ_SCREEN_MIRROR_X) { madctl |= 0x40; } if (CONFIG_TQ_SCREEN_SWAP_XY) { madctl |= 0x20; } #if CONFIG_TQ_SCREEN_COLOR_ORDER_BGR madctl |= 0x08; #endif uint8_t ramctrl[] = {0x00, 0xF0}; ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(s_panel_io, LCD_CMD_MADCTL, &madctl, 1), TAG, "panel MADCTL failed"); ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(s_panel_io, LCD_CMD_RAMCTRL, ramctrl, sizeof(ramctrl)), TAG, "panel RAMCTRL failed"); ESP_RETURN_ON_ERROR(esp_lcd_panel_invert_color(s_panel, CONFIG_TQ_SCREEN_INVERT_COLOR), TAG, "panel invert failed"); ESP_RETURN_ON_ERROR(esp_lcd_panel_set_gap(s_panel, CONFIG_TQ_SCREEN_X_GAP, CONFIG_TQ_SCREEN_Y_GAP), TAG, "panel set gap failed"); ESP_RETURN_ON_ERROR(esp_lcd_panel_disp_on_off(s_panel, true), TAG, "panel display on failed"); return ESP_OK; } static uint16_t lcd_direct_color_for_row(uint16_t row, const lcd_direct_request_t *request) { uint16_t band = row / 40U; if (request != NULL && request->kind == PLATFORM_LCD_PRESENTATION_SLOT_REELS) { static const uint16_t slot_colors[] = {0x001F, 0xF800, 0xFFE0, 0x07E0, 0xF81F, 0xFFFF}; return slot_colors[band % (sizeof(slot_colors) / sizeof(slot_colors[0]))]; } if (request != NULL && request->kind == PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE) { uint32_t hash = 0; for (size_t i = 0; request->asset_dir[i] != '\0'; ++i) { hash = (hash * 33U) + (uint8_t)request->asset_dir[i]; } static const uint16_t asset_colors[] = {0x0000, 0x07FF, 0xF800, 0x07E0, 0x001F, 0xFFFF}; return asset_colors[(hash + band) % (sizeof(asset_colors) / sizeof(asset_colors[0]))]; } static const uint16_t idle_colors[] = {0x0000, 0x39E7, 0x7BEF, 0xFFFF, 0x7BEF, 0x39E7}; return idle_colors[band % (sizeof(idle_colors) / sizeof(idle_colors[0]))]; } static esp_err_t lcd_direct_draw_test_pattern(const lcd_direct_request_t *request) { for (uint16_t row_start = 0; row_start < LCD_DIRECT_HEIGHT; row_start += LCD_DIRECT_ROWS_PER_TRANSFER) { uint16_t row_count = LCD_DIRECT_HEIGHT - row_start; if (row_count > LCD_DIRECT_ROWS_PER_TRANSFER) { row_count = LCD_DIRECT_ROWS_PER_TRANSFER; } for (uint16_t y = 0; y < row_count; ++y) { uint16_t color = lcd_direct_swap_color(lcd_direct_color_for_row(row_start + y, request)); for (uint16_t x = 0; x < LCD_DIRECT_WIDTH; ++x) { s_line_buffer[(uint32_t)y * LCD_DIRECT_WIDTH + x] = color; } } ESP_RETURN_ON_ERROR(lcd_direct_draw_rows(row_start, row_count), TAG, "write pixels failed"); } return ESP_OK; } static lcd_direct_rle_run_t *lcd_direct_load_icon_runs(const lcd_direct_icon_t *icon) { if (icon == NULL || icon->path == NULL || icon->run_count == 0U) { return NULL; } const size_t bytes_to_read = icon->run_count * sizeof(lcd_direct_rle_run_t); lcd_direct_rle_run_t *runs = (lcd_direct_rle_run_t *)malloc(bytes_to_read); if (runs == NULL) { ESP_LOGW(TAG, "allocate LCD icon failed path=%s bytes=%u", icon->path, (unsigned)bytes_to_read); return NULL; } errno = 0; int fd = open(icon->path, O_RDONLY); if (fd < 0) { ESP_LOGW(TAG, "open LCD icon failed path=%s errno=%d", icon->path, errno); free(runs); return NULL; } size_t offset = 0; while (offset < bytes_to_read) { ssize_t bytes_read = read(fd, ((uint8_t *)runs) + offset, bytes_to_read - offset); if (bytes_read <= 0) { ESP_LOGW(TAG, "read LCD icon failed path=%s requested=%u read=%d errno=%d", icon->path, (unsigned)bytes_to_read, (int)bytes_read, errno); close(fd); free(runs); return NULL; } offset += (size_t)bytes_read; } close(fd); return runs; } static void lcd_direct_overlay_icons(uint16_t *pixel_buffer, uint16_t row_start, uint16_t row_count, const lcd_direct_icon_draw_t *icons, size_t icon_count) { if (pixel_buffer == NULL || icons == NULL || icon_count == 0U) { return; } const uint16_t row_end = row_start + row_count; for (size_t icon_index = 0; icon_index < icon_count; ++icon_index) { const lcd_direct_icon_draw_t *draw = &icons[icon_index]; const lcd_direct_icon_t *icon = lcd_direct_icon(draw->icon_id); if (icon == NULL || icon->width == 0U || icon->height == 0U) { continue; } if (draw->x >= LCD_DIRECT_WIDTH || draw->y >= LCD_DIRECT_HEIGHT) { continue; } lcd_direct_rle_run_t *loaded_runs = NULL; const lcd_direct_rle_run_t *runs = icon->runs; if (runs == NULL) { loaded_runs = lcd_direct_load_icon_runs(icon); runs = loaded_runs; } if (runs == NULL) { continue; } uint16_t draw_width = icon->width; uint16_t draw_height = icon->height; if (draw_width > LCD_DIRECT_WIDTH - draw->x) { draw_width = LCD_DIRECT_WIDTH - draw->x; } if (draw_height > LCD_DIRECT_HEIGHT - draw->y) { draw_height = LCD_DIRECT_HEIGHT - draw->y; } const uint16_t overlay_y0 = draw->y; const uint16_t overlay_y1 = draw->y + draw_height; if (overlay_y1 <= row_start || overlay_y0 >= row_end) { free(loaded_runs); continue; } const uint16_t copy_y0 = overlay_y0 > row_start ? overlay_y0 : row_start; const uint16_t copy_y1 = overlay_y1 < row_end ? overlay_y1 : row_end; for (uint16_t y = copy_y0; y < copy_y1; ++y) { const uint16_t local_y = y - row_start; const uint16_t image_y = y - draw->y; const uint32_t icon_pixel_start = (uint32_t)image_y * icon->width; const uint32_t icon_pixel_end = icon_pixel_start + draw_width; uint32_t icon_pixel_pos = 0; for (uint32_t run_index = 0; run_index < icon->run_count && icon_pixel_pos < icon_pixel_end; ++run_index) { const uint32_t run_start = icon_pixel_pos; const uint32_t run_end = run_start + runs[run_index].count; if (run_end > icon_pixel_start && run_start < icon_pixel_end) { const uint32_t copy_start = run_start > icon_pixel_start ? run_start : icon_pixel_start; const uint32_t copy_end = run_end < icon_pixel_end ? run_end : icon_pixel_end; const uint16_t color = lcd_direct_swap_color(runs[run_index].color); for (uint32_t x = copy_start; x < copy_end; ++x) { pixel_buffer[(uint32_t)local_y * LCD_DIRECT_WIDTH + draw->x + (x - icon_pixel_start)] = color; } } icon_pixel_pos = run_end; } } free(loaded_runs); } } static esp_err_t lcd_direct_draw_rle_memory_image(const lcd_direct_image_t *image, const lcd_direct_icon_draw_t *icons, size_t icon_count) { ESP_RETURN_ON_FALSE(image != NULL && image->runs != NULL, ESP_ERR_INVALID_ARG, TAG, "invalid image"); uint32_t run_index = 0; uint32_t run_remaining = 0; uint16_t run_color = 0; for (uint16_t row_start = 0; row_start < LCD_DIRECT_HEIGHT; row_start += LCD_DIRECT_ROWS_PER_TRANSFER) { uint16_t row_count = LCD_DIRECT_HEIGHT - row_start; if (row_count > LCD_DIRECT_ROWS_PER_TRANSFER) { row_count = LCD_DIRECT_ROWS_PER_TRANSFER; } const uint32_t pixels_needed = (uint32_t)LCD_DIRECT_WIDTH * row_count; uint32_t buffered_pixels = 0; while (buffered_pixels < pixels_needed) { if (run_remaining == 0U) { if (run_index >= image->run_count) { break; } run_color = lcd_direct_swap_color(image->runs[run_index].color); run_remaining = image->runs[run_index].count; run_index++; } uint32_t pixels_to_copy = run_remaining; const uint32_t pixels_left = pixels_needed - buffered_pixels; if (pixels_to_copy > pixels_left) { pixels_to_copy = pixels_left; } for (uint32_t i = 0; i < pixels_to_copy; ++i) { s_line_buffer[buffered_pixels + i] = run_color; } buffered_pixels += pixels_to_copy; run_remaining -= pixels_to_copy; } while (buffered_pixels < pixels_needed) { s_line_buffer[buffered_pixels++] = 0; } lcd_direct_overlay_icons(s_line_buffer, row_start, row_count, icons, icon_count); ESP_RETURN_ON_ERROR(lcd_direct_draw_rows(row_start, row_count), TAG, "write image pixels failed"); } return ESP_OK; } static esp_err_t lcd_direct_draw_rle_file_image(const lcd_direct_image_t *image, const lcd_direct_icon_draw_t *icons, size_t icon_count) { ESP_RETURN_ON_FALSE(image != NULL && image->path != NULL, ESP_ERR_INVALID_ARG, TAG, "invalid file image"); errno = 0; int fd = open(image->path, O_RDONLY); if (fd < 0) { ESP_LOGE(TAG, "open LCD asset failed path=%s errno=%d", image->path, errno); return ESP_ERR_NOT_FOUND; } uint32_t run_index = 0; uint32_t chunk_index = 0; uint32_t chunk_count = 0; uint32_t run_remaining = 0; uint16_t run_color = 0; esp_err_t result = ESP_OK; for (uint16_t row_start = 0; row_start < LCD_DIRECT_HEIGHT; row_start += LCD_DIRECT_ROWS_PER_TRANSFER) { uint16_t row_count = LCD_DIRECT_HEIGHT - row_start; if (row_count > LCD_DIRECT_ROWS_PER_TRANSFER) { row_count = LCD_DIRECT_ROWS_PER_TRANSFER; } const uint32_t pixels_needed = (uint32_t)LCD_DIRECT_WIDTH * row_count; uint32_t buffered_pixels = 0; while (buffered_pixels < pixels_needed) { if (run_remaining == 0U) { if (run_index >= image->run_count) { break; } if (chunk_index >= chunk_count) { const size_t runs_left = image->run_count - run_index; const size_t runs_to_read = runs_left < (sizeof(s_rle_file_chunk) / sizeof(s_rle_file_chunk[0])) ? runs_left : (sizeof(s_rle_file_chunk) / sizeof(s_rle_file_chunk[0])); const size_t bytes_to_read = runs_to_read * sizeof(s_rle_file_chunk[0]); ssize_t bytes_read = read(fd, s_rle_file_chunk, bytes_to_read); if (bytes_read != (ssize_t)bytes_to_read) { ESP_LOGE(TAG, "read LCD asset failed path=%s requested=%u read=%d errno=%d", image->path, (unsigned)bytes_to_read, (int)bytes_read, errno); result = ESP_FAIL; goto out; } chunk_index = 0; chunk_count = runs_to_read; } run_color = lcd_direct_swap_color(s_rle_file_chunk[chunk_index].color); run_remaining = s_rle_file_chunk[chunk_index].count; chunk_index++; run_index++; } uint32_t pixels_to_copy = run_remaining; const uint32_t pixels_left = pixels_needed - buffered_pixels; if (pixels_to_copy > pixels_left) { pixels_to_copy = pixels_left; } for (uint32_t i = 0; i < pixels_to_copy; ++i) { s_line_buffer[buffered_pixels + i] = run_color; } buffered_pixels += pixels_to_copy; run_remaining -= pixels_to_copy; } while (buffered_pixels < pixels_needed) { s_line_buffer[buffered_pixels++] = 0; } lcd_direct_overlay_icons(s_line_buffer, row_start, row_count, icons, icon_count); result = lcd_direct_draw_rows(row_start, row_count); if (result != ESP_OK) { ESP_LOGE(TAG, "write image pixels failed path=%s", image->path); goto out; } } out: close(fd); return result; } static esp_err_t lcd_direct_draw_rle_image(const lcd_direct_image_t *image, const lcd_direct_icon_draw_t *icons, size_t icon_count) { ESP_RETURN_ON_FALSE(image != NULL, ESP_ERR_INVALID_ARG, TAG, "invalid image"); if (image->runs != NULL) { return lcd_direct_draw_rle_memory_image(image, icons, icon_count); } return lcd_direct_draw_rle_file_image(image, icons, icon_count); } static esp_err_t lcd_direct_draw_asset_sequence_once(const lcd_direct_request_t *request) { ESP_RETURN_ON_FALSE(request != NULL, ESP_ERR_INVALID_ARG, TAG, "invalid asset request"); const size_t frame_count = request->frame_order_count > 0U ? request->frame_order_count : 1U; for (size_t i = 0; i < frame_count; ++i) { bool stop = false; portENTER_CRITICAL(&s_state_lock); stop = s_stop_requested || s_pending_valid; portEXIT_CRITICAL(&s_state_lock); if (stop) { return ESP_ERR_INVALID_STATE; } const uint8_t frame_index = request->frame_order_count > 0U ? request->frame_order[i] : (uint8_t)i; const lcd_direct_image_t *frame = lcd_direct_asset_frame(request->asset_dir, frame_index); ESP_RETURN_ON_FALSE(frame != NULL, ESP_ERR_NOT_FOUND, TAG, "LCD asset frame not found asset=%s frame=%u", request->asset_dir, (unsigned)frame_index); ESP_RETURN_ON_ERROR(lcd_direct_draw_rle_image(frame, NULL, 0), TAG, "draw asset frame failed"); const uint32_t frame_ms = request->frame_ms > 0U ? request->frame_ms : 100U; vTaskDelay(pdMS_TO_TICKS(frame_ms)); } static TickType_t s_last_draw_success_tick; TickType_t now = xTaskGetTickCount(); if (s_last_draw_success_tick == 0 || (now - s_last_draw_success_tick) >= pdMS_TO_TICKS(5000)) { s_last_draw_success_tick = now; ESP_LOGI(TAG, "LCD asset sequence drawn asset=%s frames=%u", request->asset_dir, (unsigned)frame_count); } return ESP_OK; } static esp_err_t lcd_direct_draw_asset_sequence_for_duration(const lcd_direct_request_t *request, TickType_t start_tick, TickType_t duration_ticks) { ESP_RETURN_ON_FALSE(request != NULL, ESP_ERR_INVALID_ARG, TAG, "invalid timed asset request"); const size_t frame_count = request->frame_order_count > 0U ? request->frame_order_count : 1U; while ((xTaskGetTickCount() - start_tick) < duration_ticks) { for (size_t i = 0; i < frame_count; ++i) { bool stop = false; portENTER_CRITICAL(&s_state_lock); stop = s_stop_requested || s_pending_valid; portEXIT_CRITICAL(&s_state_lock); if (stop) { return ESP_ERR_INVALID_STATE; } const uint8_t frame_index = request->frame_order_count > 0U ? request->frame_order[i] : (uint8_t)i; const lcd_direct_image_t *frame = lcd_direct_asset_frame(request->asset_dir, frame_index); ESP_RETURN_ON_FALSE(frame != NULL, ESP_ERR_NOT_FOUND, TAG, "LCD asset frame not found asset=%s frame=%u", request->asset_dir, (unsigned)frame_index); ESP_RETURN_ON_ERROR(lcd_direct_draw_rle_image(frame, NULL, 0), TAG, "draw timed asset frame failed"); const TickType_t now = xTaskGetTickCount(); const TickType_t elapsed_ticks = now - start_tick; if (elapsed_ticks >= duration_ticks) { return ESP_OK; } const uint32_t frame_ms = request->frame_ms > 0U ? request->frame_ms : 100U; const TickType_t frame_ticks = pdMS_TO_TICKS(frame_ms); const TickType_t remaining_ticks = duration_ticks - elapsed_ticks; vTaskDelay(frame_ticks < remaining_ticks ? frame_ticks : remaining_ticks); } } return ESP_OK; } static esp_err_t lcd_direct_play_asset_sequence(const lcd_direct_request_t *request) { ESP_RETURN_ON_FALSE(request != NULL, ESP_ERR_INVALID_ARG, TAG, "invalid asset sequence"); if (request->asset_dir[0] == '\0' || lcd_direct_asset_frame(request->asset_dir, request->frame_order_count > 0U ? request->frame_order[0] : 0U) == NULL) { ESP_LOGW(TAG, "LCD asset sequence unavailable, fallback to basic face asset=%s", request->asset_dir); const lcd_direct_request_t fallback = { .kind = PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE, .asset_dir = "jichubiaoqing", .frame_order = {0, 1, 2}, .frame_order_count = 3, .repeat_count = 1, .frame_ms = 100, }; esp_err_t fallback_err = lcd_direct_draw_asset_sequence_once(&fallback); if (fallback_err == ESP_OK && request->repeat_count > 0U) { lcd_direct_signal_completion(); } return fallback_err; } if (request->duration_ms > 0U) { const TickType_t start_tick = xTaskGetTickCount(); const TickType_t duration_ticks = pdMS_TO_TICKS(request->duration_ms); esp_err_t err = lcd_direct_draw_asset_sequence_for_duration(request, start_tick, duration_ticks); if (err != ESP_OK) { return err; } lcd_direct_signal_completion(); return ESP_OK; } uint16_t remaining = request->repeat_count; do { esp_err_t err = lcd_direct_draw_asset_sequence_once(request); if (err != ESP_OK) { return err; } if (request->repeat_count > 0U) { remaining--; } } while (request->repeat_count == 0U || remaining > 0U); lcd_direct_signal_completion(); return ESP_OK; } static esp_err_t lcd_direct_play_step_sequence(const lcd_direct_request_t *request) { ESP_RETURN_ON_FALSE(request != NULL, ESP_ERR_INVALID_ARG, TAG, "invalid step sequence"); for (size_t i = 0; i < request->step_count; ++i) { bool stop = false; portENTER_CRITICAL(&s_state_lock); stop = s_stop_requested || s_pending_valid; portEXIT_CRITICAL(&s_state_lock); if (stop) { return ESP_ERR_INVALID_STATE; } const platform_lcd_presentation_step_t *step = &request->steps[i]; const lcd_direct_image_t *frame = lcd_direct_asset_frame(step->asset_dir, step->frame_index); ESP_RETURN_ON_FALSE(frame != NULL, ESP_ERR_NOT_FOUND, TAG, "LCD step frame not found asset=%s frame=%u", step->asset_dir != NULL ? step->asset_dir : "", (unsigned)step->frame_index); lcd_direct_icon_draw_t icon = {0}; const lcd_direct_icon_draw_t *icons = NULL; size_t icon_count = 0; if (step->icon_id != PLATFORM_LCD_PRESENTATION_NO_ICON) { icon = (lcd_direct_icon_draw_t) { .icon_id = step->icon_id, .x = step->icon_x, .y = step->icon_y, }; icons = &icon; icon_count = 1; } ESP_RETURN_ON_ERROR(lcd_direct_draw_rle_image(frame, icons, icon_count), TAG, "draw step frame failed"); uint32_t delay_ms = step->delay_ms > 0U ? step->delay_ms : 100U; vTaskDelay(pdMS_TO_TICKS(delay_ms)); } lcd_direct_signal_completion(); return ESP_OK; } static esp_err_t lcd_direct_play_basic_face_until_pending(void) { const lcd_direct_request_t request = { .kind = PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE, .asset_dir = "jichubiaoqing", .frame_order = {0, 1, 2}, .frame_order_count = 3, .repeat_count = 0, .frame_ms = 100, }; while (true) { bool stop = false; portENTER_CRITICAL(&s_state_lock); stop = s_stop_requested || s_pending_valid; portEXIT_CRITICAL(&s_state_lock); if (stop) { return ESP_OK; } esp_err_t err = lcd_direct_draw_asset_sequence_once(&request); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { return err; } } } static void lcd_direct_signal_completion(void) { portENTER_CRITICAL(&s_state_lock); s_completion_count++; portEXIT_CRITICAL(&s_state_lock); if (s_completion_sem != NULL) { (void)xSemaphoreGive(s_completion_sem); } } static bool lcd_direct_take_pending(lcd_direct_request_t *out) { bool has_pending = false; portENTER_CRITICAL(&s_state_lock); if (s_pending_valid) { *out = s_pending; s_pending_valid = false; memset(&s_pending, 0, sizeof(s_pending)); has_pending = true; } portEXIT_CRITICAL(&s_state_lock); return has_pending; } esp_err_t lcd_direct_init_device(void) { if (s_initialized) { return ESP_OK; } s_lock = xSemaphoreCreateMutex(); ESP_RETURN_ON_FALSE(s_lock != NULL, ESP_ERR_NO_MEM, TAG, "lock create failed"); s_completion_sem = xSemaphoreCreateBinary(); ESP_RETURN_ON_FALSE(s_completion_sem != NULL, ESP_ERR_NO_MEM, TAG, "completion sem create failed"); ESP_RETURN_ON_ERROR(lcd_direct_gpio_init(), TAG, "gpio init failed"); ESP_RETURN_ON_ERROR(lcd_direct_panel_init(), TAG, "esp_lcd panel init failed"); s_initialized = true; ESP_LOGI(TAG, "Direct LCD initialized (MOSI=%d SCLK=%d CS=%d DC=%d RST=%d BL=%d clock=%d)", CONFIG_TQ_SCREEN_SPI_MOSI_GPIO, CONFIG_TQ_SCREEN_SPI_CLK_GPIO, CONFIG_TQ_SCREEN_SPI_CS_GPIO, CONFIG_TQ_SCREEN_DC_GPIO, CONFIG_TQ_SCREEN_RESET_PIN, CONFIG_TQ_SCREEN_BACKLIGHT_GPIO, CONFIG_TQ_SCREEN_SPI_PIXEL_CLOCK_HZ); return ESP_OK; } void lcd_direct_start_render_loop(void) { if (!s_initialized) { ESP_LOGW(TAG, "Direct LCD is not initialized, skip render loop"); return; } portENTER_CRITICAL(&s_state_lock); if (s_render_loop_running) { portEXIT_CRITICAL(&s_state_lock); ESP_LOGI(TAG, "Direct LCD render loop already running"); return; } s_render_loop_running = true; s_stop_requested = false; s_render_task = xTaskGetCurrentTaskHandle(); portEXIT_CRITICAL(&s_state_lock); ESP_LOGI(TAG, "Direct LCD render loop started"); while (true) { bool stop = false; portENTER_CRITICAL(&s_state_lock); stop = s_stop_requested; portEXIT_CRITICAL(&s_state_lock); if (stop) { break; } lcd_direct_request_t request = {0}; if (lcd_direct_take_pending(&request)) { if (xSemaphoreTake(s_lock, pdMS_TO_TICKS(1000)) == pdTRUE) { esp_err_t draw_err = ESP_OK; if (request.kind == PLATFORM_LCD_PRESENTATION_IDLE) { draw_err = lcd_direct_play_basic_face_until_pending(); } else if (request.kind == PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE) { draw_err = lcd_direct_play_asset_sequence(&request); if (draw_err == ESP_OK && request.return_to_idle) { lcd_direct_request_t idle = { .kind = PLATFORM_LCD_PRESENTATION_IDLE, }; portENTER_CRITICAL(&s_state_lock); s_pending = idle; s_pending_valid = true; portEXIT_CRITICAL(&s_state_lock); } } else if (request.kind == PLATFORM_LCD_PRESENTATION_STEP_SEQUENCE) { draw_err = lcd_direct_play_step_sequence(&request); if (draw_err == ESP_OK && request.return_to_idle) { lcd_direct_request_t idle = { .kind = PLATFORM_LCD_PRESENTATION_IDLE, }; portENTER_CRITICAL(&s_state_lock); s_pending = idle; s_pending_valid = true; portEXIT_CRITICAL(&s_state_lock); } } else { draw_err = lcd_direct_draw_test_pattern(&request); } xSemaphoreGive(s_lock); if (draw_err != ESP_OK && draw_err != ESP_ERR_INVALID_STATE) { ESP_LOGW(TAG, "LCD presentation draw failed: %s", esp_err_to_name(draw_err)); } } } else { if (xSemaphoreTake(s_lock, pdMS_TO_TICKS(1000)) == pdTRUE) { esp_err_t idle_err = lcd_direct_play_basic_face_until_pending(); xSemaphoreGive(s_lock); if (idle_err != ESP_OK && idle_err != ESP_ERR_INVALID_STATE) { ESP_LOGW(TAG, "LCD idle draw failed: %s", esp_err_to_name(idle_err)); vTaskDelay(pdMS_TO_TICKS(100)); } } } (void)ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(20)); } portENTER_CRITICAL(&s_state_lock); s_render_task = NULL; s_render_loop_running = false; s_stop_requested = false; portEXIT_CRITICAL(&s_state_lock); ESP_LOGI(TAG, "Direct LCD render loop stopped"); } esp_err_t lcd_direct_stop_render_loop(int timeout_ms) { if (timeout_ms < 0) { return ESP_ERR_INVALID_ARG; } TaskHandle_t task = NULL; portENTER_CRITICAL(&s_state_lock); task = s_render_task; if (task != NULL) { s_stop_requested = true; } portEXIT_CRITICAL(&s_state_lock); if (task == NULL || timeout_ms == 0) { return ESP_OK; } xTaskNotifyGive(task); TickType_t timeout_ticks = pdMS_TO_TICKS(timeout_ms); if (timeout_ticks == 0) { timeout_ticks = 1; } TickType_t start = xTaskGetTickCount(); while ((xTaskGetTickCount() - start) < timeout_ticks) { bool stopped = false; portENTER_CRITICAL(&s_state_lock); stopped = !s_render_loop_running && s_render_task == NULL; portEXIT_CRITICAL(&s_state_lock); if (stopped) { return ESP_OK; } vTaskDelay(pdMS_TO_TICKS(10)); } if (task != xTaskGetCurrentTaskHandle()) { ESP_LOGW(TAG, "Direct LCD render loop stop timed out after %d ms; force deleting", timeout_ms); vTaskDelete(task); portENTER_CRITICAL(&s_state_lock); if (s_render_task == task) { s_render_task = NULL; } s_render_loop_running = false; s_stop_requested = false; portEXIT_CRITICAL(&s_state_lock); } return ESP_ERR_TIMEOUT; } esp_err_t lcd_direct_request_presentation(const platform_lcd_presentation_request_t *request) { if (!s_initialized || request == NULL) { return ESP_ERR_INVALID_STATE; } lcd_direct_request_t copied = { .kind = request->kind, .priority = request->priority, .interrupt_current = request->interrupt_current, .return_to_idle = request->return_to_idle, .repeat_count = request->repeat_count, .frame_ms = request->frame_ms, .duration_ms = request->duration_ms, }; if (request->asset_dir != NULL) { strlcpy(copied.asset_dir, request->asset_dir, sizeof(copied.asset_dir)); } if (request->frame_order != NULL && request->frame_order_count > 0U) { copied.frame_order_count = request->frame_order_count; if (copied.frame_order_count > PLATFORM_LCD_PRESENTATION_FRAME_ORDER_MAX) { copied.frame_order_count = PLATFORM_LCD_PRESENTATION_FRAME_ORDER_MAX; } memcpy(copied.frame_order, request->frame_order, copied.frame_order_count); } if (request->steps != NULL && request->step_count > 0U) { copied.step_count = request->step_count; if (copied.step_count > PLATFORM_LCD_PRESENTATION_STEP_MAX) { copied.step_count = PLATFORM_LCD_PRESENTATION_STEP_MAX; } memcpy(copied.steps, request->steps, copied.step_count * sizeof(copied.steps[0])); } TaskHandle_t task = NULL; portENTER_CRITICAL(&s_state_lock); s_pending = copied; s_pending_valid = true; task = s_render_task; portEXIT_CRITICAL(&s_state_lock); if (task != NULL) { xTaskNotifyGive(task); } ESP_LOGI(TAG, "Direct LCD presentation queued kind=%d asset=%s", copied.kind, copied.asset_dir); return ESP_OK; } esp_err_t lcd_direct_preheat_presentation(const platform_lcd_presentation_request_t *request) { (void)request; return s_initialized ? ESP_OK : ESP_ERR_INVALID_STATE; } uint32_t lcd_direct_presentation_completion_count(void) { uint32_t count; portENTER_CRITICAL(&s_state_lock); count = s_completion_count; portEXIT_CRITICAL(&s_state_lock); return count; } esp_err_t lcd_direct_wait_presentation_completion(uint32_t since_count, uint32_t timeout_ms) { if (!s_initialized) { return ESP_ERR_INVALID_STATE; } TickType_t start_tick = xTaskGetTickCount(); TickType_t timeout_ticks = pdMS_TO_TICKS(timeout_ms); for (;;) { if (lcd_direct_presentation_completion_count() != since_count) { return ESP_OK; } if (timeout_ms == 0 || (xTaskGetTickCount() - start_tick) >= timeout_ticks) { return ESP_ERR_TIMEOUT; } TickType_t elapsed = xTaskGetTickCount() - start_tick; TickType_t remaining = timeout_ticks > elapsed ? timeout_ticks - elapsed : 0; if (s_completion_sem != NULL && remaining > 0) { (void)xSemaphoreTake(s_completion_sem, remaining); } else { vTaskDelay(pdMS_TO_TICKS(LCD_DIRECT_PRESENTATION_WAIT_POLL_MS)); } } }