สแกน Wi-Fi และแสดงรายการเครือข่าย
เป้าหมาย
หัวข้อที่มีชื่อว่า “เป้าหมาย”- สแกน Wi-Fi ผ่าน WHD/cy_wcm แล้วแสดงรายการพร้อม RSSI และชนิด security
- แยก scan service ออกจากหน้า UI และส่งผลสแกนเข้าหน้าอย่างปลอดภัย
- อ่านค่า RSSI และบอกได้ว่าเครือข่ายใดสัญญาณดีพอจะเชื่อมต่อ
สแกน Wi-Fi ต้องผ่าน stack สามชั้น
หัวข้อที่มีชื่อว่า “สแกน Wi-Fi ต้องผ่าน stack สามชั้น”การสแกนบน PSoC Edge ไล่ผ่าน whd (WiFi Host Driver คุย SDIO กับโมดูลวิทยุ) → cy_wcm (Connection Manager ห่อ
whd เป็น API scan/connect ระดับสูง) → callback ของแอปที่ cy_wcm เรียกทุกครั้งที่เจอ AP ใหม่ episode นี้ห่อทั้ง
สามชั้นไว้ใน service layer (wifi_scan_service.c) เพื่อให้หน้า UI (ui_wifi_list_page.c) เรียกแค่
wifi_scan_service_start() / wifi_scan_service_process() โดยไม่ต้องรู้จัก cy_wcm เลย
pre-init: warm up radio ตั้งแต่ boot ไม่ใช่ตอนกดปุ่ม
หัวข้อที่มีชื่อว่า “pre-init: warm up radio ตั้งแต่ boot ไม่ใช่ตอนกดปุ่ม”example_main() เรียก wifi_scan_service_preinit() ก่อนสร้าง UI เสียอีก ฟังก์ชันนี้ทำ SDIO bring-up (ตั้ง
interrupt handler ของ SDIO และ host-wake, ลงทะเบียน deep-sleep callback ของ SDHC controller) และ cy_wcm_init()
ให้เสร็จตั้งแต่ boot ถ้าไปเรียกตอนกดปุ่ม “Scan” ครั้งแรกโดยตรง ผู้ใช้จะเห็น UI ค้าง 1-3 วินาทีระหว่างที่ radio
กำลัง bring-up — pre-init pattern นี้ทำให้การกดปุ่มครั้งแรกเร็วเท่ากับครั้งถัดไป ถ้า pre-init ล้มเหลว โค้ดแค่ log
แล้วปล่อยให้ UI เปิดต่อได้ (แค่ปุ่ม Scan จะ fail เมื่อลอง)
callback จาก WHD ไม่แตะ LVGL เลยแม้แต่บรรทัดเดียว — ต่างจากที่ README ต้นทางอธิบาย
หัวข้อที่มีชื่อว่า “callback จาก WHD ไม่แตะ LVGL เลยแม้แต่บรรทัดเดียว — ต่างจากที่ README ต้นทางอธิบาย”README ของ episode บน Developer Hub อธิบายว่าใช้ lv_async_call() ส่งงานจาก callback ของ WHD กลับเข้า LVGL thread
แต่โค้ดจริงที่ commit 9a8e3ed ใช้อีก pattern หนึ่งคือ critical section + poll timer: wifi_scan_callback()
(เรียกจาก WCM internal task ไม่ใช่ LVGL task) แค่คัดลอกผล AP แต่ละตัวเข้า array ภายใน taskENTER_CRITICAL() /
taskEXIT_CRITICAL() ของ FreeRTOS แล้วตั้งธง scan_done_pending/scan_error_pending เท่านั้น ไม่เรียก widget API
ของ LVGL แม้แต่ตัวเดียว — วิธีนี้ก็ปลอดภัยจาก race เหมือนกับ lv_async_call() เพียงแค่เป็นคนละกลไก
ฝั่ง LVGL ใช้ lv_timer มา “poll” ธงแทนที่จะรอถูกปลุก
หัวข้อที่มีชื่อว่า “ฝั่ง LVGL ใช้ lv_timer มา “poll” ธงแทนที่จะรอถูกปลุก”ui_wifi_list_page_create() สร้าง lv_timer_create(ui_wifi_poll_timer_cb, UI_WIFI_POLL_MS, NULL) ที่ UI_WIFI_POLL_MS = 150 — ทุก 150 มิลลิวินาที ui_wifi_poll_timer_cb() ซึ่งรันอยู่บน LVGL thread อยู่แล้ว (ปลอดภัยที่จะเรียก
widget API) จะเรียก wifi_scan_service_process() ซึ่งอ่านและเคลียร์ธง scan_done_pending/scan_error_pending
ภายใน critical section เดียวกัน ถ้าธงบอกว่าสแกนเสร็จ ก็ค่อยอ่าน list ผลลัพธ์แล้ว render ใหม่ — สรุปคือ ข้อมูล
ข้าม thread ด้วย critical section, การแจ้งเตือนข้าม thread ด้วยการ poll เป็นจังหวะ แทนที่จะ push เข้า LVGL
ทันทีแบบ lv_async_call() ทั้งสองวิธีถูกต้องและปลอดภัยเท่ากัน แต่เป็นคนละกลไก — บทเรียนนี้อธิบายตามโค้ดจริง
กันสแกนซ้อนสแกนที่ตัว service ไม่ใช่แค่ที่ปุ่ม
หัวข้อที่มีชื่อว่า “กันสแกนซ้อนสแกนที่ตัว service ไม่ใช่แค่ที่ปุ่ม”wifi_scan_service_start() เช็ค service->scanning เป็นด่านแรกและคืน false ทันทีถ้ากำลังสแกนอยู่ ฝั่ง UI เอง
ก็ใส่ LV_STATE_DISABLED ให้ปุ่ม Scan ระหว่างรอผลเช่นกัน — การกันซ้อนสองชั้นนี้ (service ชั้นใน + ปุ่ม UI ชั้นนอก)
ทำให้ระบบยังปลอดภัยแม้ฝั่ง UI จะลืม disable ปุ่มเอง
RSSI, SSID ที่มองไม่เห็น และการเรียงผล
หัวข้อที่มีชื่อว่า “RSSI, SSID ที่มองไม่เห็น และการเรียงผล”ผลสแกนถูกเรียงจากสัญญาณแรงไปอ่อน (wifi_scan_sort_by_rssi_desc) ก่อนแสดง AP ที่ SSID ไม่ใช่ตัวอักษรที่พิมพ์ได้
(hidden network) จะถูกแสดงเป็นข้อความ <hidden> แทน (WIFI_SCAN_HIDDEN_SSID_TEXT) โครง struct
wifi_scan_ap_t เก็บแค่ ssid, rssi (int16_t หน่วย dBm) และ security (string ที่แปลงจาก enum
cy_wcm_security_t แล้ว) ไม่มีฟิลด์ bssid ตามที่ README ต้นทางกล่าวถึง และรับได้สูงสุด WIFI_SCAN_MAX_APS = 12
เครือข่ายต่อการสแกนหนึ่งครั้ง
ตัวอย่างสมบูรณ์
หัวข้อที่มีชื่อว่า “ตัวอย่างสมบูรณ์”โค้ดของ episode นี้อยู่ใน Developer Hub (อ้างอิงที่ commit 9a8e3ed) — อ่าน Why ของ README ต้นทาง เพื่อเข้าใจจุดประสงค์ แต่ โค้ดตัวอย่างด้านล่างคัดลอกจากไฟล์จริง (Apache-2.0, tesaiot/developer-hub, commit เดียวกัน) เพราะกลไก thread-safety ในโค้ดจริงต่างจากที่ README ต้นทางอธิบาย
wifi_list/wifi_scan_service.c — callback ของ WHD เขียนแค่ข้อมูล+ธงภายใน critical section ไม่แตะ LVGL:
if(status == CY_WCM_SCAN_COMPLETE) { taskENTER_CRITICAL(); wifi_scan_sort_by_rssi_desc(service); service->scan_done_pending = true; taskEXIT_CRITICAL();}wifi_scan_service_process() ที่ poll timer เรียกทุก 150 ms — อ่านและเคลียร์ธงในจังหวะเดียว:
bool wifi_scan_service_process(wifi_scan_service_t *service){ bool done; bool error;
taskENTER_CRITICAL(); done = service->scan_done_pending; error = service->scan_error_pending; service->scan_done_pending = false; service->scan_error_pending = false; taskEXIT_CRITICAL();
if(done) { service->scanning = false; service->scan_sequence++; return true; } /* ... error handling ... */ return false;}wifi_list/ui_wifi_list_page.c — poll timer ฝั่ง LVGL ที่เรียก process() แล้ว render ใหม่:
static void ui_wifi_poll_timer_cb(lv_timer_t *timer){ uint16_t count = 0U; LV_UNUSED(timer);
if(wifi_scan_service_process(&s_ctx.service)) { lv_obj_clear_state(s_ctx.scan_btn, LV_STATE_DISABLED); (void)wifi_scan_service_get_list(&s_ctx.service, &count); ui_wifi_update_status_labels(); ui_wifi_render_ap_list(); }}/* ... */s_ctx.poll_timer = lv_timer_create(ui_wifi_poll_timer_cb, UI_WIFI_POLL_MS, NULL);main_example.cเรียกwifi_scan_service_preinit()แล้ว forward เข้า UI ตรงตามที่ README ต้นทางอธิบายwifi_list/wifi_scan_types.h— structwifi_scan_ap_tจริง (ไม่มี bssid)- ดูโฟลเดอร์เต็มที่
hmi_ep05_wifi_list/— shell จาก EP04 (nav/) ถูกใช้ซ้ำโดยแทน Home ด้วยหน้า WiFi List
จุดที่มักพลาด
หัวข้อที่มีชื่อว่า “จุดที่มักพลาด”- คิดว่า episode นี้ใช้
lv_async_call()— README ต้นทางบอกอย่างนั้น แต่โค้ดจริงใช้ critical section + lv_timer poll ทุก 150 ms ให้ยึดโค้ดจริงเมื่ออธิบายกลไก thread-safety - เรียก LVGL widget API จาก callback ของ
cy_wcm/whdโดยตรง — callback รันบน WCM internal task ไม่ใช่ LVGL task การเรียกlv_label_set_text()ตรงนั้นจะชนกับlv_timer_handler()ที่กำลังวาดอยู่ ต้องผ่าน critical section + poll (หรือlv_async_call()) เท่านั้น - ลืมกันสแกนซ้อน — ถ้าไม่เช็ค
service->scanningก่อนcy_wcm_start_scan()การกดปุ่ม Scan รัว ๆ จะยิง scan ซ้อนหลายครั้ง - สับสนชื่อฟิลด์ struct — README ต้นทางพูดถึง
bssidแต่wifi_scan_ap_tจริงมีแค่ssid,rssi,security
build และ flash
หัวข้อที่มีชื่อว่า “build และ flash”# ในโฟลเดอร์ master template (ดูบทเรียน 1.1)# 1) ลบไฟล์ของ episode เก่าใน proj_cm55/apps/# 2) คัดลอกไฟล์ทั้งหมดของ episode นี้ลงใน proj_cm55/apps/make buildmake program # flash ผ่าน KitProg3หรือเปิด ตัวอย่างนี้บน Developer Hub แล้ว flash เฟิร์มแวร์สำเร็จรูป
ดูของจริงก่อน
หัวข้อที่มีชื่อว่า “ดูของจริงก่อน”
ก่อนอ่านโค้ด ให้ทายว่าหน้าจอนี้มี object อะไรบ้าง และอะไรเปลี่ยนเมื่อผู้ใช้แตะหรือเมื่อค่าเซนเซอร์เปลี่ยน
- ทาย ก่อนแก้: เลือกค่าหนึ่งค่าที่ README ของตัวอย่างอธิบายไว้ในส่วน How แล้วเขียนว่าจะเห็นอะไรเปลี่ยนบนจอหรือใน log
- แก้และรัน build + flash แล้วเทียบกับที่ทายไว้ ถ้าไม่ตรง ให้หาว่าเข้าใจส่วนไหนผิด
- ทำเพิ่ม ต่อยอดหนึ่งอย่างที่ตัวอย่างยังไม่มี แล้วเก็บภาพหรือวิดีโอไว้ใน portfolio
เช็กความเข้าใจ
หัวข้อที่มีชื่อว่า “เช็กความเข้าใจ”- RSSI -45 dBm กับ -85 dBm อันไหนดีกว่า
- ทำไมไม่ควรสแกน Wi-Fi ใน callback ของปุ่มโดยตรง
- ชนิด security ในรายการบอกอะไรกับผู้ใช้
คำตอบอยู่ใน README ของตัวอย่างและในโค้ด ถ้าตอบข้อใดไม่ได้ ให้กลับไปอ่านส่วน Why / What / How อีกครั้ง
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- README ของ episode · โฟลเดอร์โค้ด · commit
9a8e3ed - เปิดตัวอย่างนี้บน Developer Hub
- โค้ดเป็นของ Developer Hub และอ้างอิงด้วยลิงก์ ไม่ได้คัดลอกเข้าคลังนี้
คำถามทบทวน
ลองตอบเองก่อน แล้วค่อยเปิดดูเฉลย
-
ทำไม example_main() เรียก wifi_scan_service_preinit() ก่อนสร้าง UI (เป้าหมายข้อ 1)
- เพื่อสแกนเครือข่ายรอบแรกตั้งแต่ boot
- เพื่อเชื่อมต่อ AP ที่บันทึกไว้
- เพื่อเตรียม SDIO และ cy_wcm_init() ซึ่งกินเวลาไว้ตั้งแต่ boot ผู้ใช้จะไม่เห็นจอค้างตอนแตะ Scan ครั้งแรก ถ้าล้มเหลวก็แค่ log และ UI ยังเปิดได้
- เพราะ LVGL ต้องใช้ Wi-Fi ก่อนวาดจอ
ดูเฉลย
คำตอบ: C. เพื่อเตรียม SDIO และ cy_wcm_init() ซึ่งกินเวลาไว้ตั้งแต่ boot ผู้ใช้จะไม่เห็นจอค้างตอนแตะ Scan ครั้งแรก ถ้าล้มเหลวก็แค่ log และ UI ยังเปิดได้
preinit เรียก wifi_scan_radio_init_once() ที่ตั้ง SDIO host แล้ว cy_wcm_init() แบบ STA ครั้งเดียว (มี mutex กันเรียกซ้อน) โดยยังไม่สแกนจริง ถ้า preinit ล้ม wifi_scan_service_start() จะลอง init อีกครั้งตอนผู้ใช้กด Scan
-
wifi_scan_callback() ถูก cy_wcm เรียกจาก task ของ WCM ไม่ใช่ task ของ LVGL โค้ดส่งผลสแกนเข้าหน้า UI อย่างปลอดภัยอย่างไร (เป้าหมายข้อ 2)
- callback เรียก lv_label_set_text() ตรง ๆ เพราะเร็วพอ
- callback คัดลอกข้อมูล AP ลง array ของ service ภายใน critical section และตั้งธง scan_done_pending ส่วน lv_timer ทุก 150 ms ใน LVGL context เรียก wifi_scan_service_process() แล้ววาดรายการใหม่
- callback สร้าง FreeRTOS task ใหม่เพื่อวาดรายการ
- UI หยุด LVGL ชั่วคราวระหว่างที่ callback ทำงาน
ดูเฉลย
คำตอบ: B. callback คัดลอกข้อมูล AP ลง array ของ service ภายใน critical section และตั้งธง scan_done_pending ส่วน lv_timer ทุก 150 ms ใน LVGL context เรียก wifi_scan_service_process() แล้ววาดรายการใหม่
ในโค้ดที่ commit นี้ callback ไม่แตะ LVGL เลย มันเขียนแค่ข้อมูลและธงภายใน taskENTER_CRITICAL() ส่วน ui_wifi_poll_timer_cb() ที่สร้างด้วย lv_timer_create(…, UI_WIFI_POLL_MS = 150) เป็นผู้ตรวจธงแล้วเรียก ui_wifi_render_ap_list() ใน LVGL context ซึ่งเป็นที่เดียวที่เรียก LVGL ได้อย่างปลอดภัย
-
ผู้ใช้แตะ Scan อีกครั้งระหว่างที่การสแกนครั้งแรกยังไม่จบ จะเกิดอะไร (เป้าหมายข้อ 2)
- ครั้งที่สองถูกเพิกเฉย: service->scanning ยังเป็น true wifi_scan_service_start() จึงคืน false และ UI log ว่า SCAN_IGNORED busy (ปุ่มยังถูก disable ระหว่างสแกนด้วย)
- เริ่มสแกนใหม่ซ้อนกัน ผลจึงซ้ำสองชุด
- ยกเลิกการสแกนแรกแล้วเริ่มใหม่
- บอร์ดรีเซ็ตเพราะ cy_wcm ถูกเรียกซ้อน
ดูเฉลย
คำตอบ: A. ครั้งที่สองถูกเพิกเฉย: service->scanning ยังเป็น true wifi_scan_service_start() จึงคืน false และ UI log ว่า SCAN_IGNORED busy (ปุ่มยังถูก disable ระหว่างสแกนด้วย)
wifi_scan_service_start() ตรวจ service->scanning ก่อนเสมอ และ ui_wifi_scan_click_cb() ใส่ LV_STATE_DISABLED ให้ปุ่มจนกว่า poll timer จะเห็นว่าสแกนจบ การกันซ้อนไว้ที่ service ทำให้ปลอดภัยแม้ฝั่ง UI จะลืม disable ปุ่ม
-
สแกนเจอ 3 เครือข่าย: Cafe -85 dBm, Lab -45 dBm, Office -67 dBm เรียงตามลำดับที่จะแสดงในรายการจากบนลงล่าง (เป้าหมายข้อ 3)
- Cafe -85 dBm
- Lab -45 dBm
- Office -67 dBm
ดูเฉลย
ลำดับที่ถูก: B. Lab -45 dBm → C. Office -67 dBm → A. Cafe -85 dBm
เมื่อสแกนจบ callback เรียก wifi_scan_sort_by_rssi_desc() ซึ่งเรียงจาก RSSI มากไปน้อย ค่า dBm เป็นลบ ยิ่งใกล้ 0 ยิ่งแรง -45 dBm จึงแรงที่สุดและน่าเลือกเชื่อมต่อที่สุด ส่วน -85 dBm อ่อนมาก อาจต่อได้แต่หลุดง่ายและช้า
-
ข้อใดจริงเกี่ยวกับรายการที่ได้จากตัวอย่างนี้ (เลือกทุกข้อที่ถูก) (เป้าหมายข้อ 1)
- ถ้ามี AP รอบตัว 20 ตัว รายการเก็บได้สูงสุด 12 ตัว และเป็น 12 ตัวแรกที่การสแกนรายงาน ไม่ใช่ 12 ตัวที่แรงที่สุดเสมอ
- AP ที่ซ่อน SSID แสดงเป็น <hidden>
- รายการเรียงตามตัวอักษรของ SSID
- SSID ที่มีไบต์นอกช่วง ASCII ที่พิมพ์ได้ เช่นชื่อภาษาไทย จะไม่ถูกใส่ในรายการ
- AP แบบ OPEN ถูกกรองออกเพื่อความปลอดภัย
ดูเฉลย
คำตอบ: A. ถ้ามี AP รอบตัว 20 ตัว รายการเก็บได้สูงสุด 12 ตัว และเป็น 12 ตัวแรกที่การสแกนรายงาน ไม่ใช่ 12 ตัวที่แรงที่สุดเสมอ · B. AP ที่ซ่อน SSID แสดงเป็น <hidden> · D. SSID ที่มีไบต์นอกช่วง ASCII ที่พิมพ์ได้ เช่นชื่อภาษาไทย จะไม่ถูกใส่ในรายการ
callback เพิ่ม AP เฉพาะเมื่อ ap_count < WIFI_SCAN_MAX_APS (12) และ wifi_scan_is_ssid_printable() ผ่าน ซึ่งยอมเฉพาะไบต์ 0x20–0x7E การเรียงตาม RSSI เกิดตอนสแกนจบ จึงเรียงเฉพาะ 12 ตัวที่เก็บได้ SSID ว่างถูกแทนด้วย <hidden> และชนิด security ทุกแบบรวมถึง OPEN ถูกแสดงเป็นข้อความ
อ้างอิงบทเรียนนี้
ถ้านำบทเรียนนี้ไปสอน ทำสไลด์ หรือทำเอกสารต่อ ให้อ้างอิงด้วยข้อความนี้ ถ้าดัดแปลงเนื้อหา ให้เติม (ดัดแปลง)ต่อท้ายชื่อบทเรียน
"สแกน Wi-Fi และแสดงรายการเครือข่าย" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0
ข้อความอ้างอิงภาษาอังกฤษ: "Wi-Fi scan and a network list" from TESA Open Knowledge by the Thai Embedded Systems Association (TESA), https://github.com/tesaiot/tesa-qualification-program, licensed under CC BY-NC 4.0
ลิงก์บทเรียน: https://tesaiot.github.io/tesa-qualification-program/courses/tesaiot-firmware-stack/m02-hmi-menu-setting/l05-wifi-list/
บทเรียนนี้ดัดแปลงจากต้นฉบับด้านล่าง เมื่ออ้างอิงให้คงเครดิตต้นฉบับไว้ด้วย
https://github.com/tesaiot/developer-hub/blob/9a8e3ed1d813bfd67fabf6b7ac15c6ff9750b465/hmi_ep05_wifi_list · Code stays in the Developer Hub and is linked at pinned commits, never copied: the episodes, practice codes and main-branch examples are Apache-2.0; the master template and the OPTIGA client carry Infineon/Cypress EULAs.
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0
เนื้อหาเผยแพร่ภายใต้ CC BY-NC 4.0 นำไปใช้ต่อในงานที่ไม่ใช่เพื่อการค้าได้ โปรดอ้างอิงสมาคมสมองกลฝังตัวไทย (TESA) ทุกครั้ง · วิธีอ้างอิง TESA