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

Wi-Fi Manager สมบูรณ์: เชื่อมต่อ ลองใหม่ และต่ออัตโนมัติ

  1. รวม scan, profile และ connect เป็น Wi-Fi manager ที่ต่ออัตโนมัติจากโปรไฟล์ที่บันทึกไว้
  2. ออกแบบ state machine ของการเชื่อมต่อ (ต่อ หลุด ลองใหม่) และแสดงสถานะบนจอ
  3. ใช้ ping ไปยัง gateway ตรวจว่าเครือข่ายตอบจริง และอธิบายว่าในตัวอย่างนี้ผลของ ping ไม่ได้สั่งให้ต่อใหม่

service ถือ state ทั้งหมด UI เป็นแค่ observer ที่ copy snapshot ออกมาดู

หัวข้อที่มีชื่อว่า “service ถือ state ทั้งหมด UI เป็นแค่ observer ที่ copy snapshot ออกมาดู”

หัวใจของ ep07 คือ wifi_connection_service ที่รันเป็น background task ของตัวเอง ถือ state machine (wifi_conn_state_t: IDLE, CONNECTING, CONNECTED, DISCONNECTING, RECONNECT_WAIT, ERROR) และข้อมูล ทั้งหมด (SSID, RSSI, IP, retry stage ฯลฯ) ไว้ในตัวเอง หน้า UI (ui_wifi_status_page.c) ไม่เก็บ state ของ connection เอง — มันแค่สร้าง lv_timer ที่เรียก wifi_connection_service_get_snapshot() เป็นระยะเพื่อคัดลอกค่า ออกมาแสดง เหตุผลที่ต้องออกแบบแบบนี้: หน้า UI อาจถูก lv_menu_set_page() สลับออกจากจอ (ดูบทเรียน 2.4) ถ้า state อยู่ใน UI widget เอง ข้อมูลจะหายเมื่อสลับหน้า แต่ service อยู่ยงคงกระพันไม่ว่า UI จะสลับไปไหน

wifi_connection_service_get_snapshot() ใช้ FreeRTOS semaphore (xSemaphoreTake/xSemaphoreGive) ล็อกก่อน memcpy ค่าจาก state ภายในออกมาใส่ struct wifi_connection_snapshot_t ที่ผู้เรียกส่งมา แล้วปลดล็อก — ปลอดภัยจาก race เพราะ background task ก็ต้อง lock semaphore ตัวเดียวกันนี้ทุกครั้งก่อนแก้ state

retry ladder ตัวจริงคือ 1s → 2s → 5s → 10s ไม่ใช่ 1s → 5s → 15s → 60s ตามที่ README ต้นทางบอก

หัวข้อที่มีชื่อว่า “retry ladder ตัวจริงคือ 1s → 2s → 5s → 10s ไม่ใช่ 1s → 5s → 15s → 60s ตามที่ README ต้นทางบอก”

โค้ดจริงประกาศ static const uint32_t s_reconnect_backoff_ms[] = { 1000U, 2000U, 5000U, 10000U }; พร้อมคอมเมนต์ กำกับตรง ๆ ว่า “Reconnect backoff: 1s -> 2s -> 5s -> 10s (cap at max stage)” — wifi_conn_schedule_retry_locked() ใช้ retry_stage เป็น index เข้าตาราง ถ้า index เกินขนาด array จะ clamp ไว้ที่ตัวสุดท้าย (10s) ไม่ปล่อยให้โตไม่ สิ้นสุด นี่คือ exponential-ish backoff ที่มี เพดาน ชัดเจน ป้องกันการยิง cy_wcm_connect_ap() รัวเกินไปเมื่อ AP หายไปนาน แต่ก็ไม่รอนานเกินไปเมื่อ AP กลับมาเร็ว

ping watchdog เช็ค gateway อยู่แล้ว และไม่ได้สั่งให้ต่อใหม่

หัวข้อที่มีชื่อว่า “ping watchdog เช็ค gateway อยู่แล้ว และไม่ได้สั่งให้ต่อใหม่”

wifi_conn_probe_internet_once() เรียก cy_wcm_ping() ไปที่ gateway address (ที่ได้จาก DHCP ตอนต่อ AP) ทุก WIFI_CONN_PING_INTERVAL_MS = 20000 (20 วินาที) โดย default อยู่แล้ว ไม่ใช่ IP สาธารณะตายตัวอย่าง 8.8.8.8 ตามที่ README ต้นทางแนะนำให้ “ลองเปลี่ยน” ที่สำคัญกว่านั้นคือผลของ ping ไม่ได้ทำให้ state machine เปลี่ยน เลย — ถ้า ping fail ครบ WIFI_CONN_PING_FAIL_THRESHOLD = 3 ครั้งติดกัน โค้ดแค่ตั้ง internet_ok = false ให้ UI แสดงผล (“ping fail 3”) เท่านั้น ไม่เรียก wifi_conn_schedule_retry_locked() หรือเปลี่ยน state แต่อย่างใด สิ่งที่ ทำให้ state เปลี่ยนไป RECONNECT_WAIT จริง ๆ คือเหตุการณ์ตัดการเชื่อมต่อจาก WCM เอง (wifi_conn_handle_disconnect_event) หรือตรวจพบว่า cy_wcm_is_connected_to_ap() == 0 ตอน refresh ข้อมูล — พูด อีกแบบคือ ping watchdog เป็นแค่ เกจวัด อินเทอร์เน็ตให้ผู้ใช้ดู ไม่ใช่ตัวขับ retry

คำสั่งจากผู้ใช้ทั้งหมดถูก serialize ผ่าน command queue เดียว

หัวข้อที่มีชื่อว่า “คำสั่งจากผู้ใช้ทั้งหมดถูก serialize ผ่าน command queue เดียว”

ปุ่ม Connect/Disconnect/Retry now ในหน้า UI ไม่ได้เรียกแก้ state ตรง ๆ แต่เรียก wifi_connection_service_connect_profile() / _disconnect() / _retry_now() ซึ่งแต่ละตัวส่งคำสั่งเข้าคิวที่ background task เดียว (wifi_conn_task) เป็นผู้ประมวลผลทีละคำสั่ง (wifi_conn_process_cmd) การ serialize แบบนี้ กันไม่ให้เกิดสองคำสั่งชนกัน (เช่น user กด Disconnect พร้อมกับที่ retry ladder กำลังจะเรียก connect เอง)

main_example.c เรียก wifi_connection_service_init() ก่อนสร้าง UI ภายในจะ cy_wcm_init(), โหลด profile จาก NVM (บทเรียน 2.6) ผ่าน wifi_profile_store_load(), ถ้าโหลดสำเร็จและ valid จะตั้ง have_profile = true แล้วส่ง คำสั่ง connect เข้าคิวทันที —ผู้ใช้ไม่ต้องกดอะไรเลยถ้าเคย save profile ไว้ก่อนหน้า

โค้ดของ episode นี้อยู่ใน Developer Hub (อ้างอิงที่ commit 9a8e3ed) — อ่าน Why ของ README ต้นทาง เพื่อเข้าใจจุดประสงค์ แต่ โค้ดตัวอย่างด้านล่างคัดลอกจากไฟล์จริง (Apache-2.0, tesaiot/developer-hub, commit เดียวกัน) เพราะค่า retry ladder และพฤติกรรมของ ping ต่างจากที่ README ต้นทางอธิบาย

wifi_conn/wifi_connection_service.c — retry ladder ตัวจริงพร้อมเพดาน:

static const uint32_t s_reconnect_backoff_ms[] = { 1000U, 2000U, 5000U, 10000U };
/* Reconnect backoff: 1s -> 2s -> 5s -> 10s (cap at max stage). */
static bool wifi_conn_schedule_retry_locked(void)
{
uint8_t idx = s_ctx.retry_stage;
if(idx >= (uint8_t)(sizeof(s_reconnect_backoff_ms) / sizeof(s_reconnect_backoff_ms[0]))) {
idx = (uint8_t)(sizeof(s_reconnect_backoff_ms) / sizeof(s_reconnect_backoff_ms[0])) - 1U;
}
s_ctx.retry_wait_ms = s_reconnect_backoff_ms[idx];
if(s_ctx.retry_stage < ((uint8_t)(sizeof(s_reconnect_backoff_ms) / sizeof(s_reconnect_backoff_ms[0])) - 1U)) {
s_ctx.retry_stage++;
}
wifi_conn_set_state_locked(WIFI_CONN_STATE_RECONNECT_WAIT);
return true;
}

ping watchdog เปลี่ยนแค่ internet_ok ไม่แตะ state machine:

if(0 == wifi_conn_ping_ok) {
if(s_ctx.ping_fail_streak < 0xFFU) {
s_ctx.ping_fail_streak++;
}
if(s_ctx.ping_fail_streak >= WIFI_CONN_PING_FAIL_THRESHOLD) {
s_ctx.internet_ok = false; /* display only — no state transition here */
}
}

snapshot getter ล็อกด้วย semaphore ก่อน copy ออก:

bool wifi_connection_service_get_snapshot(wifi_connection_snapshot_t *out_snapshot)
{
if((out_snapshot == NULL) || (!s_ctx.initialized) || (s_ctx.lock == NULL)) {
return false;
}
if(pdTRUE != xSemaphoreTake(s_ctx.lock, portMAX_DELAY)) {
return false;
}
(void)memset(out_snapshot, 0, sizeof(*out_snapshot));
out_snapshot->state = s_ctx.state;
/* ... copy every field ... */
(void)xSemaphoreGive(s_ctx.lock);
return true;
}
  • main_example.c เรียก wifi_connection_service_init() แล้ว forward เข้า UI ตรงตามที่ README ต้นทางอธิบาย
  • wifi_conn/wifi_connection_service.h — enum wifi_conn_state_t ตัวจริง (ตรงกับที่ README ต้นทางระบุ)
  • ดูโฟลเดอร์เต็มที่ hmi_ep07_final_wifi_manager/
  • จำค่า retry ladder ผิดเป็น 1s/5s/15s/60s — ค่าจริงในโค้ดคือ 1s/2s/5s/10s ตามคอมเมนต์ในซอร์สตรง ๆ
  • คิดว่า ping fail จะสั่งให้ retry ใหม่ทันที — ในโค้ดชุดนี้ ping แค่ตั้งธง internet_ok ให้ UI แสดงผล ไม่ได้ เปลี่ยน state หรือสั่ง reconnect เลย การ reconnect มาจาก WCM disconnect event หรือ cy_wcm_is_connected_to_ap() เท่านั้น
  • ให้ UI แก้ state ของ connection ตรง ๆ — ปุ่มทุกปุ่มต้องเรียกผ่าน service API ที่ส่งเข้า command queue ไม่ใช่ ไปแก้ struct ของ service เอง เพราะ background task เป็นเจ้าของ state ที่แท้จริง
  • อ่าน field ของ service state โดยไม่ล็อก semaphore — ต้องผ่าน wifi_connection_service_get_snapshot() เท่านั้น การอ่านตรงจะชนกับ background task ที่กำลังแก้ค่าเดียวกันอยู่
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 เฟิร์มแวร์สำเร็จรูป

หน้าจอของ EP07 — Final WiFi Manager บน TESAIoT Dev Kit

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

  1. ทาย ก่อนแก้: เลือกค่าหนึ่งค่าที่ README ของตัวอย่างอธิบายไว้ในส่วน How แล้วเขียนว่าจะเห็นอะไรเปลี่ยนบนจอหรือใน log
  2. แก้และรัน build + flash แล้วเทียบกับที่ทายไว้ ถ้าไม่ตรง ให้หาว่าเข้าใจส่วนไหนผิด
  3. ทำเพิ่ม ต่อยอดหนึ่งอย่างที่ตัวอย่างยังไม่มี แล้วเก็บภาพหรือวิดีโอไว้ใน portfolio
  • ทำไม “ต่อ AP ได้” ยังไม่พอจะบอกว่าอินเทอร์เน็ตใช้ได้
  • state ใดบ้างที่จำเป็นใน state machine ของการเชื่อมต่อ
  • ถ้าลองใหม่ถี่เกินไปจะเกิดปัญหาอะไร

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

คำถามทบทวน

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

  1. หลังเปิดเครื่อง Wi-Fi Manager จะเชื่อมต่ออัตโนมัติเมื่อใด (เป้าหมายข้อ 1)

    1. ทุกครั้ง โดยเลือก AP ที่สัญญาณแรงที่สุดจากการสแกน
    2. ทุกครั้งที่มีโปรไฟล์ใน NVM ไม่ว่าจะตั้งค่าอย่างไร
    3. เมื่อโหลดโปรไฟล์จาก NVM ได้ และโปรไฟล์นั้นเปิด auto_connect ไว้ จึงส่งคำสั่ง CONNECT_PROFILE (user_initiated = false) เข้าคิวของ wifi_conn_task
    4. เมื่อผู้ใช้กด Connect Saved เท่านั้น
    ดูเฉลย

    คำตอบ: C. เมื่อโหลดโปรไฟล์จาก NVM ได้ และโปรไฟล์นั้นเปิด auto_connect ไว้ จึงส่งคำสั่ง CONNECT_PROFILE (user_initiated = false) เข้าคิวของ wifi_conn_task

    ui_wifi_status_page_startup_auto_connect() ทำงานครั้งเดียวหลังสร้างหน้า ถ้าไม่มีโปรไฟล์จะ log AUTO_CONNECT skip no profile ถ้า auto_connect ปิดจะ log AUTO_CONNECT disabled in profile นอกนั้นจึงเรียก wifi_connection_service_connect_profile(&profile, false)

  2. ทำไมปุ่ม Connect, Disconnect, Retry และ event จาก cy_wcm จึงแค่ส่งคำสั่งเข้าคิวเดียว แทนการเรียก cy_wcm_connect_ap() เอง (เป้าหมายข้อ 2)

    1. ให้ wifi_conn_task เป็นผู้เดียวที่เรียก cy_wcm ทีละคำสั่ง การเชื่อมต่อจึงไม่ซ้อนกัน และ UI ไม่ค้างระหว่าง cy_wcm_connect_ap() ที่อาจใช้เวลาหลายวินาที
    2. เพราะ cy_wcm_connect_ap() เรียกจากโค้ดของ LVGL ไม่ได้ในทางเทคนิค
    3. เพื่อให้เชื่อมต่อหลาย AP พร้อมกันได้
    4. เพื่อประหยัด RAM ของ LVGL
    ดูเฉลย

    คำตอบ: A. ให้ wifi_conn_task เป็นผู้เดียวที่เรียก cy_wcm ทีละคำสั่ง การเชื่อมต่อจึงไม่ซ้อนกัน และ UI ไม่ค้างระหว่าง cy_wcm_connect_ap() ที่อาจใช้เวลาหลายวินาที

    comment ในโค้ดเขียนว่า Commands are serialized through one queue to avoid overlapping WiFi operations ส่วน WCM callback แค่ post คำสั่ง และหน้าสถานะอ่านค่าผ่าน wifi_connection_service_get_snapshot() ที่คัดลอกข้อมูลภายใต้ mutex UI จึงเป็นแค่ผู้สังเกต ไม่ถือ state เอง

  3. AP หายไปและต่อไม่ติดซ้ำ ๆ ตามโค้ดที่ commit นี้ ระยะรอก่อนลองใหม่แต่ละรอบเป็นอย่างไร (เป้าหมายข้อ 2)

    1. 1 → 5 → 15 → 60 วินาที
    2. 1 วินาทีทุกครั้ง
    3. เพิ่มเป็นสองเท่าไปเรื่อย ๆ ไม่มีเพดาน
    4. 1 → 2 → 5 → 10 วินาที แล้วค้างที่ 10 วินาที
    ดูเฉลย

    คำตอบ: D. 1 → 2 → 5 → 10 วินาที แล้วค้างที่ 10 วินาที

    s_reconnect_backoff_ms[] = {1000, 2000, 5000, 10000} และ wifi_conn_schedule_retry_locked() ไม่ให้ index เกินตัวสุดท้าย (README ของ episode ยังเขียนลำดับ 1/5/15/60 ซึ่งไม่ตรงกับโค้ด) การรอนานขึ้นช่วยไม่ให้ยิงคำขอใส่ driver และ AP ถี่เกินไป ส่วนเพดานทำให้กลับมาต่อได้ภายในราว 10 วินาทีเมื่อ AP กลับมา

  4. ผู้ใช้กด Disconnect ขณะเชื่อมต่ออยู่ state machine จะไปอยู่ที่ใด และจะลองต่อใหม่เองหรือไม่ (เป้าหมายข้อ 2)

    1. RECONNECT_WAIT แล้วลองใหม่ใน 1 วินาที
    2. IDLE และไม่ลองใหม่ เพราะ manual_disconnect = true และ reconnect_enabled = false ทำให้ event DISCONNECTED ที่ตามมาไม่ถูกนับเป็นการหลุด
    3. ERROR เพราะการตัดการเชื่อมต่อถือเป็นความผิดพลาด
    4. CONNECTED ต่อไปจนกว่าจะรีเซ็ตบอร์ด
    ดูเฉลย

    คำตอบ: B. IDLE และไม่ลองใหม่ เพราะ manual_disconnect = true และ reconnect_enabled = false ทำให้ event DISCONNECTED ที่ตามมาไม่ถูกนับเป็นการหลุด

    wifi_conn_do_user_disconnect() ตั้ง DISCONNECTING เรียก cy_wcm_disconnect_ap() แล้วตั้ง IDLE ส่วน wifi_conn_handle_disconnect_event() เห็น manual_disconnect จึงไม่เรียก schedule_retry การแยก ‘หลุดเอง’ กับ ‘ผู้ใช้สั่งตัด’ ทำให้ปุ่ม Disconnect มีความหมายจริง

  5. ข้อใดถูกต้องเกี่ยวกับ ping watchdog ของ episode นี้ (เลือกทุกข้อที่ถูก) (เป้าหมายข้อ 3)

    1. ping ไปที่ default gateway ทุก 20 วินาที (timeout 1.2 วินาที)
    2. ping ล้มติดกัน 3 ครั้ง service จะสั่งตัดการเชื่อมต่อแล้วเข้า retry ladder
    3. ping ล้มติดกัน 3 ครั้ง internet_ok เป็น false หน้าจอขึ้น No Internet แต่ state ยังเป็น Connected
    4. ถ้าอินเทอร์เน็ตภายนอกล่มแต่ router ยังตอบ ping จอยังแสดง Online
    5. ping ใช้แทนการตรวจว่ายังต่อ AP อยู่หรือไม่
    ดูเฉลย

    คำตอบ: A. ping ไปที่ default gateway ทุก 20 วินาที (timeout 1.2 วินาที) · C. ping ล้มติดกัน 3 ครั้ง internet_ok เป็น false หน้าจอขึ้น No Internet แต่ state ยังเป็น Connected · D. ถ้าอินเทอร์เน็ตภายนอกล่มแต่ router ยังตอบ ping จอยังแสดง Online

    wifi_conn_probe_internet_once() เรียก cy_wcm_get_gateway_ip_address() แล้ว cy_wcm_ping() ทุก WIFI_CONN_PING_INTERVAL_MS และตั้ง internet_ok = false เมื่อ ping_fail_streak ถึง 3 โดยไม่เปลี่ยน state การหลุดจาก AP ตรวจอีกทางผ่าน cy_wcm_is_connected_to_ap() และ event DISCONNECTED การ ping gateway บอกได้ว่าเครือข่ายชั้น IP ใช้ได้จริงถึง router ซึ่งมากกว่าแค่ต่อ AP ได้ แต่ยังไม่ใช่การพิสูจน์ว่าออกอินเทอร์เน็ตได้

อ้างอิงบทเรียนนี้

ถ้านำบทเรียนนี้ไปสอน ทำสไลด์ หรือทำเอกสารต่อ ให้อ้างอิงด้วยข้อความนี้ ถ้าดัดแปลงเนื้อหา ให้เติม (ดัดแปลง)ต่อท้ายชื่อบทเรียน

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

ข้อความอ้างอิงภาษาอังกฤษ: "Complete Wi-Fi manager: connect, retry and auto-connect" 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/l07-final-wifi-manager/

บทเรียนนี้ดัดแปลงจากต้นฉบับด้านล่าง เมื่ออ้างอิงให้คงเครดิตต้นฉบับไว้ด้วย
https://github.com/tesaiot/developer-hub/blob/9a8e3ed1d813bfd67fabf6b7ac15c6ff9750b465/hmi_ep07_final_wifi_manager · 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