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()