diff --git a/binding/emit_c_micropython_style.py b/binding/emit_c_micropython_style.py index a42974b..3845282 100644 --- a/binding/emit_c_micropython_style.py +++ b/binding/emit_c_micropython_style.py @@ -1015,6 +1015,17 @@ def emit_c(ctx): return result; } +// How many Python callbacks LVGL is inside right now. The MicroPython port's +// assert handler reads it: at zero, an exception raised from a failed LVGL +// assert unwinds out of every LVGL frame, so the handler first clears the +// state LVGL holds for the length of a pass; above zero, the callback's own +// nlr frame catches it and LVGL carries on, so that state must stay. +int mp_lv_nesting_depth(void); +int mp_lv_nesting_depth(void) +{ + return _nesting; +} + // Function pointers wrapper static mp_obj_t mp_lv_funcptr(const mp_lv_obj_fun_builtin_var_t *mp_fun, void *lv_fun, void *lv_callback, qstr func_name, void *user_data) diff --git a/generated/lvgl_circuitpython.c b/generated/lvgl_circuitpython.c index 77d511c..93448d6 100644 --- a/generated/lvgl_circuitpython.c +++ b/generated/lvgl_circuitpython.c @@ -854,6 +854,17 @@ static mp_obj_t mp_lv_nesting_call(mp_obj_t func, size_t n_args, const mp_obj_t return result; } +// How many Python callbacks LVGL is inside right now. The MicroPython port's +// assert handler reads it: at zero, an exception raised from a failed LVGL +// assert unwinds out of every LVGL frame, so the handler first clears the +// state LVGL holds for the length of a pass; above zero, the callback's own +// nlr frame catches it and LVGL carries on, so that state must stay. +int mp_lv_nesting_depth(void); +int mp_lv_nesting_depth(void) +{ + return _nesting; +} + // Function pointers wrapper static mp_obj_t mp_lv_funcptr(const mp_lv_obj_fun_builtin_var_t *mp_fun, void *lv_fun, void *lv_callback, qstr func_name, void *user_data) diff --git a/generated/lvgl_micropython.c b/generated/lvgl_micropython.c index 0b54672..7283ca7 100644 --- a/generated/lvgl_micropython.c +++ b/generated/lvgl_micropython.c @@ -852,6 +852,17 @@ static mp_obj_t mp_lv_nesting_call(mp_obj_t func, size_t n_args, const mp_obj_t return result; } +// How many Python callbacks LVGL is inside right now. The MicroPython port's +// assert handler reads it: at zero, an exception raised from a failed LVGL +// assert unwinds out of every LVGL frame, so the handler first clears the +// state LVGL holds for the length of a pass; above zero, the callback's own +// nlr frame catches it and LVGL carries on, so that state must stay. +int mp_lv_nesting_depth(void); +int mp_lv_nesting_depth(void) +{ + return _nesting; +} + // Function pointers wrapper static mp_obj_t mp_lv_funcptr(const mp_lv_obj_fun_builtin_var_t *mp_fun, void *lv_fun, void *lv_callback, qstr func_name, void *user_data) diff --git a/lv_conf.h b/lv_conf.h index 908c88a..2c760f5 100644 --- a/lv_conf.h +++ b/lv_conf.h @@ -299,9 +299,21 @@ #define LV_ASSERT_HANDLER_INCLUDE __attribute__((weak)) void lv_circuitpython_assert_failed(void); #define LV_ASSERT_HANDLER { if (lv_circuitpython_assert_failed) { lv_circuitpython_assert_failed(); } while(1); } -#else +#elif defined(LV_CPYTHON_BUILD) #define LV_ASSERT_HANDLER_INCLUDE #define LV_ASSERT_HANDLER while(1); /*Halt by default*/ +#else + /* On MicroPython a spinning assert locks the scheduler out: USB serial and + * WebREPL go silent until a power cycle. The MicroPython port's handler + * raises RuntimeError naming the function, file and line instead, which + * the nearest Python frame or LVGL callback reports, and the board stays + * reachable. It is weak here, so a build without it still links and + * halts as before. */ + #define LV_ASSERT_HANDLER_INCLUDE + #if !defined(PYCPARSER) && !defined(__ASSEMBLY__) + __attribute__((weak)) void lv_micropython_assert_failed(const char *file, int line, const char *func); + #endif + #define LV_ASSERT_HANDLER { if (lv_micropython_assert_failed) { lv_micropython_assert_failed(__FILE__, __LINE__, __func__); } while(1); } #endif /*------------- diff --git a/ports/micropython/micropython.cmake b/ports/micropython/micropython.cmake index cbdeac6..0af131d 100644 --- a/ports/micropython/micropython.cmake +++ b/ports/micropython/micropython.cmake @@ -45,7 +45,7 @@ if(NOT "${USER_C_MODULES}" MATCHES "jpegio") endif() add_library(lv_micropython INTERFACE) -target_sources(lv_micropython INTERFACE ${LVMP_C} ${LVMP_DIR}/src/lvgl_micropython_build.c) +target_sources(lv_micropython INTERFACE ${LVMP_C} ${LVMP_DIR}/src/lvgl_micropython_build.c ${LVMP_DIR}/src/lv_assert_micropython.c) # --- which commit this firmware was built from --------------------- # Computed at build time from this repository's git, never stored; "unknown" when diff --git a/ports/micropython/micropython.mk b/ports/micropython/micropython.mk index 9964130..420a9c3 100644 --- a/ports/micropython/micropython.mk +++ b/ports/micropython/micropython.mk @@ -70,6 +70,9 @@ CFLAGS_USERMOD += -I$(BINDINGS_DIR) -I$(LVMP_DIR) -Wno-unused-function SRC_USERMOD_LIB_C += $(SOURCES) SRC_USERMOD_C += $(LVMP_C) SRC_USERMOD_C += $(LVMP_DIR)/src/lvgl_micropython_build.c +# What a failed LVGL assert does (lv_conf.h). Here rather than with LVGL's +# sources so its message text is scanned like the binding's. +SRC_USERMOD_C += $(LVMP_DIR)/src/lv_assert_micropython.c # --- which commit this firmware was built from --------------------- # Computed at build time from this repository's git, never stored; "unknown" when diff --git a/ports/micropython/src/lv_assert_micropython.c b/ports/micropython/src/lv_assert_micropython.c new file mode 100644 index 0000000..8351ebe --- /dev/null +++ b/ports/micropython/src/lv_assert_micropython.c @@ -0,0 +1,62 @@ +/** + * @file lv_assert_micropython.c + * + * What a failed LVGL assertion does on MicroPython (lv_conf.h names this + * function in LV_ASSERT_HANDLER for MicroPython builds). LVGL's default is + * to spin forever, and on a board that locks the MicroPython scheduler out: + * USB serial and WebREPL go silent and only a power cycle brings it back. + * + * Instead this raises RuntimeError("LVGL assert failed in + * (:)"). LVGL's log is off in MicroPython builds, so the + * function and line are how to find the assertion's message in LVGL's source. + * + * Where the exception lands decides what LVGL state needs repair: + * + * - Inside a Python callback (a flush callback, an event, a timer), the + * binding's callback wrapper catches it, prints it and returns to LVGL, + * which carries on with its pass. The call that asserted is abandoned and + * nothing else changes. This is the usual case: Python code changing a + * widget while LVGL renders. + * - With no Python callback in between, it unwinds out of every LVGL frame + * to whoever called LVGL from Python. A pass in progress is abandoned, so + * the flags LVGL holds for its length are cleared first: otherwise + * lv_timer_handler() would return at once forever after, or every later + * invalidation would assert again, and neither resets on a soft reset. + * + * With no Python frame at all to catch it, nlr_jump() ends in the port's + * nlr_jump_fail(), the port's own fatal path. + */ +#include "py/runtime.h" + +#include "lvgl/lvgl.h" +#include "lvgl/src/core/lv_global.h" +#include "lvgl/src/display/lv_display_private.h" + +#include + +void lv_micropython_assert_failed(const char *file, int line, const char *func); + +// From the generated binding: how many Python callbacks LVGL is inside. +int mp_lv_nesting_depth(void); + +static void lv_micropython_end_pass(void) { + if (!lv_is_initialized()) { + return; + } + LV_GLOBAL_DEFAULT()->timer_state.already_running = false; + for (lv_display_t *disp = lv_display_get_next(NULL); disp != NULL; disp = lv_display_get_next(disp)) { + disp->rendering_in_progress = 0; + } +} + +void lv_micropython_assert_failed(const char *file, int line, const char *func) { + const char *base = strrchr(file, '/'); + if (base == NULL) { + base = strrchr(file, '\\'); + } + base = base ? base + 1 : file; + if (mp_lv_nesting_depth() == 0) { + lv_micropython_end_pass(); + } + mp_raise_msg_varg(&mp_type_RuntimeError, MP_ERROR_TEXT("LVGL assert failed in %s (%s:%d)"), func, base, line); +}