Thank you, that would be great. I lack the knowledge from the _thread
module since I used asyncio
previously with lvgl8.
asynchio has a huge amount of overhead associated with it and it’s overhead that you really don’t need to have when working on a constrained device like the ESP32.
Give me a little bit to locate the thread worker code. I have it saved somewhere, just have to locate it.
Here is some pseudo code for ya…
import _thread
import sys
import lvgl as lv
import time
import task_handler
class Worker:
def __init__(self, func, refresh_now, *args, **kwargs):
self.args = args
self.kwargs = kwargs
self.func = func
self.err = None
self.retval = None
self.refresh_now = refresh_now
self.lock = _thread.allocate_lock()
def __call__(self):
try:
self.retval = self.func(*self.args, **self.kwargs)
except Exception as err:
self.err = err
self.lock.release()
class LVGLThreadWorker:
def __init__(self):
self.queue = []
self.queue_lock = _thread.allocate_lock()
self.process_lock = _thread.allocate_lock()
self.is_running = False
self._exit = False
self.start_time = time.ticks_ms()
self.th = task_handler.TaskHandler()
# we are going to take control of the task_handler to utilize the timer
# it has running to signal our threadworker to add a worker that is going
# to refresh the display. By returning `False` from the callback that stops
# the task handler from calling lv.task_handler. It does update the tick
# so we only need to measure the time from when the callback is called
# to when the worker gets run. This might be a while depending on
# how much has been queued.
self.th.add_event_cb(task_handler.TASK_HANDLER_STARTED, self.__task_handler_cb)
def __task_handler_cb(self, *_):
def _do():
stop_time = time.ticks_ms()
diff = time.ticks_diff(stop_time, self.start_time)
lv.tick_inc(diff)
lv.task_handler()
self.start_time = time.ticks_ms()
self.add(_do)
return False
# the refresh now will update the display immediatly after
# the worker has run. This is a nice feature to have if you want the
# display to show updates ASAP. It i CPU instesive to do the refreshes so it
# is advised to use this feature sparingly.
def add(self, func, refresh_now=False, *args, **kwargs):
worker = Worker(func, refresh_now, args, kwargs)
with self.queue_lock:
self.queue.append(worker)
self.process_lock.release()
# this is the method you use to queue a job if there is a return value
# that is needed. calling this will cause the calling thread to release
# it's context so the thread working will continue to run and only when
# the job is ran will the context flip to go back to the calling thread
def addwait(self, func, *args, **kwargs):
worker = Worker(func, args, kwargs)
# aquire the lock so the next call to acquire will stall
worker.lock.acquire()
with self.queue_lock:
self.queue.append(worker)
if self.process_lock.locked():
self.process_lock.release()
# stalling the calling thread so the thread worker\ is able to
# process more jobs
worker.lock.acquire()
# job has finished running and at the moment the processing has been paused
# to allow this thread to continue to run. we want to release that stall so
# when the next context switch happens the processing thread will start to run
# again
worker.lock.release()
# check if there is an exception that needs to be raised
if worker.err is not None:
raise worker.err
return worker.retval
def start(self):
if self.is_running:
return
def stop(self):
self._exit = True
self.process_lock.release()
def _loop(self):
self.process_lock.acquire()
self.is_running = True
self._exit = False
while not self._exit:
self.process_lock.acquire()
with self.queue_lock:
queue_len = len(self.queue)
while queue_len:
with self.queue_lock:
worker = self.queue.pop(0)
queue_len = len(self.queue)
worker()
if worker.lock.locked():
worker.lock.acquire()
if worker.err is not None:
sys.print_exception(worker.err)
elif worker.refresh_now:
scrn = lv.screen_active()
disp = scrn.get_display()
lv.refr_now(disp)
self.is_running = False
self.process_lock.release()
and here are some examples of how to use it.
# startup code
# do driver startup code here
import machine
pin1 = machine.Pin(10, machine.Pin.IN)
sensor1 = machine.ADC(pin1)
pin2 = machine.Pin(12, machine.Pin.IN)
sensor2 = machine.ADC(pin2)
scrn = lv.screen_active()
bar1 = lv.bar(scrn)
bar1.set_pos(20, 20)
bar2 = lv.bar(scrn)
bar2.set_pos(20, 80)
tw = LVGLThreadWorker()
tw.start()
# example 1
def read_pins():
while True:
time.sleep_ms(5)
value1 = sensor1.read_u16()
value2 = sensor2.read_u16()
s1_value = int(value1 / 4096.0 * 100.0)
s2_value = int(value2 / 4096.0 * 100.0)
tw.add(bar1.set_value, s1_value)
tw.add(bar2.set_value, s2_value)
_thread.create_new_thread(read_pins)
# example 2
def read_pins():
while True:
time.sleep_ms(5)
value1 = sensor1.read_u16()
value2 = sensor2.read_u16()
s1_value = int(value1 / 4096.0 * 100.0)
s2_value = int(value2 / 4096.0 * 100.0)
tw.add(bar1.set_value, s1_value)
tw.add(bar2.set_value, s2_value, refresh_now=True)
_thread.create_new_thread(read_pins)
# example3
def read_pins():
while True:
time.sleep_ms(5)
value1 = sensor1.read_u16()
value2 = sensor2.read_u16()
s1_value = int(value1 / 4096.0 * 100.0)
s2_value = int(value2 / 4096.0 * 100.0)
def _do(v1, v2):
bar1.set_value(v1)
bar1.set_value(v2)
tw.add(_do, s1_value, s2_value, refresh_now=True)
_thread.create_new_thread(read_pins)
There are bound to be some things that need to be fixed in it. I just keyed it up really fast. It is used for showing the idea more so than anything else.
Thank you so much!
Great, this example should definitely be showcased n the documentation !
This is for really advanced uses. I also added in a FreeRTOS binding that exposes almost all of the FreeRTOS functions an macros. Using that will actually allow you to specify what CPU core to run a task (thread) on. Using it is for really advanced users that know their way around FreeRTOS.