SDK for TESAIoT Dev Kit
API reference & tutorials (ModusToolbox)
Loading...
Searching...
No Matches
F1 — Adding a screen: three files, plus the Makefile truth
Variant
mtb-mpy and mtb-only

CM55 is variant-agnostic; everything in this chapter applies to both packages.

Learning goal

Wire a new page into the CM55 UI so that it is reachable, and know — from having reproduced each one — what every partial wiring state looks like. The worked example is Edge AI, the only small page in the shipped template with all three fragments present and a documented create/render/destroy contract. The README's suggested templates, motion and environ, are not used here, for a reason given in the traps.

Real firmware sequence

(1) The enum entry — page_manager.h:65, PAGE_ID_EDGE_AI = 13. The surrounding contract is ABI:

/* ...context: top of the page_id_t enum ... */
/* EXPLICIT VALUES, NEVER GUARDED — this ordering is ABI.
*
* The prebuilt archives in lib/ bake page ids as immediates
* (lib/ipc_core/PROVENANCE.txt records PAGE_ID_PLAYGROUND = 7). These
* entries used to be wrapped in #if ENABLE_PAGE_* — so flipping any menu
* flag renumbered every page after it, and the archives then compared the
* wrong page at runtime while linking clean. page_id_ordinal_assert.c
* caught exactly that when the Motor menu was turned off, 2026-08-28.
*
* An enum value costs nothing when the page is compiled out, so every
* page owns its number forever. Values 0-13 are the layout the archives
* were built against (recovered from the build's own DWARF); pages that
* were not in that build park stably from 14 up. Add new pages at the
* end with the next free number. NEVER renumber, NEVER re-guard. */

PM_MAX_PAGES is 24U (page_manager.h:21) and a _Static_assert at :79-80 refuses a page_id_t that exceeds it.

(2) The callback triple — page_def_t (page_manager.h:87-95):

typedef struct {
const char *name; /* Display name (e.g., "Dashboard") */
const char *subtitle; /* Brief description for home card */
uint32_t accent_color; /* Card border + title color (hex) */
lv_obj_t *(*create_cb)(void); /* Build screen */
void (*render_cb)(sensorhub_snapshot_t *snap); /* Update data */
void (*destroy_cb)(void); /* Pre-destroy cleanup */
bool cacheable; /* If true, screen survives nav-away (not destroyed) */
} page_def_t;

Registered for Edge AI at sensorhub_ui.c:254-266, guarded by BENTO_HAS_EDGE_AI:

/* ...context: inside sensorhub_ui_init() page registration ... */
#if defined(BENTO_HAS_EDGE_AI) && (BENTO_HAS_EDGE_AI == 1)
/* Edge AI hub — ONE page hosting every compiled-in DEEPCRAFT model. */
{
page_def_t def = {
.name = "Edge AI",
.subtitle = "On-device inference",
.accent_color = UI_COLOR_ACCENT_PURPLE,
.create_cb = page_edge_ai_create,
.render_cb = page_edge_ai_render,
.destroy_cb = page_edge_ai_destroy,
};
pm_register(&s_pm, PAGE_ID_EDGE_AI, &def);
}

page_edge_ai_create/render/destroy are never called directly — the page manager owns the lifecycle and calls them in GFX-task context. Their bodies are prebuilt (proj_cm55/Makefile:994 ignores page_edge_ai.c; the symbols come from libbento_cm55.a), so for the source of a triple read page_motion.c:106/:210/:267.

(3) The Home card — page_home.c:147-149, inside s_card_defs[]:

typedef struct {
page_id_t id;
const char *title;
const char *icon; /* LV_SYMBOL_* (FontAwesome subset) */
uint32_t color;
const lv_font_t *icon_font; /* NULL = default UI_FONT_H2, else custom */
const lv_image_dsc_t *icon_img; /* NULL = draw the glyph above; else this
* hand-drawn asset, for concepts the
* FontAwesome subset has no glyph for */
} home_card_def_t;
static const home_card_def_t s_card_defs[] = {
/* ---- Primary order (top of the Home grid) ---- */
{ PAGE_ID_DASHBOARD, "Sensor Dashboard", NULL, UI_COLOR_SENSOR_IMU, NULL, &icon_dashboard },
#if ENABLE_PAGE_GPIO_RGB
{ PAGE_ID_GPIO_RGB, "GPIO & RGB Matrix", NULL, UI_COLOR_ACCENT_GREEN, NULL, &icon_touch_rgb },
#endif
#if ENABLE_PAGE_MOTOR_CTRL
{ PAGE_ID_MOTOR_CTRL, "Motor Controller", LV_SYMBOL_REFRESH, UI_COLOR_ACCENT_ORANGE, NULL, NULL },
#endif
#if defined(BENTO_HAS_EDGE_AI) && (BENTO_HAS_EDGE_AI == 1)
{ PAGE_ID_EDGE_AI, "Edge AI", NULL, UI_COLOR_ACCENT_PURPLE, NULL, &icon_ai_chip },
#endif

Where the three meet at runtime. Each card binds card_click_cb with its page id:

/* ...context: inside the home card build loop ... */
s_ctx.card_page_ids[i] = def->id;
lv_obj_add_flag(card, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(card, card_click_cb, LV_EVENT_CLICKED,
&s_ctx.card_page_ids[i]);

…which calls pm_navigate:

void pm_navigate(page_manager_t *pm, page_id_t target)
{
/* M1: Guard against double-tap during animation */
if (pm->animating) return;
/* Bounds check */
if (target >= pm->page_count) return;
if (pm->pages[target].create_cb == NULL) return;
/* M4: Self-navigation guard */
if (target == pm->current_page) return;
page_id_t leaving = pm->current_page;
bool cache_leaving = pm->pages[leaving].cacheable;
/* Resume sensor_auto_task when leaving Playground (safety net) */
if (leaving == PAGE_ID_PLAYGROUND) {
pm_notify_sensor_resume();
}
/* For cacheable pages, skip destroy — keep screen alive off-screen.
* For normal pages, notify about impending destruction. */
if (cache_leaving) {
pm->cached_screens[leaving] = lv_screen_active();
} else {
if (pm->pages[leaving].destroy_cb) {
pm->pages[leaving].destroy_cb();
}
}
/* Push current page to nav stack */
if (pm->nav_top < PM_NAV_STACK_DEPTH - 1) {
pm->nav_top++;
pm->nav_stack[pm->nav_top] = leaving;
}
/* Restore cached screen or create new one */
lv_obj_t *new_scr;
if (pm->pages[target].cacheable && pm->cached_screens[target] != NULL) {
new_scr = pm->cached_screens[target];
pm->cached_screens[target] = NULL;
} else {
new_scr = pm->pages[target].create_cb();
}
if (new_scr == NULL) return;
/* Register unload hook on leaving screen (cleanup safety net) */
lv_obj_t *leaving_scr = lv_screen_active();
lv_obj_remove_event_cb(leaving_scr, pm_unloaded_cb);
lv_obj_add_event_cb(leaving_scr, pm_unloaded_cb,
LV_EVENT_SCREEN_UNLOADED, pm);
/* Animate transition. auto_del=false for cacheable leaving pages. */
pm->animating = true;
lv_screen_load_anim(new_scr, LV_SCR_LOAD_ANIM_MOVE_LEFT,
PM_ANIM_TIME_MS, 0, !cache_leaving);

create_cb runs, the screen slides left over PM_ANIM_TIME_MS (300, page_manager.h:29), render_cb is driven only by pm_render from the 33 ms timer (sensorhub_ui.c:166) with the tick's sensorhub_snapshot_t, and Back calls destroy_cb (page_manager.c:107-108).

The five Makefile touch points the README omits

template/README.md:95-97 says "three places". A new page directory needs these as well, all by hand (bento.sh:245-253 offers doctor|menus|enable|disable|remove|build|flash|verify|clean — there is no add):

# What proj_cm55/Makefile
1 Flag default ENABLE_PAGE_X ?= 1 :23-30
2 Auto-guard: $(if $(filter 1,$(ENABLE_PAGE_X)),$(if $(wildcard $(_PD)/x/.),,$(eval ENABLE_PAGE_X:=0))) :65-72
3 CY_IGNORE line when the flag is 0 :636-700
4 INCLUDES+=modules/page-components/<dir> — no wildcard; only _core and listed dirs are on the include path :737-798
5 DEFINES+=ENABLE_PAGE_X=$(ENABLE_PAGE_X) :897-898

Plus the #include "page_x.h" in sensorhub_ui.c:34-79.

Step by step — reproduce all four wiring states deliberately

Do these in order on a copy of the Edge AI fragments renamed to your page; rebuild CM55 between steps. There is no UART output on this path (proj_cm55/main.c:9-10) — every observable is screen state.

State 1 — enum only

Add PAGE_ID_X = 24, at the end of page_id_t (next free number) and raise PM_MAX_PAGES if the static assert fires.

What you should observe. Build succeeds. Home grid unchanged. Silent.

State 2 — enum + pm_register, no card

Add the pm_register block and the Makefile touch points, but no s_card_defs[] entry.

What you should observe. Build succeeds; the board boots; the Home grid is identical; the page is dead. This is the shipped state of Motion and Environ — registered at sensorhub_ui.c:235 and :250, enabled by default (proj_cm55/Makefile:23-24), absent from page_home.c:138-200. README.md:216 names the diagnostic: "Menu is missing from Home | its flag is 0, or s_card_defs[] has no entry. ./bento.sh menus tells you which."

State 3 — card, no pm_register

Comment out the pm_register block; keep the card.

What you should observe. The card renders. Tapping it does nothing — pm_navigate returns on create_cb == NULL (page_manager.c:92). It is silently ignored, not a crash. README.md:100 and :217 say it "crashes the moment it is tapped"; that claim is stale.

State 4 — all three

Restore the pm_register block.

What you should observe. A new card at the position of its s_card_defs[] entry, with the accent colour from .color; a tap slides left over 300 ms; render_cb begins firing at ~33 Hz; Back slides right (page_manager.c:198) and destroy_cb runs.

Two compile-time failure signatures worth seeing once

  • Omit touch point 4 → fatal error: page_x.h: No such file or directory from sensorhub_ui.c. A compile error, not a link error.
  • Renumber an existing page → page_id_ordinal_assert.c:22-25 fires: "PAGE_ID_PLAYGROUND has moved. lib/ipc_core/libbento_ipc.a was built for ordinal 7 and stores it as an immediate; a mismatch links cleanly and compares the wrong page at runtime."

Traps

Warning
Do not copy motion or environ as your template. Both are the "registered, no card" state the README itself calls the most common mistake. A reader who mirrors all three sites from a model that only wires two will see nothing.
page_id_t ordinals are ABI. libbento_ipc.a bakes PAGE_ID_PLAYGROUND == 7 (lib/ipc_core/PROVENANCE.txt). Add at the end; never renumber, never re-guard. The rationale comment in page_id_ordinal_assert.c:11-13 still describes the old guarded enum — the assert is right, its comment is stale.
All LVGL work in the GFX task. Your create_cb/render_cb/destroy_cb already run there. If your page hosts IPC-driven widgets, bind the containers on create and NULL them on destroy (Chapter F2, and B3).
page_edge_ai.h:12-14 says "PM_MAX_PAGES is 20 … 18 pages". Actual: 24 and 25 registrations (id-indexed, so safe). Do not size anything from that comment.

Variant box

Identical on both variants. The only variant-specific line in proj_cm55/Makefile is the MPY gate at :237 and the version label; page wiring does not touch it.