Update objects dynamically

What do you want to achieve?

I need to dynamically update some states and values ​​of objects already rendered on the screen as I receive new data from the serial port, such as text values ​​or styles like background color.

What have you tried so far?

I’m using PlatformIO with Arduino and creating my LVGL components within setup(), using the function:

component_cicles_sensors(lv_scr_act()),

This function creates 6 sets of 2 circles (one for “going up” and one for “going down”).

After creation, I tested it by calling the function:

update_circles_from_serial() 

within the loop()

However, nothing happens on the screen — the circles don’t change color or update as expected.

What am I doing wrong?
How can I dynamically update the components (e.g., change the colors of the circles) based on new data?

Code to reproduce

const char *raise_state = "001101";
const char *lower_state = "110010";
void loop()
{

  lv_tick_inc(5);
  delay(5);

  update_circles_from_serial(raise_state, lower_state);
  delay(1000);
}
#define NUM_GROUPS 6

static lv_obj_t *circle_up[NUM_GROUPS];
static lv_obj_t *circle_down[NUM_GROUPS];

void update_circles_from_serial(const char *sensors_raise, const char *sensors_lower)
{

    for (int i = 0; i < NUM_GROUPS; i++)
    {
        if (sensors_raise[i] == '1')
        {
            lv_obj_set_style_bg_color(circle_up[i], COLOR_PRIMARY, 0);
            lv_obj_set_style_border_color(circle_up[i], COLOR_TERTIARY, 0);
        }
        else
        {
            lv_obj_set_style_bg_color(circle_up[i], COLOR_BACKGROUND_DISABLE, 0);
            lv_obj_set_style_border_color(circle_up[i], COLOR_GREY_DARK, 0);
        }

        if (sensors_lower[i] == '1')
        {
            lv_obj_set_style_bg_color(circle_down[i], COLOR_PRIMARY, 0);
            lv_obj_set_style_border_color(circle_down[i], COLOR_TERTIARY, 0);
        }
        else
        {
            lv_obj_set_style_bg_color(circle_down[i], COLOR_BACKGROUND_DISABLE, 0);
            lv_obj_set_style_border_color(circle_down[i], COLOR_GREY_DARK, 0);
        }
    }
}

static void component_cicles_sensors(lv_obj_t *parent)
{
    const int circle_size = 25;

    const int positions[NUM_GROUPS][3] = {
        {13, -34, -65},  // 1º grup
        {214, -34, -65}, // 2º grup
        {325, -26, -57}, // 3º grup
        {364, -26, -57}, // 4º grup
        {498, -34, -65}, // 5º grup
        {743, -34, -65}  // 6º grup
    };

    for (int i = 0; i < NUM_GROUPS; i++)
    {
        circle_down[i] = lv_obj_create(parent);
        lv_obj_set_size(circle_down[i], circle_size, circle_size);
        lv_obj_set_style_radius(circle_down[i], LV_RADIUS_CIRCLE, 0);
        lv_obj_set_style_border_width(circle_down[i], 2, 0);
        lv_obj_set_style_border_color(circle_down[i], COLOR_GREY_DARK, 0);
        lv_obj_set_style_bg_color(circle_down[i], COLOR_BACKGROUND_DISABLE, 0);
        lv_obj_set_style_pad_all(circle_down[i], 0, 0);
        lv_obj_align(circle_down[i], LV_ALIGN_BOTTOM_LEFT, positions[i][0], positions[i][1]);

        circle_up[i] = lv_obj_create(parent);
        lv_obj_set_size(circle_up[i], circle_size, circle_size);
        lv_obj_set_style_radius(circle_up[i], LV_RADIUS_CIRCLE, 0);
        lv_obj_set_style_border_width(circle_up[i], 2, 0);
        lv_obj_set_style_border_color(circle_up[i], COLOR_GREY_DARK, 0);
        lv_obj_set_style_bg_color(circle_up[i], COLOR_BACKGROUND_DISABLE, 0);
        lv_obj_set_style_pad_all(circle_up[i], 0, 0);
        lv_obj_align(circle_up[i], LV_ALIGN_BOTTOM_LEFT, positions[i][0], positions[i][2]);
    }
}

I didn’t include the void setup() code because it’s just the call to the component_cicles_sensors function.

Screenshot and/or video

Environment

  • MCU/MPU/Board:
  • LVGL version: See v9.2.2

Hi

Where do you have the call to lv_timer_handler() in order for LVGL to process the screen updates?

Assuming this is Single Thread (no OS), your loop has a delay(1000); this would force LVGL to process stuff very slowly since you are also calling lv_tick_inc(5) in the same loop that would only be called once every second telling LVGL that only 5ms have passed, since the default display refresh rate is 30ms it would take at least 6 seconds to see something different on the display.

Try something like this in your loop:

    static uint32_t _startTime = 0;
    lv_tick_inc(5);
    delay(5);
    lv_timer_handler();

    if ((lv_tick_get() - _startTime) > 250)
    {
      _startTime = lv_tick_get();	
      static uint32_t _cycle = 0;

      if (_cycle == 0)
      {
        _cycle = 1;
        const char *raise_state = "001101";
        const char *lower_state = "110010";
        update_circles_from_serial(raise_state, lower_state);
      }
      else
      {
        _cycle = 0;
        const char *raise_state = "110010";
        const char *lower_state = "001101";
        update_circles_from_serial(raise_state, lower_state);
      }

Should see the “circles” changing state every 250ms…

As I’m new to lvgl, I got a demo code for the version on my screen, so I don’t understand much, but I’ll send you where I found it:

no main.cpp:

void setup()
{
  Serial.begin(115200);
  Serial.println("ESP32P4 MIPI DSI LVGL");

  pinMode(PA_CTRL_PIN, OUTPUT);
  digitalWrite(PA_CTRL_PIN, HIGH);

  // Monta SPIFFS
  if (!SPIFFS.begin(true))
  {
    Serial.println("Erro ao montar SPIFFS!");
    while (1)
      delay(1000);
  }

  espSerial.begin(GPS_BAUD, SERIAL_8N1, RXD1, TXD2);
  espSerial.println("PORTA TELA");

  i2c_init();

  rtc.begin(bus_handle);

  lvgl_configuration_screen(bus_handle);
  Serial.println("Setup down");

  theme_init();

  if (lvgl_port_lock(-1))
  {
    lv_obj_t *scr = lv_scr_act();
    lv_obj_set_style_pad_all(scr, 0, 0);

    create_top_bar(scr);

    content_area = lv_obj_create(scr);
    lv_obj_set_size(content_area, LV_PCT(100), LV_VER_RES - 75);
    lv_obj_set_style_radius(content_area, 0, 0);
    lv_obj_set_style_pad_all(content_area, 0, 0);
    lv_obj_set_style_border_width(content_area, 0, 0);
    lv_obj_align(content_area, LV_ALIGN_BOTTOM_MID, 0, 0);

    create_home_page(content_area);

    lvgl_port_unlock();
  }
}
void lvgl_configuration_screen(i2c_master_bus_handle_t i2c_handle_param)
{
    bsp_display_brightness_init();

    static esp_ldo_channel_handle_t phy_pwr_chan = NULL;
    esp_ldo_channel_config_t ldo_cfg = {
        .chan_id = BSP_MIPI_DSI_PHY_PWR_LDO_CHAN,
        .voltage_mv = BSP_MIPI_DSI_PHY_PWR_LDO_VOLTAGE_MV,
    };
    esp_ldo_acquire_channel(&ldo_cfg, &phy_pwr_chan);
    ESP_LOGI(TAG, "MIPI DSI PHY Powered on");

    esp_lcd_dsi_bus_handle_t mipi_dsi_bus;
    esp_lcd_dsi_bus_config_t bus_config = JD9365_PANEL_BUS_DSI_2CH_CONFIG();

    esp_lcd_new_dsi_bus(&bus_config, &mipi_dsi_bus);

    ESP_LOGI(TAG, "Install MIPI DSI LCD control panel");
    // we use DBI interface to send LCD commands and parameters
    esp_lcd_panel_io_handle_t io = NULL;
    esp_lcd_dbi_io_config_t dbi_config = JD9365_PANEL_IO_DBI_CONFIG();

    esp_lcd_new_panel_io_dbi(mipi_dsi_bus, &dbi_config, &io);

    esp_lcd_panel_handle_t disp_panel = NULL;
    ESP_LOGI(TAG, "Install LCD driver of st7701");
    esp_lcd_dpi_panel_config_t dpi_config = {
        .dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT,
        .dpi_clock_freq_mhz = 63,
        .virtual_channel = 0,
        .pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565,
        .num_fbs = LVGL_PORT_LCD_BUFFER_NUMS,
        .video_timing = {
            .h_size = 800,
            .v_size = 1280,
            .hsync_back_porch = 20,
            .hsync_pulse_width = 20,
            .hsync_front_porch = 40,
            .vsync_back_porch = 8,
            .vsync_pulse_width = 4,
            .vsync_front_porch = 20,
        },
        .flags.use_dma2d = true,
    };

    jd9365_vendor_config_t vendor_config = {
        .init_cmds = lcd_cmd,
        .init_cmds_size = sizeof(lcd_cmd) / sizeof(jd9365_lcd_init_cmd_t),
        .mipi_config = {
            .dsi_bus = mipi_dsi_bus,
            .dpi_config = &dpi_config,
        },
    };

    const esp_lcd_panel_dev_config_t panel_config = {
        .reset_gpio_num = GPIO_NUM_27,
        .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
        .bits_per_pixel = 16,
        .vendor_config = &vendor_config,
    };
    ESP_ERROR_CHECK(esp_lcd_new_panel_jd9365(io, &panel_config, &disp_panel));
    esp_lcd_panel_reset(disp_panel);
    esp_lcd_panel_init(disp_panel);

    esp_lcd_dpi_panel_event_callbacks_t cbs = {
#if LVGL_PORT_AVOID_TEAR_MODE
        .on_refresh_done = mipi_dsi_lcd_on_vsync_event,
#else
        .on_color_trans_done = mipi_dsi_lcd_on_vsync_event,
#endif
    };
    esp_lcd_dpi_panel_register_event_callbacks(disp_panel, &cbs, NULL);
    esp_lcd_panel_io_handle_t tp_io_handle = NULL;
    esp_lcd_touch_handle_t tp_handle;
    esp_lcd_panel_io_i2c_config_t tp_io_config = ESP_LCD_TOUCH_IO_I2C_GSL3680_CONFIG();
    tp_io_config.scl_speed_hz = 100000;
    esp_lcd_new_panel_io_i2c(i2c_handle_param, &tp_io_config, &tp_io_handle);
    const esp_lcd_touch_config_t tp_cfg = {
        .x_max = BSP_LCD_H_RES,
        .y_max = BSP_LCD_V_RES,
        .rst_gpio_num = BSP_LCD_TOUCH_RST, // Shared with LCD reset
        .int_gpio_num = BSP_LCD_TOUCH_INT,
        .levels = {
            .reset = 0,
            .interrupt = 0,
        },
        .flags = {
            .swap_xy = 0,
            .mirror_x = 0,
            .mirror_y = 1,
        },
    };

    esp_lcd_touch_new_i2c_gsl3680(tp_io_handle, &tp_cfg, &tp_handle);

    lvgl_port_interface_t interface = (dpi_config.flags.use_dma2d) ? LVGL_PORT_INTERFACE_MIPI_DSI_DMA : LVGL_PORT_INTERFACE_MIPI_DSI_NO_DMA;
    ESP_LOGI(TAG, "interface is %d", interface);
// here
    ESP_ERROR_CHECK(lvgl_port_init(disp_panel, tp_handle, interface));

    bsp_display_brightness_set(100);
}
esp_err_t lvgl_port_init(esp_lcd_panel_handle_t lcd_handle, esp_lcd_touch_handle_t tp_handle, lvgl_port_interface_t interface)
{
    lvgl_port_task_param_t lvgl_task_param = {
        .lcd_handle = lcd_handle,
        .tp_handle = tp_handle,
        .is_init = false};

    lvgl_port_interface = interface;
#if LVGL_PORT_AVOID_TEAR_ENABLE
    switch (interface)
    {
#if SOC_LCDCAM_RGB_LCD_SUPPORTED
    case LVGL_PORT_INTERFACE_RGB:
        lvgl_get_lcd_frame_buffer = esp_lcd_rgb_panel_get_frame_buffer;
        break;
#endif

#if SOC_MIPI_DSI_SUPPORTED
    case LVGL_PORT_INTERFACE_MIPI_DSI_DMA:
    case LVGL_PORT_INTERFACE_MIPI_DSI_NO_DMA:
        lvgl_get_lcd_frame_buffer = esp_lcd_dpi_panel_get_frame_buffer;
        break;
#endif

    default:
        ESP_LOGE(TAG, "Invalid interface type");
        return ESP_ERR_INVALID_ARG;
    }
#endif

    lvgl_mux = xSemaphoreCreateRecursiveMutex();
    assert(lvgl_mux);

    ESP_LOGI(TAG, "Create LVGL task");
    BaseType_t core_id = (LVGL_PORT_TASK_CORE < 0) ? tskNO_AFFINITY : LVGL_PORT_TASK_CORE;
    BaseType_t ret = xTaskCreatePinnedToCore(lvgl_port_task, "lvgl", LVGL_PORT_TASK_STACK_SIZE, &lvgl_task_param,
                                             LVGL_PORT_TASK_PRIORITY, &lvgl_task_handle, core_id);
    if (ret != pdPASS)
    {
        ESP_LOGE(TAG, "Failed to create LVGL task");
        return ESP_FAIL;
    }

    while (!lvgl_task_param.is_init)
    {
        vTaskDelay(pdMS_TO_TICKS(10));
    }

    return ESP_OK;
}
static void lvgl_port_task(void *arg)
{
    ESP_LOGD(TAG, "Starting LVGL task");

    lvgl_port_task_param_t *param = (lvgl_port_task_param_t *)arg;

    lv_init();
    ESP_ERROR_CHECK(tick_init());

    lv_display_t *disp = display_init(param->lcd_handle);
    assert(disp);

    if (param->tp_handle)
    {
        lv_indev_t *indev = indev_init(param->tp_handle);
        assert(indev);

#if EXAMPLE_LVGL_PORT_ROTATION_90
        esp_lcd_touch_set_swap_xy(param->tp_handle, true);
        esp_lcd_touch_set_mirror_x(param->tp_handle, false);
        esp_lcd_touch_set_mirror_y(param->tp_handle, false);
#elif EXAMPLE_LVGL_PORT_ROTATION_180
        esp_lcd_touch_set_mirror_x(param->tp_handle, false);
        esp_lcd_touch_set_mirror_y(param->tp_handle, true);
#elif EXAMPLE_LVGL_PORT_ROTATION_270
        esp_lcd_touch_set_swap_xy(param->tp_handle, true);
        esp_lcd_touch_set_mirror_y(param->tp_handle, false);
#endif
    }

    param->is_init = true;

    uint32_t task_delay_ms = LVGL_PORT_TASK_MAX_DELAY_MS;
    while (1)
    {
        if (lvgl_port_lock(-1))
        {
            task_delay_ms = lv_timer_handler();
            lvgl_port_unlock();
        }
        if (task_delay_ms > LVGL_PORT_TASK_MAX_DELAY_MS)
        {
            task_delay_ms = LVGL_PORT_TASK_MAX_DELAY_MS;
        }
        else if (task_delay_ms < LVGL_PORT_TASK_MIN_DELAY_MS)
        {
            task_delay_ms = LVGL_PORT_TASK_MIN_DELAY_MS;
        }
        vTaskDelay(pdMS_TO_TICKS(task_delay_ms));
    }
}

I don’t know if it’s confusing but I tried to send it separately, this is the way the demo has, but I don’t know if it helps to know this, but I have other components like buttons that I made events like when pressing it changes the background color and it worked, that is, the screen updates normally.

I did the following test:

In the void setup() code, I have this line:

create_top_bar(scr);

Inside this function, there is this button:

lv_obj_t *btn_left = lv_obj_create(buttons_row);
lv_obj_set_size(btn_left, 65, 55);
lv_obj_set_style_radius(btn_left, 2, 0); // rounded corners
lv_obj_set_style_pad_all(btn_left, 0, 0);
lv_obj_set_style_border_width(btn_left, 0, 0);
lv_obj_set_style_bg_color(btn_left, COLOR_PRIMARY, 0);
lv_obj_add_event_cb(btn_left, btn_left_event_cb, LV_EVENT_CLICKED, NULL);

I added an event to test, and it worked!

static void btn_left_event_cb(lv_event_t *e)
{
    const char *raise_state = "001101";
    const char *lower_state = "110010";
    update_circles_from_serial(raise_state, lower_state);
}

The colors changed correctly, but what I don’t understand is the following: why doesn’t it work in the loop()?

I used your example, but it didn’t work. However, when I called the function update_circles_from_serial inside the callback btn_left_event_cb, it worked.

What do you think about this? I will use the void loop() to call the function and update the screen data from the serial.

don’t use Arduinno… but from what i can see/undestand, try to set your loop to this:

static uint32_t _cycle = 0;

delay(250);

if (_cycle == 0)
{
	_cycle = 1;
        Serial.println("Set 1"); //just to make sure that this is "running"
	const char *raise_state = "001101";
	const char *lower_state = "110010";
	if (lvgl_port_lock(-1))
	{
		update_circles_from_serial(raise_state, lower_state);
		lvgl_port_unlock();
	}
}
else
{
	_cycle = 0; 
        Serial.println("Set 0"); //just to make sure that this is "running"
	const char *raise_state = "110010";
	const char *lower_state = "001101";
	if (lvgl_port_lock(-1))
	{
		update_circles_from_serial(raise_state, lower_state);
		lvgl_port_unlock();
	}
}

It should “blink” the circles on the screen.

You don’t need to call lv_tick_inc(5); in tour loop, I am assuming that the function ESP_ERROR_CHECK(tick_init()); will setup that automatically (if is anything similar to when setting up LVGL using ESP-IDF.
The call to lv_timer_handler() is also not needed since it’s already handled lvgl_port_task.

In order to access any LVGL function outside it’s task you need to call lvgl_port_lock(-1) and when done “touching” in LVGL stuff call lvgl_port_unlock();, it works when using the button event callback because the callback is called from “inside” LVGL…

Thank you very much, it worked.
Actually, I saw several examples that most developers use ESP_IDF, I intend to learn how to use it instead of Arduino, thank you very much for your help.

Marked solution is only workaround. Start with learn how LVGL is and how work. Primary is event based system and never you can delay 1000 !
Too v9 uses tick callback and not call tick_inc in loop, that is next disaster.

void loop()
{
static unsigned long looper5=0;
if(looper5==0) lv_tick_set_cb(millis);
  lv_timer_handler();
  delay(5);
looper5++;
if((looper5%200)==0)
  update_circles_from_serial(raise_state, lower_state);
}

Why is it a workaround?

Didn’t you see the rest of the code he posted?

The lv_timer_handler() is already handled in a separated task, the same thing regarding the lv_tick callback.

The code posted was an example to get started, especially regarding the need to call lv_port_lock/lv_port_unlock when accessing LVGL functions.

because this your “…”