LVGL button add event says my callback is not declared

I’m just beginning to wrap my head around LVGL. I have a EleCrow 7” with the ESP32.

I’ve successfully added a button, compiles fine, uploads, and works on the display. My issue is now adding an event callback to it always compiles with my callback not being declared in this scope. I’m at a loss. My callback is there. I’m running release 8 of the LGVL library. I know there is newer but it wreaks havic with other code that will need updating and I’m beating my head against the wall already without going down that path too at the moment.

/***************************/
/* Build the GIU unterface */
/***************************/
lv_obj_t *ui_GUI;
lv_obj_t *ui_Label2;

lv_obj_t *ui_btn_control;
lv_obj_t *ui_control_label;

lv_obj_t *ui_btn_cab_select;
lv_obj_t *ui_btn_full_stop;
lv_obj_t *ui_btn_headlight_toggle;
lv_obj_t *ui_btn_play_sound;
lv_obj_t *ui_slider_throttle;

/***********************************/
/* Respond to Control button event */
/***********************************/
static void control_cb(lv_event_t * e){
  Serial.println("A1");
}

/************************************/
/* Build the GIU interface controls */
/************************************/
void ui_init() {
  lv_disp_t *dispp = lv_disp_get_default();
  lv_theme_t *theme = lv_theme_default_init(dispp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED), true, LV_FONT_DEFAULT);
  lv_disp_set_theme(dispp, theme);

  ui_GUI = lv_obj_create(NULL);                           // Create a screen object
  lv_obj_clear_flag(ui_GUI, LV_OBJ_FLAG_SCROLLABLE);

  //ui_Label2 = lv_label_create(ui_Inicio);               // Create a label
  //lv_obj_set_align(ui_Label2, LV_ALIGN_CENTER);         // Align the label at the center
  //lv_label_set_text(ui_Label2, "Hello :D");             // Set label text
  //lv_obj_set_style_text_font(ui_Label2, &lv_font_montserrat_36, LV_PART_MAIN | LV_STATE_DEFAULT);
  
  ui_btn_control = lv_btn_create(ui_GUI);            // Button to toggle control on/off
  lv_obj_set_pos(ui_btn_control,20,20);
  lv_obj_set_size(ui_btn_control, 100, 50);             // Width: 100px, Height: 50px
  lv_obj_add_event_cb(ui_btn_control, control_cb, LV_EVENT_ALL, NULL);

  ui_control_label = lv_label_create(ui_btn_control);
  lv_label_set_text(ui_control_label, "Control");

  lv_disp_load_scr(ui_GUI);                        // Load the screen into the display
}

Try defining the objects inside ui_init function

 lv_obj_t *ui_btn_control = lv_btn_create(ui_GUI);

 lv_obj_t *ui_control_label = lv_label_create(ui_btn_control);

and delete the prior definitions

Tried that too. No change. I have this code in a separate .h with an #include in the main .ino program. I can call the ui_init() function at the end of my setup() function and it builds the buttons on the screen. I moved all of the code to the .ino ahead of the setup() and it works. So…why when it’s in the .h file and included in the .ino can’t it find the callback function? It finds the ui_init() function no problem. I’d rather not have all of the GUI code in the .ino file. Just gets messy to look at and work on.

I am relatively new using lvgl so although I can't pinpoint your exact problem all I can do is pass on what I have tried and works, this is my .h file and ui_int is called in setup of the main file.


/***********************************/
/* Respond to Control button event */
/***********************************/
static void control_cb(lv_event_t * e){
  Serial.println("A1");
}

void ui_init(){
    lv_obj_t * btn1 = lv_btn_create(lv_screen_active());
    lv_obj_set_size(btn1, 150, 75);
    lv_obj_add_event_cb(btn1, control_cb, LV_EVENT_RELEASED, NULL);
    lv_obj_align(btn1, LV_ALIGN_CENTER, 0, 0);
}

hopefully someone with more experience with lvgl will step in.

Even after reading the explanation in post #3, I still don't really understand which files define what.

However, it seems like the problem isn't with how LVGL functions and headers are defined, but rather with how they're defined in C/C++.

I'm using LVGL9.x, and the following code compiles successfully (but won't run).

lvgl_button_callback_test.ino

#include <Arduino.h>
#include "ui.h"

void setup() {
  Serial.begin(9600);

  ui_init();
}

void loop() {}

ui.c

#include <stdio.h>
#include "ui.h"

/***************************/
/* Build the GIU unterface */
/***************************/
lv_obj_t *ui_GUI;

lv_obj_t *ui_btn_control;
lv_obj_t *ui_control_label;

/***********************************/
/* Respond to Control button event */
/***********************************/
static void control_cb(lv_event_t * e){
  printf("A1\n");
}

/************************************/
/* Build the GIU interface controls */
/************************************/
void ui_init(void) {
  lv_disp_t *dispp = lv_disp_get_default();
  lv_theme_t *theme = lv_theme_default_init(dispp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED), true, LV_FONT_DEFAULT);
  lv_disp_set_theme(dispp, theme);

  ui_GUI = lv_obj_create(NULL);                           // Create a screen object
  lv_obj_clear_flag(ui_GUI, LV_OBJ_FLAG_SCROLLABLE);
  
  ui_btn_control = lv_btn_create(ui_GUI);            // Button to toggle control on/off
  lv_obj_set_pos(ui_btn_control,20,20);
  lv_obj_set_size(ui_btn_control, 100, 50);             // Width: 100px, Height: 50px
  lv_obj_add_event_cb(ui_btn_control, control_cb, LV_EVENT_ALL, NULL);

  ui_control_label = lv_label_create(ui_btn_control);
  lv_label_set_text(ui_control_label, "Control");

  lv_disp_load_scr(ui_GUI);                        // Load the screen into the display
}

ui.h

#pragma once
#include <lvgl.h>

#ifdef __cplusplus
extern "C" {
#endif

/***************************/
/* Build the GIU unterface */
/***************************/
extern lv_obj_t *ui_GUI;

extern lv_obj_t *ui_btn_control;
extern lv_obj_t *ui_control_label;

extern void ui_init(void);

#ifdef __cplusplus
} /*extern "C"*/
#endif

If you could explain in detail which files define what, the dependencies between them, and any error messages you get when compiling, I think we might be able to find a solution to your problem.

I agree it’s not an LVGL library issue. I have my code to build the GUI and the event response functions contained in ui.h In my .ino file I have #include “ui.h” It won’t compile saying my callback functions are not declared in this scope.

Compilation error: 'btn_control_cb' was not declared in this scope

If i move all of the code from the ui.h and put it just before the .ino setup() function everything works fine which to my understanding placing it there makes it global. I don’t understand why if I have ui.h included in the .ino file why it’s not locating the callback functions during compiling. It’s locating the ui_init() function just fine when it’s part of the ui.h file. I can call it as the last step of my setup() function and it builds the GUI. The callbacks are not being visible during compile when they are located in the ui.h file.

.ino

#include <PCA9557.h>
#include <lvgl.h>
#include <LovyanGFX.hpp>
#include <lgfx/v1/platforms/esp32s3/Panel_RGB.hpp>
#include <lgfx/v1/platforms/esp32s3/Bus_RGB.hpp>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <esp32-hal-ledc.h>
#include "display_setup.h"
#include "ui.h"

ui.h

#include <Arduino.h>

lv_obj_t *GUI;              

lv_obj_t *btn_control;      
lv_obj_t *control_label;

lv_obj_t *btn_full_stop;
lv_obj_t *stop_label;

lv_obj_t *slider_throttle;
lv_obj_t *throttle_label;


/************************************/
/* Build the GIU interface controls */
/************************************/
void ui_init() {
  lv_disp_t *dispp = lv_disp_get_default();
  lv_theme_t *theme = lv_theme_default_init(dispp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED), true, LV_FONT_DEFAULT);
  lv_disp_set_theme(dispp, theme);

  GUI = lv_obj_create(NULL);                      // Create a screen object
  lv_obj_clear_flag(GUI, LV_OBJ_FLAG_SCROLLABLE);
  
  /* Active/Inactive control of the cab */
    btn_control = lv_btn_create(GUI);           
    lv_obj_set_pos(btn_control,20,20);
    lv_obj_set_size(btn_control, 100, 50);        //Width, Height
    lv_obj_add_event_cb(btn_control, btn_control_cb, LV_EVENT_CLICKED, NULL);
    control_label = lv_label_create(btn_control);
    lv_label_set_text(control_label, "Control");

  /* Cab full stop */
    btn_full_stop = lv_btn_create(GUI);           
    lv_obj_set_pos(btn_full_stop,675,410);
    lv_obj_set_size(btn_full_stop, 100, 50);      //Width, Height
    lv_obj_add_event_cb(btn_full_stop, btn_full_stop_cb, LV_EVENT_CLICKED, NULL);
    stop_label = lv_label_create(btn_full_stop);
    lv_label_set_text(stop_label, "Full Stop");
  
  /* Cab throttle slider */
    slider_throttle = lv_slider_create(GUI);           
    lv_obj_set_pos(slider_throttle,715,25);                 //x,y    
    lv_obj_set_size(slider_throttle, 25, 350);              //Width:, Height:
    lv_slider_set_range(slider_throttle, 0, 127);           //Maximum DCC throttle steps
    lv_slider_set_value(slider_throttle, 0, LV_ANIM_OFF);   

  /* Load the controls to the display */
    lv_disp_load_scr(GUI);           
}
/* End of the GUI control builds */


/***********************************/
/* Respond to Control button event */
/***********************************/
static void btn_control_cb(lv_event_t * e){
  if (active == 0){
    active=1;
    Serial.println(F("{A1}"));
    lv_label_set_text(control_label, "Active");
    lv_disp_load_scr(GUI);   
  }
  else{
    active=0;
    Serial.println(F("{A0}"));delay(50);
    Serial.println(F("{C0}"));
    lv_label_set_text(control_label, "Inactive");
    lv_disp_load_scr(GUI);   
  }  
}

/*************************************/
/* Respond to Full Stop button event */
/*************************************/
static void btn_full_stop_cb(lv_event_t * e){
  Serial.println("{F}");
  lv_slider_set_value(slider_throttle, 0, LV_ANIM_ON);   
  lv_disp_load_scr(GUI); 
}

Well…..I feel stupid. I moved the callback functions to above the LVGL object code and it works fine. It didn’t dawn on me that having the callback functions listed after the LVGL object code was putting the cart in front of the horse…..

ui.h this works

#include <Arduino.h>
extern uint8_t active = 0;
lv_obj_t *GUI;              

lv_obj_t *btn_control;      
lv_obj_t *control_label;

lv_obj_t *btn_full_stop;
lv_obj_t *stop_label;

lv_obj_t *slider_throttle;
lv_obj_t *throttle_label;

/***********************************/
/* Respond to Control button event */
/***********************************/
static void btn_control_cb(lv_event_t * e){
  if (active == 0){
    active=1;
    Serial.println(F("{A1}"));
    lv_label_set_text(control_label, "Active");
    lv_disp_load_scr(GUI);   
  }
  else{
    active=0;
    Serial.println(F("{A0}"));delay(50);
    Serial.println(F("{C0}"));
    lv_label_set_text(control_label, "Inactive");
    lv_disp_load_scr(GUI);   
  }  
}

/*************************************/
/* Respond to Full Stop button event */
/*************************************/
static void btn_full_stop_cb(lv_event_t * e){
  Serial.println("{F}");
  lv_slider_set_value(slider_throttle, 0, LV_ANIM_ON);   
  lv_disp_load_scr(GUI); 
}

/************************************/
/* Build the GIU interface controls */
/************************************/
void ui_init() {
  lv_disp_t *dispp = lv_disp_get_default();
  lv_theme_t *theme = lv_theme_default_init(dispp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED), true, LV_FONT_DEFAULT);
  lv_disp_set_theme(dispp, theme);

  GUI = lv_obj_create(NULL);                      // Create a screen object
  lv_obj_clear_flag(GUI, LV_OBJ_FLAG_SCROLLABLE);
  
  /* Active/Inactive control of the cab */
    btn_control = lv_btn_create(GUI);           
    lv_obj_set_pos(btn_control,20,20);
    lv_obj_set_size(btn_control, 100, 50);        //Width, Height
    lv_obj_add_event_cb(btn_control, btn_control_cb, LV_EVENT_CLICKED, NULL);
    control_label = lv_label_create(btn_control);
    lv_label_set_text(control_label, "Control");

  /* Cab full stop */
    btn_full_stop = lv_btn_create(GUI);           
    lv_obj_set_pos(btn_full_stop,675,410);
    lv_obj_set_size(btn_full_stop, 100, 50);      //Width, Height
    lv_obj_add_event_cb(btn_full_stop, btn_full_stop_cb, LV_EVENT_CLICKED, NULL);
    stop_label = lv_label_create(btn_full_stop);
    lv_label_set_text(stop_label, "Full Stop");
  
  /* Cab throttle slider */
    slider_throttle = lv_slider_create(GUI);           
    lv_obj_set_pos(slider_throttle,715,25);                 //x,y    
    lv_obj_set_size(slider_throttle, 25, 350);              //Width:, Height:
    lv_slider_set_range(slider_throttle, 0, 127);           //Maximum DCC throttle steps
    lv_slider_set_value(slider_throttle, 0, LV_ANIM_OFF);   

  /* Load the controls to the display */
    lv_disp_load_scr(GUI);           
}
/* End of the GUI control builds */

Congratulations, you've found a solution!

There are two things to keep in mind.

First, the compiler parses the source code in the order it is written and generates object code.

This means that while parsing ui_init(), the compiler needs to know the specification (e.g. arguments and return type) of btn_control_cb() before encountering it.

In addition to the solution you found, you can also place a prototype declaration of btn_control_cb() before ui_init(), as shown below.

// Prototype declaration
static void btn_control_cb(lv_event_t * e);

void ui_init() {
...
}

// Entity definition
static void btn_control_cb(lv_event_t * e) {
...
}

Second, header files generally contain prototype declarations of variables and functions, rather than their actual entity definitions. The code written in post #5 follows this principle, with ui.h serving the specifications of variables and functions to the compiler.

There is another reason to write only prototype declarations of variables and functions in header files:

As your application becomes more complex, you may need to split your code into multiple .ino, .c, and .cpp files and include a common ui.h from each of them.

In this case, if you define the actual variables and functions in ui.h, there will be multiple instances of those variables and functions, which could result in unintended behavior.

Imagine yourself as a compiler, be aware of the order of parsing, and use proper scoping and prototyping of variables and functions.

So if I understand this correctly the prototype declaration ahead of the object code gives the object code something to reference to in the instance where the actual entity definition hasn’t occurred yet during compiling?

That's almost right.

But more precisely, prototype declarations are not "instances." They simply indicate "specifications" of variable types, function types and arguments.

Defining high-level functionality (i.e. overview) first, followed by lower-level functionality (i.e. details) makes it easier for the someone reading your code to understand.

However, a compiler's job is not to "understand", but to "translate into machine code". Prototype declarations help it do this efficiently. (And of course, for anyone who reads the code.)

Prototype declarations let the "compiler" know what to push on the stack and what to pop off the stack when the callee returns, so it can translate into machine code. On the other hand, identifying the callee is the job of the "linker."

As the name suggests, the linker "links" the caller and the callee.

Behind the scenes, the Arduino IDE run a compiler and a linker.

I'm writing it in reverse order because it's a pain to write prototypes ... :wink:

This isn’t an LVGL bug but a C++ scope issue. Your callback isn't visible to the compiler at the point where lv_obj_add_event_cb() is called.

you should define your handler above the setup function, and then double-check that your signature matches exactly like this one>void your_cb(lv_event_t * e).

Understood. I wasn’t using the term instance as a in coding reference. I was meaning …in the situation of ……… the object code coming before the function code from the compile standpoint

1 Like