บทเรียน 2.4 — โครง navigation: แถบเมนู หน้า และการสลับหน้า

โครง navigation หลัก — top nav bar พร้อมปุ่มสลับหน้า + icon action buttons + หน้าต่าง ๆ ที่ lv_menu สร้างไว้ครั้งเดียวแล้วสลับตามเมนูที่เลือก (README ของ episode เล่าแบบ stage container แต่โค้ดใช้ lv_menu ให้ยึดตามโค้ด)

TESAIoT Dev Kit · PSoC Edge E84 · ภาษา C · ModusToolbox

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

เป้าหมาย

เมื่อจบบทเรียนนี้ คุณจะ

  1. สร้างเมนูนำทางด้วย lv_menu ที่สร้างทุกหน้าไว้ครั้งเดียว แล้วสลับหน้าที่แสดงตามเมนูที่เลือก
  2. แยก layout, navigation logic และหน้าแต่ละหน้าออกจากกันตามโครงไฟล์ของ episode
  3. เพิ่มหน้าใหม่หนึ่งหน้าเข้าเมนูโดยไม่แก้หน้าที่มีอยู่
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

ก่อนเริ่ม

  • ผ่านบทเรียนก่อนหน้ามาแล้ว: fw-stack.m02.l03
  • บอร์ด: TESAIoT Dev Kit
  • แพลตฟอร์ม: PSoC Edge E84 · ModusToolbox
  • เวลาโดยประมาณ: เนื้อหา 15 + ฝึกตาม 25 + แล็บ 20 + เช็กความเข้าใจ 5 = 65 นาที
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

ดูของจริงก่อน

หน้าจอของ EP04 — Menu Navigation บน TESAIoT Dev Kit

ก่อนอ่านโค้ด ให้ทายว่าหน้าจอนี้มี object อะไรบ้าง และอะไรเปลี่ยนเมื่อผู้ใช้แตะหรือเมื่อค่าเซนเซอร์เปลี่ยน

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

แนวคิด — โค้ดจริงใช้ lv_menu ไม่ใช่ 4-container

README ต้นทางส่วน How เล่าว่าใช้ container header/nav/stage/footer + lv_obj_clean() rebuild ทุกครั้ง

โค้ดจริงที่ commit 9a8e3ed ใช้ lv_menu — widget สำเร็จรูปสำหรับ multi-page navigation

สร้างทุกหน้าไว้ล่วงหน้าด้วย lv_menu_page_create() แล้วสลับด้วย lv_menu_set_page() เท่านั้น ไม่มี clean/rebuild

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

แนวคิด — โครงหน้าจอ

header (settings icon + title) คงที่ → top nav (Home/WiFi/Display/Info/Back) คงที่ → lv_menu ตรงกลาง → footer

lv_menu มี header ของตัวเอง ต้องซ่อนด้วย LV_OBJ_FLAG_HIDDEN ทั้ง main header และ sidebar header — ไม่งั้นซ้อนกันสองชั้น

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

แนวคิด — สองทางเข้าหน้าเดียวกัน

top nav ที่แสดงตลอด + sidebar ("Navigate") ที่ซ่อนได้ เปิด/ปิดด้วยปุ่ม settings icon

ทั้งสองทางเรียกคนละ callback แต่ไปจบที่ menu_nav_set_page() เดียวกัน — ปลายทางเดียว สองประตู

active-state ต้องซิงก์สองฝั่งพร้อมกัน ทุกครั้งที่เปลี่ยนหน้า ไม่งั้นปุ่มอีกฝั่ง highlight ค้าง

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

แนวคิด — ทำไมต้อง lv_async_call

menu_nav_queue_page_switch() ไม่เรียก lv_menu_set_page() ทันที เก็บ pending_page แล้วส่งผ่าน lv_async_call()

คอมเมนต์ในซอร์ส: "Always defer actual page switching to avoid lv_menu state race after sidebar transitions"

ปุ่ม top nav อาจยุบ sidebar ก่อนสลับหน้า — ถ้าสลับ synchronous จะชนกับ layout state ของ lv_menu ที่ยังปรับตัวไม่เสร็จ

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

แนวคิด — ทักษะที่ฝึกในบทเรียนนี้

บทเรียนนี้พัฒนาทักษะต่อไปนี้ (ตามระดับ 1–5 ของหลักสูตร)

  • gui.hmi (ระดับ 2)
  • prog.design-patterns (ระดับ 2)
  • prog.state-machines (ระดับ 1)
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

ตัวอย่างสมบูรณ์ — เลื่อนการสลับหน้าด้วย lv_async_call

โค้ดจาก tesaiot/developer-hub (Apache-2.0) · commit 9a8e3ed · nav/menu_nav_logic.c

static void menu_nav_async_apply_pending_page(void *user_data)
{
    menu_nav_state_t *state = (menu_nav_state_t *)user_data;
    if(state == NULL || !state->page_switch_pending) {
        return;
    }
    state->page_switch_pending = false;
    menu_nav_apply_page_now(state, state->pending_page);
}

static void menu_nav_queue_page_switch(menu_nav_state_t *state,
                                       menu_nav_page_id_t page_id)
{
    state->pending_page = page_id;
    if(!state->page_switch_pending) {
        state->page_switch_pending = true;
        lv_async_call(menu_nav_async_apply_pending_page, state);
    }
}
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

ตัวอย่างสมบูรณ์ — สร้าง lv_menu และหน้าทั้งสี่

nav/ui_menu_navigation.c

lv_obj_t *menu = lv_menu_create(content);
lv_menu_set_mode_header(menu, LV_MENU_HEADER_TOP_FIXED);
lv_menu_set_mode_root_back_button(menu,
    LV_MENU_ROOT_BACK_BUTTON_DISABLED);

lv_obj_t *home_page = create_full_content_page(menu, "Home", ...);
lv_obj_t *page_wifi = create_full_content_page(menu, "WiFi Manager", ...);
lv_obj_t *page_display = create_full_content_page(menu, "Display Setting", ...);
lv_obj_t *page_device = create_full_content_page(menu, "Device Info", ...);

lv_menu_set_page(menu, home_page);

ทุกหน้าถูกสร้างไว้ล่วงหน้าครั้งเดียว — สลับหน้าคือแค่เปลี่ยนว่าจะแสดงอันไหน

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

จุดที่มักพลาด

  • README ต้นทางเล่าแบบ stage+clean/rebuild แต่โค้ดจริงใช้ lv_menu — โค้ดกับคำบรรยายขัดกัน ให้ยึดโค้ด
  • ลืมซ่อน header ภายในของ lv_menu → เห็น header ซ้อนกันสองชั้น
  • เรียก lv_menu_set_page() ทันทีหลัง toggle sidebar โดยไม่ผ่าน lv_async_call → ชนกับ layout state
  • อัปเดต active-state แค่ฝั่งเดียว (top nav หรือ sidebar) → อีกฝั่ง highlight ค้าง
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

ตัวอย่างสมบูรณ์ — build และ flash

# ในโฟลเดอร์ master template (ดูบทเรียน 1.1)
# 1) ลบไฟล์ของ episode เก่าใน proj_cm55/apps/
# 2) คัดลอกไฟล์ทั้งหมดของ episode นี้ลงใน proj_cm55/apps/
make build
make program     # flash ผ่าน KitProg3

หรือเปิด ตัวอย่างนี้บน Developer Hub แล้ว flash เฟิร์มแวร์สำเร็จรูป

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

ฝึกเติม / แล็บ

  1. ทาย ก่อนแก้: เลือกค่าหนึ่งค่าที่ README ของตัวอย่างอธิบายไว้ในส่วน How แล้วเขียนว่าจะเห็นอะไรเปลี่ยนบนจอหรือใน log
  2. แก้และรัน build + flash แล้วเทียบกับที่ทายไว้ ถ้าไม่ตรง ให้หาว่าเข้าใจส่วนไหนผิด
  3. ทำเพิ่ม ต่อยอดหนึ่งอย่างที่ตัวอย่างยังไม่มี แล้วเก็บภาพหรือวิดีโอไว้ใน portfolio
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

เช็กความเข้าใจ

  • lv_menu เก็บหน้าทั้งหมดไว้อย่างไร และการสร้างทุกหน้าไว้ครั้งเดียวมีข้อดีข้อเสียอะไร
  • ถ้าเพิ่มหน้าใหม่ ต้องแก้ไฟล์ใดบ้าง
  • ถ้าเปลี่ยนเป็นสร้างหน้าใหม่ทุกครั้งที่สลับเมนู โดยไม่ลบหน้าเก่า จะเกิดอะไรกับหน่วยความจำ

คำตอบอยู่ใน README ของตัวอย่างและในโค้ด ถ้าตอบข้อใดไม่ได้ ให้กลับไปอ่านส่วน Why / What / How อีกครั้ง

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

ไปต่อ

  • ทบทวนเป้าหมายทั้ง 3 ข้อของบทเรียนนี้ก่อนไปต่อ
  • ถ้าตอบคำถามหน้าที่แล้วไม่ได้ครบ กลับไปอ่าน README ของตัวอย่างส่วน Why / What / How อีกครั้ง
  • พร้อมแล้วไปเปิดบทเรียนถัดไปในโมดูลเดียวกัน
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

แหล่งอ้างอิง

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

เครดิต

"TESAIoT Firmware Stack: เฟิร์มแวร์ภาษา C บน TESAIoT Dev Kit" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0

ตัวอย่างโค้ดเป็นผลงานของ TESAIoT Firmware Stack โดยสมาคมสมองกลฝังตัวไทย (TESA) ใน tesaiot/developer-hub รายละเอียดการให้เครดิตอยู่ที่ ATTRIBUTION.md

ภาพหน้าจอในสไลด์นี้มาจาก tesaiot/developer-hub (hmi_ep04_menu_navigation) ที่ commit 9a8e3ed

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0