Repository navigation
MicroPython: a failed LVGL assert raises RuntimeError instead of hanging - #13
Merged
Merged
Conversation
On MicroPython, LV_ASSERT_HANDLER was while(1): a failed assertion spun
with the scheduler locked, so a board's USB serial and WebREPL went silent
until a power cycle. The MicroPython port now provides
lv_micropython_assert_failed(), which raises
RuntimeError("LVGL assert failed in <function> (<file>:<line>)").
Inside a Python callback the binding's callback wrapper catches it, prints
it and returns to LVGL, which carries on. With no Python callback in between
it unwinds out of LVGL to the Python caller, so the handler first clears
lv_timer_handler()'s running flag and each display's rendering flag, which
would otherwise stay set past a soft reset. The generated binding exports
mp_lv_nesting_depth() so the handler can tell the two apart.
Checked on a unix build: a flush callback that changes a widget mid-render
(lv_inv_area) hung the previous build and now raises, and rendering carries
on; a draw-task allocation failing mid-render (lv_draw_add_task, heap full)
hung the previous build and now raises out of lv.timer_handler(), and the
next refresh draws. With the flag repair removed, every later invalidation
raised again. An RPI_PICO2 build links the handler.
CircuitPython's unix coverage build compiles the generated binding with -Werror=missing-declarations, which refused the new exported function.
This was referenced Nov 8, 2026
bdbarnett
added a commit
that referenced
this pull request
Nov 10, 2026
The assert handler from #13 passed the full source path, the line and the function name, and each of LVGL's ~1,230 assert sites tested the weak handler before calling it. On an ESP32-S3 that added 50.8 KB to the firmware (23 KB of code, 20 KB of function names, 6 KB of paths). With CONFIG_SPIRAM_XIP_FROM_PSRAM, where ESP-IDF copies code and constants into PSRAM in 64 KB pages, it pushed both over a page boundary and took 128 KB from the heap. Now each site passes the file's base name (__FILE_NAME__, or __FILE__ on a compiler without it) and the line to a noreturn handler: a load, a move and a call. The message is "LVGL assert failed at lv_display.c:614". Built for ESP32_GENERIC_S3, the growth over v9.5.53 is 6.3 KB and stays within the same pages. The smoke test triggers an assert (set_tile_cnt(256)) on MicroPython, checks the message and that LVGL keeps working; it fails against the previous handler.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
On MicroPython a failed LVGL assertion spun in
while(1)with the scheduler locked, so a board's USB serial and WebREPL went silent until a power cycle. Now the MicroPython port provideslv_micropython_assert_failed()(weak inlv_conf.h, like the CircuitPython hook), which raisesRuntimeError("LVGL assert failed in lv_inv_area (lv_refr.c:286)"). LVGL's log is off in these builds, so the function, file and line are how to find the assertion's message. CircuitPython and the CPython wheel are unchanged.Where the exception lands decides what happens next. Inside a Python callback (a flush callback, an event, a timer), the binding's callback wrapper already catches exceptions, prints them and returns to LVGL, so the call that asserted is abandoned and LVGL finishes its pass. This is the case in the issue: Python code changing a widget while LVGL renders. With no Python callback in between, as when an allocation fails inside a render, the exception unwinds out of LVGL to whoever called
lv.timer_handler()orlv.refr_now(). Before raising, the handler clearslv_timer_handler()'s running flag and each display's rendering flag, because they'd otherwise stay set and survive a soft reset. To tell the two cases apart, the generated binding exportsmp_lv_nesting_depth(), the callback depth it already keeps. Both generated C files are regenerated, and./regenerate_all.sh --checkmatches. I didn't add a separate print before the raise: the exception already shows up wherever it's caught, so a second copy would only repeat it. If no Python frame at all can catch it,nlr_jump()ends in the port's ownnlr_jump_fail().I checked this on a unix build with this branch. First, a flush callback that calls
label.set_text()mid-render. The previous build hung (killed by timeout after 15 s). This build prints the RuntimeError from inside the callback,lv.refr_now()returns, and a second refresh flushes. Second, with the heap filled so thatlv_draw_add_task()fails mid-render: the previous build hung, and this build raises out oflv.timer_handler(); once the heap is freed, the next refresh draws two flushes. With the flag repair taken out (a planted build), every later invalidation raisedlv_inv_areaagain, which is why it's there. An RPI_PICO2 build compiles and links the handler (CMake path, compressed error text).pytest ports/micropython/tests tests/test_nesting_export.py tests/test_generation_tools.pypasses.I didn't run this on a board, and I didn't check every LVGL assert. An assert that fires halfway through changing a linked list can still leave that object inconsistent, but the board stays reachable.
Fixes #12