Use LVGL with Python3

I’ve tried the lv_cpython repo, added lvgl with the modifications but still got:

)python3 builder.py 
Traceback (most recent call last):
  File "/home/kisvegabor/projects/lvgl/python3/lv_cpython/builder.py", line 217, in <module>
    cdef = cdef.format(ast=str(generator.visit(ast)))
  File "/home/kisvegabor/projects/lvgl/python3/lv_cpython/builder.py", line 177, in visit
    ret = getattr(self, method, self.generic_visit)(node)
  File "/home/kisvegabor/.local/lib/python3.9/site-packages/pycparser/c_generator.py", line 217, in visit_FileAST
    s += self.visit(ext) + ';\n'
  File "/home/kisvegabor/projects/lvgl/python3/lv_cpython/builder.py", line 177, in visit
    ret = getattr(self, method, self.generic_visit)(node)
  File "/home/kisvegabor/.local/lib/python3.9/site-packages/pycparser/c_generator.py", line 145, in visit_Decl
    s = n.name if no_type else self._generate_decl(n)
  File "/home/kisvegabor/.local/lib/python3.9/site-packages/pycparser/c_generator.py", line 426, in _generate_decl
    s += self._generate_type(n.type)
  File "/home/kisvegabor/.local/lib/python3.9/site-packages/pycparser/c_generator.py", line 480, in _generate_type
    return self.visit(n)
  File "/home/kisvegabor/projects/lvgl/python3/lv_cpython/builder.py", line 177, in visit
    ret = getattr(self, method, self.generic_visit)(node)
  File "/home/kisvegabor/.local/lib/python3.9/site-packages/pycparser/c_generator.py", line 181, in visit_Enum
    return self._generate_struct_union_enum(n, name='enum')
  File "/home/kisvegabor/.local/lib/python3.9/site-packages/pycparser/c_generator.py", line 377, in _generate_struct_union_enum
    s += body_function(members)
  File "/home/kisvegabor/.local/lib/python3.9/site-packages/pycparser/c_generator.py", line 387, in _generate_enum_body
    return ''.join(self.visit(value) for value in members)[:-2] + '\n'
  File "/home/kisvegabor/.local/lib/python3.9/site-packages/pycparser/c_generator.py", line 387, in <genexpr>
    return ''.join(self.visit(value) for value in members)[:-2] + '\n'
  File "/home/kisvegabor/projects/lvgl/python3/lv_cpython/builder.py", line 177, in visit
    ret = getattr(self, method, self.generic_visit)(node)
  File "/home/kisvegabor/.local/lib/python3.9/site-packages/pycparser/c_generator.py", line 196, in visit_Enumerator
    value=self.visit(n.value),
  File "/home/kisvegabor/projects/lvgl/python3/lv_cpython/builder.py", line 178, in visit
    if res.strip() == ';':
NameError: name 'res' is not defined

Maybe I’m doing something wrong with modifying LVGL. Could you add it as well? Or is this issue related to something else?

Really nothing special, just -lSDL2.

OK the error is fixed and I also added the SDL2 library to the build.

I have tested it under Linux and it works. I added a test script you can run after it compiles. you don’t have to worry about moving the so file, the test script adds the location so it can be imported.

funny thing is it doesn’t crash when I move the SDL window or just randomly like the MicroPython version does when compiled for Linux. That’s a bonus!

you can look at the test.py script to see how it works. I am going to add a style to the script so you can understand how structures need to be created.

you ask and you shall receive.

so I added an import override so you can do the following.

import lvgl as lv

screen = lv.scr_act()

button = lv.create_btn(screen)

I also added wrapper classes around the functions and also the structures/unions. these classes handle type conversions.

I have updated the repo with the wrapper classes and import override. The test script has also been updated for the new import system and wrapper classes.

2 Likes

Thank you for the update! Now it creates the so file, but test.py fails:

$ python3 builder.py
...
extension module is located here: "/home/kisvegabor/projects/lvgl/python3/lv_cpython/lvgl/_lib_lvgl.cpython-39-x86_64-linux-gnu.so"

$ python3 test.py 
starting
importing lvgl
lv_init
Traceback (most recent call last):
  File "/home/kisvegabor/projects/lvgl/python3/lv_cpython/lvgl.py", line 273, in __getattr__
    _lib_lvgl.ffi.new('lv_' + item + ' *')
ffi.error: undefined type name
lv_lib *
^

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/kisvegabor/projects/lvgl/python3/lv_cpython/test.py", line 14, in <module>
    lvgl.lib.lv_init()
  File "/home/kisvegabor/projects/lvgl/python3/lv_cpython/lvgl.py", line 275, in __getattr__
    raise AttributeError(item)
AttributeError: lib

I screwed up and committed/pushed the files to the wrong repo. I am a ding dong. Too many things going on all at the same time has got me all kinds of scatter brained. Well that and age. 47 years old does have some impact as well.

It is all fixed.

You did it! It’s working for me too! :heart_eyes:
I’ve test it more deeply on Monday.

I have to mess about with it on Windows. It compiles without error but it just tanks when it is run. I believe the issue is with SDL2. I am going to access the SDL2 bits directly and see if I can get a window to open up. I think the issue might be in the thread that is created by LVGL to increment the tick in LVGL. To be honest I am not exactly sure why SDL2 is even bundled like it is with LVGL. There are no other display drivers or input drivers that are bundled with LVGL.

Technically speaking instead of using SDL2 I could use wxPython to handle the Window creation. It is cross platform Linux, Windows and Macintosh and is based on wxwidgets which has been around a really long while. The implimentation can be written in Python too because it is simply a matter of passing off the buffer to a ClientDC for a wxFrame instance in wxPython. This would give a native looking window and a menu bar could be added to the top to carry out specific tasks if wanted. It would also do a proper shutdown that wouldn’t cause a segmentation fault.

wxwidgets and wxPython have been around a really long time. at least since 2007. It is a stable way to get the buffer data drawn and to also capture touch mouse and keyboard.

I’ve tried out the events like this:


def my_event(e):
    print("Clicked")


btn = lv.btn_create(screen)
lv.obj_set_pos(btn, 10, 10)
lv.obj_set_size(btn, 120, 50)
lv.obj_add_event(btn, my_event, lv.EVENT_CLICKED, None)

but got:

LVGL initilized
disp: <lvgl._lv_disp_t object at 0x7f62337ebbe0>
group: <lvgl._lv_group_t object at 0x7f62337ec2e0>
mouse: <lvgl._lv_indev_t object at 0x7f62336f2250>
keyboard: <lvgl._lv_indev_t object at 0x7f6233894bb0>
lv_indev_set_group
Traceback (most recent call last):
  File "/home/kisvegabor/projects/lvgl/python3/lv_cpython/lvgl.py", line 273, in __getattr__
    _lib_lvgl.ffi.new('lv_' + item + ' *')
ffi.error: undefined type name
lv_EVENT_CLICKED *
^

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/kisvegabor/projects/lvgl/python3/lv_cpython/test.py", line 33, in <module>
    lv.obj_add_event(btn, my_event, lv.EVENT_CLICKED, None)
  File "/home/kisvegabor/projects/lvgl/python3/lv_cpython/lvgl.py", line 275, in __getattr__
    raise AttributeError(item)
AttributeError: EVENT_CLICKED

It seems LV_EVENT_CLICKED is translated to lv_EVENT_CLICKED.

I do not believe the callbacks work properly. I have to build into it special handling. You might be able to register a single callback but if you do a second one it will not function properly. I have to make a wrapper around the C callbacks. There might need to be some code changes made to LVGL to make this work. The actual callback would need to be passed via user data that the C code would send to the registered callback in Python and the Python callback wound call the actual callback in in the user data.

Also I have not looked to see how enums are stored. They are not listed in the available types because an enum item is not a type it acts more like a macro. I will more than likely have to create a constant with the same name during the build program. It should be accessible then.

You can read about how to implement callbacks using CFFI here.

https://cffi.readthedocs.io/en/latest/using.html#extern-python-new-style-callbacks

If you run the following script you can list everything that is accessible from LVGL

import lvgl

import _lib_lvgl

for item in sorted(list(dir(_lib_lvgl.lib))):
    print(item)

for item in _lib_lvgl.ffi.list_types()[0]:
    print(item)

I just checked and the enumerations are exported. The LV_ is going to remain as this is the way I am going to be able to identify an enumeration. I also have to create an enumeration for the macros like LV_CONTENT_SIZE

OK go ahead and test it again. just like you had it already. the “LV_” will be added automatically.

funny that you already started messing about with it. little excited to see it working and it was actually pretty easy to do.

With @amirgon 's help we should be able to modify the gen_mpy script to output the python code needed to make it identical to what the MIcroPython binding currently is.

1 Like

I got it running on Windows finally. I also have a test version working with wxPython as an alternative to SDL. This way if someone wanted to make an offline IDE for writing a GUI that have the ability to use the code editor and repl that wxPython has built into it coupled with the display output from LVGL. Pretty neat actually. I bet Thonny would be interested in adding something like this into their IDE. Once it is finished up that is.

I did get the callbacks working but it required a decent amount of alteration to LVGL. This is because CFFI is not able to deal with callbacks defined as a parameter or as a filed in a structure. It has to be declared as a typedef and that then gets used as the type for the parameter of field. I don’t know how to declare a callback in one header file and make it available in another header file that doesn’t include the first header file. structures you can do that with using struct name but I am not sure how to declare a callback so that can be done.

as a note. I do have to say that 3 post thing is rather annoying. especially if I have written 3 posts just giving updates and I want to add a 4th. if a person has already read the 3 if I am forced to edit the 3rd because I cannot make a 4th the person is not going to get a notification that there is additional information.

I’ve tried the latest version, run builder.py but got this error for test.py:

Traceback (most recent call last):
  File "/home/kisvegabor/projects/lvgl/python3/lv_cpython/test.py", line 1, in <module>
    import lvgl as lv
  File "/home/kisvegabor/projects/lvgl/python3/lv_cpython/lvgl.py", line 720, in <module>
    def __ayout_update_cb_t_callback_func(*args):
ffi.error: ffi.def_extern('py_lv_ayout_update_cb_t'): no 'extern "Python"' function with this name

This is my build folder: build.zip (2.8 MB)

So it needs to

typedef void(*hello_cb)(int, int);

void set_cb(hello_cb my_cb);

instead of

void set_cb(void (*my_cb)(int, int))

right?

It’s the default setting, I guess to prevent spamming. Anyway, I’ve changed it to 5.

Thank god. 5 is better than 3. I tend to have thoughts after I post something, a different idea. so instead of putting it all on a single post that is the size of a book it is better to break the ideas apart into separate posts.

I have not fully tested the code yet that I pushed to the repo. I know it compiles and I did a test run and it does work. Then I wrote into the script the automatic addition of the callbacks and that I have not tested as of yet. It appears as tho it does have problems, which I knew it would have some kinks that need to be ironed out.

I didn’t want to leave it up to the user to have to manually code in all of the callbacks the way CFFI does it. I didn’t want to make them responsible for creating a C reference to a python function and having to keep track of the reference so it doesn’t go out of scope. I also didn’t want them to have to pass that reference to the user_data field of a structure that get passed to the callback when the callback gets called. Better to handle those kinds of things behind the scenes.

I didn’t want to hard code all of those callbacks either. So I opted to have the build script automatically generate them.

to answer you question regarding the callbacks and how they are declared in LVGL. yes you are correct, exactly how you have it. The private header files are what causes some issues because they are not imported into the “public” header file. the private header gets imported into the source file but in order to not have a bunch of stuff that is not needed/used by the user the build system only reads the header files that are included in lvgl.h and propagates out from there. since none of the private header files are included in the public ones the callback would not be made available unless it is declared in the public header file… The callback would no longer be available in the private one if the callback gets moved. So how does one overcome that issue?

also in the case of LVGL it would actually be

typedef void(*hello_cb_t)(int, int);

void set_cb(hello_cb_t my_cb);


typedef struct {
    hello_cb_t my_cb;
} my_struct_t;

The problem I am having is when the structure is not located in the same header file as the function and there are no includes between the 2 header files because one is private and one is public.

Can you mention some examples (ideally all, if you can export them somehow) where we have this issue?

specifically in lv_disp.h and lv_disp_private.h and also lv_indev.h with lv_indev_private.h. those are the 2 I remember off the top of my head.

lf_disp.h contains the function definitions while lv_disp_private contains the lv_disp_t structure. the source file (lv_disp.c) includes both of those files but a callback declared in the private file is not able to be accessed in the public file and same goes if it is vise versa. You don’t want to include the private file in the public one because it would no longer be private. I don’t remember what happens if you include the public file into the private one. I think something pitches a fit about it.

in lv_disp.h

typedef struct _lv_disp_t lv_disp_t;



/**
 * Set the flush callback whcih will be called to copy the rendered image to the display.
 * @param disp      pointer to a display
 * @param flush_cb  the flush callback (`px_map` contains the rendered image as raw pixel map and it should be copied to `area` on the display)
 */
void lv_disp_set_flush_cb(lv_disp_t * disp, void (*flush_cb)(struct _lv_disp_t * disp, const lv_area_t * area,
                                                             lv_color_t * px_map));
/**
 * Set the color format of the display.

and this is in lv_disp_private.h


/**********************
 *      TYPEDEFS
 **********************/
struct _lv_disp_t {

    /*---------------------
     * Resolution
     *--------------------*/

    /** Horizontal resolution.*/
    lv_coord_t hor_res;

    /** Vertical resolution.*/
    lv_coord_t ver_res;

    /** Horizontal resolution of the full / physical display. Set to -1 for fullscreen mode.*/
    lv_coord_t physical_hor_res;

    /** Vertical resolution of the full / physical display. Set to -1 for fullscreen mode.*/
    lv_coord_t physical_ver_res;

    /** Horizontal offset from the full / physical display. Set to 0 for fullscreen mode.*/
    lv_coord_t offset_x;

    /** Vertical offset from the full / physical display. Set to 0 for fullscreen mode.*/
    lv_coord_t offset_y;

    uint32_t dpi;              /** DPI (dot per inch) of the display. Default value is `LV_DPI_DEF`.*/

    /*---------------------
     * Buffering
     *--------------------*/

    /** First display buffer.*/
    void * draw_buf_1;

    /** Second display buffer.*/
    void * draw_buf_2;

    /** Internal, used by the library*/
    void * draw_buf_act;

    /** In byte count*/
    uint32_t draw_buf_size;

    /** MANDATORY: Write the internal buffer (draw_buf) to the display. 'lv_disp_flush_ready()' has to be
     * called when finished*/
    void (*flush_cb)(struct _lv_disp_t * disp_drv, const lv_area_t * area, lv_color_t * color_p);

    /*1: flushing is in progress. (It can't be a bit field because when it's cleared from IRQ Read-Modify-Write issue might occur)*/
    volatile int flushing;

    /*1: It was the last chunk to flush. (It can't be a bit field because when it's cleared from IRQ Read-Modify-Write issue might occur)*/
    volatile int flushing_last;
    volatile uint32_t last_area         : 1; /*1: the last area is being rendered*/
    volatile uint32_t last_part         : 1; /*1: the last part of the current area is being rendered*/

    lv_disp_render_mode_t render_mode;
    uint32_t antialiasing : 1;       /**< 1: anti-aliasing is enabled on this display.*/

    /** 1: The current screen rendering is in progress*/
    uint32_t rendering_in_progress : 1;

    lv_color_format_t   color_format;

    /** Invalidated (marked to redraw) areas*/
    lv_area_t inv_areas[LV_INV_BUF_SIZE];
    uint8_t inv_area_joined[LV_INV_BUF_SIZE];
    uint16_t inv_p;
    int32_t inv_en_cnt;

    /*---------------------
     * Draw context
     *--------------------*/

    lv_draw_ctx_t * draw_ctx;
    void (*draw_ctx_init)(struct _lv_disp_t * disp, lv_draw_ctx_t * draw_ctx);
    void (*draw_ctx_deinit)(struct _lv_disp_t * disp, lv_draw_ctx_t * draw_ctx);
    size_t draw_ctx_size;

    /*---------------------
     * Screens
     *--------------------*/


    /** Screens of the display*/
    struct _lv_obj_t ** screens;    /**< Array of screen objects.*/
    struct _lv_obj_t * act_scr;     /**< Currently active screen on this display*/
    struct _lv_obj_t * prev_scr;    /**< Previous screen. Used during screen animations*/
    struct _lv_obj_t * scr_to_load; /**< The screen prepared to load in lv_scr_load_anim*/
    struct _lv_obj_t * bottom_layer;    /**< @see lv_disp_get_layer_bottom*/
    struct _lv_obj_t * top_layer;       /**< @see lv_disp_get_layer_top*/
    struct _lv_obj_t * sys_layer;       /**< @see lv_disp_get_layer_sys*/
    uint32_t screen_cnt;
    uint8_t draw_prev_over_act  : 1;/** 1: Draw previous screen over active screen*/
    uint8_t del_prev  : 1; /** 1: Automatically delete the previous screen when the screen load animation is ready*/

    /*---------------------
     * Others
     *--------------------*/

    void * driver_data; /**< Custom user data*/

    void * user_data; /**< Custom user data*/

    lv_event_list_t event_list;

    uint32_t sw_rotate : 1; /**< 1: use software rotation (slower)*/
    uint32_t rotation  : 2; /**< Element of  @lv_disp_rotation_t*/

    /**< The theme assigned to the screen*/
    struct _lv_theme_t * theme;

    /** A timer which periodically checks the dirty areas and refreshes them*/
    lv_timer_t * refr_timer;

    /*Miscellaneous data*/
    uint32_t last_activity_time;        /**< Last time when there was activity on this display*/

    uint32_t last_render_start_time;

    /** OPTIONAL: Called periodically while lvgl waits for operation to be completed.
     * For example flushing or GPU
     * User can execute very simple tasks here or yield the task*/
    void (*wait_cb)(struct _lv_disp_t * disp_drv);

    /** On CHROMA_KEYED images this color will be transparent.
     * `LV_COLOR_CHROMA_KEY` by default. (lv_conf.h) */
    lv_color_t color_chroma_key;
};

so where would the typedef for the callback get put??

OH I remember now why you cannot include lv_disp.h into lv_disp_private.h. It’s because the source file doesn’t get processed when generating the code that needs to get compiled for the extension module. only the header files that are included in lvgl.h and propagating outwards using the includes in the header files recursively. so the generation script never sees the lv_disp_private header file.

I’ve added lv_indev_read_cb_t and lv_disp_flush_cb_t in: