SDK สำหรับ TESAIoT Dev Kit
คู่มืออ้างอิง API และ Tutorial (ModusToolbox)
Loading...
Searching...
No Matches

เป้าหมายของหัวข้อนี้

2 เรื่อง ซึ่งนำไปใช้ต่อได้ไกลเกินกว่าเรื่อง WiFi:

  1. สำนวน IPC แบบ msg.value พาพอยน์เตอร์ของคำตอบไปด้วย CM55 วางที่อยู่ของ struct คำตอบไว้ในข้อความ (message) ที่ส่งผ่าน pipe จากนั้น CM33_NS เติมค่าลง struct แล้วตั้ง ready = 1 หลัง barrier ทางหน่วยความจำ ส่วน CM55 วนถาม (polling) ค่า ready คำสั่ง IPC_CMD_WIFI_* ทุกตัวใช้สำนวนนี้
  2. กลไก slot ของ OPTIGA แบบ ไม่ประสานเวลา (async) ที่อยู่เบื้องหลังรายการเครือข่ายที่บันทึกไว้ — wifi_saved_probe_* แล้วตามด้วย wifi_saved_read_* — และข้อเท็จจริงที่สำคัญที่สุด: ดัชนีของรายการบน UI ไม่ใช่ ดัชนี slot ของที่เก็บ

บทนี้คือครึ่งหลังฝั่งที่เก็บ OPTIGA ของเรื่องที่บท C1 เริ่มไว้

ลำดับการทำงานจริงของเฟิร์มแวร์

Scan: CM55 ถาม CM33_NS ตอบ

ปุ่ม Scan ของหน้า WiFi Connect ตั้งสถานะเป็น WIFI_STATE_SCANNING แล้วเลื่อนออกไป 50 ms เพื่อให้ LVGL วาดจอเสร็จก่อนจะแตะ pipe (wifi_connect_native.c:406-412) callback ที่ถูกเลื่อนนั้นคือทั้งกระบวนการ scan/connect ของหน้านี้:

static void wifi_native_deferred_scan_cb(lv_timer_t *timer)
{
lv_timer_delete(timer);
if (s_ctx.ui == NULL) return;
if (!wifi_manager_scan_start()) {
aic_wifi_set_state(s_ctx.ui, WIFI_STATE_ERROR);
aic_wifi_show_error(s_ctx.ui, WIFI_ERR_SCAN_FAILED, "Scan failed");
return;
}
/* Poll every 100ms — each callback returns instantly until scan completes.
* LVGL processes input events between polls → buttons stay responsive. */
lv_timer_create(wifi_native_scan_poll_cb, 100, NULL);
}
static void wifi_native_on_scan(void)
{
if (s_ctx.ui == NULL) return;
aic_wifi_set_state(s_ctx.ui, WIFI_STATE_SCANNING);
/* Non-blocking: send IPC + start poll timer */
lv_timer_create(wifi_native_deferred_scan_cb, 50, NULL);
}
static void wifi_native_on_select(int index, const char *ssid)
{
/* No IPC call here — details already populated by scan_and_update.
* Calling get_status blocks LVGL thread for 5s per tap. */
(void)index;
(void)ssid;
}
static void wifi_native_deferred_connect_cb(lv_timer_t *timer)
{
lv_timer_delete(timer);
if (s_ctx.ui == NULL) {
return;
}
/* ...context: inside the deferred connect callback, after a confirmed association ... */
/* Auto-retry: cy_wcm_connect_ap() often fails on the first attempt
* after a scan due to internal WCM state transition. Retry once. */
bool ok = wifi_manager_connect(s_ctx.pending_ssid, s_ctx.pending_password);
if (!ok) {
ok = wifi_manager_connect(s_ctx.pending_ssid, s_ctx.pending_password);
}
if (!ok) {
aic_wifi_set_state(s_ctx.ui, WIFI_STATE_ERROR);
aic_wifi_show_error(s_ctx.ui, WIFI_ERR_AUTH_FAILED, "Connect failed");
return;
}
/* Auto-save credentials on successful connect */
wifi_saved_add(s_ctx.pending_ssid, s_ctx.pending_password, s_ctx.pending_security);
wifi_native_refresh_saved();
wifi_native_scan_and_update();
}
static void wifi_native_on_connect(const char *ssid, const char *password, uint8_t security)
{
(void)security;
if (s_ctx.ui == NULL) {
return;
}
/* Stash parameters for deferred callback */
strncpy(s_ctx.pending_ssid, ssid, sizeof(s_ctx.pending_ssid) - 1);
s_ctx.pending_ssid[sizeof(s_ctx.pending_ssid) - 1] = '\0';
strncpy(s_ctx.pending_password, password, sizeof(s_ctx.pending_password) - 1);
s_ctx.pending_password[sizeof(s_ctx.pending_password) - 1] = '\0';
aic_wifi_set_state(s_ctx.ui, WIFI_STATE_CONNECTING);
/* Defer connect so LVGL can render "Connecting..." before blocking IPC */
lv_timer_create(wifi_native_deferred_connect_cb, 50, NULL);
}

wifi_manager_scan_start() เป็นฝั่งผู้ส่ง ข้อสังเกต: ไฟล์นี้อยู่ใต้ bento_libs/claw/common/modules/wifi_manager/ ไม่ได้อยู่ใต้ proj_cm55/ ดังนั้นการ grep หา IPC_CMD_WIFI_SCAN เฉพาะใน proj_cm55/ จะไม่พบอะไรเลย และดูเหมือนว่าเส้นทางนี้หายไป ซึ่งไม่จริง

bool wifi_manager_scan_start(void)
{
wifi_manager_ensure_ipc();
memset((void *)&s_wifi_ipc_resp, 0, sizeof(s_wifi_ipc_resp));
s_wifi_ipc_resp.ready = 0;
memset(&s_wifi_ipc_msg, 0, sizeof(s_wifi_ipc_msg));
s_wifi_ipc_msg.client_id = CM33_IPC_SENSOR_CTRL_CLIENT_ID;
s_wifi_ipc_msg.intr_mask = CY_IPC_CYPIPE_INTR_MASK_EP1;
s_wifi_ipc_msg.cmd = IPC_CMD_WIFI_SCAN;
s_wifi_ipc_msg.value = (uint32_t)&s_wifi_ipc_resp;
int retries = WIFI_IPC_SEND_RETRIES;
cy_en_ipc_pipe_status_t send_status;
do {
send_status = Cy_IPC_Pipe_SendMessage(
CM33_IPC_PIPE_EP_ADDR, CM55_IPC_PIPE_EP_ADDR,
(void *)&s_wifi_ipc_msg, NULL);
if (send_status == CY_IPC_PIPE_SUCCESS) {
break;
}
Cy_SysLib_DelayUs(WIFI_IPC_RETRY_DELAY_US);
} while (--retries > 0);
if (retries <= 0) {
s_last_error = WIFI_ERR_IPC_SEND_TIMEOUT;
return false;
}
s_last_error = CY_RSLT_SUCCESS;
return true;
}
bool wifi_manager_scan_ready(void)
{
__DMB();
return (s_wifi_ipc_resp.ready != 0);
}

พอยน์เตอร์ของคำตอบเดินทางไปใน msg.value บรรทัดเดียวนั้นคือข้อกำหนดการเรียกใช้ของคำสั่งทั้งตระกูล

บน CM33_NS callback ของ pipe ทำงานใน ISR context และทำเพียงอย่างเดียว คือดึงพอยน์เตอร์ของคำตอบออกมา แล้วส่งคำขอเข้าคิวให้ worker

/* ...context: inside the IPC ISR callback - ISR context, no printf ... */
if (msg->cmd >= IPC_CMD_WIFI_SCAN && msg->cmd <= IPC_CMD_WIFI_SOFTAP) {
/* NOTE: No printf here — this is ISR context, printf is NOT safe */
ipc_response_t *resp = (ipc_response_t *)msg->value;
if (s_wifi_req_queue == NULL) {
wifi_ipc_reply_error_isr(resp, msg->cmd, AUTO_WIFI_ERR_QUEUE_FULL);
return;
}
wifi_ipc_req_t req;
memset(&req, 0, sizeof(req));
req.cmd = msg->cmd;
req.value = msg->value;
req.resp = resp;
memcpy(req.data, msg->data, sizeof(req.data));
BaseType_t hpw = pdFALSE;
if (xQueueSendFromISR(s_wifi_req_queue, &req, &hpw) != pdTRUE) {
wifi_ipc_reply_error_isr(resp, msg->cmd, AUTO_WIFI_ERR_QUEUE_FULL);
}
/* WiFiIPC worker wakes automatically via xQueueReceive */
portYIELD_FROM_ISR(hpw);
return;
}

worker ดึงงานออกจากคิว และสำหรับการ scan จะทำตามลำดับนี้: wifi_ensure_ready() (เริ่มใช้งาน WiFi แบบเลื่อนเวลา — ฮาร์ดแวร์ไม่ถูกแตะจนกว่าจะใช้ครั้งแรก) กรอง RSSI ที่ −90 dBm, cy_wcm_start_scan(), ตัดรายการซ้ำตาม SSID โดยเก็บตัวที่ RSSI แรงที่สุด, เรียงลำดับแบบ insertion sort ตาม RSSI, ตัดให้เหลือ IPC_WIFI_SCAN_MAX_ENTRIES (6) แล้วตอบกลับ

static void wifi_ipc_handle_request(const wifi_ipc_req_t *req)
{
uint8_t status_blob[AUTO_WIFI_STATUS_DATA_LEN];
uint32_t err = 0;
if (req == NULL || req->resp == NULL) {
return;
}
switch (req->cmd) {
case IPC_CMD_WIFI_SCAN: {
printf("[WiFiIPC] SCAN cmd received\r\n");
if (!wifi_ensure_ready()) {
err = (uint32_t)s_wifi_last_error;
printf("[WiFiIPC] SCAN: wifi not ready, err=0x%08lX\r\n", (unsigned long)err);
wifi_ipc_reply(req->resp, req->cmd, 1, &err, 4);
break;
}
printf("[WiFiIPC] SCAN: wifi ready, starting scan...\r\n");
static wifi_scan_ctx_t scan_ctx; /* static: safe, worker is single-threaded */
memset(&scan_ctx, 0, sizeof(scan_ctx));
scan_ctx.done = xSemaphoreCreateBinary();
if (scan_ctx.done == NULL) {
err = AUTO_WIFI_ERR_BAD_ARG;
wifi_ipc_reply(req->resp, req->cmd, 2, &err, 4);
break;
}
memset(&filter, 0, sizeof(filter));
filter.param.rssi_range = -90;
cy_rslt_t r = cy_wcm_start_scan(wifi_scan_callback, &scan_ctx, &filter);
if (r != CY_RSLT_SUCCESS) {
s_wifi_last_error = r;
err = (uint32_t)r;
printf("[WiFiIPC] SCAN: start_scan failed, err=0x%08lX\r\n", (unsigned long)err);
vSemaphoreDelete(scan_ctx.done);
wifi_ipc_reply(req->resp, req->cmd, 3, &err, 4);
break;
}
if (xSemaphoreTake(scan_ctx.done, pdMS_TO_TICKS(AUTO_WIFI_SCAN_TIMEOUT_MS)) != pdTRUE) {
(void)cy_wcm_stop_scan();
err = AUTO_WIFI_ERR_TIMEOUT;
printf("[WiFiIPC] SCAN: semaphore timeout (%d ms)\r\n", AUTO_WIFI_SCAN_TIMEOUT_MS);
vSemaphoreDelete(scan_ctx.done);
wifi_ipc_reply(req->resp, req->cmd, 4, &err, 4);
break;
}
vSemaphoreDelete(scan_ctx.done);
printf("[WiFiIPC] SCAN: collected %u raw networks\r\n", (unsigned)scan_ctx.count);
/* --- Deduplicate: keep strongest RSSI per SSID --- */
for (uint8_t i = 0; i < scan_ctx.count; i++) {
for (uint8_t j = i + 1; j < scan_ctx.count; ) {
if (strncmp(scan_ctx.entries[i].ssid, scan_ctx.entries[j].ssid, 32) == 0) {
if (scan_ctx.entries[j].rssi > scan_ctx.entries[i].rssi) {
scan_ctx.entries[i] = scan_ctx.entries[j];
}
scan_ctx.entries[j] = scan_ctx.entries[scan_ctx.count - 1];
scan_ctx.count--;
} else {
j++;
}
}
}
/* --- Sort by RSSI descending (insertion sort) --- */
for (uint8_t i = 1; i < scan_ctx.count; i++) {
ipc_wifi_scan_entry_t tmp = scan_ctx.entries[i];
uint8_t j = i;
while (j > 0 && scan_ctx.entries[j - 1].rssi < tmp.rssi) {
scan_ctx.entries[j] = scan_ctx.entries[j - 1];
j--;
}
scan_ctx.entries[j] = tmp;
}
/* --- Truncate to IPC limit (top 6 strongest) --- */
uint8_t send_count = scan_ctx.count;
if (send_count > IPC_WIFI_SCAN_MAX_ENTRIES) {
send_count = IPC_WIFI_SCAN_MAX_ENTRIES;
}
printf("[WiFiIPC] SCAN: sending top %u of %u unique networks\r\n",
(unsigned)send_count, (unsigned)scan_ctx.count);
uint16_t data_len = (uint16_t)(send_count * sizeof(ipc_wifi_scan_entry_t));
wifi_ipc_reply(req->resp, req->cmd, 0, scan_ctx.entries, data_len);
break;

ตัว barrier อยู่ในจังหวะตอบกลับนี้เอง:

static void wifi_ipc_reply(ipc_response_t *resp, uint32_t cmd, uint8_t status,
const void *data, uint16_t data_len)
{
if (resp == NULL) {
return;
}
memset((void *)resp, 0, sizeof(*resp));
resp->cmd = (uint8_t)cmd;
resp->status = status;
if (data != NULL && data_len > 0) {
if (data_len > IPC_RESPONSE_DATA_MAX) {
data_len = IPC_RESPONSE_DATA_MAX;
}
memcpy(resp->data, data, data_len);
resp->data_len = data_len;
}
__DMB();
resp->ready = 1;
}

__DMB() ก่อน แล้วจึง resp->ready = 1 ฝั่งผู้อ่านทำสลับกันเป็นเงาสะท้อน — wifi_manager_scan_ready() เรียก __DMB() แล้วจึงอ่าน ready (wifi_manager.c:294-298) ถ้าถอด barrier ตัวใดตัวหนึ่งออก หน้าบน CM55 จะอ่านรายการที่เขียนค้างอยู่ครึ่งเดียวเป็นครั้งคราว

Connect: การแพ็ก payload การลองใหม่ และการบันทึก

bool wifi_manager_connect(const char *ssid, const char *password)
{
if (ssid == NULL) {
s_last_error = WIFI_ERR_INVALID_ARG;
return false;
}
uint8_t payload[IPC_DATA_MAX_LEN];
memset(payload, 0, sizeof(payload));
size_t ssid_len = strnlen(ssid, 32);
memcpy(payload, ssid, ssid_len);
payload[ssid_len] = '\0';
const char *pass = (password != NULL) ? password : "";
size_t pass_len = strnlen(pass, 63);
memcpy(&payload[ssid_len + 1], pass, pass_len);
payload[ssid_len + 1 + pass_len] = '\0';
size_t payload_len = ssid_len + 1 + pass_len + 1;
if (!wifi_manager_ipc_request(IPC_CMD_WIFI_CONNECT, payload, payload_len,
WIFI_IPC_RESPONSE_TIMEOUT_CONN_MS)) {
return false;
}
(void)wifi_manager_ipc_request(IPC_CMD_WIFI_STATUS, NULL, 0,
WIFI_IPC_RESPONSE_TIMEOUT_MS);
wifi_manager_decode_status(&s_wifi_ipc_resp, &s_status);
return true;
}

ssid\0password\0 ถูกแพ็กลงใน payload ของข้อความ จากนั้นรอค่า ready แบบบล็อก โดยมี timeout แยกตามคำสั่ง:

/* ...context: inside wifi_manager_ipc_request() ... */
while (!s_wifi_ipc_resp.ready && timeout > 0) {
vTaskDelay(pdMS_TO_TICKS(1)); /* Yield to other tasks (LVGL, sensors) */
timeout--;
}
__DMB(); /* Data Memory Barrier — prevent stale cache reads on shared memory */
if (!s_wifi_ipc_resp.ready) {
/* No response: CM33 may never have seen the message (wedged channel)
* — clean up so the next request isn't doomed too. */
cm55_ipc_pipe_drain_release();
g_wifi_ipc_pipe_recoveries++;
s_last_error = WIFI_ERR_IPC_RESP_TIMEOUT;
return false;
}

บน CM33_NS กรณี connect จะแกะ payload ออก เข้าร่วมเครือข่ายด้วย wifi_connect_robust() ที่พยายาม 6 ครั้งจากบท C1 ตอบกลับ ผลักสถานะ WiFi ไปยัง CM55 ซิงก์ NTP หนึ่งครั้ง แล้วจึงเตรียมหรือเขียนข้อมูลรับรองลง LFS (ทางแยกของบท C1)

/* ...context: inside wifi_ipc_handle_request() ... */
case IPC_CMD_WIFI_CONNECT: {
printf("[WiFiIPC] CONNECT cmd received\r\n");
const char *ssid = (const char *)req->data;
size_t ssid_len = strnlen(ssid, 32);
const char *pass = (const char *)&req->data[ssid_len + 1];
size_t pass_len = strnlen(pass, 63);
if (ssid_len == 0U) {
err = AUTO_WIFI_ERR_BAD_ARG;
wifi_ipc_reply(req->resp, req->cmd, 1, &err, 4);
break;
}
if (!wifi_ensure_ready()) {
err = (uint32_t)s_wifi_last_error;
wifi_ipc_reply(req->resp, req->cmd, 2, &err, 4);
break;
}
cy_wcm_connect_params_t params;
cy_wcm_ip_address_t ip;
memset(&params, 0, sizeof(params));
memset(&ip, 0, sizeof(ip));
memcpy(params.ap_credentials.SSID, ssid, ssid_len);
memcpy(params.ap_credentials.password, pass, pass_len);
params.ap_credentials.security = (pass_len == 0U)
: CY_WCM_SECURITY_WPA3_WPA2_PSK; /* SAE + PMF; backward-compatible w/ WPA2 */
/* Robust multi-attempt connect — the WiFi Menu drives this IPC path and
* previously made a SINGLE cy_wcm_connect_ap() call, so a marginal cold
* join surfaced as "Try Again". */
cy_rslt_t r = wifi_connect_robust(&params, &ip, "WiFiIPC");
if (r != CY_RSLT_SUCCESS) {
s_wifi_last_error = r;
err = (uint32_t)r;
wifi_ipc_reply(req->resp, req->cmd, 3, &err, 4);
break;
}
s_wifi_state.mode = AUTO_WIFI_MODE_STA;
s_wifi_state.connected = true;
s_wifi_state.rssi = 0;
memset(s_wifi_state.ssid, 0, sizeof(s_wifi_state.ssid));
strncpy(s_wifi_state.ssid, ssid, sizeof(s_wifi_state.ssid) - 1);
wifi_update_ip_from_wcm();
s_wifi_last_error = CY_RSLT_SUCCESS;
wifi_ipc_reply(req->resp, req->cmd, 0, NULL, 0);
/* Push WiFi connected state to CM55 topbar */
push_wifi_state_to_cm55(true);
/* NTP → RTC sync (one-shot after first successful connect) */
if (!s_ntp_synced) {
if (ntp_sync_rtc()) {
s_ntp_synced = true;
push_time_to_cm55();
tesaiot_bridge_ntp_synced();
}
}
/* Stage credential for deferred QSPI save.
* lfs_wifi_creds_write() requires MicroPython task context, so we
* update the shared globals here and let mpy_main.c flush to QSPI
* at the next soft reset or REPL idle. This also makes the
* credential immediately available for boot auto-connect if the
* board soft-resets (Ctrl+D).
*
* The dual-band BLE worker writes the same array on its own
* task; lock around the entire mutate-and-set-dirty sequence so
* a concurrent writer never sees a half-updated entry or a
* count that races the entry write. */
wifi_creds_lock();
{
bool already_saved = false;
for (int i = 0; i < g_boot_wifi_creds_count; i++) {
if (strncmp(g_boot_wifi_creds[i].ssid, ssid, 32) == 0) {
/* Update password in case it changed */
memset(g_boot_wifi_creds[i].password, 0, 65);
strncpy(g_boot_wifi_creds[i].password, pass, 64);
g_boot_wifi_creds[i].flags = 0x01;
already_saved = true;
break;
}
}
if (!already_saved) {
int idx;
if (g_boot_wifi_creds_count < QSPI_WIFI_CREDS_MAX) {
idx = g_boot_wifi_creds_count;
g_boot_wifi_creds_count = idx + 1;
} else {
idx = 0; /* overwrite oldest slot */
}
memset(&g_boot_wifi_creds[idx], 0, sizeof(qspi_wifi_entry_t));
strncpy(g_boot_wifi_creds[idx].ssid, ssid, 32);
strncpy(g_boot_wifi_creds[idx].password, pass, 64);
g_boot_wifi_creds[idx].security = CY_WCM_SECURITY_WPA2_AES_PSK;
g_boot_wifi_creds[idx].flags = 0x01;
}
g_boot_wifi_creds_dirty = true;
printf("[WiFiIPC] Credential staged for QSPI save (%d entries)\r\n",
(int)g_boot_wifi_creds_count);
}
wifi_creds_unlock();
/* ...context: inside the IPC_CMD_WIFI_CONNECT success path ... */
#if !BENTO_HAS_MPY
/* No VM means no mpy_main.c and therefore no deferred flusher — the
* "requires MicroPython task context" above is about the Python-VFS
* store, not this one. This runs in the WiFi worker task, and the C
* store is plain lfs2 over serial memory, so write it out now.
* Snapshot under the lock, write outside it: the flash write takes
* milliseconds and the BLE worker shares these globals. */
{
qspi_wifi_entry_t snap[QSPI_WIFI_CREDS_MAX];
int n;
wifi_creds_lock();
n = (int)g_boot_wifi_creds_count;
if (n > QSPI_WIFI_CREDS_MAX) n = QSPI_WIFI_CREDS_MAX;
memcpy(snap, g_boot_wifi_creds, (size_t)n * sizeof(qspi_wifi_entry_t));
wifi_creds_unlock();
extern bool lfs_wifi_creds_write(const qspi_wifi_entry_t *entries, int count);
if (lfs_wifi_creds_write(snap, n)) {
wifi_creds_lock();
g_boot_wifi_creds_dirty = false;
wifi_creds_unlock();
printf("[WiFiIPC] Credentials persisted (%d entries)\r\n", n);
} else {
printf("[WiFiIPC] ERROR: credential save failed — kept dirty\r\n");
}
}
#endif
break;

กลับมาที่ CM55 หน้านี้ลอง connect ซ้ำ เพียงครั้งเดียวเท่านั้นcy_wcm_connect_ap() มักล้มเหลวในครั้งแรกหลังการ scan เพราะการเปลี่ยนสถานะภายในของ WCM (wifi_connect_native.c:429-430) — และจะเรียก wifi_saved_add() ต่อเมื่อยืนยันแล้ว ว่าเข้าร่วมเครือข่ายสำเร็จเท่านั้น:

/* ...context: inside the deferred connect callback, after a confirmed association ... */
/* Auto-retry: cy_wcm_connect_ap() often fails on the first attempt
* after a scan due to internal WCM state transition. Retry once. */
bool ok = wifi_manager_connect(s_ctx.pending_ssid, s_ctx.pending_password);
if (!ok) {
ok = wifi_manager_connect(s_ctx.pending_ssid, s_ctx.pending_password);
}
if (!ok) {
aic_wifi_set_state(s_ctx.ui, WIFI_STATE_ERROR);
aic_wifi_show_error(s_ctx.ui, WIFI_ERR_AUTH_FAILED, "Connect failed");
return;
}
/* Auto-save credentials on successful connect */
wifi_saved_add(s_ctx.pending_ssid, s_ctx.pending_password, s_ctx.pending_security);
wifi_native_refresh_saved();
wifi_native_scan_and_update();
}

wifi_saved_add() ไม่เคยถูกเรียกแบบเผื่อไว้ก่อน ผู้เรียกไม่ต้องเติมค่าใด ๆ ลงในรายการล่วงหน้า เพราะการตัดรายการซ้ำ แฟล็ก last_used และการปิดท้ายด้วย NUL เป็นเรื่องภายในทั้งหมด (wifi_saved.c:266-304)

รายการเครือข่ายที่บันทึกไว้: probe ก่อน แล้วจึง read ทีละ slot ต่อหนึ่ง tick

รายการนี้ถูกเติมจาก slot ของ OPTIGA แบบไม่ประสานเวลา เพื่อไม่ให้หน้าจอบล็อก GFX task นานราว 1.5 s อย่างที่การโหลดแบบประสานเวลาต้องเสีย ลำดับต้องเคร่งครัด:

/* Phase 3b: Probe poll — fires every 100ms until OPTIGA probe completes */
static void wifi_native_saved_probe_poll_cb(lv_timer_t *timer)
{
if (!wifi_saved_probe_ready()) return;
lv_timer_delete(timer);
/* Start async slot loading — state machine: send → poll → next */
s_saved_count = 0;
s_saved_load_idx = 0;
s_slot_state = SLOT_SEND;
lv_timer_create(wifi_native_saved_slot_cb, 50, NULL);
}
/* Phase 3a: Start async OPTIGA probe */
static void wifi_native_deferred_load_saved_cb(lv_timer_t *timer)
{
lv_timer_delete(timer);
if (s_ctx.ui == NULL) return;
/* Already probed (cached) — skip poll, go straight to slot loading */
s_saved_count = 0;
s_saved_load_idx = 0;
s_slot_state = SLOT_SEND;
lv_timer_create(wifi_native_saved_slot_cb, 50, NULL);
} else {
/* Probe IPC in flight — poll until ready */
lv_timer_create(wifi_native_saved_probe_poll_cb, 100, NULL);
}
}

probe_start() → วนถาม probe_ready() บนตัวจับเวลาของ LVGL ทุก 100 ms → probe_finish() เพียงครั้งเดียว และต่อเมื่อ probe_ready() คืนค่า true แล้วเท่านั้น probe_ready() อาจเป็น true ทันที หลัง probe_start() (ผลที่แคชไว้) จึงต้องรองรับเส้นทางที่เร็วนี้ด้วย ห้ามคิดเอาเองว่าไม่เกิด ทุก callback ตรวจ s_ctx.ui == NULL ซ้ำ เพราะหน้านี้ถูกทำลายกลางคัน IPC ได้

จากนั้นอ่านทีละหนึ่งรายการเท่านั้นในเวลาหนึ่ง:

/* Phase 3c: Async slot read — state machine: SEND (instant) → POLL (instant) */
static void wifi_native_saved_slot_cb(lv_timer_t *timer)
{
if (s_ctx.ui == NULL) { lv_timer_delete(timer); return; }
if (s_slot_state == SLOT_SEND) {
if (s_saved_load_idx >= WIFI_SAVED_MAX) {
lv_timer_delete(timer);
aic_wifi_set_saved_networks(s_ctx.ui, s_saved_buf, s_saved_count);
return;
}
/* Send IPC for this slot — returns instantly */
if (wifi_saved_read_start(s_saved_load_idx)) {
s_slot_state = SLOT_POLL;
} else {
/* Send failed — skip this slot */
s_saved_load_idx++;
}
return;
}
/* SLOT_POLL: check if IPC response is ready */
if (!wifi_saved_read_ready()) return; /* Not ready — try next tick */
memcpy(&s_saved_buf[s_saved_count], &tmp, sizeof(tmp));
s_saved_count++;
}
s_saved_load_idx++;
s_slot_state = SLOT_SEND; /* Next tick: send for next slot */
}

read_start(slot) → ถ้ายังไม่พร้อมแปลว่า ให้ลองใหม่ใน tick ถัดไปread_result() เติมค่าลง wifi_saved_entry_t บน stack ที่ผู้เรียกเป็นเจ้าของ ให้คัดลอกออกมาทันที ค่า false จาก read_start() คือเส้นทางความผิดพลาดจริง: ให้ข้าม slot นั้นแล้วเดินหน้าต่อ ห้ามลองใหม่ และห้ามยกเลิกทั้งรายการ

หลังการเปลี่ยนแปลงข้อมูล (เพิ่มหรือลบ) หน้านี้ใช้เส้นทางแบบ ประสานเวลา แทน ซึ่งยอมรับได้เพราะผู้ใช้เพิ่งแตะหน้าจอและคาดหวังให้รายการรีเฟรช:

static void wifi_native_refresh_saved(void)
{
if (!s_ctx.ui) return;
int count = wifi_saved_load_all(entries);
aic_wifi_set_saved_networks(s_ctx.ui, entries, count);
}

บัฟเฟอร์ ต้องเป็น wifi_saved_entry_t[WIFI_SAVED_MAX] เพราะไม่มีอาร์กิวเมนต์สำหรับระบุความจุ

Erase: คลี่หา slot ให้ได้ก่อน

static void wifi_native_on_saved_delete(int saved_index)
{
/* Find the OPTIGA slot index for this entry */
if (!s_ctx.ui) return;
if (saved_index < 0 || saved_index >= s_ctx.ui->saved_count) return;
/* Find the matching slot by SSID (saved_index is UI order, need OPTIGA slot) */
const char *ssid = s_ctx.ui->saved_entries[saved_index].ssid;
int slot = wifi_saved_find(ssid);
if (slot >= 0) {
}
/* Refresh saved networks UI */
wifi_native_refresh_saved();
}

wifi_saved_find(ssid) คืนค่าเป็น slot ของที่เก็บ (คืน < 0 เมื่อไม่พบ) แล้วจึงค่อยเรียก wifi_saved_erase(slot) ค่า bool ที่ได้จาก erase ถูกละทิ้งโดยเจตนา เพราะการกู้คืนคือการรีเฟรชรายการทั้งหมด

ทีละขั้น

ขั้นที่ 1 — Scan จากหน้าจอ

Home → WiFi → Scan

สิ่งที่ควรสังเกต เครือข่ายสูงสุด 6 รายการ เรียงจากแรงที่สุดก่อน และไม่มีรายการซ้ำ เมื่อล้มเหลว หน้าจอแสดงข้อความ "Scan failed" (wifi_connect_native.c:397) ไม่มีอะไรบน UART เพราะทุกบรรทัด [WiFiIPC] ในไฟล์ของ worker ถูกปิดเสียง (sensor_auto_task.c:36) และ callback ฝั่ง ISR ไม่มีการพิมพ์โดยเจตนา ตามคอมเมนต์ของมันที่ระบุว่า No printf here — this is ISR context (sensor_auto_task.c:272)

ขั้นที่ 2 — Connect จากหน้าจอ

เลือกเครือข่าย พิมพ์รหัสผ่าน แล้วกด Connect หน้าจอจะแสดง "Connecting…" ก่อนเพราะการเลื่อน 50 ms จากนั้น worker บน CM33 จึงเข้าร่วมเครือข่ายแบบพยายาม 6 ครั้ง

สิ่งที่ควรสังเกต เมื่อสำเร็จ: glyph WiFi บน topbar เลิกซ่อน (IPC_CMD_WIFI_STATE_PUSHipc_sensorhub_wifi_connected()) นาฬิกาปรากฏหลัง NTP และเครือข่ายนั้นปรากฏในรายการที่บันทึกไว้ เมื่อล้มเหลว หน้าจอแสดง "Connect failed" (wifi_connect_native.c:437) จะเห็นบรรทัด [wifi-glue] จากบท C1 เพิ่มขึ้นมาก็ต่อเมื่อส่วนเชื่อม BLE เป็นตัวสั่งเชื่อมต่อเท่านั้น (build ที่ตั้ง ENABLE_PAGE_BENTO_BUDDY=1 พร้อมการสลับคลื่นวิทยุจากเดสก์ท็อป บท I1)

ขั้นที่ 3 — ดูรายการโหลดแบบไม่ประสานเวลา

ออกจากหน้านี้แล้วกลับเข้ามาใหม่ รายการที่บันทึกไว้จะทยอยเติมขึ้นในหลาย tick แทนที่จะปรากฏพร้อมกันทีเดียว นั่นคือกลไก probe แล้ว read ทีละ slot ต่อหนึ่ง tick ขนาด 50 ms กำลังทำงาน

สิ่งที่ควรสังเกต รายการทยอยปรากฏ ส่วนนาฬิกาบน topbar ยังเดินต่อเนื่อง (GFX task ไม่เคยถูกบล็อก) หากการอ่าน slot ใดล้มเหลว จะได้รายการที่สั้นลง ไม่ใช่หน้าจอที่ค้าง

ขั้นที่ 4 — ลบรายการที่สองในรายการ

ลบเครือข่ายที่บันทึกไว้เป็นลำดับที่สอง

สิ่งที่ควรสังเกต เครือข่ายนั้น และเฉพาะเครือข่ายนั้น หายไปหลังการรีเฟรช หากเห็นเครือข่ายอื่นหายไป แสดงว่ามีคนส่งดัชนีของ UI เข้า wifi_saved_erase() แทนที่จะคลี่หาผ่าน wifi_saved_find() ก่อน — กับดัก 1

ขั้นที่ 5 — รีบูตแล้วตรวจที่เก็บทั้ง 2 แห่ง

ตัดไฟแล้วจ่ายไฟใหม่ เครือข่ายที่เชื่อมต่อไว้ในขั้นที่ 2 จะเชื่อมต่ออัตโนมัติ (เพราะถูก worker บน CM33 เตรียมลงในที่เก็บ LFS ไว้แล้ว — บท C1) และยังคงแสดงอยู่ในรายการ (เพราะอยู่ใน OPTIGA)

สิ่งที่ควรสังเกต mtb-only: [HB] t=lus tasks=u แล้วตามด้วย glyph และนาฬิกา mtb-mpy: [MPY] GC heap u KB @ p in s แล้วตามด้วย glyph และนาฬิกา ยกเว้นกรณีที่ /main.py วนไม่รู้จบ ซึ่งรายการจะยังแสดงเครือข่ายนั้นแต่บอร์ดจะไม่เชื่อมต่ออัตโนมัติ (อันตรายเรื่องข้อมูลรับรองสูญหายในบท C1 ซึ่งตอนนี้มองเห็นได้ในรูปความไม่ตรงกันของที่เก็บทั้ง 2 แห่ง)

กับดัก

กับดัก 1 — ดัชนีของรายการบน UI ไม่ใช่ดัชนี slot ของ OPTIGA
รายการถูกสร้างจาก slot ที่ตอบกลับมาเท่านั้น เรียงตามลำดับที่ตอบ หลังจากที่การขับออกแบบ LRU อาจสลับตำแหน่งไปแล้ว wifi_saved_find() คือสะพานเดียวที่ถูกต้องจาก "แถวที่ผู้ใช้แตะ" ไปสู่ "slot ที่ต้องลบ" (ภาคผนวก X ข้อ 10)
กับดัก 2 — เรียก probe_finish() 2 ครั้ง หรือเรียกก่อน probe_ready()
ครั้งเดียว และต่อเมื่อพร้อมแล้วเท่านั้น หน้านี้ลบตัวจับเวลาของตนเองก่อนเดินหน้าเข้าสู่ช่วง read ให้ทำตามวินัยเดียวกัน
กับดัก 3 — ออก read_start() ซ้ำก่อน read_ready()
ให้มีการอ่านค้างอยู่ได้ครั้งเดียว กลไกที่ส่งมอบมาเป็นคู่ SEND → POLL ที่ชัดเจน ทีละ slot ต่อหนึ่ง tick การอ่านที่ซ้อนกันทำให้ slot ของคำตอบเสียหาย
กับดัก 4 — หน้าจออาจถูกทำลายขณะที่คำตอบยังค้างอยู่
lv_screen_load_anim(auto_del) คืนหน่วยความจำอ็อบเจ็กต์ของหน้านั้น callback ที่ยิงหลังจากนั้นจะอ้างถึงหน่วยความจำ LVGL ที่ถูกปลดไปแล้ว ทุก callback ในหน้าที่ส่งมอบมาตรวจ s_ctx.ui == NULL ซ้ำ ให้ทำแบบเดียวกันในหน้าของตน
กับดัก 5 — ถอด barrier ทิ้ง
ฝั่งผู้ผลิตต้องมี __DMB(); resp->ready = 1; และฝั่งผู้อ่านต้องมี __DMB() ก่อนอ่าน ready ต้องมีครบทั้งสองฝั่ง เสมอ
กับดัก 6 — เฝ้าหา [WiFiIPC] SCAN cmd received
ถูกปิดเสียง ดูบท C1 กับดัก 1

Variant

variant ที่ใช้ได้
mtb-mpy และ mtb-only

CM55 ไม่ขึ้นกับ variant ทั้งตัวหน้าจอ wifi_manager.c และกลไก wifi_saved_* เหมือนกันทั้งสอง variant ส่วน worker บน CM33_NS ก็เป็นไฟล์เดียวกันทั้งสอง variant มีเพียงการ บันทึก ข้อมูลรับรองที่ท้ายกรณี connect เท่านั้นที่แยกทาง (บท C1) ที่เก็บ OPTIGA ถูกเข้าถึงผ่านคำสั่ง IPC ฝั่งข้อมูลรับรองของ HSM ซึ่งหมายความว่าวินัยการเข้าถึงชิปในบท D1 มีผลอยู่ใต้การเรียก wifi_saved_* ทุกครั้ง — optiga_manager_acquire()/release() ทำงานบน CM33_NS ในทุกการอ่านและเขียน slot