SDK สำหรับ TESAIoT Dev Kit
คู่มืออ้างอิง API และ Tutorial (MTB & µPython)
Loading...
Searching...
No Matches
F2 — การขับ widget จาก MicroPython ผ่าน IPC
variant ที่ใช้ได้
mtb-mpy และ mtb-only

กลไกของ handler ฝั่ง CM55 ใช้ได้กับทั้งสอง variant ส่วนฝั่ง ที่ขับ ในบทนี้ — โมดูล MicroPython ui.* และ ui._diag() — เป็น mtb-mpy เท่านั้น บน mtb-only มี handler ตัวเดียวกันอยู่ แต่ไม่มีสิ่งใดในเฟิร์มแวร์ที่ส่งมอบจริงส่งคำสั่งไปหามัน

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

เข้าใจว่าฟังก์ชัน ui_widget_mgr_* ทั้ง 25 ตัวไม่ใช่ C API สำหรับโค้ด page ที่เขียนเอง ผู้เรียกเพียงรายเดียวคือ switch ของคำสั่ง IPC ใน process_ui_command() ส่วนผู้ใช้ที่แท้จริงคือสคริปต์ MicroPython ที่ข้ามมาทาง IPC เมื่อจบบทนี้จะไล่รอยการเรียก ui.Label(...) จาก REPL ไปจนถึงอ็อบเจ็กต์ LVGL ได้ และรู้ว่า symbol 7 ตัวใดที่ต้อง นิยามเอง แทนที่จะเรียกใช้

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

ข้อกำหนดการเรียกใช้ในบรรทัดเดียวipc_ui.h:7-8: ฝั่ง ISR ทำเพียงใส่คิว ส่วนงาน LVGL เกิดขึ้นบน timer 50 ms ใน GFX-task context ข้อกำหนดในไฟล์ header ui_widget_mgr.h:6 ระบุว่า "Creates, modifies, deletes LVGL widgets in GFX task context"

การ init ipc_ui_init(NULL) เป็นการเรียกลำดับสุดท้ายของการ bring-up ฝั่ง CM55 โดยมี display_ok เป็น gate (ด่านกั้น) (tesaiot_display.c:470-479) และตัวมันเรียก ui_widget_mgr_init() เองอยู่แล้ว (ipc_ui.c:890) — ห้ามเรียกฟังก์ชันนั้นเอง container ทั้งสองรับค่า NULL ได้ (deferred binding — ผูกค่าจริงในภายหลัง):

ที่มา
ยกมาจาก tesaiot_display.c:470-479 (คอมไพล์รวมอยู่ใน archive สำเร็จรูป ไม่ได้ส่งมอบมาเป็นซอร์ส)

ผูก container ตอน create และตั้งเป็น NULL ตอน destroy page Playground คือตัวอย่างที่มีอยู่จริงในของที่ส่งมอบ:

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

การกระจายคำสั่ง process_ui_command() (ipc_ui.c:393 ระบายคิวจาก :875) เป็น switch ครอบ IPC_CMD_UI_* และ 22 ใน 25 ฟังก์ชันพื้นฐานกระจายอยู่ภายในนั้น ตัวอย่างที่เป็นตัวแทน:

ที่มา
ยกมาจาก ipc_ui.c:393-394,418-477 (คอมไพล์รวมอยู่ใน archive สำเร็จรูป ไม่ได้ส่งมอบมาเป็นซอร์ส)

การระบาย event — ฝั่งผู้ผลิตคือ ui_widget_mgr_event_push (ui_widget_mgr.c:333 และ :406 ซึ่งอยู่หลัง event mask ของแต่ละ widget) ฝั่งผู้อ่านคือ case POLL_EVENTS:

ที่มา
ยกมาจาก ipc_ui.c:516-532 (คอมไพล์รวมอยู่ใน archive สำเร็จรูป ไม่ได้ส่งมอบมาเป็นซอร์ส)

มี 2 ฟังก์ชันที่ไม่ได้กระจายจาก switch นี้ ได้แก่ ui_widget_mgr_init (อยู่ภายใน ipc_ui_init) และ ui_widget_mgr_set_parent (ผู้เรียกรายเดียวคือ ipc_ui_set_container(), ipc_ui.c:918-921) และอีกหนึ่งตัวคือ ui_widget_mgr_count ซึ่งไม่มีผู้เรียกที่ใดเลย ตัวอย่างของมันจึงเป็นตัวอย่างที่เขียนขึ้นเอง

parent เป็น NULL ได้ call site ทั้ง 5 จุดตรวจ NULL ทุกจุด — ค่าเป็น NULL ในช่วงระหว่าง set_container(NULL) ตอน destroy กับการผูกของ page ถัดไป:

ที่มา
ยกมาจาก ipc_ui.c:516-532 (คอมไพล์รวมอยู่ใน archive สำเร็จรูป ไม่ได้ส่งมอบมาเป็นซอร์ส)

ทีละขั้น

ขั้นที่ 1 — เปิด page Playground (ทั้งสอง variant)

Home → Playground

สิ่งที่ควรสังเกต พาเนลคอนโซล ชั้นคอนโซลกับชั้น widget ใช้พร้อมกันไม่ได้ ตัวสลับเป็นการสลับแบบ ไม่รู้สถานะ จึงมี 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);
}
}

ขั้นที่ 2 — สร้าง widget จาก REPL (mtb-mpy)

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

สิ่งที่ควรสังเกต container ของ widget เลิกซ่อนตัวเมื่อเกิด POLL_EVENTS ครั้งแรก และ label ปรากฏขึ้น text() ปรับค่าให้ภายในหนึ่ง tick 50 ms (SET_TEXT ยังปลุกโหมดเร็วด้วย, ipc_ui.c:463) สตริงของ label ถูกตัดที่ 95 ไบต์ในตัว constructor: UI_CREATE_TEXT_MAX มีค่า 96 (ipc_ui_protocol.h:658) และ constructor ตัดเหลือ UI_CREATE_TEXT_MAX - 1 (modui.c:1223)

ขั้นที่ 3 — พิสูจน์ว่าเฟรมกำลังไหลอยู่ (mtb-mpy)

>>> ui._diag()

สิ่งที่ควรสังเกต เวิร์ด 10 ตัวจาก ipc_ui_platform_diag() โดย flush_start_count เพิ่มขึ้นระหว่างการเรียก 2 ครั้ง บน mtb-only ให้เรียก ipc_ui_platform_diag(out, 10) จากโค้ด CM55 ที่เขียนเองแทน

ขั้นที่ 4 — เริ่มสคริปต์ใหม่อย่างสะอาด

การเริ่มใหม่ของ Playground รื้อ widget ทิ้ง ก่อนคำสั่ง restart ทาง IPC เพื่อไม่ให้ CM33_NS ดันข้อมูลเข้าตารางที่ล้างค้างอยู่ครึ่งเดียว:

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

สิ่งที่ควรสังเกต widget หายไป แล้ว widget ของสคริปต์ปรากฏขึ้นใหม่ คำสั่ง CLEAR_ALL ยังรีเซ็ตการนำทางอัตโนมัติของ LCD ด้วย (ipc_ui.c:556)

hook ชนิด weak 7 ตัว — ให้นิยามเอง ห้ามเรียก

ตรงกับรายการใน dist/ipc_core/overridable.txt พอดี ได้แก่ cm55_controls_snapshot (เขียนทับในเทมเพลตที่ 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 ทำงานใน GFX context และอาจแตะจอแสดงผล, ipc_ui.h:40-41), ipc_ui_ext_clear_all (เป็น W ใน archive), ipc_ui_platform_diag (strong definition เก็บไว้ใน archive ที่ tesaiot_display.c:593-609) หน้าคู่มืออ้างอิงที่บอกให้ "call this" กับตัวใดตัวหนึ่งในนี้คือหน้าที่ผิด

กับดัก

Warning
การลืม set_container(NULL) ตอน destroy เป็น use-after-free lv_screen_load_anim ที่เปิด auto-delete คืนหน่วยความจำของอ็อบเจ็กต์ใน page นั้นไปแล้ว คำสั่ง IPC ที่มาทีหลังจึงเขียนลงหน่วยความจำ LVGL ที่คืนไปแล้ว ให้ผูกลำดับ ui แล้วตามด้วย lcd ตอน create และใช้ลำดับเดียวกันตอน destroy
ห้ามเรียก LVGL นอก GFX task timer 50 ms เป็นที่เดียวที่ทำได้ ส่วนฝั่ง ISR ทำเพียงใส่คิว
ipc_sensorhub_snapshot ล้างแฟล็ก changed ทิ้ง — ต่อหนึ่ง tick มีผู้อ่านได้รายเดียว ฝั่ง UI อ่าน snapshot หนึ่งครั้งทุก 33 ms แล้วกระจายต่อผ่าน pm_render()
dist/ipc_core/PROVENANCE.txt ฝังลำดับเลข 7 ของ PAGE_ID_PLAYGROUND ไว้ตายตัว โปรเจกต์ที่เรียงลำดับ page_id_t ต่างออกไปจะ "succeeds with no diagnostic and compares the wrong page" (บท F1)

กล่อง variant

mtb-mpy mtb-only
ฝั่งที่ขับ โมดูล ui.* ผ่าน IPC ไม่มีผู้ส่งในของที่ส่งมอบ แต่มี handler อยู่
การพิสูจน์เฟรม ui._diag() ipc_ui_platform_diag() ฝั่ง C
กลไกของ page เหมือนกัน เหมือนกัน