SDK สำหรับ TESAIoT Dev Kit
คู่มืออ้างอิง API และ Tutorial (MTB & µPython)
Loading...
Searching...
No Matches
F1 — การเพิ่มหน้าจอ: 3 ไฟล์ กับความจริงเรื่อง Makefile
variant ที่ใช้ได้
mtb-mpy และ mtb-only

CM55 ไม่ขึ้นกับ variant ทุกเรื่องในบทนี้ใช้ได้กับทั้งสองแพ็กเกจ

เป้าหมายของหัวข้อนี้

ต่อสาย page ใหม่เข้ากับ UI ของ CM55 ให้เข้าถึงได้ และรู้ว่าสถานะการต่อสายที่ไม่ครบแต่ละแบบมีหน้าตาอย่างไรโดยรู้จากการสร้างสถานะนั้นขึ้นมาจริงทีละแบบ ตัวอย่างที่ใช้เดินเรื่องคือ Edge AI ซึ่งเป็น page ขนาดเล็กหน้าเดียวในเทมเพลตที่ส่งมอบจริงที่มีชิ้นส่วนครบทั้งสามชิ้น และมีข้อกำหนดการเรียกใช้ create/render/destroy เขียนไว้ส่วนเทมเพลตที่ README แนะนำไว้คือ motion และ environ นั้น บทนี้ ไม่ใช้ ด้วยเหตุผลที่ระบุไว้ในหัวข้อกับดัก

ลำดับการทำงานจริงของเฟิร์มแวร์

(1) รายการใน enumpage_manager.h:65, PAGE_ID_EDGE_AI = 13 ข้อกำหนดการเรียกใช้ที่ล้อมรอบอยู่คือ 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 มีค่า 24U (page_manager.h:21) และ _Static_assert ที่ :79-80 ปฏิเสธ page_id_t ที่เกินค่านั้น

(2) callback ทั้งสามตัว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;

ลงทะเบียนสำหรับ Edge AI ที่ sensorhub_ui.c:254-266 โดยมี BENTO_HAS_EDGE_AI เป็นตัวกัน (guard):

/* ...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 โดยตรง — page manager เป็นเจ้าของวงจรชีวิต (lifecycle) และเรียกทั้ง 3 ตัวใน GFX-task context ตัวฟังก์ชันเหล่านี้คอมไพล์มาแล้ว (proj_cm55/Makefile:994 ละ page_edge_ai.c ไว้ symbol มาจาก libbento_cm55.a) ฉะนั้นหากต้องการอ่าน ซอร์ส ของ callback 3 ตัว ให้อ่าน page_motion.c:106/:210/:267

(3) การ์ดบนหน้า Homepage_home.c:147-149 ภายใน 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

จุดที่ทั้งสามชิ้นมาบรรจบกันขณะทำงาน การ์ดแต่ละใบผูก card_click_cb ไว้กับ 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]);

…ซึ่งเรียก 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 ทำงาน หน้าจอเลื่อนไปทางซ้ายภายในเวลา PM_ANIM_TIME_MS (300, page_manager.h:29) render_cb ขับด้วย pm_render จาก timer 33 ms (sensorhub_ui.c:166) เท่านั้น พร้อม sensorhub_snapshot_t ของ tick นั้น และปุ่ม Back เรียก destroy_cb (page_manager.c:107-108)

จุดแก้ไข 5 จุดใน Makefile ที่ README ไม่ได้กล่าวถึง

template/README.md:95-97 ระบุว่ามี "three places" แต่ ไดเรกทอรี ของ page ใหม่ยังต้องการจุดต่อไปนี้ด้วยและต้องแก้ด้วยมือทั้งหมด (bento.sh:245-253 มีคำสั่ง doctor|menus|enable|disable|remove|build|flash|verify|clean — ไม่มี add):

# สิ่งที่ต้องแก้ proj_cm55/Makefile
1 ค่าเริ่มต้นของ flag ENABLE_PAGE_X ?= 1 :23-30
2 ตัวกันอัตโนมัติ: $(if $(filter 1,$(ENABLE_PAGE_X)),$(if $(wildcard $(_PD)/x/.),,$(eval ENABLE_PAGE_X:=0))) :65-72
3 บรรทัด CY_IGNORE เมื่อ flag เป็น 0 :636-700
4 INCLUDES+=modules/page-components/<dir> — ไม่มี wildcard มีเฉพาะ _core และไดเรกทอรีที่ระบุไว้เท่านั้นที่อยู่บน include path :737-798
5 DEFINES+=ENABLE_PAGE_X=$(ENABLE_PAGE_X) :897-898

เพิ่มเติมคือ #include "page_x.h" ใน sensorhub_ui.c:34-79

ทีละขั้น — สร้างสถานะการต่อสายทั้ง 4 แบบขึ้นมาโดยตั้งใจ

ทำตามลำดับนี้บนสำเนาของชิ้นส่วน Edge AI ที่เปลี่ยนชื่อเป็น page ของตนเอง และสร้าง CM55 ใหม่ระหว่างแต่ละขั้น เส้นทางนี้ไม่มีผลลัพธ์ออกทาง UART (proj_cm55/main.c:9-10) — สิ่งที่สังเกตได้ทั้งหมดคือสถานะบนหน้าจอ

สถานะที่ 1 — มีเฉพาะ enum

เพิ่ม PAGE_ID_X = 24, ไว้ท้าย page_id_t (เลขว่างตัวถัดไป) และเพิ่มค่า PM_MAX_PAGES หาก static assert ทำงาน

สิ่งที่ควรสังเกต build ผ่าน Home grid ไม่เปลี่ยน ไม่มีสัญญาณใด ๆ

สถานะที่ 2 — enum + pm_register แต่ไม่มีการ์ด

เพิ่มบล็อก pm_register และจุดแก้ไขใน Makefile แต่ไม่เพิ่มรายการใน s_card_defs[]

สิ่งที่ควรสังเกต build ผ่าน บอร์ดบูตขึ้น Home grid เหมือนเดิม และ page นั้นตายสนิท นี่คือสถานะของ Motion และ Environ ในของที่ส่งมอบจริง — ลงทะเบียนไว้ที่ sensorhub_ui.c:235 และ :250 เปิดใช้งานเป็นค่าเริ่มต้น (proj_cm55/Makefile:23-24) แต่ไม่ปรากฏใน page_home.c:138-200 README.md:216 ระบุข้อมูลวินิจฉัยไว้ว่า "Menu is missing from Home | its flag is 0, or s_card_defs[] has no entry. ./bento.sh menus tells you which."

สถานะที่ 3 — มีการ์ด แต่ไม่มี pm_register

คอมเมนต์บล็อก pm_register ทิ้งไว้ และคงการ์ดไว้

สิ่งที่ควรสังเกต การ์ดเรนเดอร์ออกมาตามปกติ แต่การแตะการ์ด ไม่เกิดอะไรขึ้นpm_navigate คืนค่าออกไปเมื่อ create_cb == NULL (page_manager.c:92) เป็นการเพิกเฉยอย่างเงียบ ๆ ไม่ใช่การ crash README.md:100 และ :217 ระบุว่า "crashes the moment it is tapped" ข้ออ้างนั้นล้าสมัยแล้ว

สถานะที่ 4 — ครบทั้งสามชิ้น

นำบล็อก pm_register กลับเข้ามา

สิ่งที่ควรสังเกต การ์ดใบใหม่ปรากฏที่ตำแหน่งตามรายการใน s_card_defs[] พร้อมสีเน้นจาก .color การแตะทำให้หน้าจอเลื่อนไปทางซ้ายภายใน 300 ms render_cb เริ่มทำงานที่ประมาณ 33 Hz ปุ่ม Back เลื่อนกลับไปทางขวา (page_manager.c:198) และ destroy_cb ทำงาน

ลายเซ็นความล้มเหลวตอนคอมไพล์ 2 แบบที่ควรเห็นสักครั้ง

  • ละจุดแก้ไขที่ 4 → fatal error: page_x.h: No such file or directory จาก sensorhub_ui.c เป็น error ตอนคอมไพล์ ไม่ใช่ error ตอนลิงก์
  • เปลี่ยนหมายเลขของ page ที่มีอยู่เดิม → page_id_ordinal_assert.c:22-25 ทำงาน: "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."

กับดัก

Warning
ห้ามคัดลอก motion หรือ environ ไปใช้เป็นเทมเพลต ทั้งสองอยู่ในสถานะ "registered, no card" ซึ่ง README เองเรียกว่าเป็นความผิดพลาดที่พบบ่อยที่สุด ผู้อ่านที่ลอกครบทั้ง 3 จุดจากต้นแบบที่ต่อสายไว้เพียง 2 จุดจะไม่เห็นอะไรเลย
ลำดับเลข (ordinal) ของ page_id_t เป็น ABI libbento_ipc.a ฝังค่า PAGE_ID_PLAYGROUND == 7 ไว้ (lib/ipc_core/PROVENANCE.txt) ให้เพิ่มไว้ท้ายสุดเสมอ ห้ามเปลี่ยนหมายเลข ห้ามใส่ตัวกันใหม่ คอมเมนต์อธิบายเหตุผลที่ page_id_ordinal_assert.c:11-13 ยังบรรยาย enum แบบเดิมที่มีตัวกัน — ตัว assert ถูกต้อง แต่คอมเมนต์ล้าสมัย
งาน LVGL ทั้งหมดต้องอยู่ใน GFX task create_cb/render_cb/destroy_cb ของ page ที่เขียนเองทำงานอยู่ที่นั่นอยู่แล้ว หาก page นั้นมี widget ที่ขับด้วย IPC ให้ผูก container ตอน create และตั้งเป็น NULL ตอน destroy (บท F2 และ B3)
page_edge_ai.h:12-14 ระบุว่า "PM_MAX_PAGES is 20 … 18 pages" ค่าจริงคือ 24 และมีการลงทะเบียน 25 รายการ (จัดทำดัชนีด้วย id จึงปลอดภัย) ห้ามกำหนดขนาดของสิ่งใดจากคอมเมนต์นั้น

กล่อง variant

เหมือนกันทั้งสอง variant บรรทัดเดียวใน proj_cm55/Makefile ที่ขึ้นกับ variant คือ gate ของ MPY ที่ :237 และป้ายเวอร์ชัน การต่อสาย page ไม่ไปแตะบรรทัดนั้น