Skip to content

Wi-Fi scan and a network list

  1. Scan Wi-Fi through WHD/cy_wcm and list networks with RSSI and security type
  2. Keep the scan service apart from the UI page and hand results to the page safely
  3. Read RSSI values and judge which network is strong enough to join

Scanning Wi-Fi goes through a three-layer stack

Section titled “Scanning Wi-Fi goes through a three-layer stack”

Scanning on the PSoC Edge passes through whd (the WiFi Host Driver, talking SDIO to the radio module) → cy_wcm (the Connection Manager, wrapping whd in a higher-level scan/connect API) → the application’s own callback, which cy_wcm calls every time it discovers a new AP. This episode wraps all three layers in a service layer (wifi_scan_service.c) so the UI page (ui_wifi_list_page.c) only ever calls wifi_scan_service_start() / wifi_scan_service_process(), without needing to know cy_wcm at all.

Pre-init: warm up the radio at boot, not on the button press

Section titled “Pre-init: warm up the radio at boot, not on the button press”

example_main() calls wifi_scan_service_preinit() before the UI is even created. This function does the SDIO bring-up (setting up the SDIO and host-wake interrupt handlers, registering the SDHC controller’s deep-sleep callback) and cy_wcm_init(), all at boot time. If that were done on the first “Scan” tap instead, the user would see the UI freeze for 1–3 seconds while the radio bring-up runs. This pre-init pattern makes the first tap just as fast as every later one. If pre-init fails, the code merely logs it and lets the UI open anyway (only Scan itself will then fail when tried).

The WHD callback never touches LVGL at all — unlike what the upstream README describes

Section titled “The WHD callback never touches LVGL at all — unlike what the upstream README describes”

The episode’s README on the Developer Hub says it uses lv_async_call() to hand work from the WHD callback back to the LVGL thread. The actual code at commit 9a8e3ed uses a different pattern instead: a critical section plus a poll timer. wifi_scan_callback() (called from the WCM’s own internal task, not the LVGL task) only copies each AP’s result into an array inside FreeRTOS’s taskENTER_CRITICAL() / taskEXIT_CRITICAL(), then sets the scan_done_pending/scan_error_pending flags — it never calls a single LVGL widget API. This is just as safe from races as lv_async_call(), just a different mechanism.

The LVGL side polls the flag instead of being woken up

Section titled “The LVGL side polls the flag instead of being woken up”

ui_wifi_list_page_create() creates lv_timer_create(ui_wifi_poll_timer_cb, UI_WIFI_POLL_MS, NULL) with UI_WIFI_POLL_MS = 150. Every 150 milliseconds, ui_wifi_poll_timer_cb() — which already runs on the LVGL thread and so may safely call widget APIs — calls wifi_scan_service_process(), which reads and clears the scan_done_pending/scan_error_pending flags inside the same critical section. If the flag says the scan is done, it then reads the result list and re-renders. In short: data crosses threads through a critical section, notification crosses threads through periodic polling, rather than being pushed into LVGL immediately the way lv_async_call() would. Both approaches are equally correct and safe; they are simply different mechanisms — this lesson follows the actual code.

Preventing overlapping scans lives in the service, not just at the button

Section titled “Preventing overlapping scans lives in the service, not just at the button”

wifi_scan_service_start() checks service->scanning first and returns false right away if a scan is already running. The UI side also sets LV_STATE_DISABLED on the Scan button while waiting for results. This two-layer guard (the service layer inside, the UI button outside) keeps the system safe even if the UI side ever forgets to disable the button itself.

RSSI, invisible SSIDs, and sorting the results

Section titled “RSSI, invisible SSIDs, and sorting the results”

Scan results are sorted strongest-to-weakest signal (wifi_scan_sort_by_rssi_desc) before being displayed. An AP whose SSID is not printable text (a hidden network) is shown as the literal text <hidden> (WIFI_SCAN_HIDDEN_SSID_TEXT) instead. The wifi_scan_ap_t struct holds only ssid, rssi (an int16_t in dBm) and security (a string already converted from the cy_wcm_security_t enum) — it has no bssid field, as the upstream README mentions — and holds at most WIFI_SCAN_MAX_APS = 12 networks per scan.

This episode’s code lives on the Developer Hub (pinned to commit 9a8e3ed) — read the Why section of the upstream README to understand its purpose, but the excerpts below are copied from the actual files (Apache-2.0, tesaiot/developer-hub, same commit), because the real thread-safety mechanism differs from what the upstream README describes.

wifi_list/wifi_scan_service.c — the WHD callback only writes data and flags inside a critical section, never touching 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(), called by the poll timer every 150 ms — reads and clears the flag atomically:

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 — the LVGL-side poll timer that calls process() and re-renders:

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 calls wifi_scan_service_preinit() then forwards into the UI, exactly as the upstream README describes
  • wifi_list/wifi_scan_types.h — the real wifi_scan_ap_t struct (no bssid)
  • See the full folder at hmi_ep05_wifi_list/ — EP04’s shell (nav/) is reused, with the Home page replaced by the WiFi List page
  • Assuming this episode uses lv_async_call() — the upstream README says so, but the real code uses a critical section plus an lv_timer polling every 150 ms. Trust the real code when explaining the thread-safety mechanism.
  • Calling LVGL widget APIs directly from the cy_wcm/whd callback — the callback runs on the WCM’s internal task, not the LVGL task. Calling lv_label_set_text() right there would race with lv_timer_handler(), which may be drawing at the same time. It must go through a critical section plus polling (or lv_async_call()).
  • Forgetting to guard against overlapping scans — without checking service->scanning before cy_wcm_start_scan(), mashing the Scan button repeatedly would fire multiple overlapping scans.
  • Mixing up the struct’s field names — the upstream README mentions bssid, but the real wifi_scan_ap_t only has ssid, rssi and security.
Terminal window
# In the master template folder (see lesson 1.1)
# 1) Delete the old episode's files in proj_cm55/apps/
# 2) Copy all of this episode's files into proj_cm55/apps/
make build
make program # flash through KitProg3

Or open this example on the Developer Hub and flash the ready-made firmware.

Screen of EP05 — WiFi List on the TESAIoT Dev Kit

Before reading the code, guess what objects this screen has, and what changes when the user taps it or when a sensor value changes.

  1. Guess before you change anything: pick one value the example’s README explains in the How section, and write down what you expect to change on the screen or in the log.
  2. Change and run: build + flash, then compare against your guess. If it does not match, find which part you misunderstood.
  3. Extend: add one thing the example does not yet have, and keep a photo or video in your portfolio.
  • Which is better, an RSSI of -45 dBm or -85 dBm?
  • Why should you not scan for Wi-Fi directly inside a button’s callback?
  • What does the security type in the list tell the user?

The answers are in the example’s README and in the code. If you cannot answer one, go back and read the Why / What / How section again.

Review questions

Answer on your own first, then open the answer.

  1. Why does example_main() call wifi_scan_service_preinit() before building the UI? (Objective 1)

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

    Answer: 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() is called by cy_wcm from a WCM task, not the LVGL task. How does the code hand results to the UI safely? (Objective 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 ทำงาน
    Show answer

    Answer: 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. The user taps Scan again while the first scan is still running. What happens? (Objective 2)

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

    Answer: 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. A scan finds Cafe at -85 dBm, Lab at -45 dBm and Office at -67 dBm. Put them in the order they appear in the list, top to bottom. (Objective 3)

    1. Cafe -85 dBm
    2. Lab -45 dBm
    3. Office -67 dBm
    Show answer

    Correct order: 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. Which statements about the list this example produces are true? (choose all that apply) (Objective 1)

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

    Answer: 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 ถูกแสดงเป็นข้อความ

Cite this lesson

If you teach from this lesson or reuse it in slides or documents, credit it with the text below. If you changed it, add (adapted) after the title.

"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

Thai attribution: "สแกน Wi-Fi และแสดงรายการเครือข่าย" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0

Lesson link: https://tesaiot.github.io/tesa-qualification-program/en/courses/tesaiot-firmware-stack/m02-hmi-menu-setting/l05-wifi-list/

This lesson adapts the source below; keep its credit too.
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.

Full guide: how to cite TESA

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

Content is licensed CC BY-NC 4.0. Reuse it non-commercially and credit the Thai Embedded Systems Association (TESA) every time. · How to cite TESA