Skip to content

LVGL - Internals~

Below are notes about the LVGL-Berry mapping in Tasmota. You will find information for curious people and maintainers.

Build system~

Berry mapping to LVGL is entirely automated.

Most of the components are generated C code from the LVGL's C source code, similar to MicroPython approach.

Phase 1: Parse LVGL source

This first phase parses most C headers from the LVGL source tree and generates two files: - lv_enum.h containing all the enum values from LVGL (constants) - lv_funcs.h containing all the functions of the LVGL API normalized to 1 function per line, and with cleaned argument signature.

(in folder Tasmota/lib/libesp32_lvgl/lv_berry/tools)

❯ python3 preprocessor.py

(no output)

Phase 2: Generate automatic Berry mapping

From the two files created in the previous step, all the requires C files are created for the Berry mapping.

(in folder Tasmota/lib/libesp32_lvgl/lv_berry/tools)

> python3 convert.py
| callback types['lv_group_focus_cb', 'lv_event_cb', 'lv_constructor_cb', 'lv_layout_update_cb', 'lv_obj_tree_walk_cb', 'lv_theme_apply_cb', 'lv_color_filter_cb']

The output should look as above, and indicates the C function that have been ignored (if any) if their return type is listed above. It also lists the callback types supported.

Phase 3: Generate the Berry pre-compiled stubs

This phase is specific to Berry pre-compiled modules and classes.

(in folder Tasmota/lib/libesp32/berry)

> ./gen.sh

(no output)

Phase 4: compile Tasmota using platform.io as usual

lv module~

Tasmota automatically and implicitly imports lv module if compiled with LVGL.

import lv

The lv module is solidified in Flash, so to make it extensible, there is a trick applied to it.

When you do import lv the first time, a hidden lv_new module is created in memory (writable) and a member function is added so that all members requested that are not part of lv_new are diverted to lv.

Concretely, this means that the new lv module is a facade to the read-only solidified lv module, but you can still add methods.

This is how it is done internally:

lv = module("lv")

# rename `lv` to `lv_ntv` and replace `lv` with `lv_tasmota`
def lv_module_init(lv_solidified)
  var lv_new = module("lv")   # create a dynamic module
  lv_new.member = lv_solidified.member
  # lv_new.lv_solidified = lv_solidified
  return lv_new
end

lv.init = lv_module_init

def lv0_member_ntv() end

lv.member = lv0_member_ntv

return lv

Tasmota then does import lv_tasmota to add all Tasmota specific extensions to module lv.

Constants~

The lv module is a placeholder for all LVGL constants, the equivalent of C enums.

As a rule of thumb, all C constants are mapped with a similar name. Just replace LV_<name> with lv.<name>.

Example: C API LV_LABEL_ALIGN_LEFT becomes in Berry lv.LABEL_ALIGN_LEFT

Implementation~

The C enum constants are all compiled in a single file tools/lv_berry/lv_module.h. Only names are listed, the actual values are retrieved by the C compiler at runtime (which avoids many mistakes).

Internally constants are handled by a virtual member in lvgl module. The module lvgl has a member() function that is called when the Berry runtime does not know the member name.

The search happens in lv0_member() which first searches for a static member name, and if not found, looks for a widget class name.

Constants are put in a C table in lib/libesp32/Berry/default/be_lv_lvgl_module.c as lv0_constants[]. The table is sorted by member name to allow for fast binary search (dichotomy).

const be_constint_t lv0_constants[] = {

    { "ALIGN_CENTER", LV_ALIGN_CENTER },
    { "ALIGN_IN_BOTTOM_LEFT", LV_ALIGN_IN_BOTTOM_LEFT },
   [...]
    { "WIN_PART_SCROLLBAR", LV_WIN_PART_SCROLLBAR },
    { "YELLOW", 16776960 },
};

Colors~

An exception for LVGL colors, they are defined as 32 bits RGB values as follows, and not based on their C representation:

COLOR_WHITE=0xFFFFFF
COLOR_SILVER=0xC0C0C0
COLOR_GRAY=0x808080
COLOR_BLACK=0x000000
COLOR_RED=0xFF0000
COLOR_MAROON=0x800000
COLOR_YELLOW=0xFFFF00
COLOR_OLIVE=0x808000
COLOR_LIME=0x00FF00
COLOR_GREEN=0x008000
COLOR_CYAN=0x00FFFF
COLOR_AQUA=0x00FFFF
COLOR_TEAL=0x008080
COLOR_BLUE=0x0000FF
COLOR_NAVY=0x000080
COLOR_MAGENTA=0xFF00FF
COLOR_PURPLE=0x800080

Example: lv.COLOR_RED

Widgets classes~

Although LVGL is C code and is not formally object oriented, LVGL widget follow an inheritance model. Each widget is a virtual subclass of lv_obj structure.

Berry builds an actual Object Oriented class system, with a base class lv_obj and subclasses.

The class names supported are defined in convert.py and are currently:

'lv_arc', 'lv_bar', 'lv_btn', 'lv_btnmatrix', 'lv_calendar', 'lv_canvas', 'lv_chart', 'lv_checkbox',
'lv_cont', 'lv_cpicker', 'lv_dropdown', 'lv_gauge', 'lv_img', 'lv_imgbtn', 'lv_keyboard', 'lv_label', 'lv_led', 'lv_line',
'lv_linemeter', 'lv_list', 'lv_msgbox', 'lv_objmask', 'lv_templ', 'lv_page', 'lv_roller', 'lv_slider', 'lv_spinbox',
'lv_spinner', 'lv_switch', 'lv_table', 'lv_tabview', 'lv_textarea', 'lv_tileview', 'lv_win'

Additional 'special' classes are (they do not inherit from lv_obj):

'lv_obj', 'lv_group', 'lv_style', 'lv_indev'

Parsing~

The parsing is done by convert.py which parses tools/lv_berry/lv_widgets.h. This file contains all the C function signatures as single lines. convert.py checks if the types are supported and converts it as a Berry signature.

The resulting signatures are used to generate class stubs for all Berry classes in lib/libesp32/Berry/default/be_lvgl_widgets_lib.c and the Berry signatures are in tasmota/lvgl_berry/be_lv_c_mapping.h

Example:

The C signature:

bool lv_obj_area_is_visible(const lv_obj_t * obj, lv_area_t * area);

is recognized to be part of lv_obj class (by prefix) and has the following signature:

{ "area_is_visible", (void*) &lv_obj_area_is_visible, "b", "(lv_obj)(lv_area)" },

Decomposed as: - "area_is_visible": name of the Berry method - (void*) &lv_obj_area_is_visible: pointer to the C implementation - "b": return type, here boolean - "(lv_obj)(lv_area)": input types, 2 arguments of classes lv_obj and lv_area

Other example:

void lv_btnmatrix_set_align(lv_obj_t * btnm, lv_label_align_t align);  

{ "set_align", (void*) &lv_btnmatrix_set_align, "", "(lv_obj)i" },

The parsing of the signature is done in be_check_arg_type()

Input and output types are:

  • "b": boolean
  • "s": string
  • "i": int (signed 32 bits)- ".": any type
  • "&": where n is a digit, Berry callback by class (see below)
  • "(lv_)": an instance of lv_. Note if you pass 0 (NULL) to a class argmunent it is accepted without warning.

Note: any missing argument or nil argument is converted to 0.

In case of an argument mismatch, a warning is printed but the call is still proceed.

Warning: you can easily crash Tasmota if you send wrong types arguments.

Widgets instantiation~

Instantiation of a widget is marked as a specific signature. The return type is prefixed with +:

lv_obj_t * lv_canvas_create(lv_obj_t * par, const lv_obj_t * copy);

{ "create", (void*) &lv_canvas_create, "+lv_canvas", "(lv_obj)(lv_obj)" },

All widgets constructor always take 2 arguments, the first is the parent object, the second is the copy object (generally null or ignored)

Example:

scr = lv.scr_act()
log = lv_label(scr)   # scr is parent object of log

Internally, widget constructors call lvx_init_2(). LVGL object are allocated by LVGL, the Berry object only contains a reference to the C structure (a pointer). These objects can be garbage collected without any impact.

lv_obj and widget constructors also accept a specific form: log2 = lv_label(-1, log) which just creates a second reference to the same LVGL object - it is mostly used internally to dynamically create an instance from a returned C pointed.

Callbacks~

Callbacks are a challenge in Berry. A callback is only a C pointer to a function and does not natively hold any other information. However we would like to match a single C address to multiple Berry closures.

We take into account the fact that the first argument of any LVGL callback has always an instance as first argument, from the type list: 'lv_group_focus_cb', 'lv_event_cb', 'lv_signal_cb', 'lv_design_cb', 'lv_gauge_format_cb'

We define 5 different C functions with 5 distinct addresses, one for each callback type. Then we use the first argument to dispatch the call to the appropriate Berry closure.

Here is the call used at startup:

import lvgl as lv

# for each callback type, mapping between first argument and closure
_lvgl_cb = [ {}, {}, {}, {}, {}, {} ]

# for each callback type, mapping between first argument and the actual Berry object with the correct type (C arguments are not explicitly typed)
_lvgl_cb_obj = [ {}, {}, {}, {}, {}, {} ]

def _lvgl_cb_dispatch(idx, obj, v1, v2, v3, v4)
  var func = _lvgl_cb[idx].find(obj)
  var inst = _lvgl_cb_obj[idx].find(obj)
  if func != nil
    return func(inst, v1, v2, v3, v4)
  end
  return nil
end

Styles~

lv_style is not a subclass of lv_obj but uses a similar mechanism to map the members.

Main difference, it uses a distinct constructor lvs_init().

Note: lv_style needs to allocate memory and must not be garbage collected. For this reason lv_style allocates static memory which is never freed. Be aware that it may be a cause of memory leak (although not very likely).

Colors~

lv_color is a simple class that maps RGB 32 bits colors (as 32 bits int) to the internal representation of colors (usually 16 bits).

Don't be surprised that getting back a value is the 16 bits color converted to 32 bits - rounding errors may occur:

[Berry Console]
> c = lv_color(0x808080)
> c
lv_color(0xff838183 - native:0x1084)

Note:

  • 0xff838183 - is the 32 bits color, with alpha channel (opaque)
  • 0x1084 - is the native internal representation as 16 bits color with swapped bytes

Groups~

lv_group behaves like lv_obj but does not inherit from it.

Indev~

Indev or 'Input Device' is a simple class wrapper to handle touch screens and input buttons. It is similar to lv_obj but uses a simple constructor lv0_init() that just wraps the C pointer into the Berry instance.