How to Convert proper Bin Images for LVGL Ver 9.4.0
I use an ESP32-S3 and used the phyton script LVGLImage.py but that is not working.
How have you set up your code to read the bin files?
I got it python LVGLImage.py --cf RGB565 buddha.png --output e:\bin but next question when I Read it from SD Card the images is odd
this is the code I’m using
I converted it to RGB565
#include "LVGL_Manager.h"
#include <lvgl.h>
#include <SD_MMC.h>
#include "Display/JC3248W535_GFX.h"
#include "Display/JC3248W535_Touch.h"
#include "ui/ui.h"
#include "../Color/Color.h"
#include <esp_heap_caps.h>
//#include "/../.pio/libdeps/esp32-s3/lvgl/src/draw/lv_image_dsc.h"
/* ----------------------------------------------------
* Statische Member
* ---------------------------------------------------- */
lv_display_t* LVGL_Manager::disp = nullptr;
lv_indev_t* LVGL_Manager::indev = nullptr;
lv_color_t* LVGL_Manager::buf = nullptr;
JC3248_GFX* LVGL_Manager::screen_ptr = nullptr;
JC3248_Touch* LVGL_Manager::touch_ptr = nullptr;
/* ----------------------------------------------------
* LVGL 9 FS Driver (SD_MMC)
* ---------------------------------------------------- */
static File sd_files[10];
static const char* convert_path(const char* path)
{
// "S:/file.bin" → "/file.bin"
if(path[0] == 'S' && path[1] == ':' && path[2] == '/')
return path + 2;
return path;
}
static void* fs_open_cb(lv_fs_drv_t*, const char* path, lv_fs_mode_t mode)
{
const char* real = convert_path(path);
for(int i = 0; i < 10; i++) {
if(!sd_files[i]) {
sd_files[i] = SD_MMC.open(
real,
(mode == LV_FS_MODE_RD) ? FILE_READ : FILE_WRITE
);
if(sd_files[i])
return (void*)(intptr_t)(i + 1);
}
}
return nullptr;
}
static lv_fs_res_t fs_close_cb(lv_fs_drv_t*, void* file_p)
{
int idx = (int)(intptr_t)file_p - 1;
if(idx >= 0 && idx < 10 && sd_files[idx]) {
sd_files[idx].close();
sd_files[idx] = File();
return LV_FS_RES_OK;
}
return LV_FS_RES_UNKNOWN;
}
static lv_fs_res_t fs_read_cb(lv_fs_drv_t*, void* file_p,
void* buf, uint32_t btr, uint32_t* br)
{
int idx = (int)(intptr_t)file_p - 1;
if(idx >= 0 && idx < 10 && sd_files[idx]) {
*br = sd_files[idx].read((uint8_t*)buf, btr);
return LV_FS_RES_OK;
}
return LV_FS_RES_UNKNOWN;
}
static lv_fs_res_t fs_seek_cb(lv_fs_drv_t*, void* file_p,
uint32_t pos, lv_fs_whence_t whence)
{
int idx = (int)(intptr_t)file_p - 1;
if(idx >= 0 && idx < 10 && sd_files[idx]) {
SeekMode m =
(whence == LV_FS_SEEK_SET) ? SeekSet :
(whence == LV_FS_SEEK_CUR) ? SeekCur : SeekEnd;
if(sd_files[idx].seek(pos, m))
return LV_FS_RES_OK;
}
return LV_FS_RES_UNKNOWN;
}
static lv_fs_res_t fs_tell_cb(lv_fs_drv_t*, void* file_p, uint32_t* pos_p)
{
int idx = (int)(intptr_t)file_p - 1;
if(idx >= 0 && idx < 10 && sd_files[idx]) {
*pos_p = sd_files[idx].position();
return LV_FS_RES_OK;
}
return LV_FS_RES_UNKNOWN;
}
void LVGL_Manager::initFilesystem()
{
static lv_fs_drv_t drv;
lv_fs_drv_init(&drv);
drv.letter = 'S';
drv.cache_size = 0;
drv.open_cb = fs_open_cb;
drv.close_cb = fs_close_cb;
drv.read_cb = fs_read_cb;
drv.seek_cb = fs_seek_cb;
drv.tell_cb = fs_tell_cb;
lv_fs_drv_register(&drv);
Serial.println(BOLDGREEN "✓ LVGL 9 FS Driver (S:/) registriert" RESET);
}
/* ----------------------------------------------------
* LVGL 9 Init
* ---------------------------------------------------- */
bool LVGL_Manager::initLVGL(JC3248_GFX* screen, JC3248_Touch* touch)
{
screen_ptr = screen;
touch_ptr = touch;
if(!screen_ptr || !touch_ptr || !screen_ptr->gfx) {
Serial.println(BOLDRED "✗ Display/Touch ungültig" RESET);
return false;
}
lv_init();
constexpr size_t DRAW_PIXELS = ScreenWidth * 10;
constexpr size_t BUF_BYTES = DRAW_PIXELS * sizeof(lv_color_t);
buf = (lv_color_t*)heap_caps_malloc(
BUF_BYTES, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT
);
if(!buf) return false;
disp = lv_display_create(ScreenWidth, ScreenHeight);
lv_display_set_buffers(disp, buf, nullptr,
BUF_BYTES, LV_DISPLAY_RENDER_MODE_PARTIAL);
lv_display_set_flush_cb(disp, my_disp_flush);
indev = lv_indev_create();
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(indev, touchscreen_read);
lv_display_set_default(disp);
return true;
}
void LVGL_Manager::loadEEZUI()
{
Serial.println(BOLDCYAN "\n╔════════════════════════════════════════╗" RESET);
Serial.println(BOLDCYAN "║ LADE EEZ STUDIO UI... ║" RESET);
Serial.println(BOLDCYAN "╚════════════════════════════════════════╝" RESET);
Serial.printf("Vor ui_init() - PSRAM: %u | Heap: %u\n\r",
ESP.getFreePsram(), ESP.getFreeHeap());
// EEZ UI initialisieren
ui_init();
// ui_show_screen(SCREEN_ID_MAIN);
Serial.printf("Nach ui_init() - PSRAM: %u | Heap: %u\n\r",
ESP.getFreePsram(), ESP.getFreeHeap());
Serial.println(BOLDGREEN "✓ EEZ Studio UI geladen!" RESET);
// Force Refresh
lv_obj_invalidate(lv_screen_active());
lv_refr_now(disp);
}
/* ----------------------------------------------------
* Display Flush (LVGL 9)
* ---------------------------------------------------- */
void LVGL_Manager::my_disp_flush(lv_display_t* d, const lv_area_t* a, uint8_t* p)
{
if(!screen_ptr || !screen_ptr->gfx) {
lv_display_flush_ready(d);
return;
}
int16_t w = a->x2 - a->x1 + 1;
int16_t h = a->y2 - a->y1 + 1;
screen_ptr->gfx->draw16bitRGBBitmap(
a->x1, a->y1, (uint16_t*)p, w, h
);
screen_ptr->gfx->flush();
lv_display_flush_ready(d);
}
/* ----------------------------------------------------
* Touch
* ---------------------------------------------------- */
void LVGL_Manager::touchscreen_read(lv_indev_t*, lv_indev_data_t* data)
{
if(!touch_ptr) {
data->state = LV_INDEV_STATE_RELEASED;
return;
}
uint16_t x, y;
if(touch_ptr->getTouchPoint(x, y)) {
data->point.x = x;
data->point.y = y;
data->state = LV_INDEV_STATE_PRESSED;
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
}
/* ----------------------------------------------------
* File Exists (LVGL 9 FS)
* ---------------------------------------------------- */
bool LVGL_Manager::lv_file_exists(const char* path)
{
lv_fs_file_t f;
if(lv_fs_open(&f, path, LV_FS_MODE_RD) == LV_FS_RES_OK) {
lv_fs_close(&f);
return true;
}
return false;
}
/* ----------------------------------------------------
* LVGL Images – LVGL 9 IMAGE API
* ---------------------------------------------------- */
void LVGL_Manager::Set_LVGL_image(lv_obj_t *obj, const char * path)
{
lv_image_set_src(obj, path);
lv_obj_invalidate(lv_screen_active());
lv_refr_now(disp);
}
void LVGL_Manager::Get_Specs_Image(const char* path) {
lv_image_header_t header;
// In LVGL v9 liefert lv_image_decoder_get_info direkt ein Resultat
lv_result_t res = lv_image_decoder_get_info(path, &header);
if(res == LV_RESULT_OK) {
// Hinweis: In v9 heißt das Feld oft 'format' statt 'cf'
LV_LOG_USER(_YELLOW "Image: %d x %d magic=%d stride=%d \n\r\r" RESET, header.w, header.h, header.magic, header.stride) ;
} else
LV_LOG_USER(_RED " Get_Specs_Image : Error ") ;
}
// Bild von SD in PSRAM laden
uint8_t* LVGL_Manager::loadImageToPSRAM(const char* filename, size_t* out_size)
{
File file = SD_MMC.open(filename);
if (!file) return nullptr;
size_t size = file.size();
// In PSRAM allokieren
uint8_t* buffer = (uint8_t*)heap_caps_malloc(size, MALLOC_CAP_SPIRAM);
if (!buffer) {
file.close();
return nullptr;
}
file.read(buffer, size);
file.close();
*out_size = size;
return buffer;
}
lv_image_dsc_t* LVGL_Manager::createImageDescriptor(uint8_t* data, uint16_t w, uint16_t h,
lv_color_format_t cf)
{
lv_image_dsc_t* img_dsc = (lv_image_dsc_t*)heap_caps_malloc(
sizeof(lv_image_dsc_t), MALLOC_CAP_SPIRAM);
if (!img_dsc) return nullptr;
// Header setzen (LVGL 9.4)
img_dsc->header.magic = LV_IMAGE_HEADER_MAGIC;
img_dsc->header.cf = cf; // z.B. LV_COLOR_FORMAT_RGB565
img_dsc->header.flags = 0;
img_dsc->header.w = w;
img_dsc->header.h = h;
img_dsc->header.stride = w * lv_color_format_get_size(cf);
img_dsc->header.reserved_2 = 0;
img_dsc->data_size = w * h * lv_color_format_get_size(cf);
img_dsc->data = data;
return img_dsc;
}
void LVGL_Manager::LoadImageToPSRAM(lv_obj_t *obj, const char* filename,int Width, int Height, uint32_t ColorFormat)
{
size_t img_size;
uint8_t* img_data = loadImageToPSRAM(filename, &img_size);
auto cf = static_cast<lv_color_format_t>(ColorFormat);
if (img_data) {
lv_image_dsc_t* img_dsc = createImageDescriptor(
img_data,
Width, // Breite
Height, // Höhe
cf );
if (img_dsc) {
lv_img_set_src(obj, img_dsc);
}
}
}
bool LVGL_Manager::Read_Lvgl_Header(const char* filename, uint32_t* width, uint32_t* height,
uint32_t* cf, uint32_t* header_size)
{
File file = SD_MMC.open(filename, "r");
if(!file) {
Serial.printf("Failed to open %s\n\r", filename);
return false;
}
// Ersten 12 Bytes lesen (LVGL v8 header)
uint8_t header[12];
if(file.read(header, 12) != 12) {
file.close();
return false;
}
// Magic number prüfen (optional)
uint32_t magic = header[0] | (header[1] << 8) | (header[2] << 16) | (header[3] << 24);
// Breite und Höhe auslesen (little endian)
*width = header[4] | (header[5] << 8);
*height = header[6] | (header[7] << 8);
// Color format (typisch RGB565 = 0x04)
*cf = header[8] | (header[9] << 8);
*header_size = 12;
file.close();
Serial.printf("Header: w=%d, h=%d, cf=0x%04X, magic=0x%08X\n\r",
*width, *height, *cf, magic);
return true;
}
void* LVGL_Manager::load_image_2_psram_with_header(const char* filename,
uint32_t* total_size,
uint32_t* width,
uint32_t* height,
uint32_t* cf)
{
File file = SD_MMC.open(filename, "r");
if(!file) {
Serial.printf("Failed to open %s\n\r", filename);
return NULL;
}
uint32_t file_size = file.size();
// Header lesen (12 Bytes)
uint8_t header[12];
if(file.read(header, 12) != 12) {
file.close();
return NULL;
}
// Breite und Höhe auslesen
*width = header[4] | (header[5] << 8);
*height = header[6] | (header[7] << 8);
*cf = header[8] | (header[9] << 8);
// Nur Pixeldaten laden
uint32_t data_size = file_size - 12;
*total_size = data_size;
// PSRAM allokieren
void* psram_ptr = heap_caps_malloc(data_size, MALLOC_CAP_SPIRAM);
if(psram_ptr == NULL) {
Serial.printf("PSRAM allocation failed for %d bytes\n\r", data_size);
file.close();
return NULL;
}
// Daten lesen - FIX: Type-cast für min()
uint8_t* buffer = (uint8_t*)psram_ptr;
uint32_t bytes_read = 0;
while(bytes_read < data_size) {
// FIX: Expliziter Cast zu uint32_t
size_t to_read = min((uint32_t)4096, data_size - bytes_read);
size_t read = file.read(buffer + bytes_read, to_read);
if(read == 0) break;
bytes_read += read;
}
file.close();
Serial.printf("Loaded %s: %dx%d, %d bytes\n\r", filename, *width, *height, bytes_read);
return psram_ptr;
}
// Vollständige preload Funktion
bool LVGL_Manager::preload_animation_frames()
{
char filename[64];
Serial.println("Loading animation frames to PSRAM...");
for(int i = 0; i < ANIM_FRAME_COUNT; i++) {
sprintf(filename, "/animate/frame-%d.bin", i + 1);
uint32_t width, height, cf;
anim_frames[i].img_data = load_image_2_psram_with_header(
filename,
&anim_frames[i].data_size,
&width,
&height,
&cf
);
if(anim_frames[i].img_data == NULL) {
Serial.printf("Failed to load frame %d\n\r", i + 1);
return false;
}
// LVGL Image Descriptor
anim_frames[i].img_dsc.header.w = width;
anim_frames[i].img_dsc.header.h = height;
// FIX: Korrektes Color Format für LVGL v9
#if LV_VERSION_CHECK(9, 0, 0)
anim_frames[i].img_dsc.header.cf = LV_COLOR_FORMAT_RGB565;
#else
anim_frames[i].img_dsc.header.cf = LV_IMG_CF_TRUE_COLOR;
#endif
anim_frames[i].img_dsc.data_size = anim_frames[i].data_size;
anim_frames[i].img_dsc.data = (uint8_t*)anim_frames[i].img_data;
anim_frame_ptrs[i] = &anim_frames[i].img_dsc;
Serial.printf("Frame %d: %dx%d loaded\n\r", i + 1, width, height);
}
Serial.println("All frames loaded successfully!");
return true;
}
// Alternative: Fixed Size Version
bool LVGL_Manager::preload_animation_frames_fixed_size()
{
const uint32_t WIDTH = 200;
const uint32_t HEIGHT = 200;
for(int i = 0; i < ANIM_FRAME_COUNT; i++) {
char filename[64];
sprintf(filename, "/animate/frame-%d.bin", i + 1);
LV_LOG_USER(_BLUE "Animate : %s " RESET, filename);
psramChecker.printMemoryInfo();
File file = SD_MMC.open(filename, "r");
if(!file)
{
LV_LOG_USER(_RED "File ERROR : %s " RESET );
return false;
}
// Header überspringen (12 Bytes)
file.seek(12);
uint32_t data_size = file.size() - 12;
// PSRAM allokieren
anim_frames[i].img_data = heap_caps_malloc(data_size, MALLOC_CAP_SPIRAM);
if(anim_frames[i].img_data == NULL) {
file.close();
LV_LOG_USER(_RED "PSRam ERROR : %s " RESET );
return false;
}
// Daten lesen
uint8_t* buffer = (uint8_t*)anim_frames[i].img_data;
uint32_t bytes_read = 0;
while(bytes_read < data_size) {
size_t to_read = min((uint32_t)4096, data_size - bytes_read);
size_t read = file.read(buffer + bytes_read, to_read);
if(read == 0) break;
bytes_read += read;
}
file.close();
// Descriptor setzen
anim_frames[i].data_size = data_size;
anim_frames[i].img_dsc.header.w = WIDTH;
anim_frames[i].img_dsc.header.h = HEIGHT;
#if LV_VERSION_CHECK(9, 0, 0)
anim_frames[i].img_dsc.header.cf = LV_COLOR_FORMAT_RGB565;
#else
anim_frames[i].img_dsc.header.cf = LV_IMG_CF_TRUE_COLOR;
#endif
anim_frames[i].img_dsc.data_size = data_size;
anim_frames[i].img_dsc.data = (uint8_t*)anim_frames[i].img_data;
anim_frame_ptrs[i] = &anim_frames[i].img_dsc;
}
return true;
}
void LVGL_Manager::create_animation(lv_obj_t *parent_obj)
{
Serial.println(BOLDGREEN"\n============================================= ANIMATION DEBUG START ==========");
// 1. Parent prüfen
Serial.printf("Parent Object: %p\n\r", parent_obj);
if (!parent_obj) {
Serial.println("❌ ERROR: Parent is NULL!");
return;
}
Serial.printf("Parent visible: %d\n\r", !lv_obj_has_flag(parent_obj, LV_OBJ_FLAG_HIDDEN));
Serial.printf("Parent size: %dx%d\n\r", lv_obj_get_width(parent_obj), lv_obj_get_height(parent_obj));
// 2. Frame Array prüfen
Serial.printf("Frame Count: %d\n\r", ANIM_FRAME_COUNT);
Serial.printf("Frame Array Pointer: %p\n\r", anim_frame_ptrs);
if (!anim_frame_ptrs) {
Serial.println("❌ ERROR: Frame array is NULL!");
return;
}
// Jeden Frame prüfen
for (int i = 0; i < ANIM_FRAME_COUNT; i++) {
Serial.printf("Frame[%d]: %p", i, anim_frame_ptrs[i]);
if (anim_frame_ptrs[i]) {
// ✅ Cast zu const lv_img_dsc_t*
const lv_img_dsc_t *img = (const lv_img_dsc_t*)anim_frame_ptrs[i];
Serial.printf(" | Size: %dx%d | Format: %d | Data: %p\n\r",
img->header.w,
img->header.h,
img->header.cf,
img->data);
// Prüfe ob Bilddaten vorhanden
if (!img->data) {
Serial.printf(" ⚠️ WARNING: Frame %d has no data!\n", i);
}
} else {
Serial.println(" | ❌ NULL POINTER!");
}
}
// 3. Animimg erstellen
Serial.println("\nCreating animimg object...");
lv_obj_t *anim = lv_animimg_create(parent_obj);
if (!anim) {
Serial.println("❌ ERROR: Failed to create animimg!");
return;
}
Serial.printf("Animimg created: %p\n\r", anim);
// 4. Größe und Position
lv_obj_set_size(anim, 320, 240);
lv_obj_center(anim);
Serial.printf("Animimg size: %dx%d\n\r", lv_obj_get_width(anim), lv_obj_get_height(anim));
Serial.printf("Animimg pos: x=%d, y=%d\n\r", lv_obj_get_x(anim), lv_obj_get_y(anim));
// 5. Animation konfigurieren
Serial.println("\nConfiguring animation...");
lv_animimg_set_src(anim, (const void **)anim_frame_ptrs, ANIM_FRAME_COUNT);
lv_animimg_set_duration(anim, 1000);
lv_animimg_set_repeat_count(anim, LV_ANIM_REPEAT_INFINITE);
// 6. Animation starten
Serial.println("Starting animation...");
lv_animimg_start(anim);
// 7. Status prüfen
Serial.printf("Animation visible: %d\n\r", !lv_obj_has_flag(anim, LV_OBJ_FLAG_HIDDEN));
// ✅ Korrekter Parameter: LV_PART_MAIN statt 0
Serial.printf("Animation opacity: %d\n\r", lv_obj_get_style_opa(anim, LV_PART_MAIN));
// 8. Explizit nach vorne bringen
lv_obj_move_foreground(anim);
Serial.println("Moved to foreground");
// 9. Test: Setze temporär eine sichtbare Hintergrundfarbe
lv_obj_set_style_bg_opa(anim, 255, LV_PART_MAIN); // ✅ LV_PART_MAIN hinzugefügt
lv_obj_set_style_bg_color(anim, lv_color_hex(0xFF0000), LV_PART_MAIN); // Rot zum Testen
Serial.println("Set red background for visibility test");
Serial.println("========== ANIMATION DEBUG END ==========\n");
}
// Speicher freigeben
void LVGL_Manager::free_animation_frames(void)
{
for(int i = 0; i < ANIM_FRAME_COUNT; i++) {
if(anim_frames[i].img_data != NULL) {
heap_caps_free(anim_frames[i].img_data);
anim_frames[i].img_data = NULL;
}
}
}
psram_image_t LVGL_Manager::anim_frames[ANIM_FRAME_COUNT];
const void* LVGL_Manager::anim_frame_ptrs[ANIM_FRAME_COUNT];
What do you mean by odd? Does the image load?