Skip to content

Complete Wi-Fi manager: connect, retry and auto-connect

  1. Combine scan, profile and connect into a Wi-Fi manager that auto-connects from the stored profile
  2. Design a connection state machine (connect, drop, retry) and show its state on screen
  3. Ping the gateway to check that the network really answers, and explain that in this example the ping result does not trigger a reconnect

The service owns all the state; the UI is just an observer copying a snapshot

Section titled “The service owns all the state; the UI is just an observer copying a snapshot”

The heart of EP07 is wifi_connection_service, which runs as its own background task, owns the state machine (wifi_conn_state_t: IDLE, CONNECTING, CONNECTED, DISCONNECTING, RECONNECT_WAIT, ERROR), and holds all the data (SSID, RSSI, IP, retry stage, and so on) itself. The UI page (ui_wifi_status_page.c) does not keep any connection state of its own — it only creates an lv_timer that periodically calls wifi_connection_service_get_snapshot() to copy values out for display. The reason for this design: the UI page can be switched away by lv_menu_set_page() (see lesson 2.4). If the state lived in a UI widget, it would vanish whenever the page switched, but the service survives no matter where the UI goes.

wifi_connection_service_get_snapshot() locks a FreeRTOS semaphore (xSemaphoreTake/xSemaphoreGive) before memcpy-ing the internal state into the caller’s wifi_connection_snapshot_t struct, then unlocks. This is safe from races because the background task also locks that same semaphore every time before it changes the state.

The real retry ladder is 1s → 2s → 5s → 10s, not 1s → 5s → 15s → 60s as the upstream README says

Section titled “The real retry ladder is 1s → 2s → 5s → 10s, not 1s → 5s → 15s → 60s as the upstream README says”

The real code declares static const uint32_t s_reconnect_backoff_ms[] = { 1000U, 2000U, 5000U, 10000U };, with a comment stating it directly: “Reconnect backoff: 1s -> 2s -> 5s -> 10s (cap at max stage).” wifi_conn_schedule_retry_locked() uses retry_stage as an index into this table; if the index would run past the end of the array, it clamps to the last entry (10s) instead of growing without bound. This is a bounded, exponential-ish backoff: it stops the code from hammering cy_wcm_connect_ap() when an AP has been gone a long time, but it also does not wait too long when the AP comes back quickly.

The ping watchdog already checks the gateway, and it does not trigger a reconnect

Section titled “The ping watchdog already checks the gateway, and it does not trigger a reconnect”

wifi_conn_probe_internet_once() calls cy_wcm_ping() against the gateway address (obtained from DHCP when the AP connection was made) every WIFI_CONN_PING_INTERVAL_MS = 20000 (20 seconds) by default — not a fixed public IP such as 8.8.8.8, which is what the upstream README suggests trying as a change. More importantly, the ping result never changes the state machine at all. If ping fails WIFI_CONN_PING_FAIL_THRESHOLD = 3 times in a row, the code only sets internet_ok = false for the UI to display (“ping fail 3”) — it never calls wifi_conn_schedule_retry_locked() or touches state. What actually moves the state to RECONNECT_WAIT is a real disconnect event from WCM (wifi_conn_handle_disconnect_event) or discovering cy_wcm_is_connected_to_ap() == 0 while refreshing connection info. In other words, the ping watchdog is only a gauge shown to the user, not the thing driving reconnection.

Every user command is serialized through one command queue

Section titled “Every user command is serialized through one command queue”

The Connect/Disconnect/Retry now buttons in the UI never mutate the state directly. They call wifi_connection_service_connect_profile() / _disconnect() / _retry_now(), each of which pushes a command into a queue that the single background task (wifi_conn_task) processes one at a time (wifi_conn_process_cmd). This serialization prevents two commands from colliding — for example, the user pressing Disconnect at the exact moment the retry ladder is about to call connect on its own.

wifi_connection_service_init() auto-connects at boot if a profile exists

Section titled “wifi_connection_service_init() auto-connects at boot if a profile exists”

main_example.c calls wifi_connection_service_init() before building the UI. Inside, it calls cy_wcm_init(), loads the profile from NVM (lesson 2.6) through wifi_profile_store_load(), and if that succeeds and is valid, sets have_profile = true and immediately queues a connect command — the user does not need to tap anything if a profile was saved earlier.

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 retry ladder values and the ping behavior differ from what the upstream README describes.

wifi_conn/wifi_connection_service.c — the real retry ladder with its cap:

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;
}

The ping watchdog only changes internet_ok, never the 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 */
}
}

The snapshot getter locks a semaphore before copying out:

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;
}
  • Misremembering the retry ladder as 1s/5s/15s/60s — the real values in the code are 1s/2s/5s/10s, straight from the source’s own comment.
  • Assuming a ping failure triggers an immediate retry — in this code, ping only sets the internet_ok flag for the UI to display; it never changes the state or triggers a reconnect. Reconnection comes only from a WCM disconnect event or cy_wcm_is_connected_to_ap().
  • Letting the UI mutate the connection state directly — every button must go through the service API, which pushes a command into the queue, rather than editing the service’s struct directly, because the background task is the true owner of that state.
  • Reading a service state field without locking the semaphore — you must go through wifi_connection_service_get_snapshot() only; reading directly would race with the background task changing the same value.
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 EP07 — Final WiFi Manager 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.
  • Why is “connected to the AP” not enough on its own to say the internet works?
  • Which states does the connection state machine need?
  • What goes wrong if retries happen too often?

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. After power-on, when does the Wi-Fi Manager connect automatically? (Objective 1)

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

    Answer: 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. Why do the Connect, Disconnect and Retry buttons and cy_wcm events only post commands to one queue instead of calling cy_wcm_connect_ap() themselves? (Objective 2)

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

    Answer: 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. The AP disappears and every attempt fails. Per the code at this commit, how long does the manager wait before each retry? (Objective 2)

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

    Answer: 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. The user presses Disconnect while connected. Which state does the machine end in, and will it retry by itself? (Objective 2)

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

    Answer: 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. Which statements about this episode's ping watchdog are correct? (choose all that apply) (Objective 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 อยู่หรือไม่
    Show answer

    Answer: 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 ได้ แต่ยังไม่ใช่การพิสูจน์ว่าออกอินเทอร์เน็ตได้

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.

"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

Thai attribution: "Wi-Fi Manager สมบูรณ์: เชื่อมต่อ ลองใหม่ และต่ออัตโนมัติ" จาก 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/l07-final-wifi-manager/

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

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