SDK for TESAIoT Dev Kit
API reference & tutorials (ModusToolbox)
Loading...
Searching...
No Matches
B3 — The IPC backbone: setup, deferred binding, snapshots

Learning goal

The three IPC contracts every CM55 page relies on: the setup order (from B2), the deferred container binding that lets IPC handlers write into a page that exists, and the single-consumer snapshot that feeds every page at 33 ms. Plus the two idioms built on them — edge-detected topbar state and the console/widget toggle. By the end you can write a page that binds and unbinds correctly, and you know which of these calls may be made from where.

The real firmware sequence

Contract 1 — the ordering prologue

tesaiot_display.c:223-238 (compiled into libbento_cm55.a; source in the Kit tree): cm55_ipc_communication_setup() → ipc_sensorhub_init() → ipc_service_init() → (deepcraft_task_init() if model-link) → … display steps … → ipc_only: → (void)ipc_lcd_init(NULL) → if (display_ok) (void)ipc_ui_init(NULL). Chapter B2 walked it; here the point is the two NULLs: both handlers are initialised without a container.

Contract 2 — deferred container binding

The Playground page is the consumer of the IPC LCD console and the IPC widget manager. It binds on create, ui-then-lcd:

/* ...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) */

and it must NULL both on destroy, in the same order:

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;

ipc_ui_set_container() is a one-line forward to ui_widget_mgr_set_parent() (ipc_ui.c:918-921). Why the NULL matters: the page is torn down by lv_screen_load_anim(..., auto_del); if the IPC handlers still hold the old container, the next command from CM33_NS writes into freed LVGL objects — a use-after-free that surfaces as a HardFault some time later, on an unrelated command. ui_widget_mgr_get_parent() is nullable for exactly this window (between set_container(NULL) and the next page's bind) and every one of its five callers null-checks it (ipc_ui.c:443,516-524,560,859).

The related teardown-before-restart idiom: widgets are cleared before the restart command goes to CM33_NS, so the far side 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);
}

Contract 3 — the single-consumer snapshot

The 33 ms timer (armed at the end of sensorhub_ui_init, B2 step 16) takes one snapshot into a stack-owned sensorhub_snapshot_t and fans it out through pm_render():

static void sensorhub_timer_cb(lv_timer_t *timer)
{
(void)timer;
/* Take snapshot of all sensor data from IPC */
/* Dispatch render to current page (skips if animating) */
pm_render(&s_pm, &snap);
/* Update global topbar (time + WiFi) on current page */
if (!s_pm.animating) {
pm_update_topbar(&s_pm);
}
}

ipc_sensorhub.h:66-67: "Safe to call from any task context" — but "Clears the 'changed' flags after reading." Therefore exactly one consumer per tick: a page that takes its own snapshot inside render_cb steals the changed flags from every other page. Pages receive the snapshot as an argument; they do not call ipc_sensorhub_snapshot() themselves.

Idiom — edge-detected topbar state

ipc_sensorhub_wifi_connected() is polled; there is no callback API. The topbar compares against the current LVGL flag and only adds/clears on a change — an unconditional add/clear every tick causes visible flicker:

/* ...context: inside pm_update_topbar() - GFX task context ... */
/* WiFi icon — only toggle visibility when state actually changes
* to prevent unnecessary LVGL invalidation (card row flicker). */
if (pm->wifi_lbl) {
bool connected = ipc_sensorhub_wifi_connected();
bool hidden = lv_obj_has_flag(pm->wifi_lbl, LV_OBJ_FLAG_HIDDEN);
if (connected && hidden) {
lv_obj_clear_flag(pm->wifi_lbl, LV_OBJ_FLAG_HIDDEN);
} else if (!connected && !hidden) {
lv_obj_add_flag(pm->wifi_lbl, LV_OBJ_FLAG_HIDDEN);
}
}

The clock is gated on ipc_sensorhub_ntp_synced() before the RTC is read at all (ipc_sensorhub.h:99: Cy_RTC_GetDateAndTime only after the CM33_NS NTP notification). True is necessary, not sufficient — the fields are still range-validated, and the label is strcmp-checked before lv_label_set_text to avoid card-row flicker, at roughly 6 fps and skipped while animating:

/* ...context: inside pm_update_topbar() - GFX task context ... */
/* RTC time display — only after NTP sync notification from CM33_NS */
if (pm->time_lbl && ipc_sensorhub_ntp_synced()) {
static const char * const dow[] = {
"", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
};
static const char * const mon[] = {
"", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};
cy_stc_rtc_config_t rtc;
Cy_RTC_GetDateAndTime(&rtc);
if (rtc.month >= 1 && rtc.month <= 12 &&
rtc.dayOfWeek >= 1 && rtc.dayOfWeek <= 7) {
char buf[32];
snprintf(buf, sizeof(buf), "%s %d %s %02d:%02d",
dow[rtc.dayOfWeek], (int)rtc.date,
mon[rtc.month], (int)rtc.hour, (int)rtc.min);
/* Only invalidate if text actually changed (prevents card row flicker) */
if (strcmp(lv_label_get_text(pm->time_lbl), buf) != 0) {
lv_label_set_text(pm->time_lbl, buf);
}

Idiom — the console/widget toggle

ipc_lcd_toggle_panel() is a blind toggle with no idempotent set form. Always guard it with ipc_lcd_is_panel_visible(); console and widgets are mutually exclusive; the order is asymmetric (hide widgets then show console, but hide console then show widgets); the unread counter is cleared explicitly only on the transition into console mode:

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);
}
}

ipc_lcd_has_unread() is a pure read — it does not clear — and is only meaningful while the console is hidden, polled from the per-tick render hook:

void page_playground_render(sensorhub_snapshot_t *snap)
{
(void)snap;
/* Show/hide notification badge when new console text arrives while hidden */
if (s_console_badge && !s_console_mode) {
lv_obj_remove_flag(s_console_badge, LV_OBJ_FLAG_HIDDEN);
}
}
}

All of the above runs in GFX-task context (LVGL event callbacks and LVGL timers). The IPC ISR half only enqueues; LVGL work happens on the 50 ms timer (ipc_ui.h:7-8).

Step-by-step

Step 1 — Watch the topbar WiFi glyph un-hide on connect

Connect the board to WiFi by whichever route you have (the UI page, Chapter C2; or on mtb-mpy wifi.connect(...) at the REPL, Chapter C1).

What you should observe
The topbar WiFi glyph appears once, on the transition, and stays. It does not blink. On disconnect it disappears once. On mtb-mpy the REPL path prints [WiFi] Connecting… / [WiFi] Connected! from modwifi.c:240-282. The UI path prints nothing — the glyph itself is the observable. Do not wait for a [wifi-glue] line: those print only inside app_wifi_connect_direct() (wifi_init.c:225-242), which nothing on the REPL, UI or boot path calls — its only caller is the archived BLE radio scheduler, linked under ENABLE_PAGE_BENTO_BUDDY=1 (Chapter I1). [WiFi-Boot] and [WiFiIPC] are muted and will not appear.

Step 2 — Watch the clock appear only after NTP

What you should observe
No clock text in the topbar until CM33_NS has completed an NTP sync and notified CM55. Then the time appears and updates about six times a second (not every 33 ms tick). If the board has no route to an NTP server the clock never appears; that is the gate working, not a fault.

Step 3 — Exercise the unread badge on the Playground page

Open the Playground page. Run something on the mtb-mpy REPL that prints to the IPC console (any print() while the console panel is hidden).

What you should observe
With the console hidden, the unread indicator becomes visible on the next render tick (ipc_lcd_has_unread() polled from the render hook). Toggle the console: the indicator is cleared on the transition into console mode (ipc_lcd_clear_unread() is explicit and separate from the read). Toggle back: widgets return, console hides — in that order.

On mtb-only there is no REPL to print from; the console panel and toggle still exist and the mechanics apply, but this step has no driver on that variant.

Step 4 — Read the negative case; do not perform it

Read the unbind snippet again and imagine it absent.

What you should observe
Nothing — this is a described failure, not a performed one. What would happen: navigate away from Playground, then send any widget command from CM33_NS; the handler writes through a container pointer to an LVGL object that lv_screen_load_anim(auto_del) already freed. The fault arrives later and elsewhere, as LED1+LED2 in groups of three and 0xDEAD0003 at 0x28000000 (Chapter A3). There is no safe recipe for it and none is given.

Traps

  • Appendix X #7 — blind ipc_lcd_toggle_panel() without is_panel_visible() inverts state. There is no set form; guard every toggle.
  • Appendix X #8 — forgetting set_container(NULL) on page destroy = use-after-free. Both ipc_ui_set_container(NULL) and ipc_lcd_set_container(NULL), in the destroy callback, in the same ui-then-lcd order as the bind.
  • Appendix X #9 — ipc_sensorhub_snapshot clears changed flags; one consumer per tick. Pages take the snapshot they are handed.
  • Appendix X #14 — PAGE_ID ordinals are ABI. The archive hard-codes PAGE_ID_PLAYGROUND's ordinal (dist/ipc_core/PROVENANCE.txt); a project with a different page_id_t order links with no diagnostic and compares the wrong page.
  • Calling ui_widget_mgr_init() yourself. ipc_ui_init() calls it (ipc_ui.c:890); a second call double-initialises the widget table.
  • Treating the ui_widget_mgr_* primitives as a C API. Their only caller is the IPC command switch process_ui_command(); the real callers are MicroPython scripts crossing IPC (Group F, Chapter F2).
  • Calling cm55_ipc_communication_setup() twice (wifi_manager.c:65).
  • LVGL from the wrong task. Every call in this chapter is GFX-task context. Widget creation from an IPC ISR, or from any other task, is a HardFault.

Variant applicability

Variant
mtb-mpy and mtb-only. Every function in this chapter is in libbento_ipc.a and CM55 is variant-agnostic. Step 3's driver (printing to the IPC console) is a MicroPython action and so has no mtb-only equivalent; the CM55-side mechanics are identical.