+ rearranged files

+ improved ui log
+ added image browser
- image loading fails at the moment
- arrangement of ui in image browser not checked
This commit is contained in:
Timo Niemann
2026-07-01 13:03:58 +02:00
parent 229d828271
commit 4e8282a64b
10 changed files with 358 additions and 80 deletions
+22
View File
@@ -0,0 +1,22 @@
# ESP32 Image Viewer on LCD from SD
## Introduction
This project utilizes an ELECROW ESP32 Display to read images from SD card and display them.
## Usage
Insert SD card into the TF slot on the board. Flash the project to the ESP32. Use touch to switch between UI-Log and Image-Browser.
## Hardware
- ELECROW ESP32 5" HMI Display (800x480) RGB TFT Touchscreen compatible with LVGL
- USB-C cable to flash firmware
- optional but recommended:
- SD card
## Software
- ESP-IDF
- optional but recommended:
- VS Code with ESP-IDF extension
+2 -2
View File
@@ -1,3 +1,3 @@
idf_component_register(SRCS main.c lcd/lcd.c sd/sd.c idf_component_register(SRCS main.c lcd/lcd.c sd/sd.c lcd/ui.c
PRIV_REQUIRES spi_flash esp_psram esp_lcd esp_driver_gpio esp_driver_i2c esp_driver_sdspi esp_driver_spi esp_lvgl_port esp_lcd_touch_gt911 fatfs sdmmc PRIV_REQUIRES spi_flash esp_psram esp_lcd esp_driver_gpio esp_driver_i2c esp_driver_sdspi esp_driver_spi esp_lvgl_port esp_lcd_touch_gt911 fatfs sdmmc
INCLUDE_DIRS ".") INCLUDE_DIRS "." "include")
+12
View File
@@ -0,0 +1,12 @@
#pragma once
typedef enum {
UI_VIEW_LOG,
UI_VIEW_IMAGE_BROWSER,
} ui_view_t;
void ui_log_append(const char *text);
void create_log_ui(void);
void create_image_browser_ui(void);
void ui_switch_view_to(ui_view_t view);
+4 -9
View File
@@ -71,8 +71,7 @@ static esp_lcd_touch_handle_t touch_handle;
static lv_display_t *lvgl_display; static lv_display_t *lvgl_display;
static lv_indev_t *lvgl_touch_indev; static lv_indev_t *lvgl_touch_indev;
static esp_err_t init_backlight(void) static esp_err_t init_backlight(void){
{
const gpio_config_t backlight_config = { const gpio_config_t backlight_config = {
.mode = GPIO_MODE_OUTPUT, .mode = GPIO_MODE_OUTPUT,
.pin_bit_mask = 1ULL << LCD_GPIO_BACKLIGHT, .pin_bit_mask = 1ULL << LCD_GPIO_BACKLIGHT,
@@ -83,8 +82,7 @@ static esp_err_t init_backlight(void)
return ESP_OK; return ESP_OK;
} }
static esp_err_t probe_gt911_address(i2c_master_bus_handle_t i2c_handle, uint32_t *dev_addr) static esp_err_t probe_gt911_address(i2c_master_bus_handle_t i2c_handle, uint32_t *dev_addr){
{
const uint8_t addresses[] = { const uint8_t addresses[] = {
ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS, ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS,
ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS_BACKUP, ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS_BACKUP,
@@ -265,9 +263,8 @@ static esp_err_t init_lvgl(void){
lvgl_touch_indev = lvgl_port_add_touch(&touch_config); lvgl_touch_indev = lvgl_port_add_touch(&touch_config);
ESP_RETURN_ON_FALSE(lvgl_touch_indev, ESP_FAIL, TAG, "LVGL touch add failed"); ESP_RETURN_ON_FALSE(lvgl_touch_indev, ESP_FAIL, TAG, "LVGL touch add failed");
ESP_LOGI(TAG, "LVGL touch input ready"); ESP_LOGI(TAG, "LVGL touch input ready");
} else {
ESP_LOGW(TAG, "LVGL touch input disabled");
} }
else ESP_LOGW(TAG, "LVGL touch input disabled");
ESP_LOGI(TAG, "LVGL ready"); ESP_LOGI(TAG, "LVGL ready");
@@ -277,9 +274,7 @@ static esp_err_t init_lvgl(void){
esp_err_t lcd_init(void){ esp_err_t lcd_init(void){
ESP_RETURN_ON_ERROR(init_rgb_panel(), TAG, "LCD panel init failed"); ESP_RETURN_ON_ERROR(init_rgb_panel(), TAG, "LCD panel init failed");
const esp_err_t touch_ret = init_touch(); const esp_err_t touch_ret = init_touch();
if (touch_ret != ESP_OK) { if(touch_ret != ESP_OK) ESP_LOGW(TAG, "Continuing without touch: %s", esp_err_to_name(touch_ret));
ESP_LOGW(TAG, "Continuing without touch: %s", esp_err_to_name(touch_ret));
}
return init_lvgl(); return init_lvgl();
} }
+294
View File
@@ -0,0 +1,294 @@
#include <stdbool.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include "lvgl.h"
#include "lcd/lcd.h"
#include "esp_lvgl_port.h"
#include "esp_log.h"
#include "lcd/ui.h"
#include "sd/sd.h"
#define UI_LOG_MAX_LINES 41
#define UI_IMAGE_PATH_PREFIX "A:/sdcard/"
#define UI_IMAGE_PATH_MAX_LENGTH 160
#define UI_IMAGE_INFO_BAR_HEIGHT 42
static lv_obj_t *log_textarea = NULL;
static lv_obj_t *image_tileview = NULL;
static size_t log_line_length = 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_reset_active_screen_locked(void){
lv_obj_t *screen = lv_screen_active();
lv_obj_clean(screen);
log_textarea = NULL;
image_tileview = NULL;
image_browser_image_count = 0;
image_browser_current_index = 0;
}
static void ui_image_browser_set_index_locked(size_t image_index, lv_anim_enable_t anim_en){
if(image_tileview == NULL || image_browser_image_count == 0) return;
image_browser_current_index = image_index % image_browser_image_count;
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;
ui_image_browser_set_index_locked(image_browser_current_index + 1, LV_ANIM_ON);
}
static void ui_image_browser_value_changed_event_cb(lv_event_t *event){
if(lv_event_get_code(event) != LV_EVENT_VALUE_CHANGED) return;
lv_obj_t *tileview = lv_event_get_current_target(event);
lv_obj_t *active_tile = lv_tileview_get_tile_active(tileview);
if(active_tile == NULL) return;
image_browser_current_index = (size_t)(uintptr_t)lv_obj_get_user_data(active_tile);
}
static bool ui_char_equals_ignore_case(char left_char, char right_char){
if(left_char >= 'A' && left_char <= 'Z') left_char = (char)(left_char + ('a' - 'A'));
if(right_char >= 'A' && right_char <= 'Z') right_char = (char)(right_char + ('a' - 'A'));
return left_char == right_char;
}
static bool ui_has_extension(const char *filename, const char *extension){
if(filename == NULL || extension == NULL) return false;
const size_t filename_length = strlen(filename);
const size_t extension_length = strlen(extension);
if(filename_length < extension_length) return false;
const char *filename_extension = filename + filename_length - extension_length;
for(size_t char_index = 0; char_index < extension_length; char_index++){
if(!ui_char_equals_ignore_case(filename_extension[char_index], extension[char_index])) return false;
}
return true;
}
static bool ui_is_image_file(const char *filename){
return ui_has_extension(filename, ".bmp") ||
ui_has_extension(filename, ".jpg") ||
ui_has_extension(filename, ".jpeg") ||
ui_has_extension(filename, ".png");
}
static size_t ui_count_sd_images(void){
const size_t file_count = sd_get_file_count();
size_t image_count = 0;
for(size_t file_index = 0; file_index < file_count; file_index++){
const char *filename = sd_get_next_file();
if(ui_is_image_file(filename)) image_count++;
}
return image_count;
}
static void ui_create_empty_image_browser_locked(lv_obj_t *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);
lv_obj_center(label);
}
static lv_dir_t ui_get_tile_directions(size_t image_index, size_t image_count){
if(image_count <= 1) return LV_DIR_NONE;
if(image_index == 0) return LV_DIR_BOTTOM;
if(image_index + 1 == image_count) return LV_DIR_TOP;
return (lv_dir_t)(LV_DIR_TOP | LV_DIR_BOTTOM);
}
static void ui_create_image_tile_locked(lv_obj_t *tile, const char *filename, size_t image_index, size_t image_count){
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);
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);
lv_obj_set_style_radius(image_frame, 0, 0);
lv_obj_set_style_pad_all(image_frame, 0, 0);
lv_obj_remove_flag(image_frame, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_center(image_frame);
lv_obj_t *image = lv_image_create(image_frame);
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);
lv_image_set_src(image, image_source);
lv_obj_center(image);
lv_obj_t *info_bar = lv_obj_create(tile);
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_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);
lv_obj_set_style_radius(info_bar, 0, 0);
lv_obj_set_style_pad_hor(info_bar, 14, 0);
lv_obj_set_style_pad_ver(info_bar, 0, 0);
lv_obj_remove_flag(info_bar, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t *caption = lv_label_create(info_bar);
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);
lv_label_set_text_fmt(caption, "%u/%u %s", (unsigned int)(image_index + 1), (unsigned int)image_count, filename);
lv_obj_center(caption);
if(lv_image_get_src_width(image) <= 0 || lv_image_get_src_height(image) <= 0){
lv_obj_t *error_label = lv_label_create(tile);
lv_label_set_text(error_label, "Image cannot be loaded");
lv_obj_set_style_text_color(error_label, lv_color_hex(0xffd166), 0);
lv_obj_align(error_label, LV_ALIGN_CENTER, 0, -18);
}
}
static void ui_create_image_browser_tiles_locked(size_t image_count){
const size_t file_count = sd_get_file_count();
size_t image_index = 0;
for(size_t file_index = 0; file_index < file_count && image_index < image_count; file_index++){
const char *filename = sd_get_next_file();
if(!ui_is_image_file(filename)) continue;
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;
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);
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);
ui_reset_active_screen_locked();
lv_obj_t *screen = lv_screen_active();
lv_obj_set_style_bg_color(screen, lv_color_hex(0x101418), 0);
lv_obj_t *title = lv_label_create(screen);
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);
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);
lv_textarea_set_cursor_click_pos(log_textarea, false);
lv_textarea_set_text(log_textarea, "");
lv_obj_set_style_bg_color(log_textarea, lv_color_hex(0x182028), 0);
lv_obj_set_style_text_color(log_textarea, lv_color_hex(0xdde7ef), 0);
lv_obj_set_style_border_color(log_textarea, lv_color_hex(0x4b6478), 0);
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");
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();
lvgl_port_lock(0);
ui_reset_active_screen_locked();
lv_obj_t *screen = lv_screen_active();
lv_obj_set_style_bg_color(screen, lv_color_hex(0x070a0d), 0);
if(image_count == 0) ui_create_empty_image_browser_locked(screen);
else{
image_tileview = lv_tileview_create(screen);
lv_obj_set_size(image_tileview, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_bg_color(image_tileview, lv_color_hex(0x070a0d), 0);
lv_obj_set_style_border_width(image_tileview, 0, 0);
lv_obj_set_style_pad_all(image_tileview, 0, 0);
lv_obj_set_scrollbar_mode(image_tileview, LV_SCROLLBAR_MODE_OFF);
lv_obj_add_event_cb(image_tileview, ui_image_browser_value_changed_event_cb, LV_EVENT_VALUE_CHANGED, NULL);
image_browser_image_count = image_count;
ui_create_image_browser_tiles_locked(image_count);
ui_image_browser_set_index_locked(0, LV_ANIM_OFF);
}
current_view = UI_VIEW_IMAGE_BROWSER;
lvgl_port_unlock();
ESP_LOGI(TAG, "Image browser UI created with %u images", (unsigned int)image_count);
}
void ui_switch_view_to(ui_view_t view){
if(view == current_view) return;
switch(view){
case UI_VIEW_LOG:
create_log_ui();
break;
case UI_VIEW_IMAGE_BROWSER:
create_image_browser_ui();
break;
default:
ESP_LOGW(TAG, "Unknown UI view: %d", view);
break;
}
}
+13 -58
View File
@@ -1,67 +1,28 @@
#include "esp_check.h" #include "esp_check.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_lvgl_port.h"
#include "lcd/lcd.h"
#include "sd/sd.h" #include "sd/sd.h"
#include "lvgl.h" #include "lcd/lcd.h"
#include "lcd/ui.h"
#include "freertos/FreeRTOS.h"
static const char *TAG = "app"; static const char *TAG = "app";
static lv_obj_t *log_textarea = NULL;
static void ui_log_append(const char *text) void app_main(void){
{
if(log_textarea == NULL || text == 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);
lvgl_port_unlock();
}
static void create_log_ui(void)
{
lv_obj_t *screen = lv_screen_active();
lv_obj_set_style_bg_color(screen, lv_color_hex(0x101418), 0);
lv_obj_t *title = lv_label_create(screen);
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);
log_textarea = lv_textarea_create(screen);
lv_obj_set_size(log_textarea, lv_pct(92), lv_pct(84));
lv_obj_align(log_textarea, LV_ALIGN_BOTTOM_MID, 0, -14);
lv_textarea_set_one_line(log_textarea, false);
lv_textarea_set_cursor_click_pos(log_textarea, false);
lv_textarea_set_text(log_textarea, "");
lv_obj_set_style_bg_color(log_textarea, lv_color_hex(0x182028), 0);
lv_obj_set_style_text_color(log_textarea, lv_color_hex(0xdde7ef), 0);
lv_obj_set_style_border_color(log_textarea, lv_color_hex(0x4b6478), 0);
lv_obj_set_style_radius(log_textarea, 4, 0);
lv_obj_set_style_pad_all(log_textarea, 12, 0);
lv_textarea_add_text(log_textarea, "Boot UI ready\n");
lv_textarea_set_cursor_pos(log_textarea, LV_TEXTAREA_CURSOR_LAST);
}
void app_main(void)
{
ESP_LOGI(TAG, "Start display application"); ESP_LOGI(TAG, "Start display application");
ESP_ERROR_CHECK(lcd_init()); ESP_ERROR_CHECK(lcd_init());
ESP_LOGI(TAG, "Creating log UI"); ESP_LOGI(TAG, "Creating log UI");
lvgl_port_lock(0);
create_log_ui(); create_log_ui();
lvgl_port_unlock();
ESP_LOGI(TAG, "Initializing SD card"); ESP_LOGI(TAG, "Initializing SD card");
ui_log_append("Initializing SD card..."); ui_log_append("\nInitializing SD card...");
sd_init(); sd_init();
print_sd_card_info(); print_sd_card_info();
ui_log_append("SD initialization done"); ui_log_append("SD initialization done");
ESP_LOGI(TAG, "SD card initialization complete"); ESP_LOGI(TAG, "SD card initialization complete");
ui_log_append("\nListing files in root directory:");
size_t file_count = sd_get_file_count(); size_t file_count = sd_get_file_count();
ESP_LOGI(TAG, "Found %d files in root directory", file_count); ESP_LOGI(TAG, "Found %d files in root directory", file_count);
const char* filename = NULL; const char* filename = NULL;
@@ -71,19 +32,13 @@ void app_main(void)
char line[96]; char line[96];
snprintf(line, sizeof(line), "Found file: %s", filename); snprintf(line, sizeof(line), "Found file: %s", filename);
ui_log_append(line); ui_log_append(line);
vTaskDelay(pdMS_TO_TICKS(1));
} }
/* ui_log_append("Done listing files in root directory");
FILE* file = sd_open_write("test.txt");
if(file != NULL){
fprintf(file, "Hello, SD card!\n");
sd_close_file(file);
ui_log_append("Wrote test.txt");
} else {
ui_log_append("Failed to open test.txt for writing");
}
*/
sd_destroy(); ui_switch_view_to(UI_VIEW_IMAGE_BROWSER);
ui_log_append("SD card unmounted");
// sd_destroy();
// ui_log_append("\nSD card unmounted");
} }
+2 -4
View File
@@ -71,16 +71,14 @@ void sd_init(void){
ret = esp_vfs_fat_sdspi_mount(mount_point, &host, &slot_config, &mount_config, &card); ret = esp_vfs_fat_sdspi_mount(mount_point, &host, &slot_config, &mount_config, &card);
if(ret != ESP_OK){ if(ret != ESP_OK){
if(ret == ESP_FAIL) ESP_LOGE(TAG, "Failed to mount filesystem."); if(ret == ESP_FAIL) ESP_LOGE(TAG, "Failed to mount filesystem."); else ESP_LOGE(TAG, "Failed to initialize the card (%s). ", esp_err_to_name(ret));
else ESP_LOGE(TAG, "Failed to initialize the card (%s). ", esp_err_to_name(ret));
spi_bus_free(host.slot); spi_bus_free(host.slot);
return; return;
} }
ESP_LOGI(TAG, "SD card initialized"); ESP_LOGI(TAG, "SD card initialized");
if(get_top_level_files() == ESP_OK) ESP_LOGI(TAG, "Found %d files in root directory", _file_count); if(get_top_level_files() == ESP_OK) ESP_LOGI(TAG, "Found %d files in root directory", _file_count); else ESP_LOGW(TAG, "No files found in root directory");
else ESP_LOGW(TAG, "No files found in root directory");
} }
void print_sd_card_info(void){ void print_sd_card_info(void){
+6 -4
View File
@@ -3180,9 +3180,12 @@ CONFIG_LV_USE_GRID=y
# 3rd Party Libraries # 3rd Party Libraries
# #
# default: # default:
CONFIG_LV_FS_DEFAULT_DRIVER_LETTER=0 CONFIG_LV_FS_DEFAULT_DRIVER_LETTER=65
CONFIG_LV_USE_FS_STDIO=y
# default: # default:
# CONFIG_LV_USE_FS_STDIO is not set CONFIG_LV_FS_STDIO_LETTER=65
CONFIG_LV_FS_STDIO_PATH=""
CONFIG_LV_FS_STDIO_CACHE_SIZE=0
# default: # default:
# CONFIG_LV_USE_FS_POSIX is not set # CONFIG_LV_USE_FS_POSIX is not set
# default: # default:
@@ -3207,8 +3210,7 @@ CONFIG_LV_FS_DEFAULT_DRIVER_LETTER=0
# CONFIG_LV_USE_LIBPNG is not set # CONFIG_LV_USE_LIBPNG is not set
# default: # default:
# CONFIG_LV_USE_BMP is not set # CONFIG_LV_USE_BMP is not set
# default: CONFIG_LV_USE_TJPGD=y
# CONFIG_LV_USE_TJPGD is not set
# default: # default:
# CONFIG_LV_USE_LIBJPEG_TURBO is not set # CONFIG_LV_USE_LIBJPEG_TURBO is not set
# default: # default: