Linking two sliders, so both show same information. Using a checkbox to unlink them as needed

Description

I will be the first to admit, I really don’t know what I’m doing. I found a button demo for one of my cheap yellow display boards, and started following the LVGL docs for version 8.3.0. And figured out how to make a slider, get it close to where I wanted on the screen, make a second slider, make a checkbox, and place another button. All this is fine, and works.
However my next step is wanting the two sliders linked together (IE: These will be Left and Right motor controls for a robot) When linked they should have the same values and it should not matter which slider I use to set those values. I want to use the checkbox to unlink/relink them.
In otherwords, when unlinked I will use them to set each motor independent of each other.
Everything I’ve tried has caused the ESP32 to reset/reboot. I am clearly missing something.
I am just not sure what I’m missing, or how to do what I want. Thanks for any help/advice in advance.

What MCU/Processor/Board and compiler are you using?

Cheap Yellow Display ESP32 - Arduino IDE 1.8.19

What LVGL version are you using?

8.3.0

What do you want to achieve?

Linking and unlinking two slider, so they work together or independent from each other.

What have you tried so far?

I have a flag that is set ( bool slideLock = 1 ). Inside the slider_event_cb event I check the flag and like this:

currentLeft = lv_slider_get_value(slider);
    if (slideLock == 1) { //check the checkbox
         currentRight = currentLeft;
       
         //lv_slider_set_value(slider2, currentRight, LV_ANIM_OFF);
         //lv_obj_add_event_cb(slider2, slider2_event_cb, LV_EVENT_VALUE_CHANGED, NULL);
        }

I tried to set both sides, but this causes a reboot. And as you can see is currently commented out.
I have only do this in one of the slider events so far because it keeps resetting. I didn’t want to duplicate the code just yet.

I also tried to disable/enable the RIGHT (slider2) in the event_handler for the checkbox

static void event_handler(lv_event_t * e)
{
    lv_event_code_t code = lv_event_get_code(e);
    lv_obj_t * obj = lv_event_get_target(e);
    if(code == LV_EVENT_VALUE_CHANGED) {
        const char * txt = lv_checkbox_get_text(obj);
        const char * state = lv_obj_get_state(obj) & LV_STATE_CHECKED ? "Checked" : "Unchecked";
        LV_LOG_USER("%s: %s", txt, state);
        Serial.printf("%s: %s\n", txt, state);
        slideLock = !slideLock;
        
       //if (slideLock == 1) {lv_obj_clear_flag(slider2, LV_OBJ_FLAG_CLICKABLE); } else { lv_obj_add_flag(slider2, LV_OBJ_FLAG_CLICKABLE); }
    }
}

attempting to make the slider2 clickable and not clickable - this also caused the ESP32 to reset. And really isn’t what I wanted to do. But I thought I’d try it.

Complete code below. (It’s messy, and I’m sure has a lot of mistakes I made along the way.)

Code to reproduce

Add a code snippet which can run in the simulator. It should contain only the relevant code that compiles without errors when separated from your main code base.

The code block(s) should be formatted like:

#include <lvgl.h>
#include <TFT_eSPI.h>
#include "CST820.h"

static const uint16_t screenWidth = 240;
static const uint16_t screenHeight = 320;

#define I2C_SDA 33
#define I2C_SCL 32
#define TP_RST 25
#define TP_INT 21

static lv_disp_draw_buf_t draw_buf;
static lv_color_t *buf1;
static lv_color_t *buf2;
static lv_obj_t * slider_label;
static lv_obj_t * slider2_label;
static lv_obj_t * slider;
static lv_obj_t * slider2;

TFT_eSPI tft = TFT_eSPI();                      /* TFT实例 */
CST820 touch(I2C_SDA, I2C_SCL, TP_RST, TP_INT); /* 触摸实例 */

//int prevBlueYSlider, prevRedYSlider; //Blue = Right and Red = Left
uint8_t currentLeft, currentRight;
bool slideLock = 1; //sliders locked together

//_______________________
void lv_example_btn(void)
{
  /*要转换的属性*/
    static lv_style_prop_t props[] = {
        LV_STYLE_TRANSFORM_WIDTH, LV_STYLE_TRANSFORM_HEIGHT, LV_STYLE_TEXT_LETTER_SPACE};

    /*Transition descriptor when going back to the default state.
     *Add some delay to be sure the press transition is visible even if the press was very short*/
    static lv_style_transition_dsc_t transition_dsc_def;
    lv_style_transition_dsc_init(&transition_dsc_def, props, lv_anim_path_overshoot, 250, 100, NULL);

    /*Transition descriptor when going to pressed state.
     *No delay, go to presses state immediately*/
    static lv_style_transition_dsc_t transition_dsc_pr;
    lv_style_transition_dsc_init(&transition_dsc_pr, props, lv_anim_path_ease_in_out, 250, 0, NULL);

    /*Add only the new transition to he default state*/
    static lv_style_t style_def;
    lv_style_init(&style_def);
    lv_style_set_transition(&style_def, &transition_dsc_def);

    /*Add the transition and some transformation to the presses state.*/
    static lv_style_t style_pr;
    lv_style_init(&style_pr);
    lv_style_set_transform_width(&style_pr, 10);
    lv_style_set_transform_height(&style_pr, -10);
    lv_style_set_text_letter_space(&style_pr, 10);
    lv_style_set_transition(&style_pr, &transition_dsc_pr);

    lv_obj_t *btn1 = lv_btn_create(lv_scr_act());
    //lv_obj_align(btn1, LV_ALIGN_CENTER, 0, -80);
    lv_obj_align(btn1, LV_ALIGN_OUT_RIGHT_MID, 95, 85);
    lv_obj_add_style(btn1, &style_pr, LV_STATE_PRESSED);
    lv_obj_add_event_cb(btn1, btn1_event_handler, LV_EVENT_ALL, NULL);
    lv_obj_add_style(btn1, &style_def, 0);

    lv_obj_t *label = lv_label_create(btn1);
    lv_label_set_text(label, "GO!");

    /*Init the style for the default state*/
    static lv_style_t style;
    lv_style_init(&style);

    lv_style_set_radius(&style, 3);

    lv_style_set_bg_opa(&style, LV_OPA_100);
    lv_style_set_bg_color(&style, lv_palette_main(LV_PALETTE_BLUE));
    lv_style_set_bg_grad_color(&style, lv_palette_darken(LV_PALETTE_BLUE, 2));
    lv_style_set_bg_grad_dir(&style, LV_GRAD_DIR_VER);

    lv_style_set_border_opa(&style, LV_OPA_40);
    lv_style_set_border_width(&style, 2);
    lv_style_set_border_color(&style, lv_palette_main(LV_PALETTE_GREY));

    lv_style_set_shadow_width(&style, 8);
    lv_style_set_shadow_color(&style, lv_palette_main(LV_PALETTE_GREY));
    lv_style_set_shadow_ofs_y(&style, 8);

    lv_style_set_outline_opa(&style, LV_OPA_COVER);
    lv_style_set_outline_color(&style, lv_palette_main(LV_PALETTE_BLUE));

    lv_style_set_text_color(&style, lv_color_white());
    lv_style_set_pad_all(&style, 10);

    /*Init the pressed style*/
    static lv_style_t style_pr_2;
    lv_style_init(&style_pr_2);

    /*Ad a large outline when pressed*/
    lv_style_set_outline_width(&style_pr_2, 30);
    lv_style_set_outline_opa(&style_pr_2, LV_OPA_TRANSP);

    lv_style_set_translate_y(&style_pr_2, 5);
    lv_style_set_shadow_ofs_y(&style_pr_2, 3);
    lv_style_set_bg_color(&style_pr_2, lv_palette_darken(LV_PALETTE_BLUE, 2));
    lv_style_set_bg_grad_color(&style_pr_2, lv_palette_darken(LV_PALETTE_BLUE, 4));

    /*Add a transition to the the outline*/
    static lv_style_transition_dsc_t trans;
    static lv_style_prop_t props2[] = {LV_STYLE_OUTLINE_WIDTH, LV_STYLE_OUTLINE_OPA};
    lv_style_transition_dsc_init(&trans, props2, lv_anim_path_linear, 300, 0, NULL);

    lv_style_set_transition(&style_pr_2, &trans);

    lv_obj_t *btn2 = lv_btn_create(lv_scr_act());
    lv_obj_remove_style_all(btn2); /*Remove the style coming from the theme*/
    lv_obj_add_style(btn2, &style, 0);
    lv_obj_add_event_cb(btn2, btn2_event_handler, LV_EVENT_ALL, NULL);
    lv_obj_add_style(btn2, &style_pr_2, LV_STATE_PRESSED);
    lv_obj_set_size(btn2, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
    lv_obj_center(btn2);

    lv_obj_t *label2 = lv_label_create(btn2);
    lv_label_set_text(label2, "Reverse");
    lv_obj_center(label2);

/*Create a slider in the center of the display*/
    lv_obj_t * slider = lv_slider_create(lv_scr_act());
    //lv_obj_center(slider);
    lv_obj_add_event_cb(slider, slider_event_cb, LV_EVENT_VALUE_CHANGED, NULL);
    lv_obj_set_size(slider, 20, 155);
    lv_obj_align(slider, LV_ALIGN_OUT_LEFT_BOTTOM, 10, 100);
    /*Create a label below the slider*/
    slider_label = lv_label_create(lv_scr_act());
    lv_label_set_text(slider_label, "50");
    lv_obj_align_to(slider_label, slider, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
    lv_slider_set_range(slider, 50, 128);    

/*Create a slider in the center of the display*/
    lv_obj_t * slider2 = lv_slider_create(lv_scr_act());
   // lv_obj_center(slider2);
    lv_obj_add_event_cb(slider2, slider2_event_cb, LV_EVENT_VALUE_CHANGED, NULL);
    lv_obj_set_size(slider2, 20, 155);
    lv_obj_align(slider2, LV_ALIGN_OUT_LEFT_BOTTOM, 45, 100);
    /*Create a label below the slider*/
    slider2_label = lv_label_create(lv_scr_act());
    lv_label_set_text(slider2_label, "50");
    lv_obj_align_to(slider2_label, slider2, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
    lv_slider_set_range(slider2, 50, 128);     

    //    lv_slider_set_value(slider2, 128, LV_ANIM_OFF);
    //  lv_event_send(slider2, LV_EVENT_VALUE_CHANGED, NULL);
    //lv_obj_clear_flag(slider2, LV_OBJ_FLAG_CLICKABLE); //, true); //false);
    //lv_obj_set_hidden(slider2, true);
   
//Checkbox

    lv_obj_t * cb;
    cb = lv_checkbox_create(lv_scr_act());
    lv_checkbox_set_text(cb, "Lock");
    lv_obj_add_state(cb, LV_STATE_CHECKED);
    lv_obj_add_event_cb(cb, event_handler, LV_EVENT_ALL, NULL);
    lv_obj_align(cb, LV_ALIGN_OUT_LEFT_TOP, 30, 60);

    lv_obj_update_layout(cb);
}

static void event_handler(lv_event_t * e)
{
    lv_event_code_t code = lv_event_get_code(e);
    lv_obj_t * obj = lv_event_get_target(e);
    if(code == LV_EVENT_VALUE_CHANGED) {
        const char * txt = lv_checkbox_get_text(obj);
        const char * state = lv_obj_get_state(obj) & LV_STATE_CHECKED ? "Checked" : "Unchecked";
        LV_LOG_USER("%s: %s", txt, state);
        Serial.printf("%s: %s\n", txt, state);
        slideLock = !slideLock;
        
       //if (slideLock == 1) {lv_obj_clear_flag(slider2, LV_OBJ_FLAG_CLICKABLE); } else { lv_obj_add_flag(slider2, LV_OBJ_FLAG_CLICKABLE); }
    }
}

static void slider_event_cb(lv_event_t * e)
{
    // This is the Left Slider (RED)
    lv_obj_t * slider = lv_event_get_target(e);
    char buf[8];
    lv_snprintf(buf, sizeof(buf), "%d%", lv_slider_get_value(slider));
    lv_label_set_text(slider_label, buf);
    lv_obj_align_to(slider_label, slider, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
    
    currentLeft = lv_slider_get_value(slider);
    if (slideLock == 1) { //check the checkbox
         currentRight = currentLeft;
       
         //lv_slider_set_value(slider2, currentRight, LV_ANIM_OFF);
         //lv_obj_add_event_cb(slider2, slider2_event_cb, LV_EVENT_VALUE_CHANGED, NULL);
        }
    Serial.print("Slider Left : "); Serial.println(currentLeft);
    Serial.print("Slider Right: "); Serial.println(currentRight);
}

static void slider2_event_cb(lv_event_t * e)
{
    lv_obj_t * slider2 = lv_event_get_target(e);
    char buf[8];
    lv_snprintf(buf, sizeof(buf), "%d%", lv_slider_get_value(slider2));
    lv_label_set_text(slider2_label, buf);
    lv_obj_align_to(slider2_label, slider2, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
    Serial.print("Slider 2: ");Serial.println(buf);
    Serial.print("Slider Left: "); Serial.println(currentLeft);
}

static void btn1_event_handler(lv_event_t * e)
{
    lv_event_code_t code = lv_event_get_code(e);

    if (code == LV_EVENT_PRESSING) {
        Serial.println("Green On!");
        digitalWrite(16, LOW); //!digitalRead(4));
    }
    else if (code == LV_EVENT_RELEASED) {
        Serial.println("Green Off!");
        digitalWrite(16, HIGH);
    }
}

static void btn2_event_handler(lv_event_t * e)
{
    lv_event_code_t code = lv_event_get_code(e);

    if(code == LV_EVENT_CLICKED) {
        LV_LOG_USER("Clicked");
        digitalWrite(4, !digitalRead(4));
    }
    else if(code == LV_EVENT_VALUE_CHANGED) {
        LV_LOG_USER("Toggled");
    }
}


void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p)
{
    uint32_t w = (area->x2 - area->x1 + 1);
    uint32_t h = (area->y2 - area->y1 + 1);

    //tft.startWrite();
    tft.pushImage(area->x1, area->y1, w, h, (uint16_t *)color_p);
    //tft.endWrite();

    lv_disp_flush_ready(disp);
}


void my_touchpad_read(lv_indev_drv_t *indev_driver, lv_indev_data_t *data)
{

    bool touched;
    uint8_t gesture;
    uint16_t touchX, touchY;

    touched = touch.getTouch(&touchX, &touchY, &gesture);

    if (!touched)
    {
        data->state = LV_INDEV_STATE_REL;
        
    }
    else
    {
        data->state = LV_INDEV_STATE_PR;

        //Set the coordinates
        data->point.x = touchX;
        data->point.y = touchY;
    }
}


void setup()
{
  pinMode(4, OUTPUT);
  pinMode(16, OUTPUT);
  pinMode(17, OUTPUT);
  digitalWrite(4, HIGH);
  digitalWrite(16, HIGH);
  digitalWrite(17, HIGH);
    Serial.begin(115200); /*初始化串口*/

    String LVGL_Arduino = "Hello Arduino! ";
    LVGL_Arduino += String('V') + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch();

    Serial.println(LVGL_Arduino);
    Serial.println("I am LVGL_Arduino");

    lv_init();

    pinMode(27, OUTPUT);
    digitalWrite(27, LOW);
    tft.begin();        /*初始化*/
    tft.setRotation(0); /* 旋转 */
    tft.init();      /* 初始化DMA */

    touch.begin(); /*初始化触摸板*/
    digitalWrite(27, HIGH);

    buf1 = (lv_color_t *)heap_caps_malloc(sizeof(lv_color_t) * screenWidth * 200 , MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);//screenWidth * screenHeight/2
    buf2 = (lv_color_t *)heap_caps_malloc(sizeof(lv_color_t) * screenWidth * 200 , MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);

    lv_disp_draw_buf_init(&draw_buf, buf1, buf2, screenWidth * 200);
    
    static lv_disp_drv_t disp_drv;
    lv_disp_drv_init(&disp_drv);
   
    disp_drv.hor_res = screenWidth;
    disp_drv.ver_res = screenHeight;
    disp_drv.flush_cb = my_disp_flush;
    disp_drv.draw_buf = &draw_buf;
    lv_disp_drv_register(&disp_drv);


    static lv_indev_drv_t indev_drv;
    lv_indev_drv_init(&indev_drv);
    indev_drv.type = LV_INDEV_TYPE_POINTER;
    indev_drv.read_cb = my_touchpad_read;
    lv_indev_drv_register(&indev_drv);


     lv_example_btn();

    Serial.println("Setup done");
    tft.startWrite();

   currentRight = 50; //copied from previous Joystick 03 example
   currentLeft  = 50; 
    
}

void loop()
{
    lv_timer_handler(); /* 让GUI完成它的工作 */
    delay(5);
     
}

Screenshot and/or video

If possible, add screenshots and/or videos about the current state.

The picture is pretty bad, but here it is.

I think you don’t need to add lv_obj_add_event_cb() every time the LV_EVENT_VALUE_CHANGED for the first slider occurs. It’s enough to do it once. And in the event handler for each slider check if the flag slideLock is set, and if it is set, change the value of the second slider.
Also you can use common slider_event_cb() for both sliders. To do this you can pass slider1/2 ID as user data when adding event

lv_obj_add_event_cb(slider2, slider_event_cb, LV_EVENT_VALUE_CHANGED, slider1);
lv_obj_add_event_cb(slider1, slider_event_cb, LV_EVENT_VALUE_CHANGED, slider2);

and get it with lv_event_get_user_data()

static void slider_event_cb(lv_event_t * e)
{
    // This is the Left Slider (RED)
    lv_obj_t * slider = lv_event_get_target(e);
    lv_obj_t * slider_opp = lv_event_get_user_data(e);

    current_val = lv_slider_get_value(slider);
       
    lv_slider_set_value(slider_opp, current_val, LV_ANIM_OFF);
}

don’t forget to check slider & slider_opp for NULL.

Thank you for the reply.

I think this is almost what I want, the 2nd slider doesn’t move with the first, but I’m not sure that I am using your example correctly.

What is interesting, checkbox selected or not, it does seem like the sliders are linked.

I’m going to spend some time cleaning up my code - getting rid of the stuff commented out and
I’ll post an update sometime tonight or tomorrow morning.

At very least I am not getting reboots of the device any more - so that is progress.

And your example shows what I did wrong (I think). I think I was trying to created another slider when I had a slider already named “slider2”, and that caused a big issue.

Is slider_opp part of the the library and I missed how to use it?

Thanks again. I will keep working on this now that I (think) I have a bit better understanding what I did wrong.

slider & slider_opp are just variables used in the function that identify objects that will be referenced later.
if you will use such code

static void slider_event_cb(lv_event_t * e)
{
    lv_obj_t * slider = lv_event_get_target(e);
    lv_obj_t * slider_opp = lv_event_get_user_data(e);

    current_val = lv_slider_get_value(slider);
       
    lv_slider_set_value(slider_opp, current_val, LV_ANIM_OFF);
}
...
lv_obj_t * slider1 = lv_slider_create(lv_scr_act());
...
lv_obj_t * slider2 = lv_slider_create(lv_scr_act());
...
lv_obj_add_event_cb(slider2, slider_event_cb, LV_EVENT_VALUE_CHANGED, slider1);
lv_obj_add_event_cb(slider1, slider_event_cb, LV_EVENT_VALUE_CHANGED, slider2);
...

as you can see lv_obj_add_event_cb() 4th arg is user_data.
first lv_obj_add_event_cb(slider2) call add new event handler for slider2 with user_data = slider1
second call add new event handler for slider1 with user_data = slider2

and when LV_EVENT_VALUE_CHANGED will occur for slider1

lv_obj_t * slider = lv_event_get_target(e);

will return slider1, while

lv_obj_t * slider_opp = lv_event_get_user_data(e);

will return slider2 as it was defined as user_data when lv_obj_add_event_cb() called for slider1.
and current_val = lv_slider_get_value(slider); should get value of changed slider, and lv_slider_set_value(slider_opp, current_val, LV_ANIM_OFF); should change position of another slider.

Thank you again,
So I re-read your original post, and your updated post.
I copied the code wrong. And fixed it, and now the sliders do move together. And when the checkbox is unchecked, both move independent.

Thank you so much for the help, thank you for pushing me in the right direction.

-LeRoy