lvgl / lvgl

Embedded graphics library to create beautiful UIs for any MCU, MPU and display type.

Home Page:https://lvgl.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Feature suggestion: Micropython binding

amirgon opened this issue ยท comments

Micropython is becoming mainstream on IOT and embedded development in general. It is available for quite a few of embedded architectures (this and this, partial list), and can be ported to new ones.
Today there already exist several architecture-specific micropython display modules (for example this and this), but none of them as feature rich and general as littlevgl.
I think it could be useful to provide micropython API to littlevgl Objects and Styles.
Main benefits are:

  • Exposure to a larger audience in the embedded development world.
  • Interactive developement. Use the Micropython interpreter to exercise littlevgl over a live embedded board. This would allow rapid GUI development without the build-flash cycle.
  • Some would argue Python code is easier to develop and maintain (not sure I agree about this one...)

If it turns out there is an agreement about this feature, I might try sending PR if I find the time for it.

Hi,

I also see a big potential in it. Both reaching a larger audience and the benefits of Micropython sounds very well. Earlier I made some experiments with Lua binding but Micropython is more mainstream right now.

So this feature would be very welcome!
How can this binding look like? One or more c file(s) with the interface functions?

@kisvegabor
Will it cost much more extra cpu time for the micro-Python?

@turoksama There will definitely be overhead from using Python due to the need to convert objects back and forth. I guess the hope is that Python will be used on platforms which can afford the additional overhead.

Here is what I had in mind.
The lvgl micropython module would provide a constructor for each object type, and each object would provide its methods.
Here is the "Hello World" example:

import lvgl
label1 = lvgl.label(lvgl.get_scr_act())
label1.set_text("Hello world!")
label1.align(None, lvgl.ALIGN_CENTER, 0, 0)

Creating micropython extensions requires lots of boilerplate. I don't think we should write them manually. Writing them manually would be not only tedious to start with, but also hard to maintain when objects are changed and added.
Instead I suggest creating these extensions automatically from the source code (the C headers) relying on the naming conventions and maybe some hints.
During the make process a script will run, scan the lvgl object headers (with pycparser for example) and generate the Micropython extensions in a C file that can be later compiled and linked on Micropython-enabled projects.

@amirgon Sound good to me! I also vote for the automated way.
From your example, we can see how to call LittelvGL from Micropython. But how to call Micropython from LittelvGL? It is required when clicking a button and a user-defined action should be executed.

@kisvegabor Calling a Micropython function from C code is covered by Micropython.
You can use mp_call_function_* defined here, so we get this one for free.
In case of a button (or any other) set_action micropython extension, we can give it a python function as a parameter since functions are first-class objects in python, can be passed around, used in lists dicts etc. so I think this should be pretty simple.

One thing we'll need to figure out is how to separate display/board specific code from generic lvgl code.
We would need to provide Micropython extensions to register display driver, set IO pins, configure SPI, DMA, etc.

@amirgon The only problem with your suggestion is that the performance overhead of handling the low-level display code through Python might be too high. What do you think?

@embeddedt It's true that Micropython is slower than C, and costs more resources (RAM, Flash).
However, I believe Micropython is still beneficial for platforms that can afford the extra resources (such as ESP32, for example):

  • Micropython would only be used for the Objects/Styles API layer (for setting object properties), not for the graphics rending itself which remains in C. Think about a button, for example. How many cycles are spent on creating the button object and setting its properties (action, state, style) vs. cycles spent on actually writing pixels to the display to render it? My guess is that rending would take most of the cycles (and rendering remains in C), while setting object properties would only consume a very small portion of the cycle budget.
    Also take into account that in most cases setting object properties is done once per GUI "screen" and rendering happens continuously while the user interacts with that certain "screen".

  • Micropython is optimized for architectures with limited resources (CPU and memory). More information can be found here. Micropython compiles code into bytecode and has an efficient VM that executes it. Micropython modules may also be implemented as frozen bytecode modules saving the need to compile them by the MCU before running them.

  • Micropython support for lvgl would be optional, and you would need to explicitly opt in. If you do nothing you don't pay any cycles or memory. If you are developing for a platform where you want to save cycles and memory, this feature will not get in your way, and you can still write all your code in C as today and get the same performance.

@amirgon I agree, calling lv_set_... functions from Micropython would have only a minor overhead if the rendering remains in C. I suppose we can keep the disp_fush function in C as well, right?

@kisvegabor I agree, disp_flush should remain a low level C function.

@amirgon
Did you already make some experiments with LittlevGL in Micropython?

@kisvegabor I've created a Micropython module and one object manually, and ran it on my HW setup. I didn't start working on the automatic generation of Micropython extensions yet, but I'm looking into doing this using pycparser.

@amirgon
Great! I'm really waiting for that :)

Just found this.
This is an emulated MCU running on the browser, with Micropython firmware.
When we have lvgl Micropython binding, something like this could be used for running lvgl interactively on the browser!

(btw, progress update, pycparser is great and I'm making some progress with it, creating some micropython lvgl extension functions automatically, but there's still a lot of work to be done)

Looks amazing! My old dream to make an interactive online GUI demo tool but didn't find the right way to do this.

I'm looking forward to try Micropython!

@kisvegabor Have you considered Emscripten? It converts C to JavaScript and might be faster than using Unicorn and MicroPython for a demo.

@embeddedt
I tried Emscripten once but it was extremely slow and wasn't stable (sometimes not started properly). Probably it is worth an other try.

@kisvegabor I can experiment with Emscripten if you'd like but it may take a while to get stuff working. I probably won't get to it before next week.

@embeddedt if you do have time to experiment with Emscripten, please try to build Micropython as part of lvgl sources.
This has been done before.

@embeddedt Some test would be welcome in this field. I never used Emscripten before the mentioned test so probably you will get better results than me :)

Not ready for a PR yet, but I have a working script.
It scans all *.h files in lv_objx dir and generates an Mpy module called lvgl which contains object for each lvgl object, method functions etc.
It relies heavily on naming conventions. (For example, it assumes that an object is constructed by lv_%s_create with two specific arguments.)
Luckily, these naming conventions are followed very well on lvgl sources.

Here is a working example I just run on my device:

>>> import lvgl
ILI9341 initialization.
Enable backlight.
>>> label1 = lvgl.label(lvgl.scr_act())
>>> label1.set_text("Hello world!")
>>> label1.align(None, 0, 0, 0)

A nice thing about Mpy REPL is the automatic completion (start typing and press tab), the help function etc.

For example:

>>> help(label1)
object lvgl label is of type label
  del -- <function>
  clean -- <function>
  invalidate -- <function>
  set_parent -- <function>
  set_pos -- <function>
  set_x -- <function>
  set_y -- <function>
  set_size -- <function>
  set_width -- <function>
  set_height -- <function>
  align -- <function>
  refresh_style -- <function>
  set_hidden -- <function>
  set_click -- <function>
  set_top -- <function>
  set_drag -- <function>
  set_drag_throw -- <function>
  set_drag_parent -- <function>
  set_opa_scale_enable -- <function>
  set_opa_scale -- <function>
  set_protect -- <function>
  clear_protect -- <function>
  refresh_ext_size -- <function>
  set_free_num -- <function>
  get_screen -- <function>
  get_parent -- <function>
  get_child -- <function>
  get_child_back -- <function>
  count_children -- <function>
  get_x -- <function>
  get_y -- <function>
  get_width -- <function>
  get_height -- <function>
  get_ext_size -- <function>
  get_hidden -- <function>
  get_click -- <function>
  get_top -- <function>
  get_drag -- <function>
  get_drag_throw -- <function>
  get_drag_parent -- <function>
  get_opa_scale -- <function>
  get_protect -- <function>
  is_protected -- <function>
  get_free_num -- <function>
  is_focused -- <function>
  set_text -- <function>
  set_array_text -- <function>
  set_static_text -- <function>
  set_long_mode -- <function>
  set_align -- <function>
  set_recolor -- <function>
  set_body_draw -- <function>
  set_anim_speed -- <function>
  get_text -- <function>
  get_long_mode -- <function>
  get_align -- <function>
  get_recolor -- <function>
  get_body_draw -- <function>
  get_anim_speed -- <function>
  ins_text -- <function>
  cut_text -- <function>

>>> help(lvgl)
object <module 'lvgl'> is of type module
  __name__ -- lvgl
  __init__ -- <function>
  obj -- <class 'obj'>
  arc -- <class 'arc'>
  cont -- <class 'cont'>
  btn -- <class 'btn'>
  label -- <class 'label'>
  bar -- <class 'bar'>
  btnm -- <class 'btnm'>
  cb -- <class 'cb'>
  line -- <class 'line'>
  chart -- <class 'chart'>
  page -- <class 'page'>
  ddlist -- <class 'ddlist'>
  lmeter -- <class 'lmeter'>
  gauge -- <class 'gauge'>
  img -- <class 'img'>
  kb -- <class 'kb'>
  led -- <class 'led'>
  list -- <class 'list'>
  mbox -- <class 'mbox'>
  preload -- <class 'preload'>
  roller -- <class 'roller'>
  slider -- <class 'slider'>
  sw -- <class 'sw'>
  win -- <class 'win'>
  tabview -- <class 'tabview'>
  ta -- <class 'ta'>
  font_init -- <function>
  anim_init -- <function>
  init -- <function>
  scr_act -- <function>
  layer_sys -- <function>
  disp_is_mem_fill_supported -- <function>
  tick_get -- <function>
  group_focus_obj -- <function>
  indev_enable -- <function>
  txt_ins -- <function>
  fs_init -- <function>
  fs_remove -- <function>
  fs_get_letters -- <function>
  fs_up -- <function>
  draw_aa_get_opa -- <function>

Next things to implement:

  • Add Enums to lvgl module to allow the user specify a name instead of a number
  • Callbacks (function pointers)
  • Styles
  • Structs (for example lv_color_t)
  • Missing conversions

If you want to see the code, have a look at:

  • gen_mpy.py - This script scans lvgl headers and generates the Mpy module.
  • lv_mpy.c - This is the generated file. In the future it should not be committed, but generated by the Makefile instead. It provides the Mpy module mp_module_lvgl which should then be registered in Mpy mpconfigport.h.
    It's already quite large, about 9000 generated source lines.

I'll be happy to receive comments.

@amirgon
It looks amazing!
Do you know how much flash space is required by lv_mpy.c?

I didn't use Micropython so far but I'd like to try it with my STM32F429 Discovery. I fond this: https://github.com/micropython/micropython/wiki/Board-STM32F429
Do you think lv_mpy.c can be integrated into that project? Or is it possible for me who is not familiar with Micropython? :)

@kisvegabor I didn't check how much flash it requires, however,

  • The first thing the script does it C preprocessing of the header files. So if you enable/disable features on lv_conf.h it would add/remove extensions from lv_mpy.c as well.
  • If you are using Micropython you probably have plenty of flash anyway...

I'm not familiar with STM32F429, my board is ESP32. But I don't see why it would matter, Micropython is the same.
Integrating it with Micropython is very simple:

  • Make sure lv_mpy.c compiles. Just add it to SRC_C in the Makefile.
  • Depending on your architecture you might need to edit your .ld file to make sure that lv_mpy.c goes to ROM and not RAM. On some architectures this would be automatic for const variables.
  • Edit mpconfigport.h and add mp_module_lvgl to MICROPY_PORT_BUILTIN_MODULES, this would register the module with Micropython.
  • I left one function to the user to implement: extern void lv_mp_init(). It is called when the module is loaded (after running import lvgl). I use it to initialize the LCD and lvgl, but if you do it some other way you can implement it as an empty function.

You can find more information here, let me know how it goes!

@amirgon Thank you! I hope I can ry it on the weekend

@amirgon
I'm planning to try Micropytthon on Linux with framebuffer as a display. I started to put the project together. Some parts of the build process are already working I don't know what is best way to link lvgl to micropython. Now I got a lot of similar errors:

build/../../../lvgl/mpy/lv_mpy.o: In function `mp_lv_txt_ins':
lv_mpy.c:(.text.mp_lv_txt_ins+0x31): undefined reference to `lv_txt_ins'

I suppose I need to add the source files of lvgl to the makefile but don't know what is the best to do it. Can you help with that?

@kisvegabor Yes you need to add lvgl sources to the Makefile.
First, I configured lvgl as an optional component, the following way:

  • Edit Kconfig.projbuild and add the following section under menu "Modules"
	    config MICROPY_USE_LVGL
	       bool "Use LVGL module"
	       default n
	       help
	       Include LittlevGL Embedded GUI Library
  • Run make menuconfig and enable use LVGL module under Micropython --> Modules. Alternatively you could edit sdkconfig directly and add CONFIG_MICROPY_USE_LVGL=y

  • Edit components/micropython/component.mk and add lvgl sources like this:

ifdef CONFIG_MICROPY_USE_LVGL
SRC_C += <...lvgl C files..>
endif

You don't have to explicitly specify all files, you can use wildcards to include C files from specific lvgl directories.
Alternatively you could add the source files recursively, but make sure you also read this. A better technique is something like this.
On ESP32 all it takes is adding component.mk (even empty one!) on every directory that contains files you want to include in the build (as explained here).
If you build Micropython on Linux that is probably different.

@kisvegabor Please make sure you take the latest version when you try it out.
Apart from bug fixes, I added the following features:

  • Object Inheritance. For example, when accessing a btn object, you can use btn methods, cont methods and obj methods, which are all the ancestors of btn. (The script deduces the heirarchy from the ..._ext_t structs)
  • Enums. Each enum is created as its own type, either as a child of an object type or of the lvgl module. For example,

ALIGN is a member of lvgl

>>> help(lvgl.ALIGN)
object <class 'LV_ALIGN'> is of type type
  OUT_LEFT_BOTTOM -- 17
  OUT_TOP_LEFT -- 9
  IN_RIGHT_MID -- 8
  OUT_RIGHT_MID -- 19
  IN_BOTTOM_MID -- 5
  CENTER -- 0
  OUT_RIGHT_TOP -- 18
  OUT_BOTTOM_MID -- 13
  OUT_LEFT_MID -- 16
  IN_TOP_MID -- 2
  IN_BOTTOM_LEFT -- 4
  IN_TOP_RIGHT -- 3
  OUT_TOP_MID -- 10
  OUT_BOTTOM_LEFT -- 12
  OUT_RIGHT_BOTTOM -- 20
  OUT_LEFT_TOP -- 15
  IN_BOTTOM_RIGHT -- 6
  OUT_TOP_RIGHT -- 11
  OUT_BOTTOM_RIGHT -- 14
  IN_TOP_LEFT -- 1
  IN_LEFT_MID -- 7

but STATE is a member of btn:

>>> help(lvgl.btn.STATE)
object <class 'LV_BTN_STATE'> is of type type
  TGL_REL -- 2
  NUM -- 5
  TGL_PR -- 3
  PR -- 1
  INA -- 4
  REL -- 0
>>> 

@amirgon Thank you very much! I will try it on Sunday!

@kisvegabor I have a problem with callbacks (lv_action_t).

What I'm missing is a context (also called "user_data" or "cookie" on some implementations).
The common practice is to allow the user provide a context (a void* parameter) when registering the callback and pass this context back to the user when calling his callback.
"context" is used by the user to know in which context the function was registered. Often the user wants to register the same function on different callbacks (for example, a single click handler for many button objects), and handle the the callback according to the context it was registered.
"context" is useful in many occasions. For example when using C++ it could be used to pass a pointer to the object and convert the callback from global function to object's method.

In the case of Micropython, I cannot "generate" new functions (code) on run-time. So I must register the same function for different instances of the same object. To save code size, I would prefer to have one callback globally that calls Micropython according to context.

I see several options to solve this:

  1. Add context to lv_action_t. That would require changing its signature to:
typedef lv_res_t (*lv_action_t) (struct _lv_obj_t * obj, void *context);

Then, to register the callback, the user would provide the context. For example:

void lv_btn_set_action(lv_obj_t * btn, lv_btn_action_t type, lv_action_t action, void *context);

lvgl code would need to save the context when registering the callback and pass it when calling the callback.

  1. Previous option is non backward compatible. I don't know how important this is on lvgl, but if it is we could add new callback type and new registration functions that mirror the original ones and include context. I don't like this option since it bloats lvgl code and makes it redundant.

  2. Keep the existing callbacks. I can still implement limited callbacks without it but it will cost more code space, more RAM and be less flexible. To do this I would need to add a member to lv_obj_t that correlates the lvgl object back to a Micropython object. When a callback is registered I would need to register different callback function for each callback type, and use this information (mpy object and callback type) to know which callback I should call. The disadvantages of this options are: requires more RAM, requires more ROM (many callback functions in the code), and it would allow only one callback per object per callback type (for example you can't register multiple callbacks and call them all when the event happens. I don't know if you do this anywhere on lvgl).

Until now I managed to create Micropython extensions without changing lvgl code. I think that in order to support callbacks some change to lvgl code would need to be done.

Please let me know what you think and which option you prefer.

@amirgon The issue you describe sounds similar to #316 and the solution would probably apply to both.

I think your idea of adding a context argument is probably a simple and clean solution. @kisvegabor and I recently had a brief discussion about changes that break compatiblity: #543 (comment)

@embeddedt while the problem sounds similar, the suggested solution of #316 is different. Adding something like lv_action_id is not general enough for callbacks.
I would prefer the generic solution of context/user_data parameter on each callback. It's also simpler and clear for anyone who ever worked with callbacks on other libraries.

Actually I noticed this is already used on lvgl on some places! For example, lv_indev_data_t contains user_data which was passed originally when registering the driver by lv_indev_drv_init.
Why not generalize user_data for all callbacks?

@amirgon

Test results

I made it work on PC. Finally, I used SDL like the normal PC simulator. I also tested the new enums.

label1 = lvgl.label(lvgl.scr_act())
label1.set_text("Hello world")
label1.align(lvgl.scr_act(), lvgl.ALIGN.CENTER, 0, 0)

However, I faced an issue. lv_mp_init(); was not called during import lvgl.
To make it work I added lv_mp_init(); to lv_init().
And I added lv_task_handler to the monitor refresh thread.

My Micropython project with SDL

I uploaded my SDL-Micropython project here: https://drive.google.com/file/d/16htsMz3L1knvfHWdPsmwg3YyiOtSbpF0/view?usp=sharing

Instructions:

  1. Install: libffi-dev and libsdl2-dev and clone micropython
  2. Go to: micropython/linux/micropython/ports/unix
  3. make (ignore the warnings :) )
  4. ./micropython it should start Micropython COmmand Line interface
  5. import lvgl
  6. lvgl.init() it should open a new white window
  7. label1 = lvgl.label(lvgl.scr_act())
  8. label1.set_text("Hello world")
  9. ...continue testing... :)

Actions

You can use lv_obj_set_free_ptr(obj, &context_data) and lv_obj_set_free_num(obj, some_id) to add an arbitrary pointer or number to an object which can be get in the action using lv_obj_get_free_ptr() and lv_obj_get_free_num(). It's similar to the user_data at input devices.

@kisvegabor I'm glad to hear you had a success with Micropython!

Regarding lv_mp_init(), it's registered as __init__ on mp_module_lvgl globals, so it should be called automatically (and is actually called on my application on ESP32).
I'm not sure what the problem on your side is, but please make sure you are using a relatively recent version of Micropython. I think it was not called on older versions of Micropython.
Another option to work around this it is to call lvgl.__init__() explicitly. Did you try this?

Currently I'm developing and testing Micropython integration with lvgl on ESP32, I think it's good you are trying this integration on another platform.

Regarding Actions -

Using free ptr is very much like option (3) I suggested above (using an existing lv_obj_t data member instead of adding a new one).
The disadvantage of using free ptr instead of context parameter is that once the callback is called, I can know which object it relates to, but not which specific callback was registered.
So if the same object has many callbacks, I have to define a function for each instead of using a single function to handle callbacks.
As I explained above, implementing it this way would require more RAM, require more ROM (many callback functions in the code), and it would allow only one callback per object per callback type (for example you can't register multiple callbacks and call them all when the event happens. I don't know if you do this anywhere on lvgl).

I'm using v1.9.4 which is the latest release. lvgl.__init__() is working. Strange.
In mpconfigport.h I registered lvgl like this
{ MP_OBJ_NEW_QSTR(MP_QSTR_lvgl), (mp_obj_t)&mp_module_lvgl },
Is it correct?

With @embeddedt we are talking about having only one action callback per object but with an enum parameter about the action type, such as LV_ACTION_CLICK, LV_ACTION_PRESS etc.

So the signature would be:

  • Register a action: void lv_obj_set_action(lv_obj_t * obj, lv_action_t action);
  • The callback: lv_res_t my_action(lv_obj_t * obj, lv_action_t action)

This way the user has the opportunity to have a free_prt like this:

static void * contexts[LV_ACTION_NUM];
contexts[LV_ACTION_CLICK] = &x;
contexts[LV_ACTION_PRESS] = &y;

lv_obj_set_free_ptr(obj, contexts);

So we can decide now which direction to choose for v6.0 where we can have API breaking changes. Please let us know what do you think about the actions there to keep things in one place

@kisvegabor

In mpconfigport.h I registered lvgl like this
{ MP_OBJ_NEW_QSTR(MP_QSTR_lvgl), (mp_obj_t)&mp_module_lvgl },
Is it correct?

Looks ok. I do this in a similar way, see here.

I found it. I needed to add #define MICROPY_MODULE_BUILTIN_INIT 1 to mpconfigport.h. This define was complately missing from the unix port.

The updated SDL project with mouse support is here: https://drive.google.com/open?id=1YjuaEFBrj1E0OVYZ_fX2wPvoaC_4Gw42

@kisvegabor

I found it. I needed to add #define MICROPY_MODULE_BUILTIN_INIT 1 to mpconfigport.h. This define was complately missing from the unix port.

Nice. I wasn't aware of this macro.

I downloaded, built and ran your linux port of mpy_sdl. Very nice.
A few comments:

  • I needed to install libffi-dev and libsdl2-dev for building it. Worth adding to the instructions.
  • While TAB completion works well, I noticed there is no 'help' function. Here is why. But my ESP32 have it, probably also other ports. Instead of help you can use something like print('\n'.join(dir(lvgl))) (or put any lvgl class/object in the dir instead of lvgl)

What do think would be the minimal set of additional features Micropython-lvgl-binding should have for a PR?

I was thinking of:

  • Callbacks (lv_action_t)
  • Styles
  • Specific structs such as lv_color_t

Also, I think we should add gen_mpy.py script to the Makefile. It would generate different outputs depending on v_conf.h (and also any change in the lvgl obj H files), so I don't think we want lv_mpy.c commited in git, in the long run.

Thank you for the comments. I updated my previous post.

Yes, I agree if actions, styles, and structures would be added we can already add it to lvgl. It will allow getting feedback from people. I'll add it the PC simulator too in a cleaner way than my current SDL project :)

However, I think we can commit lv_mpy.c too. If the parts would be protected with #ifs according to lv_conf.h we can leave it. Or why not leave it? Is there any disadvantage of leaving it in the code? IMO making it visible and instantly accessible is a good point.

@kisvegabor I think we can leave lv_mpy.c only as an example. The problem is that if the user changes lv_conf.h and removes some feature, lv_mpy.c will stop compiling as it assumes this feature is present.
Adding #ifs according to lv_conf.h is hard to do automatically in gen_mpy.py . The reason is that the C parsing can only be done after C-preprocessing, after which these #ifs are no longer present in the code.

@amirgon I see. In this case, as you suggested, let's leave a full featured lv_mpy.c as an example.

@kisvegabor @amirgon I think leaving an autogenerated file in Git is a bad idea. Personally I prefer to be able to compile LittlevGL+Micropython without having changes in my working tree.

https://stackoverflow.com/a/894042

@embeddedt I only meant leaving it there as an example, think of it as a text file for reference which is not meant to be compiled.
Generated C files are not like obj files. You may want to browse lv_mpy.c to see which functions were generated, which were not and why (it contains many comments, for this purpose) even before you sync from git and build your project.
I still think we should have a makefile and run gen_mpy.py as part of the build. You will not have changes in your working tree because the committed example will not be committed to the same directory where the file is generated.

the committed example will not be committed to the same directory where the file is generated.

In that case there is no issue.

@amirgon How "easy" should be also support Circuitpython?

@C47D

@amirgon How "easy" should be also support Circuitpython?

I'm not familiar with Circuitpython, but from what I read it is a derivative of Micropython. The differences from Micropython seem small enough such that you might be able to use lvgl Micropython binding as is without a change! (I didn't see any difference in the way new extensions are added).

Why don't you try and let us know how it goes?

@kisvegabor Another problem with callbacks -

I noticed that some of the callbacks return a value (most of them seem to return lv_res_t).
In Micropython, a callback from C (usually invoked by interrupt handlers to read sensor values etc.) is not called immediately. It should be scheduled for execution by the Micropython engine and will be executed as soon as current atomic operation completes and it's safe to execute it.
So in other words, when a callback is called from C, it will return before Micropython actually executes the callback (unless we assume the existence of some task scheduler that would allow us to block the C callback until the Python callback completes, but I don't think we want to do that).

So, I would like to know your opinion:

  • Is it ok to return LV_RES_OK automatically from all callbacks, since the Python callback will only run in the future?

  • Are there any callbacks that return a meaningful value? (lv_anim_path_t? lv_img_decoder_open_f_t? Some other callback functions?)
    What do you think we should do about those?

In case of actions LV_RES_INV means that the object is deleted in the action and it's invalid when the action returns. (Use case: a button is pressed to change screen -> the current screen is deleted along with the button, and the new screen is created in the action). However, as you pointed out if the action is called later the object will be still sure valid even if it's deleted later.

The other functions might have a meaningful return value. lv_anim_path_t is a good example for that. However, I think it's not a big issue if custom animation paths or image decoder interfaces can't be declared in Micropython. IMO, only actions will be used in 99.9% of the cases.

The ultimate solution would be to stop C code while the Micropython callback is executed but you said: " I don't think we want to do that". Do you concern because of the delay in execution? (I don't know how big it is)

By the way, how should we call (and/or protect) lv_task_handler() from the scheduled call of actions? I mean if an action LittlevGL related functions are executed while lv_task_handler also works with the same resources things will be messed up. In "normal" case you should use a mutex around lv_task_handler and your lv_... function calls. Or is Micropython thread-safe in this regard?

@amirgon
I'll be off from work until next Friday. See #543 (comment)

Until Friday, maybe it would be more practical to deal with styles if too many questions arise about actions and callback. Styles seems more straightforward to me.

@kisvegabor

I'll be off from work until next Friday. See #543 (comment)

Congratulations! Good luck to you and your wife with your new baby!

@amirgon Thank you :)

@kisvegabor

The ultimate solution would be to stop C code while the Micropython callback is executed but you said: " I don't think we want to do that". Do you concern because of the delay in execution? (I don't know how big it is)

In the general case, we can't assume the platform is multi-threaded. Micropython can run on a single thread and schedule its action using its internal scheduler. In such case you are not allowed to block ("stop") the C code.
Even if you are in a multi-threaded platform, Micropython usually runs in a single thread. In such case you are not allowed to run a Micropython callback from another thread (AFAIK).

By the way, how should we call (and/or protect) lv_task_handler() from the scheduled call of actions? I mean if an action LittlevGL related functions are executed while lv_task_handler also works with the same resources things will be messed up. In "normal" case you should use a mutex around lv_task_handler and your lv_... function calls. Or is Micropython thread-safe in this regard?

This is a general problem, not related only to Micropython, right?
How do you protect it today, without Micropython?
I'm using FreeRTOS and I call lv_task_handler periodically from another task (thread). Should I protect every call to lvgl by mutex (as well as lv_task_handler) to make sure they never preempts each other?


So here is a quick update: I've added limited support for actions (callbacks).

Here is an example I just ran on my board:

>>> import lvgl
ILI9341 initialization.
Enable backlight.
>>> b = lvgl.btn(lvgl.scr_act())
>>> b.align(None, lvgl.ALIGN.CENTER, 0, 0)
>>> def f(arg):
...       print('Button clicked')
...     
...     
... 
>>> b.set_action(b.ACTION.CLICK, f)
>>> 
>>> 
>>> Button clicked
Button clicked
Button clicked
Button clicked
Button clicked

Limitations:

  • It uses the internal Micropython scheduler (calls mp_sched_schedule to schedule a callback).
    Apparently the Linux port of micropython does not enable the scheduler. I tried (naively) to enable it and the thing crashed. I'm not sure why and what is the effort to support this on the Linux port.

  • Callbacks return before the action actually executes, so we assume actions never fail. (This is also related to the usage of the Micropython scheduler).

  • It only supports callbacks with certain prototype. (need to return lv_res_t, first argument of callback function must be lv_obj_t, all arguments are convertible to python). Luckily, action callbacks (lv_action_t, lv_btnm_action_t , lv_tabview_action_t) use the convention and are supported.

  • It only supports one callback per object instance. So don't try to register more than one action for the same button. This is in line with #316 for v6.0 where you plan single callback for each object instance anyway.

@amirgon

I'm using FreeRTOS and I call lv_task_handler periodically from another task (thread). Should I protect every call to lvgl by mutex (as well as lv_task_handler) to make sure they never preempts each other?

Yes, you should use a mutex. See: https://doc.littlevgl.com/#Porting (Using with an operating system
section)

Actions
As the return value is used only to indicate that the object is deleted in the action, in v6.0 I can add some improvements to automatically detect if the object was deleted. So the library will know if the object was deleted in the action and handle it as invalid. Would it solve the issue?

@kisvegabor

Yes, you should use a mutex. See: https://doc.littlevgl.com/#Porting (Using with an operating system
section)

To avoid a mutex, I would like to use Micropython scheduler same way as callbacks (call mp_sched_schedule to schedule a call to lv_task_handler every X milliseconds). This is how Micropython implements machine.Timer. Unfortunately this is platform specific, so every platform would need to handle this differently. On ESP32 for example, I'm creating a FreeRTOS task that calls mp_sched_schedule periodically. The creation of the FreeRTOS task (as well as registering the display and touch drivers) should be done in C on lv_mp_init implementation which is board specific.

As the return value is used only to indicate that the object is deleted in the action, in v6.0 I can add some improvements to automatically detect if the object was deleted. So the library will know if the object was deleted in the action and handle it as invalid. Would it solve the issue?

That sounds good. Does it mean we can change action function prototype to return nothing? (void instead of lv_res_t), That would solve the problem.
Now I'm always returning LV_RES_OK (when mp_sched_schedule succeeds) . How would that affect the user?

To avoid a mutex, I would like to use Micropython scheduler same way as callbacks (call mp_sched_schedule to schedule a call to lv_task_handler every X milliseconds).

It sounds good but you said mp_sched_schedule is not always enabled. Like it's not enabled in the unix build either.
BTW, in lack of mp_sched_schedule in the unix port I won't be able to test the actions on PC, right?

Now I'm always returning LV_RES_OK (when mp_sched_schedule succeeds) . How would that affect the user?

If the action callback is executed later i.e. not when you click the button then it still should work. However, you can try it if you call lvgl.obj_del(btn) in the action callback. You should set LV_MEM_ADD_JUNK 1 in lv_mem.c to fill the freed memories with junk to be sure the system fails is it wants. :)

@kisvegabor

It sounds good but you said mp_sched_schedule is not always enabled. Like it's not enabled in the unix build either.

I think we must enable Micropython scheduler in order to use Actions. Since Action is a Micropython callback, it cannot be called from another thread or in the middle of another Micropython command, so it must be scheduled correctly. For that reason I think we should provide Micropython lvgl biding only under the assumption that Micropython scheduler is enabled.
If we have the scheduler enabled for Actions, we might as well use it for lv_task_handler calls instead of using a mutex on every lvgl Micropython extension function.

BTW, in lack of mp_sched_schedule in the unix port I won't be able to test the actions on PC, right?

I'm not sure about this. When I tried enabling it (setting MICROPY_ENABLE_SCHEDULER macro) it crashed, but I didn't try to debug it much.
On the other hand, I noticed that on the unix Micropython port there are has several mpconfigport.h files that can be used, one of them defines MICROPY_ENABLE_SCHEDULER as 1, so it may be possible to enable the scheduler on the unix port after all. I think it's worth another try.

@kisvegabor

Good news!

I gave it another try, and the unix port supports the Micropython scheduler. I was able to run my example of Action above with your unix port!
Here are the main issues I came across:

  • The crash was not related to Micropython really. This was because in your code you changed lv_mpy.c and called lv_init in the generated file. Once I re-generated lv_mpy.c, lv_init was no longer called and that was the reason for the crash. So please.... don't change generated files....

  • Needed to add a periodic call to mp_sched_schedule which runs lv_task_handler.

  • Needed to change the read from stdin to nonblocking and add calls to mp_handle_pending which were missing in the unix port. Otherwise, Micropython is not called upon async events.

  • I used the mpconfigport_coverage.h file for configuring Micropython with scheduling.

  • I use a slightly older version of Micropython with my ESP32 board. To make lv_mpy.c compile I needed to change the calls to mp_sched_schedule (remove the last parameter). I'm planning to change gen_mpy.py to support both prototypes of mp_sched_schedule.


I suggest that instead of a zip file on google drive, add a project on github. Now I would like to show you my changes to your source code and I can't really use git diff. I'll try to manually paste the interesting parts, hopefully this would help you run it on your side:

Make command:

make CFLAGS_EXTRA='-DMP_CONFIGFILE="<mpconfigport_coverage.h>" -DMICROPY_USE_READLINE=1' DEBUG=1 -j4

Changes on lvgl/mpy/lv_mpy_hal.c:

#include "py/runtime.h"
#include "py/obj.h"

STATIC mp_obj_t mp_lv_task_handler(mp_obj_t arg)
{
    lv_task_handler();
    return mp_const_none;
}

STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_lv_task_handler_obj, mp_lv_task_handler);


/**
 * A task to measure the elapsed time for LittlevGL
 * @param data unused
 * @return never return
 */
static int tick_thread(void * data)
{
    (void)data;

    while(1) {
        SDL_Delay(5);   /*Sleep for 5 millisecond*/
        lv_tick_inc(5); /*Tell LittelvGL that 5 milliseconds were elapsed*/ 
	mp_sched_schedule((mp_obj_t)&mp_lv_task_handler_obj, mp_const_none);
    }

    return 0;
}

...

void lv_mp_init(void)
{
    lv_init();
    hal_init();
}

Changes on unix_mphal.c (inputAvailable taken from here)

static bool inputAvailable()  
{
  struct timeval tv;
  fd_set fds;
  tv.tv_sec = 0;
  tv.tv_usec = 0;
  FD_ZERO(&fds);
  FD_SET(STDIN_FILENO, &fds);
  select(STDIN_FILENO+1, &fds, NULL, NULL, &tv);
  return (FD_ISSET(0, &fds));
}

int mp_hal_stdin_rx_chr(void) {
    unsigned char c;
#if MICROPY_PY_OS_DUPTERM
    // TODO only support dupterm one slot at the moment
    if (MP_STATE_VM(dupterm_objs[0]) != MP_OBJ_NULL) {
        int c;
        do {
             c = call_dupterm_read(0);
        } while (c == -2);
        if (c == -1) {
            goto main_term;
        }
        if (c == '\n') {
            c = '\r';
        }
        return c;
    } else {
        main_term:;
#endif
	while (!inputAvailable())
	{
	    mp_handle_pending();
	    usleep(5);   
	}
        int ret = read(0, &c, 1);
        if (ret == 0) {
            c = 4; // EOF, ctrl-D
        } else if (c == '\n') {
            c = '\r';
        }
        return c;
#if MICROPY_PY_OS_DUPTERM
    }
#endif
}

@amirgon I'm not familiar with MicroPython's scheduler. Does it require the platform to have thread support?

@embeddedt
No. Micropython runs on a single thread no matter if MicroPython's scheduler is enabled or not.
MicroPython scheduler's job is to schedule an async operation (triggered by an interrupt, for example, or another thread) without interrupting current Micropython operation.

@amirgon
Awesome news! ๐ŸŽ‰

I've created a repo here: https://github.com/kisvegabor/lv_mpy

I applied your changes and added your lvgl repo as a submodule in the lib folder. I also added lvgl to py.mk similarly to other libs (like lwip)

The only issue was the extra NULL argument in mp_sched_schedule. I've committed the unchanged generated file (because it's not good the edit generated the files ๐Ÿ˜… ) so you need to delete the third argument by hand.

@amirgon

No. Micropython runs on a single thread no matter if MicroPython's scheduler is enabled or not.

No problem then. :)

@kisvegabor

I've created a repo here: https://github.com/kisvegabor/lv_mpy

Fixed and tested that Actions are working. See PR.

Merged, thank you!

I suggested releasing v5.3 (#543) and start to work on v6.0. So we will have more freedom to customize lvgl if needed.

@kisvegabor

I suggested releasing v5.3 (#543) and start to work on v6.0. So we will have more freedom to customize lvgl if needed.

How about releasing it on v5.3 before I add support for styles and structs? (actually I'm going to add support for generic structs first, as it's the general case of styles, hopefully next week), or do you want to postpone Micropython for v6.0? What is the schedule for v6.0 release?

The advantage of releasing it early is letting the users know about this, and giving them the opportunity to try it out even if some features are still missing.

What do we do about Makefile/build script that would run gen_mpy.py? Or do you suggest the user would run it manually, not as part of an automatic build process?

I vote new branch for python implementation.

How about releasing it on v5.3 before I add support for styles and structs? (actually I'm going to add support for generic structs first, as it's the general case of styles, hopefully next week), or do you want to postpone Micropython for v6.0?

Good point, I agree to add Micropython support in v5.3 to allow people to try it.

What is the schedule for v6.0 release?

There is no strict deadline. But I'm sure it will take at least 3 months to add all the planned features.

What do we do about Makefile/build script that would run gen_mpy.py? Or do you suggest the user would run it manually, not as part of an automatic build process?

The call of gen_mpy.py should be added to the Micropython's Makefile, right? So when you build Micropython for e.g. ESP32 or UNIX always get an up to date binding.

Another thing:
@rreilink added "normal" Python binding to LittlevGL. Now he is creating an example to test the binding.

So if we'd have more bindings I suggest having an lv_bindings folder where we can add the interface files and scripts.

@turoksama

I vote new branch for python implementation.

My view was to release Micropython binding with v5.3 as it is and update it in dev-6.0 after the release of v5.3. What would be the advantage of using a separate branch instead of keeping things in one place? Because a bug in the binding does not break LittelvGL itself.

It sounds good to bind as what you said.
One thing i concerned was integrity of the library, and it seems too much.

@turoksama

One thing i concerned was integrity of the library, and it seems too much.

What do you mean "integrity of the library"? Let's say there is some bug. lvgl is not affected since Micropython binding does not change anything in lvgl sources, it's just a python script that creates a .c file. So the worst thing that can happen is that someone who tries to use Micropython binding would see some problem (specific to Micropython), and that's a good thing as he would report this and we would fix that early.

Another thing:
@rreilink added "normal" Python binding to LittlevGL. Now he is creating an example to test the binding.

I wish we knew about this earlier... ๐Ÿ˜’
I don't see a point in making the same work twice. What's the point in two different implementations that achieve the same goal?

@kisvegabor I don't think it's a good thing for lvgl to integrate two different implementations for Micropython. How would the user select one? Why maintain two scripts instead of one? Why provide two different python interfaces for the same library?

@amirgon
The work of @rreilink is for Python3 and as I see there are some parts where you need to use Micropython specific things (like the scheduler).

In addition, with a good integration into the Micropython environment, maybe LittlevGL can be integrated into Micropython. At least it's worth to ask it. It would be good for the first benefit you mentioned: "Exposure to a larger audience in the embedded development world."

@kisvegabor
Python and Micropython, from the user perspective, are not so different. At least not from the core language aspect.
A user that uses Python binding and wants to migrate to Micropython would have to change his code since the API looks different, things are implemented differently etc.

Ideally we could think of one python script that could generate both Python and Micropython binding, and most of its code would be common for both.
Benefits of such script are unified python API for lvgl from the user perspective, single source code to add features / fix bugs related to binding etc.

@amirgon, @kisvegabor

I wish we knew about this earlier...

Yes, I do to ๐Ÿ˜’

Ideally we could think of one python script that could generate both Python and Micropython binding, and most of its code would be common for both.
Benefits of such script are unified python API for lvgl from the user perspective, single source code to add features / fix bugs related to binding etc.

I totally agree.

I have not looked into the MicroPython code generation, I will do so this week, and propose a way to integrate the two bindings-generators to achieve what you propose: unified API and the most commonality in the source code.

@amirgon @rreilink If you guys believe that the same script can work for Micropython and Python too, of course, it would be the best! ๐Ÿ˜„

@amirgon
@rreilink added two examples with Python3. You can check them here: https://github.com/rreilink/pylvgl

@kisvegabor
In some places you have #defines instead of an enum.
For example, colors and opacities:

#define LV_COLOR_WHITE   LV_COLOR_MAKE(0xFF,0xFF,0xFF)
#define LV_COLOR_SILVER  LV_COLOR_MAKE(0xC0,0xC0,0xC0)
#define LV_COLOR_GRAY    LV_COLOR_MAKE(0x80,0x80,0x80)
...
#define LV_OPA_TRANSP    0
#define LV_OPA_0         0
#define LV_OPA_10        25
#define LV_OPA_20        51

What is the reason for that?
Since I preprocess the sources with standard C preprocessor, I can't parse defines. I can only parse true C constructs (such as enums).
It would help a lot to converting them to enums.
What do you think?

@amirgon
Defines like LV_OPA_... can be easily turned to enums. But I'm not sure about LV_COLOR_... because they cover ((lv_color_t){{b8, g8, r8, 0xff}}) definitions. So LV_COLOR_... are rather macros then constant defines.
Defining colors as const lv_color_t would help?

@kisvegabor Not sure that all C compilers will optimize const lv_color_t to a constant value. C++ compilers would but LittlevGL is not a C++ project.

@amirgon I think colors could safely be duplicated between the Python binding and LittlevGL itself.

@kisvegabor @amirgon

I am currently refactoring my code to use pycparser as @amirgon does (I like that approach better than my flaky regex parser). pycparser comes with a C-preprocessor that is implemented in Python using PLY. Using that preprocessor, I am able to also extract all defines from the source code. I'll try to upload some code tonight.

@rreilink

Using that preprocessor, I am able to also extract all defines from the source code

That would definitely help ๐Ÿ˜€
I'll check it out.

@kisvegabor @amirgon
See sourceparser.py for how I managed to use the pycparser C-preprocessor to parse the lvgl source.

I completely refactored my bindings-generator, using pycparser. I also split out language-specific and the generic code, which would allow me to merge @amirgon 's code

Next thing I'll do, is see if I can generate an lv_mpy.c identical to @amirgon 's from the same bindings generator. That should pave the way towards unification of the two codebases.

But first I'll help my girlfriend prepare some Christmas deserts tomorrow ๐Ÿ˜€

@rreilink Happy to hear this great progress! ๐Ÿ‘ I'm curiously waiting for @amirgon 's thoughts ๐Ÿ˜„

But first I'll help my girlfriend prepare some Christmas deserts tomorrow

Yeah, not always just working ๐Ÿ˜„

See sourceparser.py for how I managed to use the pycparser C-preprocessor to parse the lvgl source

@rreilink Did you consider contributing to pycparser instead of extending pycparser.ply.cpp.Preprocessor with your CPP class?

@kisvegabor
Oh, it's Christmas time!
Merry X'mas.
Have a NICE VACATION.

@rreilink Did you consider contributing to pycparser instead of extending pycparser.ply.cpp.Preprocessor with your CPP class?

@amirgon I had not, but I think it's a great idea so I'll do that (at least for the generic issues that are solved by my extension)

@amirgon I am trying to reproduce the lv_mpy.c file from your repo, but I cannot get the same result. I am using python2.7, and am executing the command as mentioned in lv_mpy.c, within amirgon/lv_mpy, revision e8954c6 . See attached diff: diff.txt

(I am using the lv_mpy repository since the generated file depends on an lv_conf.h, which is not in the amirgon/lvgl repository)

Any ideas?

@amirgon I am trying to reproduce the lv_mpy.c file from your repo, but I cannot get the same result.

@rreilink It seems you are not running the script on the same lvgl version I was running it to generate my output file. For example, on your version there was calendar object, which is missing from my version.

Please try to follow this:

You should get the same result: lv_mpy.c on the same directory would be generated the same.

I'm working now on processing structs generically (which would also take care of styles) so expect lots of more changes ๐Ÿ˜‰

EDIT: I have found the issue. I needed to define USE_LV_LOG, USE_LV_CALENDAR and USE_LV_IMGBTN to 0 in lv_conf.h to get an identical result: lv_conf.txt

Hi @amirgon,

I had already tried using your lv_mpy repo. I have run the command exactly as specified in lv_mpy.c, and I do get the extra calendar object found. (Which seems to make sense as it is indeed present in the lvgl version that is in your repo).

A couple of things come to mind:

  • What Python version are you using? The generated file depends on the order of items in a dictionary, which is undefined in Python <3.6

  • Do you have pycparser also installed on your Python installation? Since you append โ€˜./pycparserโ€™ to sys.path instead of inserting it at the front

  • Could you try to see if with a clean checkout, you still get the same lv_mpy.c that is in the repo?
    gen_mpy.txt

  • Could you try to use Python 3.6 or 3.7 to generate lv_mpy.c and send me the result? There is only one change required to make your gen_mpy.c work under Python 3 (and still keep it working under Python 2):

-s = subprocess.check_output(pp_cmd.split())
+s = subprocess.check_output(pp_cmd.split()).decode()

(patch is attached)

If you are starting on the structs, I have already done quite some stuff for getting the styles to work under normal Python, so Iโ€™d be happy to help you out there; be sure to have a look in my pylvgl repo.

Iโ€™d like to get to integrating our efforts. My first step would be to merge the code generating scripts but still generate exactly the same code; so thatโ€™s why I am trying to replicate lv_mpy.c .

Regards

Rob

@kisvegabor @amirgon

I have managed to combine the efforts of amirgon and myself into a template-based bindings generator that generates the same lv_mpy code as amirgon's script does. It can be found here.

Within the process, I have come across several things that amirgon and myself have approached differently. I think we should try to agree on one single approach, and I would like to get your opinion on these. I have listed my personal preference at the bottom. Please let me know you thoughts.

  1. How to access enums?
    a) lvgl.LV_CURSOR_NONE
    b) lvgl.LV_CURSOR.NONE
    c) lvgl.CURSOR_NONE
    d) lvgl.CURSOR.NONE (current lv_mpy)

  2. Where to store object-specific enums like LV_DDLIST_STYLE_SEL?
    a) only store as module global (according to choice of 1) (current pylvgl)
    b) lvgl.Ddlist.STYLE_SEL (like 1c)
    c) lvgl.Ddlist.STYLE.SEL (like 1d) (current lv_mpy)

  3. in case of 2b/ 2c, also store a module global enum?
    a) yes
    b) no (current lv_mpy)

  4. what about inconsistently named enums?
    lv_mpy uses naming of enums using common prefix, however:

enum {
    LV_PRELOAD_TYPE_SPINNING_ARC,
};
typedef uint8_t lv_preloader_type_t;

--> enum is called LV_PRELOAD_TYPE_SPINNING

a) this is not an issue in case of only storing enum constants as module globals (1a + 2a)
b) lv_mpy method (common prefix) --> in this case LV_PRELOAD_TYPE_SPINNING or PRELOAD_TYPE_SPINNING
c) use typedef name, convert to upper-case style --> in this case LV_PRELOADER_TYPE or PRELOADER_TYPE

  1. Generate bindings for all functions which are not object-related?
    Some are completely irrelevant to Python (e.g. lv_mem_xx, lv_ll_xx), others may be useful e.g. lv_color_xxx_to_yyy
    a) no, only a few like scr_load etc (hal-related stuff would be done in c anyway) (current pylvgl)
    b) yes, but only as far as we can with those argument/return types we already deal with (current lv_mpy)
    c) yes, for all

  2. Which source files to use to generate bindings?
    lv_mpy uses declarations from H-files; pylvgl uses definitions from C-files. lv_mpy uses definition of lv_xxx_ext_t to determine ancestor; pylvgl uses lv_create_parent calls within the lv_create_xxx method to determine ancestor. Using C files takes a lot more time, since each C-file includes many H files, which need to be re-processed
    for every C file.
    a) Use H files (current lv_mpy)
    b) Use C files (current pylvgl)

  3. What to do for methods for which we cannot (yet) generate bindings?
    a) raise RuntimeError at runtime (current pylvgl)
    b) Do not generate mapping (method does not exist in Python/MicroPython); add remark in C source-file (current lv_mpy)
    c) Do not generate mapping, no remark in C source-file
    In any case, the bindings generator script should list which mappings were not generated

  4. How to access style struct members
    a) flattened: lvgl.style_plain.body_main_color (pylvgl)
    b) structured: lvgl.style.plain.body.main.color

  5. How to get children
    a) lv_obj_get_child/lv_obj_get_child (lv_mpy)
    b) Implement get_children, which returns a list (pylvgl)

My suggestion:

1d since it is the most Pythonic
2c since grouping the enums with the object keeps the module globals clean
3b since there should be only one obvious way to do things
4c since it is the cleanest; it allows for enums which have names which do not start with the enum name
5a since things like HAL, memory management, linked list (lv_ll) etc are not relevant for Python
6a since it is way faster and provides the same functionality
It would be good to add a test script that checks that C and H function definition and declaration are consistent
7a since it keeps the interface (object methods) the same if we support new argtypes/restypes. It also makes the
bindings generator code simpeler since there is no need to track for which methods code was generated and for which not
8a since it reduces the number of Python objects that need to be defined (only one for the Style struct; not one for every sub stuct)
9b since it is more Pythonic

alternatively: 1c / 2b

@rreilink it shows that you made some effort, I appreciate it.
On my side I'm still working and changing gen_mpy.py script, specifically, adding missing features such as structs and styles.

When writing gen_mpy.py, as a general guideline, I tried to keep my code as generic as possible: I don't care what a function or an object is doing, I just expose it syntactically to the Micropython user.
When running gen_mpy.py the user can configure the result by changing #defines on lv_conf and by providing input to the script (the -X option). But in the script itself, I tried hard to avoid references to lvgl specific types functions and objects.
Even for Style and Font, I'm making an effort to treat them as just another (more complex) struct and not handle them explicitly in the code.
The reason is that API tends to change and evolve, and changing the python script is harder than changing the inputs for it. It would make the script more "adaptable" to changes and additions.

@rreilink, what is your opinion about this? I noticed that your code makes more assumptions. For example, lv_coord_t lv_color_t and lv_opa_t appear explicitly as types, and you have an explicit Style object so you are probably treating it different than a generic struct.


About your questions:

(1), (2), (3), (8) - In general I prefer hierarchy whenever it's possible, that is the reason for the nested access to enums.
From the user perspective, clicking TAB (or help) and getting gazillion functions/globals/enums is less favorable than getting a shorter list of only the "top-level" ones and drilling down as necessary.
I'm also working on nested access to structs, even when the structs are nested in C. For example style.body.border.color. I'm not a big fan of "getters/setters" and prefer the more Pythonic "Attribute" access (so you could refer to a field in a struct more naturally, as you would in C for a struct field).

(4) I think we should consider solving inconsistencies by fixing lvgl itself. Looking forward, lvgl does not promise backward compatibility across major releases (see for example #316 where V6.0 would include a breaking change related to action handlers). Especially when this is a simple naming convention error.

(5), (9) As I mentioned, I tried not to care what is the purpose of the function. As long as it is defined in an H file that is given as an input to the script, it can probably be useful to the user. Even memory handling and file access functions. If the user would like to remove them, this can be done either by configuring lvgl not to preprocess them, or by some runtime flags to the script. Another option is some naming convention that would tell us that this is some "internal" function that we should not care about.
The downside of not knowing the function purpose is that it's harder to handle some cases in a more natural (or "pythonic") way (like your example of get_children, which returns a list). A solution for that specific problem could be defining and implementing some naming convention to describe iterators (and changing lvgl sources) such that the script could deduce the fact that this is an iterator automatically without knowing anything about the purpose of the function. The advantage would be that when a new iterator is added to lvgl it would be automatically supported.

(6) I think that H files should be sufficient for creating the binding. We only care about API, not implementation. The user provides the API H file to the script and the script generates the binding for that API.

(7) Ideally I would like to be able to generate "everything", the complete Python API for the given set of H files. Practically I'm working incrementally so some functions won't be generated (usually because of missing conversions, but possibly because of conventions or ambiguity issues). From the user perspective when a function is missing, the (power-) user would browse the C sources and try to understand what went wrong. A comment there ("we wanted to create that function and didn't do that because ____") is very useful.
I don't think the script should stop with an error when not able to generate something. It would make it less resistant to changes in lvgl. A small change in lvgl may cause some function not to be available. But the Python binding can still be useful even if it doesn't include everything.

@amirgon ,

I like the idea of minimizing the amount of 'lvgl specifics' into the script. Also, the idea of generalizing struct access seems a good idea to me. I am going to have a look on how to implement that in Python.

I'm not a big fan of "getters/setters" and prefer the more Pythonic "Attribute" access (so you could refer to a field in a struct more naturally, as you would in C for a struct field).

I agree. Struct access should be similar between C and Python, so attribute access it is.

(4) I think we should consider solving inconsistencies by fixing lvgl itself.

I agree, however if it is practical for the time being, we could implement a fix in the bindings generator for the time being I think (especially if we want this script to work for lvgl 5.x series where changing names is not so straight forward as for a new major release)

(5) ... Another option is some naming convention that would tell us that this is some "internal" function that we should not care about.
(6) I think that H files should be sufficient for creating the binding. We only care about API, not implementation.

I agree. I will change my script to use the H-files only. It will speed up processing, and it will ensure only the API is exposed. At least all internal functions that are not in the H-files will not be converted this way.

(7) ... I don't think the script should stop with an error when not able to generate something.

I agree. Maybe I was not clear, my script doesn't generate an error when it is not able to generate a binding; instead it generates a Python method which in turn raises an error when it is called. So my question here would be whether we generate a method that raises an error, or whether we generate no method at all.

Finally,

The downside of not knowing the function purpose is that it's harder to handle some cases in a more natural (or "pythonic") way (like your example of get_children, which returns a list)

For get_children, we could add it, aside from lv_obj_get_child.

I think we're on the same page for most or all of the issues, so I'll have a look into implementing these as discussed.

@rreilink

I agree, however if it is practical for the time being, we could implement a fix in the bindings generator for the time being I think (especially if we want this script to work for lvgl 5.x series where changing names is not so straight forward as for a new major release)

LittlevGL v5.3 is the last 5.x release and will be released somewhere in January (at most a month from now). Work will then start on v6.0. If you were willing to wait until February, we could skip Python support for 5.3 and make the necessary changes in 6.0.

@rreilink I fully agree with the approaches in your latest post.

I agree. Maybe I was not clear, my script doesn't generate an error when it is not able to generate a binding; instead it generates a Python method which in turn raises an error when it is called. So my question here would be whether we generate a method that raises an error, or whether we generate no method at all.

I vote for raising an error if an unimplemented lvgl function is referred. This way the user will know what is going on. Else he won't know why the method is not there and he could think that it has an other name, it's not implemented or it's just a configuration issue. So, in my opinion, it's clear to raise an error an tell the function is not implemented.

For get_children, we could add it, aside from lv_obj_get_child.

I think it's a good idea to make possible to add some custom functions too. get_children is a good example for it because it's much easier to use in Python than the original function in C. So I think it's worth to add it and probably others too.

@kisvegabor

I think it's a good idea to make possible to add some custom functions too. get_children is a good example for it because it's much easier to use in Python than the original function in C. So I think it's worth to add it and probably others too.

My opinion is that in many cases there is a better approach than "custom functions". For example, in case of get_children we can come up with a naming convention that will hint the parser that this is an iterator (it will require changing the relevant function name and function parameters on lvgl).
Based on this naming convention, the parser could automatically generate a list or a generator that will represent it in a more "pythonian" way.
The advantage of this over "custom functions" is that if someone ever wants to add a new iterator, we wouldn't need to add another custom function. As long as the iterator-naming-convention is kept, the parser could represent the new iterator correctly without any changes to the parser script.

What do you think?

@amirgon, @kisvegabor , regarding 'custom functions':

In general, I agree that 'generic' is better than 'custom'. However, while implementing pylvgl I have found that there are several places which are 'just a bit different than all the others', i.e. exceptions. Of course, one would want to avoid those if possible, but sometimes the extra effort of writing something generic does not outweigh the advantages.

For example, lv_btnm_set_map is the only function that takes a char ** as argument, which then also needs to be kept available (i.e. it cannot be on the stack). It needs to convert a Python list-of-str to char** and store a copy on the heap, and then of course free that memory when appropriate. See here for how I implemented that.

As a counter-example, I also implemented lv_label_get_letter_pos as a custom implementation, which might better be implemented in a generic way.

Concluding, I think we should strive for generic implementations, but decide on a case-by-case basis whether generic or custom is the best way to go.

@rreilink

Iterators
I agree naming conventions can work for iterators. For example, we can change:

  • lv_obj_get_child -> lv_obj_iterate_child
  • lv_obj_get_child_back -> lv_obj_iterate_child_back
  • lv_list_get_next_btn -> lv_list_iterate_btn
  • lv_list_get_prev_btn -> lv_list_iterate_btn_back

char * * in button matrix
A year ago tried to add Lua binding to LittelvGL and found that the most C specific thing in the library is the char ** variable of the button matrix. And now you found it also difficult to implement. So I'm open to get rid of it somehow or provide an other set function which allocates the map too.

generic / custom
In general, we can modify the names and even extend LittlevGL to better support generic generation and "compatibility" with high-level languages. I believe it will be sufficient in most of the cases. But as @rreilink said, "sometimes the extra effort of writing something generic does not outweigh the advantages". I hope it will be the minority of the cases.

@kisvegabor Didn't the API of lv_btnm_set_map also become a problem in #562 when we were discussing multi-language support?

@embeddedt Yes, it caused difficulties there too because it"s the only char ** in the lib and it's difficult the handle. Maybe using separator characters (e.g. \t -> "abc\tABC\t1\t2\t3") would solve both issues. This way we would have a simple string here too.

@kisvegabor What is the current behavior of lv_draw_label with regards to tabs? Are they ignored or printed as spaces?

@embeddedt Ignored. I thought then lv_btnm_design would have a harder work to extract the button's text from the long string.