Theming widgets, finding LV_COLOR_DEPTH

I am trying to use micropython framebuffer driver(s) as backend for pure-python LVGL drivers. I would like to have an easy way to use existing uPy’s framebuf drivers (they are many for different chips out there) with LVGL, for testing and perhaps creating proper LVGL drivers later. Plus it helps me to understand how all this works. Performance is not the goal, obviously.

It works like this:

  1. Create LVGL display driver as usual.
  2. Create uPy framebuffer.
  3. LVGL flush fills the uPy’s framebuffer, and calls show() if flush_is_last().

This is a quick&dirty test with the (self-written) braille mono driver for terminal:

image

A few points I need help:

  • How do I access LV_COLOR_DEPTH from micropython, is it wrapped? I can get lv.color_t.__SIZE__ but it won’t distinguish depths 1 and 8, for instance.
  • Can I set theme per-widget, and can I set monochrome theme even with LV_COLOR_DEPTH==32 (provided LV_USE_THEME_MONO is enabled in lv_conf.h). Or do I need to build LVGL with LV_COLOR_DEPTH==1 to use the mono theme?
  • Is there a function to create lv.color_t from (the correct number of) bytes? I use a,r,g,b=struct.unpack('BBBB',buf) at the moment, but have to branch for different color depths.
  • Are there some (wrapped) buffer colorspace conversion functions? Those would be useful for converting the entire LVGL buffer to the format of uPy’s framebuffer.

Cool! Could you contribute your braille mono driver?

Two options:

  • Open a PR on LVGL to add LV_EXPORT_CONST_INT(LV_COLOR_DEPTH) on lv_color.h.
    That would expose this macro to Micropython
  • Deduce color depth by calling lv.color_to8 with different values and probing the results.

Not sure about this one, I think you should be able to use the monochrome theme without LV_COLOR_DEPTH==1 but I never tried to work with monochrome theme.
Perhaps @embeddedt or @kisvegabor could answer this.

You could use lv.color_hex, for example, to convert CSS hex color to lv.color_t.
I’m not sure what that would do on monochrome, though.

There are some conversion functions on imagetools.py: convert_rgba8888_to_bgra5658, convert_rgba8888_to_swapped_bgra5658, convert_rgba8888_to_bgra8888. We can probably add more conversion functions there if needed.

C conversion functions could be more efficient than the Viper ones, I’m not sure if LVGL provides anything like that out of the box.

Thank you for a thorough answer, I will get back later.

Cool! Could you contribute your braille mono driver?

I will just paste it here for now, for contribution I’d need perhaps more guidance. Obviously, it could be turned into LVGL-only driver (bypassing uPy framebuffer), but that was not the purpose of this exercise.

import sys, struct, framebuf, sys

class TerminalFramebuffer(framebuf.FrameBuffer):
    '''
    Simple monochrome driver for micropython. Can output to (unicode) console as braille
    unicode characters (4×2 matrix per character) or as characters; this is controlled by the
    *braille* parameter. The *curse* parameter controls whether ucurses is used to paint
    to a (fixed-size 80×20, this is a limitation of ucurses) window in the terminal; without
    curses, new frame will simply scroll the terminal.

    Width must be a multiple of 2, height must be a multiple of 4.
    '''
    @staticmethod
    def rgb(r, g, b):
        'Conversion for micropython-nano-gui'
        return int((r > 127) or (g > 127) or (b > 127))
    def __init__(self,width,height,braille=False,curse=False,fg=chr(0x2588),bg=chr(0x00b7)):
        if width%2!=0: raise ValueError("width must be a multiple of 2.")
        if height%4!=0: raise ValueError("height must be a multiple of 4.")
        self.width,self.height=width,height
        self.buffer=bytearray(self.height*self.width//8)
        super().__init__(self.buffer, self.width, self.height, framebuf.MONO_HMSB)
        self.curse=curse
        self.braille=braille
        self.fg,self.bg=fg,bg
        if self.curse:
            import ucurses
            self.screen=ucurses.initscr()
    def show(self):
        if self.braille:
            frame=[]
            for r4 in range(self.height//4):
                line=[]
                for c2 in range(self.width//2):
                    def bit(r_,c_):
                        bitno=(r4*4+r_)*self.width+(c2*2+c_)
                        return ((self.buffer[bitno//8]>>(bitno%8))&1)
                    v=(
                        bit(0,0)<<0|bit(0,1)<<3|
                        bit(1,0)<<1|bit(1,1)<<4|
                        bit(2,0)<<2|bit(2,1)<<5|
                        bit(3,0)<<6|bit(3,1)<<7
                    )
                    line.append(chr(0x2800+v))
                frame.append(''.join(line))
            frame='\n'.join(frame)
        else:
            frame=[]
            for y in range(self.height):
                frame.append(''.join([self.fg if self.pixel(x,y) else self.bg for x in range(self.width)]))
            frame='\n'.join(frame)
        if self.curse:
            self.screen.addstr(0,0,frame+'\n')
            self.screen.refresh()
        else:
            sys.stdout.write(frame+'\n')
            sys.stdout.flush()
    def __del__(self):
        if self.curse:
            import ucurses
            ucurses.endwin()



wd,ht=128,64
fbout=TerminalFramebuffer(width=wd,height=ht,curse=False,braille=False)

def disp_drv_flush_cb(disp_drv,area,color):
    global lv_buf
    DP=lv.color_t.__SIZE__
    # go pixel-by-pixel in lv_buf (lv.color_t) and write to fbout.buffer (framebuf.MONO_VLSB) via fbout.pixel(x,y,c)
    wd,ht=area.x2-area.x1+1,area.y2-area.y1+1
    # nested loops are obviously highly inefficient
    for r in range(ht):
        for c in range(wd):
            px=lv_buf[r*wd+c:r*wd+c+DP]
            if DP==4:
                a,r,g,b=struct.unpack('BBBB',px)
                mono=int(r>127 and g>127 and b>127)
            elif DP==1: mono=int(struct.unpack('B',px)[0]==0)
            else: raise ValueError('Unhandled lc.color_t.__SIZE__=%d'%DP)
            fbout.pixel(c+area.x1,r+area.y1,mono)
    if disp_drv.flush_is_last(): fbout.show()
    disp_drv.flush_ready()

import lvgl as lv
lv.init()

disp_drv = lv.disp_drv_t()
disp_drv.init()

lv_buf=bytearray(wd*ht*lv.color_t.__SIZE__//4) # 4 is arbitrary, just to not paint the screen at once
disp_draw_buf=lv.disp_draw_buf_t()
disp_draw_buf.init(lv_buf,None,len(lv_buf)//lv.color_t.__SIZE__)
disp_drv.draw_buf=disp_draw_buf
disp_drv.hor_res,disp_drv.ver_res=wd,ht
disp_drv.flush_cb=disp_drv_flush_cb
disp_drv.register()

scr = lv.obj()
btn = lv.btn(scr)
lbl = lv.label( btn )
lbl.set_text( "A button!" )
btn.center()
lv.scr_load(scr)

# just render and exit
lv.task_handler()

The monochrome theme should work regardless of color depth. I used it on a 16bpp eInk screen once.

The style of the theme are added to the widgets when they are created. So if you change the theme before creating a widget, the new widget will have the new styles and you can can change to a new theme.

The mono theme should work with LV_COLOR_DEPTH 32 too.

It might be useful to have (e.g.) lv.conf module in Micropython which would expose (many) options from lv_conf.h, e.g. lv.conf.COLOR_DEPTH, for feature-testing on the uPy side. Or would it take too much space in the image for little use?

I don’t think this is a matter of image size.
Currently the parsing script parses the preprocessed sources so it simple doesn’t see any macro.

The way to expose constants to Micropython is to define enums (this is in fact what LV_EXPORT_CONST_INT does)

So we could create an enum that enumerates all constant on lv_conf.h, or simply wrap each of them with LV_EXPORT_CONST_INT, but that would be hard to maintain because we would need to update it for every change in lv_conf.h.

Instead of maintaining this manually, we could automate this by some script such as lv_conf_internal_gen.py

I’m not sure it’s worth the trouble though. We could simply add a missing macro by LV_EXPORT_CONST_INT upon need.

We could simply add a missing macro by LV_EXPORT_CONST_INT upon need.

LV_COLOR_DEPTH (lvgl.COLOR.DEPTH in Python) works fine; but… what about is LV_COLOR_16_SWAP (as 16SWAP is not a valid identifier in Python)…? Can’t find it.

LV_EXPORT_CONST_INT(LV_COLOR_DEPTH);
LV_EXPORT_CONST_INT(LV_COLOR_16_SWAP);

On imagetools.py we do this:

This also works:

>>> lv.COLOR.DEPTH
32
>>> lv.COLOR_16.SWAP
0

You can use Tab-completions on the REPL to see what was generated (click tab after lv.CO):

>>> lv.CO
COLOR           COLOR_16        COORD           COVER_RES