diff --git a/main/include/lcd/ui.h b/main/include/lcd/ui.h index 9b20810..01a42d2 100644 --- a/main/include/lcd/ui.h +++ b/main/include/lcd/ui.h @@ -6,6 +6,7 @@ typedef enum { } ui_view_t; void ui_log_append(const char *text); +void ui_log_appendf(const char *format, ...); void create_log_ui(void); void create_image_browser_ui(void); diff --git a/main/lcd/lcd.c b/main/lcd/lcd.c index 95e4971..4705cc2 100644 --- a/main/lcd/lcd.c +++ b/main/lcd/lcd.c @@ -204,7 +204,8 @@ static esp_err_t init_touch(void){ static esp_err_t init_lvgl(void){ ESP_LOGI(TAG, "Initialize LVGL port"); - const lvgl_port_cfg_t lvgl_config = ESP_LVGL_PORT_INIT_CONFIG(); + lvgl_port_cfg_t lvgl_config = ESP_LVGL_PORT_INIT_CONFIG(); + lvgl_config.task_stack = 16 * 1024; ESP_RETURN_ON_ERROR(lvgl_port_init(&lvgl_config), TAG, "LVGL port init failed"); uint32_t buffer_size = LCD_H_RES * LCD_DRAW_BUFF_HEIGHT; diff --git a/main/lcd/ui.c b/main/lcd/ui.c index 0cd2eb0..b7e77e4 100644 --- a/main/lcd/ui.c +++ b/main/lcd/ui.c @@ -1,6 +1,8 @@ +#include #include #include #include +#include #include #include "lvgl.h" @@ -11,20 +13,66 @@ #include "sd/sd.h" #define UI_LOG_MAX_LINES 41 +#define UI_LOG_LINE_MAX_LENGTH 160 #define UI_IMAGE_PATH_PREFIX "A:/sdcard/" #define UI_IMAGE_PATH_MAX_LENGTH 160 #define UI_IMAGE_INFO_BAR_HEIGHT 42 +#define UI_IMAGE_INFO_BAR_BOTTOM_OFFSET 10 +#define UI_IMAGE_PROBE_HEADER_LENGTH 16 +#define UI_SLIDESHOW_PERIOD_MS 3000 static lv_obj_t *log_textarea = NULL; static lv_obj_t *image_tileview = NULL; +static lv_timer_t *image_slideshow_timer = NULL; +static char log_lines[UI_LOG_MAX_LINES][UI_LOG_LINE_MAX_LENGTH]; static size_t log_line_length = 0; +static size_t log_line_start = 0; static size_t image_browser_image_count = 0; static size_t image_browser_current_index = 0; static const char *TAG = "ui"; static ui_view_t current_view = UI_VIEW_LOG; +static void ui_create_log_ui_locked(void); +static void ui_create_image_browser_ui_locked(void); + +static size_t ui_log_buffer_index(size_t line_offset){ + return (log_line_start + line_offset) % UI_LOG_MAX_LINES; +} + +static void ui_log_store_line(const char *text){ + if(text == NULL) return; + + size_t line_index; + if(log_line_length < UI_LOG_MAX_LINES){ + line_index = ui_log_buffer_index(log_line_length); + log_line_length++; + } + else{ + line_index = log_line_start; + log_line_start = ui_log_buffer_index(1); + } + + snprintf(log_lines[line_index], sizeof(log_lines[line_index]), "%s", text); +} + +static void ui_log_render_locked(void){ + if(log_textarea == NULL) return; + + lv_textarea_set_text(log_textarea, ""); + for(size_t line_offset = 0; line_offset < log_line_length; line_offset++){ + lv_textarea_add_text(log_textarea, log_lines[ui_log_buffer_index(line_offset)]); + lv_textarea_add_text(log_textarea, "\n"); + } + lv_textarea_set_cursor_pos(log_textarea, LV_TEXTAREA_CURSOR_LAST); +} + static void ui_reset_active_screen_locked(void){ + if(image_slideshow_timer != NULL){ + lv_timer_delete(image_slideshow_timer); + image_slideshow_timer = NULL; + } + lv_obj_t *screen = lv_screen_active(); lv_obj_clean(screen); log_textarea = NULL; @@ -40,11 +88,32 @@ static void ui_image_browser_set_index_locked(size_t image_index, lv_anim_enable lv_tileview_set_tile_by_index(image_tileview, 0, (uint32_t)image_browser_current_index, anim_en); } -static void ui_image_browser_touch_event_cb(lv_event_t *event){ - if(lv_event_get_code(event) != LV_EVENT_CLICKED) return; +static void ui_image_slideshow_timer_cb(lv_timer_t *timer){ + (void)timer; + if(current_view != UI_VIEW_IMAGE_BROWSER) return; ui_image_browser_set_index_locked(image_browser_current_index + 1, LV_ANIM_ON); } +static void ui_start_image_slideshow_locked(void){ + if(image_slideshow_timer != NULL || image_browser_image_count <= 1) return; + image_slideshow_timer = lv_timer_create(ui_image_slideshow_timer_cb, UI_SLIDESHOW_PERIOD_MS, NULL); +} + +static void ui_toggle_view_locked(void){ + if(current_view == UI_VIEW_IMAGE_BROWSER) ui_create_log_ui_locked(); + else ui_create_image_browser_ui_locked(); +} + +static void ui_toggle_view_event_cb(lv_event_t *event){ + if(lv_event_get_code(event) != LV_EVENT_CLICKED) return; + ui_toggle_view_locked(); +} + +static void ui_add_toggle_click(lv_obj_t *object){ + lv_obj_add_flag(object, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(object, ui_toggle_view_event_cb, LV_EVENT_CLICKED, NULL); +} + static void ui_image_browser_value_changed_event_cb(lv_event_t *event){ if(lv_event_get_code(event) != LV_EVENT_VALUE_CHANGED) return; @@ -76,6 +145,15 @@ static bool ui_has_extension(const char *filename, const char *extension){ return true; } +static void ui_lowercase_path_extension(char *path){ + char *extension = strrchr(path, '.'); + if(extension == NULL) return; + + for(char *current_char = extension + 1; *current_char != '\0'; current_char++){ + if(*current_char >= 'A' && *current_char <= 'Z') *current_char = (char)(*current_char + ('a' - 'A')); + } +} + static bool ui_is_image_file(const char *filename){ return ui_has_extension(filename, ".bmp") || ui_has_extension(filename, ".jpg") || @@ -83,6 +161,79 @@ static bool ui_is_image_file(const char *filename){ ui_has_extension(filename, ".png"); } +static const char *ui_detect_image_signature(const uint8_t *header, size_t header_length){ + if(header_length >= 2 && header[0] == 0xff && header[1] == 0xd8) return "jpeg"; + if(header_length >= 8 && header[0] == 0x89 && header[1] == 'P' && header[2] == 'N' && header[3] == 'G') return "png"; + if(header_length >= 2 && header[0] == 'B' && header[1] == 'M') return "bmp"; + return "unknown"; +} + +static long ui_get_open_file_size(FILE *file){ + if(file == NULL) return -1; + + const long current_position = ftell(file); + if(current_position < 0) return -1; + if(fseek(file, 0, SEEK_END) != 0) return -1; + + const long file_size = ftell(file); + fseek(file, current_position, SEEK_SET); + return file_size; +} + +static const char *ui_probe_image_load_failure(const char *filename, const char *image_source){ + if(filename == NULL || image_source == NULL) return "Invalid image path"; + + errno = 0; + FILE *file = sd_open_read(filename); + if(file == NULL){ + const int open_errno = errno; + ESP_LOGW(TAG, "Image file not readable via sd_open_read: file=%s source=%s errno=%d", filename, image_source, open_errno); + ui_log_appendf("Image read failed: %s errno=%d", filename, open_errno); + return "File not readable"; + } + + uint8_t header_bytes[UI_IMAGE_PROBE_HEADER_LENGTH] = {0}; + const size_t header_length = fread(header_bytes, 1, sizeof(header_bytes), file); + const long file_size = ui_get_open_file_size(file); + sd_close_file(file); + + const char *detected_format = ui_detect_image_signature(header_bytes, header_length); + ESP_LOGW(TAG, + "Image probe: file=%s source=%s size=%ld header=%02x %02x %02x %02x %02x %02x %02x %02x detected=%s", + filename, + image_source, + file_size, + (unsigned int)header_bytes[0], + (unsigned int)header_bytes[1], + (unsigned int)header_bytes[2], + (unsigned int)header_bytes[3], + (unsigned int)header_bytes[4], + (unsigned int)header_bytes[5], + (unsigned int)header_bytes[6], + (unsigned int)header_bytes[7], + detected_format); + ui_log_appendf("Image probe: %s size=%ld detected=%s", filename, file_size, detected_format); + + lv_image_header_t image_header; + const lv_result_t decoder_result = lv_image_decoder_get_info(image_source, &image_header); + if(decoder_result != LV_RESULT_OK){ + ESP_LOGW(TAG, "LVGL decoder rejected image: source=%s result=%d", image_source, (int)decoder_result); + ui_log_appendf("Decoder rejected: %s result=%d", filename, (int)decoder_result); + return "Decoder rejected image"; + } + + ESP_LOGW(TAG, + "LVGL decoder accepted image but widget has no size: source=%s width=%u height=%u cf=%u stride=%u flags=0x%x", + image_source, + (unsigned int)image_header.w, + (unsigned int)image_header.h, + (unsigned int)image_header.cf, + (unsigned int)image_header.stride, + (unsigned int)image_header.flags); + ui_log_appendf("Decoder info OK: %s %ux%u", filename, (unsigned int)image_header.w, (unsigned int)image_header.h); + return "Decoder info OK"; +} + static size_t ui_count_sd_images(void){ const size_t file_count = sd_get_file_count(); size_t image_count = 0; @@ -96,9 +247,12 @@ static size_t ui_count_sd_images(void){ } static void ui_create_empty_image_browser_locked(lv_obj_t *screen){ + ui_add_toggle_click(screen); + lv_obj_t *label = lv_label_create(screen); lv_label_set_text(label, "No images found on SD card"); lv_obj_set_style_text_color(label, lv_color_hex(0xdde7ef), 0); + ui_add_toggle_click(label); lv_obj_center(label); } @@ -110,11 +264,13 @@ static lv_dir_t ui_get_tile_directions(size_t image_index, size_t image_count){ } static void ui_create_image_tile_locked(lv_obj_t *tile, const char *filename, size_t image_index, size_t image_count){ + ui_add_toggle_click(tile); lv_obj_set_style_bg_color(tile, lv_color_hex(0x070a0d), 0); lv_obj_set_style_pad_all(tile, 0, 0); lv_obj_remove_flag(tile, LV_OBJ_FLAG_SCROLLABLE); lv_obj_t *image_frame = lv_obj_create(tile); + ui_add_toggle_click(image_frame); lv_obj_set_size(image_frame, LV_PCT(100), LV_PCT(100)); lv_obj_set_style_bg_color(image_frame, lv_color_black(), 0); lv_obj_set_style_border_width(image_frame, 0, 0); @@ -124,17 +280,20 @@ static void ui_create_image_tile_locked(lv_obj_t *tile, const char *filename, si lv_obj_center(image_frame); lv_obj_t *image = lv_image_create(image_frame); + ui_add_toggle_click(image); lv_obj_set_size(image, LV_PCT(100), LV_PCT(100)); lv_image_set_inner_align(image, LV_IMAGE_ALIGN_CONTAIN); char image_source[UI_IMAGE_PATH_MAX_LENGTH]; snprintf(image_source, sizeof(image_source), "%s%s", UI_IMAGE_PATH_PREFIX, filename); + ui_lowercase_path_extension(image_source); lv_image_set_src(image, image_source); lv_obj_center(image); lv_obj_t *info_bar = lv_obj_create(tile); + ui_add_toggle_click(info_bar); lv_obj_set_size(info_bar, LV_PCT(100), UI_IMAGE_INFO_BAR_HEIGHT); - lv_obj_align(info_bar, LV_ALIGN_BOTTOM_MID, 0, 0); + lv_obj_align(info_bar, LV_ALIGN_BOTTOM_MID, 0, -UI_IMAGE_INFO_BAR_BOTTOM_OFFSET); lv_obj_set_style_bg_color(info_bar, lv_color_hex(0x101418), 0); lv_obj_set_style_bg_opa(info_bar, LV_OPA_80, 0); lv_obj_set_style_border_width(info_bar, 0, 0); @@ -144,6 +303,7 @@ static void ui_create_image_tile_locked(lv_obj_t *tile, const char *filename, si lv_obj_remove_flag(info_bar, LV_OBJ_FLAG_SCROLLABLE); lv_obj_t *caption = lv_label_create(info_bar); + ui_add_toggle_click(caption); lv_label_set_long_mode(caption, LV_LABEL_LONG_DOT); lv_obj_set_width(caption, LV_PCT(100)); lv_obj_set_style_text_color(caption, lv_color_hex(0xdde7ef), 0); @@ -151,9 +311,15 @@ static void ui_create_image_tile_locked(lv_obj_t *tile, const char *filename, si lv_obj_center(caption); if(lv_image_get_src_width(image) <= 0 || lv_image_get_src_height(image) <= 0){ + const char *load_reason = ui_probe_image_load_failure(filename, image_source); + ESP_LOGW(TAG, "Image cannot be loaded by LVGL: %s reason=%s", image_source, load_reason); + ui_log_appendf("Image load failed: %s (%s)", filename, load_reason); lv_obj_t *error_label = lv_label_create(tile); - lv_label_set_text(error_label, "Image cannot be loaded"); + ui_add_toggle_click(error_label); + lv_label_set_text_fmt(error_label, "Image cannot be loaded\n%s\n%s", filename, load_reason); lv_obj_set_style_text_color(error_label, lv_color_hex(0xffd166), 0); + lv_obj_set_style_text_align(error_label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_width(error_label, LV_PCT(86)); lv_obj_align(error_label, LV_ALIGN_CENTER, 0, -18); } } @@ -168,65 +334,50 @@ static void ui_create_image_browser_tiles_locked(size_t image_count){ lv_obj_t *tile = lv_tileview_add_tile(image_tileview, 0, (uint32_t)image_index, ui_get_tile_directions(image_index, image_count)); lv_obj_set_user_data(tile, (void *)(uintptr_t)image_index); - lv_obj_add_flag(tile, LV_OBJ_FLAG_CLICKABLE); - lv_obj_add_event_cb(tile, ui_image_browser_touch_event_cb, LV_EVENT_CLICKED, NULL); ui_create_image_tile_locked(tile, filename, image_index, image_count); image_index++; } } -static void ui_log_delete_first_line(void){ - lvgl_port_lock(0); - size_t cursor_pos = lv_textarea_get_cursor_pos(log_textarea); - lv_textarea_set_cursor_pos(log_textarea, 0); - - while(1){ - const char *log_text = lv_textarea_get_text(log_textarea); - if(log_text[0] == '\0') break; - - char first_char = log_text[0]; - lv_textarea_delete_char_forward(log_textarea); - - if(first_char == '\n') break; - } - - lv_textarea_set_cursor_pos(log_textarea, cursor_pos); - - lvgl_port_unlock(); -} - void ui_log_append(const char *text){ - if(log_textarea == NULL || text == NULL) return; + if(text == NULL) return; + + ui_log_store_line(text); + if(log_textarea == NULL) return; lvgl_port_lock(0); - lv_textarea_add_text(log_textarea, text); - lv_textarea_add_text(log_textarea, "\n"); - lv_textarea_set_cursor_pos(log_textarea, LV_TEXTAREA_CURSOR_LAST); + ui_log_render_locked(); lvgl_port_unlock(); - - log_line_length++; - - while(log_line_length > UI_LOG_MAX_LINES){ - ui_log_delete_first_line(); - log_line_length--; - } } -void create_log_ui(void){ - ESP_LOGI(TAG, "Creating log UI"); - lvgl_port_lock(0); +void ui_log_appendf(const char *format, ...){ + if(format == NULL) return; + + char line[UI_LOG_LINE_MAX_LENGTH]; + va_list args; + va_start(args, format); + vsnprintf(line, sizeof(line), format, args); + va_end(args); + + ui_log_append(line); +} + +static void ui_create_log_ui_locked(void){ ui_reset_active_screen_locked(); lv_obj_t *screen = lv_screen_active(); + ui_add_toggle_click(screen); lv_obj_set_style_bg_color(screen, lv_color_hex(0x101418), 0); lv_obj_t *title = lv_label_create(screen); + ui_add_toggle_click(title); lv_label_set_text(title, "SD Image Viewer"); lv_obj_set_style_text_color(title, lv_color_white(), 0); lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 14); if(log_textarea == NULL){ log_textarea = lv_textarea_create(screen); + ui_add_toggle_click(log_textarea); lv_obj_set_size(log_textarea, 440, 730); lv_obj_align(log_textarea, LV_ALIGN_TOP_LEFT, 20, 40); lv_textarea_set_one_line(log_textarea, false); @@ -238,22 +389,29 @@ void create_log_ui(void){ lv_obj_set_style_radius(log_textarea, 4, 0); lv_obj_set_style_pad_all(log_textarea, 12, 0); } - - lvgl_port_unlock(); - - ui_log_append("Boot UI ready"); + ui_log_render_locked(); + current_view = UI_VIEW_LOG; - ESP_LOGI(TAG, "Log UI created"); } -void create_image_browser_ui(void){ - ESP_LOGI(TAG, "Creating image browser UI"); - const size_t image_count = ui_count_sd_images(); - +void create_log_ui(void){ + ESP_LOGI(TAG, "Creating log UI"); + ui_log_append("Creating log UI"); lvgl_port_lock(0); + ui_create_log_ui_locked(); + lvgl_port_unlock(); + + ui_log_append("Boot UI ready"); + ESP_LOGI(TAG, "Log UI created"); + ui_log_append("Log UI created"); +} + +static void ui_create_image_browser_ui_locked(void){ + const size_t image_count = ui_count_sd_images(); ui_reset_active_screen_locked(); lv_obj_t *screen = lv_screen_active(); + ui_add_toggle_click(screen); lv_obj_set_style_bg_color(screen, lv_color_hex(0x070a0d), 0); if(image_count == 0) ui_create_empty_image_browser_locked(screen); @@ -269,12 +427,22 @@ void create_image_browser_ui(void){ ui_create_image_browser_tiles_locked(image_count); ui_image_browser_set_index_locked(0, LV_ANIM_OFF); + ui_start_image_slideshow_locked(); } current_view = UI_VIEW_IMAGE_BROWSER; +} + +void create_image_browser_ui(void){ + ESP_LOGI(TAG, "Creating image browser UI"); + ui_log_append("Creating image browser UI"); + + lvgl_port_lock(0); + ui_create_image_browser_ui_locked(); lvgl_port_unlock(); - ESP_LOGI(TAG, "Image browser UI created with %u images", (unsigned int)image_count); + ESP_LOGI(TAG, "Image browser UI created with %u images", (unsigned int)image_browser_image_count); + ui_log_appendf("Image browser UI created with %u images", (unsigned int)image_browser_image_count); } void ui_switch_view_to(ui_view_t view){ @@ -282,13 +450,18 @@ void ui_switch_view_to(ui_view_t view){ switch(view){ case UI_VIEW_LOG: - create_log_ui(); + lvgl_port_lock(0); + ui_create_log_ui_locked(); + lvgl_port_unlock(); break; case UI_VIEW_IMAGE_BROWSER: - create_image_browser_ui(); + lvgl_port_lock(0); + ui_create_image_browser_ui_locked(); + lvgl_port_unlock(); break; default: ESP_LOGW(TAG, "Unknown UI view: %d", view); + ui_log_appendf("Unknown UI view: %d", view); break; } } \ No newline at end of file diff --git a/main/main.c b/main/main.c index e8e9b67..bb24991 100644 --- a/main/main.c +++ b/main/main.c @@ -14,29 +14,32 @@ void app_main(void){ ESP_LOGI(TAG, "Creating log UI"); create_log_ui(); + ui_log_append("Start display application"); + ui_log_append("Creating log UI"); - ESP_LOGI(TAG, "Initializing SD card"); - ui_log_append("\nInitializing SD card..."); + ESP_LOGI(TAG, "\nInitializing SD card"); + ui_log_append("Initializing SD card"); sd_init(); print_sd_card_info(); ui_log_append("SD initialization done"); - ESP_LOGI(TAG, "SD card initialization complete"); + ESP_LOGI(TAG, "SD card initialization done"); - ui_log_append("\nListing files in root directory:"); + ui_log_append("\nListing files in root directory"); size_t file_count = sd_get_file_count(); ESP_LOGI(TAG, "Found %d files in root directory", file_count); + ui_log_appendf("Found %u files in root directory", (unsigned int)file_count); const char* filename = NULL; while(file_count-- > 0 && (filename = sd_get_next_file()) != NULL){ ESP_LOGI(TAG, "Found file: %s", filename); - char line[96]; - snprintf(line, sizeof(line), "Found file: %s", filename); - ui_log_append(line); + ui_log_appendf("Found file: %s", filename); vTaskDelay(pdMS_TO_TICKS(1)); } ui_log_append("Done listing files in root directory"); + ui_log_append("\nSwitching to image browser UI"); + ESP_LOGI(TAG, "Switching to image browser UI"); ui_switch_view_to(UI_VIEW_IMAGE_BROWSER); // sd_destroy(); diff --git a/sdkconfig b/sdkconfig index 8743cdf..e12f6fb 100644 --- a/sdkconfig +++ b/sdkconfig @@ -1633,7 +1633,7 @@ CONFIG_ESP_SYSTEM_EVENT_QUEUE_SIZE=32 # default: CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=2304 # default: -CONFIG_ESP_MAIN_TASK_STACK_SIZE=3584 +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 # default: CONFIG_ESP_MAIN_TASK_AFFINITY_CPU0=y # default: @@ -3205,11 +3205,11 @@ CONFIG_LV_FS_STDIO_CACHE_SIZE=0 # default: # CONFIG_LV_USE_FS_FROGFS is not set # default: -# CONFIG_LV_USE_LODEPNG is not set +CONFIG_LV_USE_LODEPNG=y # default: # CONFIG_LV_USE_LIBPNG is not set # default: -# CONFIG_LV_USE_BMP is not set +CONFIG_LV_USE_BMP=y CONFIG_LV_USE_TJPGD=y # default: # CONFIG_LV_USE_LIBJPEG_TURBO is not set @@ -3466,7 +3466,7 @@ CONFIG_ESP_SYSTEM_MEMPROT_FEATURE=y CONFIG_ESP_SYSTEM_MEMPROT_FEATURE_LOCK=y CONFIG_SYSTEM_EVENT_QUEUE_SIZE=32 CONFIG_SYSTEM_EVENT_TASK_STACK_SIZE=2304 -CONFIG_MAIN_TASK_STACK_SIZE=3584 +CONFIG_MAIN_TASK_STACK_SIZE=8192 CONFIG_INT_WDT=y CONFIG_INT_WDT_TIMEOUT_MS=300 CONFIG_INT_WDT_CHECK_CPU1=y