ข้ามไปยังเนื้อหา

สแกน Wi-Fi และแสดงรายการเครือข่าย

  1. สแกน Wi-Fi ผ่าน WHD/cy_wcm แล้วแสดงรายการพร้อม RSSI และชนิด security
  2. แยก scan service ออกจากหน้า UI และส่งผลสแกนเข้าหน้าอย่างปลอดภัย
  3. อ่านค่า RSSI และบอกได้ว่าเครือข่ายใดสัญญาณดีพอจะเชื่อมต่อ

การสแกนบน 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 เลย

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() เพียงแค่เป็นคนละกลไก

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() ทั้งสองวิธีถูกต้องและปลอดภัยเท่ากัน แต่เป็นคนละกลไก — บทเรียนนี้อธิบายตามโค้ดจริง

wifi_scan_service_start() เช็ค service->scanning เป็นด่านแรกและคืน false ทันทีถ้ากำลังสแกนอยู่ ฝั่ง UI เอง ก็ใส่ LV_STATE_DISABLED ให้ปุ่ม Scan ระหว่างรอผลเช่นกัน — การกันซ้อนสองชั้นนี้ (service ชั้นใน + ปุ่ม UI ชั้นนอก) ทำให้ระบบยังปลอดภัยแม้ฝั่ง UI จะลืม disable ปุ่มเอง

ผลสแกนถูกเรียงจากสัญญาณแรงไปอ่อน (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 — struct wifi_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
Terminal window
# ในโฟลเดอร์ master template (ดูบทเรียน 1.1)
# 1) ลบไฟล์ของ episode เก่าใน proj_cm55/apps/
# 2) คัดลอกไฟล์ทั้งหมดของ episode นี้ลงใน proj_cm55/apps/
make build
make program # flash ผ่าน KitProg3

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

หน้าจอของ EP05 — WiFi List บน TESAIoT Dev Kit

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

  1. ทาย ก่อนแก้: เลือกค่าหนึ่งค่าที่ README ของตัวอย่างอธิบายไว้ในส่วน How แล้วเขียนว่าจะเห็นอะไรเปลี่ยนบนจอหรือใน log
  2. แก้และรัน build + flash แล้วเทียบกับที่ทายไว้ ถ้าไม่ตรง ให้หาว่าเข้าใจส่วนไหนผิด
  3. ทำเพิ่ม ต่อยอดหนึ่งอย่างที่ตัวอย่างยังไม่มี แล้วเก็บภาพหรือวิดีโอไว้ใน portfolio
  • RSSI -45 dBm กับ -85 dBm อันไหนดีกว่า
  • ทำไมไม่ควรสแกน Wi-Fi ใน callback ของปุ่มโดยตรง
  • ชนิด security ในรายการบอกอะไรกับผู้ใช้

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

คำถามทบทวน

ลองตอบเองก่อน แล้วค่อยเปิดดูเฉลย

  1. ทำไม example_main() เรียก wifi_scan_service_preinit() ก่อนสร้าง UI (เป้าหมายข้อ 1)

    1. เพื่อสแกนเครือข่ายรอบแรกตั้งแต่ boot
    2. เพื่อเชื่อมต่อ AP ที่บันทึกไว้
    3. เพื่อเตรียม SDIO และ cy_wcm_init() ซึ่งกินเวลาไว้ตั้งแต่ boot ผู้ใช้จะไม่เห็นจอค้างตอนแตะ Scan ครั้งแรก ถ้าล้มเหลวก็แค่ log และ UI ยังเปิดได้
    4. เพราะ 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

  2. wifi_scan_callback() ถูก cy_wcm เรียกจาก task ของ WCM ไม่ใช่ task ของ LVGL โค้ดส่งผลสแกนเข้าหน้า UI อย่างปลอดภัยอย่างไร (เป้าหมายข้อ 2)

    1. callback เรียก lv_label_set_text() ตรง ๆ เพราะเร็วพอ
    2. callback คัดลอกข้อมูล AP ลง array ของ service ภายใน critical section และตั้งธง scan_done_pending ส่วน lv_timer ทุก 150 ms ใน LVGL context เรียก wifi_scan_service_process() แล้ววาดรายการใหม่
    3. callback สร้าง FreeRTOS task ใหม่เพื่อวาดรายการ
    4. 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 ได้อย่างปลอดภัย

  3. ผู้ใช้แตะ Scan อีกครั้งระหว่างที่การสแกนครั้งแรกยังไม่จบ จะเกิดอะไร (เป้าหมายข้อ 2)

    1. ครั้งที่สองถูกเพิกเฉย: service->scanning ยังเป็น true wifi_scan_service_start() จึงคืน false และ UI log ว่า SCAN_IGNORED busy (ปุ่มยังถูก disable ระหว่างสแกนด้วย)
    2. เริ่มสแกนใหม่ซ้อนกัน ผลจึงซ้ำสองชุด
    3. ยกเลิกการสแกนแรกแล้วเริ่มใหม่
    4. บอร์ดรีเซ็ตเพราะ 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 ปุ่ม

  4. สแกนเจอ 3 เครือข่าย: Cafe -85 dBm, Lab -45 dBm, Office -67 dBm เรียงตามลำดับที่จะแสดงในรายการจากบนลงล่าง (เป้าหมายข้อ 3)

    1. Cafe -85 dBm
    2. Lab -45 dBm
    3. 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 อ่อนมาก อาจต่อได้แต่หลุดง่ายและช้า

  5. ข้อใดจริงเกี่ยวกับรายการที่ได้จากตัวอย่างนี้ (เลือกทุกข้อที่ถูก) (เป้าหมายข้อ 1)

    1. ถ้ามี AP รอบตัว 20 ตัว รายการเก็บได้สูงสุด 12 ตัว และเป็น 12 ตัวแรกที่การสแกนรายงาน ไม่ใช่ 12 ตัวที่แรงที่สุดเสมอ
    2. AP ที่ซ่อน SSID แสดงเป็น <hidden>
    3. รายการเรียงตามตัวอักษรของ SSID
    4. SSID ที่มีไบต์นอกช่วง ASCII ที่พิมพ์ได้ เช่นชื่อภาษาไทย จะไม่ถูกใส่ในรายการ
    5. 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 ฉบับเต็ม

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

เนื้อหาเผยแพร่ภายใต้ CC BY-NC 4.0 นำไปใช้ต่อในงานที่ไม่ใช่เพื่อการค้าได้ โปรดอ้างอิงสมาคมสมองกลฝังตัวไทย (TESA) ทุกครั้ง · วิธีอ้างอิง TESA