SDK for TESAIoT Dev Kit
API reference & tutorials (ModusToolbox)
Loading...
Searching...
No Matches
F2 — Driving widgets from MicroPython over IPC
Variant
mtb-mpy and mtb-only

The CM55 handler mechanics apply to both variants. The drive side of this chapter — the ui.* MicroPython module and ui._diag() — is mtb-mpy only; on mtb-only the same handler exists but nothing in the shipped firmware sends it commands.

Learning goal

Understand that the 25 ui_widget_mgr_* functions are not a C API for your page code. Their sole caller is the IPC command switch in process_ui_command(); the real clients are MicroPython scripts crossing IPC. After this chapter you can trace a ui.Label(...) call from the REPL to an LVGL object, and you know which seven symbols you implement rather than call.

Real firmware sequence

The contract in one line — ipc_ui.h:7-8: the ISR half only enqueues; LVGL work happens on the 50 ms timer in GFX-task context. Header contract ui_widget_mgr.h:6: "Creates, modifies, deletes LVGL widgets in GFX task context".

Init. ipc_ui_init(NULL) is called last in CM55 bring-up and gated on display_ok (tesaiot_display.c:470-479); it calls ui_widget_mgr_init() itself (ipc_ui.c:890) — never call that yourself. Both containers take NULL (deferred binding):

Origin
Lifted from tesaiot_display.c:470-479 (compiled into the prebuilt archive; not shipped as source).

Bind on page create, NULL on destroy. The Playground page is the shipped exemplar:

/* ...context: inside page_playground_create() ... */
/* Store as UX/UI container for IPC widget creation */
s_uxui_container = content;
/* Terminal container: same as content — ipc_lcd creates terminal inside */
s_terminal_container = content;
/* Bind containers to IPC handlers (deferred from boot) */
void page_playground_destroy(void)
{
/* Unbind containers from IPC handlers — prevents writing to stale objects.
* The actual LVGL objects are destroyed by lv_screen_load_anim(auto_del). */
s_uxui_container = NULL;
s_terminal_container = NULL;

Dispatch. process_ui_command() (ipc_ui.c:393, drained from :875) is a switch over IPC_CMD_UI_*; 22 of the 25 primitives dispatch inside it. Representative case:

Origin
Lifted from ipc_ui.c:393-394,418-477 (compiled into the prebuilt archive; not shipped as source).

Event drain — the producer is ui_widget_mgr_event_push (ui_widget_mgr.c:333, :406 behind the per-widget event mask), the consumer is the POLL_EVENTS case:

Origin
Lifted from ipc_ui.c:516-532 (compiled into the prebuilt archive; not shipped as source).

Two primitives do not dispatch from the switch: ui_widget_mgr_init (inside ipc_ui_init) and ui_widget_mgr_set_parent (sole caller ipc_ui_set_container(), ipc_ui.c:918-921). One, ui_widget_mgr_count, has no caller anywhere and is authored.

The parent is nullable. Every one of five call sites null-checks it — it is NULL between set_container(NULL) on destroy and the next page's bind:

Origin
Lifted from ipc_ui.c:516-532 (compiled into the prebuilt archive; not shipped as source).

Step by step

Step 1 — Open the Playground page (both variants)

Home → Playground.

What you should observe. A console panel. Console and widget layer are mutually exclusive; the toggle is a blind toggle, always guarded by ipc_lcd_is_panel_visible():

static void console_toggle_cb(lv_event_t *e)
{
(void)e;
s_console_mode = !s_console_mode;
if (s_console_mode) {
/* Switch to Console view — clear unread badge */
if (s_console_badge) {
lv_obj_add_flag(s_console_badge, LV_OBJ_FLAG_HIDDEN);
}
}
} else {
/* Switch to UI view */
}
}
/* Update button icon */
if (s_console_btn_lbl) {
lv_label_set_text(s_console_btn_lbl,
s_console_mode ? LV_SYMBOL_EYE_OPEN : LV_SYMBOL_LIST);
}
}

Step 2 — Create a widget from the REPL (mtb-mpy)

>>> import ui
>>> l = ui.Label("hello")
>>> l.text("BENTO")

What you should observe. The widget container un-hides on the first POLL_EVENTS and the label appears; text() updates it within a 50 ms tick (SET_TEXT also arms fast mode, ipc_ui.c:463). Label strings are cut at 95 bytes in the constructor: UI_CREATE_TEXT_MAX is 96 (ipc_ui_protocol.h:658) and the constructor truncates to UI_CREATE_TEXT_MAX - 1 (modui.c:1223).

Step 3 — Prove frames are flowing (mtb-mpy)

>>> ui._diag()

What you should observe. The ten words from ipc_ui_platform_diag(); flush_start_count increments between two calls. On mtb-only, call ipc_ui_platform_diag(out, 10) from your own CM55 code instead.

Step 4 — Restart a script cleanly

The Playground restart tears widgets down before the IPC restart command, so CM33_NS cannot push into a half-cleared table:

/* ...context: inside uxui_ctrl_event_cb() - GFX task context ... */
if (cmd == UXUI_CTRL_RESTART) {
/* Soft restart: clear widgets, then tell CM33_NS to re-run main.py.
* No NVIC_SystemReset — CM55/WiFi/sensors stay alive. */
send_ipc_cmd(IPC_CMD_RESTART_SCRIPT);
}

What you should observe. Widgets vanish, then the script's widgets re-appear. The CLEAR_ALL command also resets LCD auto-navigation (ipc_ui.c:556).

The seven weak hooks — implement, never call

Exactly the dist/ipc_core/overridable.txt list: cm55_controls_snapshot (template override cm55_sensor_poll.c:342), game_sprite_create / game_sprite_set (game_sprite_engine.c:16/:25), game_sprite_lookup (game_sprite_registry.c:52), ipc_ui_ext_dispatch (ipc_ui_ext_chain.c:41, runs in GFX context and may touch the display, ipc_ui.h:40-41), ipc_ui_ext_clear_all (W in the archive), ipc_ui_platform_diag (strong def archived at tesaiot_display.c:593-609). A reference page that says "call this" for any of them is wrong.

Traps

Warning
Forgetting set_container(NULL) on destroy is a use-after-free. lv_screen_load_anim with auto-delete frees the page's objects; a later IPC command then writes into freed LVGL memory. Bind ui-then-lcd on create and identically on destroy.
No LVGL call outside the GFX task. The 50 ms timer is the only legal place; the ISR half enqueues.
ipc_sensorhub_snapshot clears the changed flags — one consumer per tick. The UI takes one snapshot at 33 ms and fans out through pm_render().
dist/ipc_core/PROVENANCE.txt hard-codes PAGE_ID_PLAYGROUND ordinal 7. A project with a different page_id_t order "succeeds with no diagnostic and compares the wrong page" (Chapter F1).

Variant box

mtb-mpy mtb-only
Drive side ui.* module over IPC No shipped sender; handler present
Frame proof ui._diag() C-side ipc_ui_platform_diag()
Page mechanics identical identical