How screen rendering works

Hello!
So I have a program that is split into different functions.
I have a main menu and depending on what option I select a function is called. My problem is, the screen only renders new things when the program returns from a function. How can I make it render new things while still inside the function?

For example:

def screen_test():
    global screen, prev_screen 
    prev_screen = lv.scr_act()
    screen = lv.obj()
    
    authLabel = lv.label(screen, None)
    authLabel.align(None, lv.ALIGN.CENTER, -80, -10)
    authLabel.set_text("Test Screen")
    
    lv.scr_load(screen)
    prev_screen.delete()

    utime.sleep(5)

It will only render after the 5 seconds. Is there a way to render whenever I want from inside a function?
Thanks!

LittlevGL Micropython Binding was designed to work on a single threaded environments.
It doesn’t mean it can’t run on multi-threaded environments, it just means that everything is done on a single thread: Micropython processing, lvgl processing, lvgl event callbacks and screen rendering as well.
(It does mean, however, that LittlevGL Micropython Binding is not thread safe)

When you call utime.sleep(5) you put current thread to sleep, which means that your thread won’t do any of the above, and screen will not be rendered.

To solve this, you have the following options:

  • Trigger a timer instead of calling sleep.
    See machine.Timers. Exit your function and continue your flow on the callback function.
  • Use Python threads. See the _thread module. Using threads you can sleep on one thread and let lvgl process the screen on the other thread. Take into account, however, that LittlevGL Micropython Binding is not thread safe, and it’s up to you to prevent from both threads calling lvgl functions on the same time, or call lvgl function while lvgl renders the screen.

Another thing you can try is using uasyncio, but I haven’t explored that yet.

In general, UI tend to be Event Driven, so having a sleep command in a UI flow is a bit unusual.

Hi @amirgon that was useful info, thank you!
However in my case the sleep function was there just to exaggerate what is happening and debugging. If I delete it my problem happens anyway. In my normal program I don’t have a sleep function.
Also sleep doesn’t prevent what is before from running if I make something simple like

x=0
while (x!=10):
    x=x+1
    print(x)
print("finish")
utime.sleep(5)

it will print everything before sleep.

Anyway that is not the real problem (although I am thankful for the explanation!)
Let me try to explain better

  1. In main menu I can choose different options I have a list with callbacks.
  2. I choose for example the login option. The callback calls a Login function.
    2a. In the login function I have a different screen that asks the employee to swipe the card.
    2b. the employee swipes the card and a screen with auth successful appears.
  3. Goes back to main menu screen (with authenticated options).

This would be normal the behavior… what happens is:

  1. Choose login
  2. Stays on main menu screen but I know for a fact that the authentication algorithm is running and waiting for me to swipe my card, if I swipe my card the main menu changes to step 3. steps 2a and 2b are “jumped”.

I have the exact same Login function running when the device starts and at that time everything goes smoothly… What I found out was:

Inside a button callback (from a list for example) , I can’t refresh any screen… The screen only refreshes when it exits the function callback (and it renders the last screen that was loaded inside the callback ) BUT JUST AFTER IT EXITS THE CALLBACK.

I also tried to use this function several times in a row from outside the callback and all goes smoothly, it goes through all the steps and works fine… Is just when I am using it in a callback…

Once again, Thank you!

TL:DR Inside a event callback from a list if you call a function that renders different screen objects etc it won’t render the screen until the callback is finished , even though all the function code is ran included lv.scr_load(screen) etc. After exiting the callback, the last screen state from inside the callback will be rendered… This is a major upset… any way to fix this or work around it? I want to be able to change my screen while inside the function that is called in the callback.

You would have to change the way your code is architected. I’m not aware of any way to work around this without that. This is how way many event-driven UI systems work (including JavaScript). You have to return from the callback before anything gets redrawn.

@amirgon Is there a way in Python to schedule a function to run ASAP, but not on the same stack frame as the caller? In JavaScript this could be done, for instance, with setTimeout(function(){}, 0), but I don’t know if (Micro)Python offers similar functionality. If so, one could just schedule a function to run from inside the callback to give lv_task_handler a chance to run and redraw the screen.

If I understand correctly, your code that waits for the employee to swipe a card - is blocking.
So it’s the same behavior of “sleep” - block the thread until some event happens (employee swipes the card).

Let me suggest an event driven approach: Instead of waiting for a card swipe, trigger a callback after the card was swiped. This way your callback can exit immediately.


Let me try to explain why calling a blocking function inside lvgl callback prevents screen refresh.
What you are seeing is a behavior of Micropython’s schedule function.
According to the docs:

A scheduled function will never preempt another scheduled function.

LittlevGL Micropython Binding uses schedule to run lvgl in the same thread as Micropython.
This is actually happening in lvesp32 module.
The flow (function calls) goes something like this:

Timer expires 🡆 schedule lv_task_handler to run in Micropython
...
Micropython runs scheduled tasks 🡆 lv_task_handler 🡆btn callback 🡆 some blocking function
...
Timer expires again 🡆 schedule lv_task_handler to run in Micropython

At this point, a scheduled task is already running and blocked by some blocking function.
Even though the timer expired again, lvgl’s lv_task_handler will not run again until the previously scheduled task finishes, and that would happen only after some blocking function finishes and btn callback exits.

Doing it any other way would require lvgl to be re-entrant and using some other way to schedule lv_task_handler.

Yes, with Micropython’s schedule function.

But this will not solve the problem.
Since everything happens in the same thread, once you have a blocking function (that waits for the user input, or some other external event), the UI will be blocked.

There can be two ways around it:

  • Never block in lvgl callbacks. Use only event driven functions.
  • Support multithreading in lvgl Binding and take care of thread safety (either in lvgl or in the binding wrapper functions).
    This may not be enough though, because we would need to call lv_task_handler while still processing a callback, so lvgl would probably need to be reentrant as well.

Once again Thank you @embeddedt and @amirgon. Really learnt a lot with your help!
So taking what both said in consideration, the simplest way I got this working was creating an infinite program cycle where I check some system variables. When I click a button those variables are changed and then in this main cycle my functions are ran (auth functions etc).
I managed to make it work this way! Can you think of any reason that this may not be a sustainable solution?
At the moment for my PoC this works. But I’ll try a more event_driven approach like the one you suggested.

Once again, thank you for your time and help.

The downside of a loop that constantly polling variables is that it consumes power and cpu cycles.
That’s usually a bad thing for an embedded device.

An event-driven approach, on the other hand, would keep the device idle (or even in some sleep mode) until an even happens, and would consume power and cycles only to process events, such as user interaction or timer expiration.

1 Like