SDK for TESAIoT Dev Kit
API reference & tutorials (ModusToolbox)
Loading...
Searching...
No Matches
C2 — WiFi from the UI over IPC

Learning goal

Two things, both reused far beyond WiFi:

  1. The msg.value-carries-a-response-pointer IPC idiom. CM55 places the address of a response struct in the pipe message; CM33_NS fills the struct and sets ready = 1 behind a memory barrier; CM55 polls ready. Every IPC_CMD_WIFI_* command uses it.
  2. The async OPTIGA slot machinery behind the saved-networks list — wifi_saved_probe_* then wifi_saved_read_* — and the one fact that matters most: the UI list index is not the storage slot index.

This chapter is the OPTIGA-store half of the story that chapter C1 started.

The real firmware sequence

Scan: CM55 asks, CM33_NS answers

The WiFi Connect page's Scan button sets the state to WIFI_STATE_SCANNING and defers 50 ms so LVGL can paint before the pipe is touched (wifi_connect_native.c:406-412). The deferred callback is the page's whole scan/connect flow:

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() is the sender. Note that this file lives under bento_libs/claw/common/modules/wifi_manager/, not under proj_cm55/ — a grep for IPC_CMD_WIFI_SCAN scoped to proj_cm55/ returns nothing and looks like the path is missing. It is not.

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

The response pointer travels in msg.value. That single line is the contract for the entire family.

On CM33_NS the pipe callback runs in ISR context. It does one thing: extracts the response pointer and enqueues the request to the 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;
}

The worker dequeues, and for a scan: wifi_ensure_ready() (lazy WiFi init — the hardware is untouched until first use), RSSI filter at −90 dBm, cy_wcm_start_scan(), dedupe by SSID keeping the strongest RSSI, insertion sort by RSSI, truncate to IPC_WIFI_SCAN_MAX_ENTRIES (6), reply.

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;

The reply is where the barrier lives:

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() then resp->ready = 1. The consumer side mirrors it — wifi_manager_scan_ready() does __DMB() then reads ready (wifi_manager.c:294-298). Remove either barrier and the CM55 page will occasionally read a half-written entry list.

Connect: payload packing, the retry, and the save

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 packed into the message payload, then a blocking wait on ready with a per-command 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;
}

On CM33_NS the connect case unpacks, joins with the six-attempt wifi_connect_robust() from chapter C1, replies, pushes the WiFi state to CM55, syncs NTP once, and then stages or writes the LFS credential (C1's fork).

/* ...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;

Back on CM55, the page retries the connect exactly once — cy_wcm_connect_ap() often fails on the first attempt after a scan because of an internal WCM state transition (wifi_connect_native.c:429-430) — and only after a confirmed association does it call 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() is never speculative. Callers do not pre-populate an entry: dedupe, flags, last_used and NUL-termination are internal (wifi_saved.c:266-304).

The saved-networks list: probe, then read, one slot per tick

The list is populated from OPTIGA slots asynchronously so the page never blocks the GFX task for the ~1.5 s a synchronous load costs. Strict order:

/* 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() → poll probe_ready() on a 100 ms LVGL timer → probe_finish() exactly once, and only after probe_ready() returned true. probe_ready() may be true immediately after probe_start() (cached result) — the fast path must be handled, not assumed away. Every callback re-checks s_ctx.ui == NULL because the page can be destroyed mid-IPC.

Then one read in flight at a time:

/* 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) → not ready means try next tick → read_result() fills a caller-owned stack wifi_saved_entry_t; copy it out immediately. A false from read_start() is a real error path: skip the slot and advance — do not retry, do not abort the list.

After a mutation (add or erase) the page uses the synchronous path instead — acceptable because the user just tapped and expects a refresh:

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

The buffer must be wifi_saved_entry_t[WIFI_SAVED_MAX] — there is no capacity argument.

Erase: resolve the slot first

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) returns the storage slot (< 0 on miss); only then wifi_saved_erase(slot). The bool from erase is deliberately ignored — recovery is a full list refresh.

Step-by-step

Step 1 — Scan from the page

Home → WiFi → Scan.

What you should observe. Up to six networks, strongest first, no duplicates. On failure the page shows the string "Scan failed" (wifi_connect_native.c:397). Nothing on the UART: every [WiFiIPC] line in the worker file is muted (sensor_auto_task.c:36), and the ISR callback has no print by design — its comment reads No printf here — this is ISR context (sensor_auto_task.c:272).

Step 2 — Connect from the page

Pick a network, type the password, Connect. The page shows "Connecting…" first because of the 50 ms deferral, then the CM33 worker does the six-attempt join.

What you should observe. On success: the topbar WiFi glyph un-hides (IPC_CMD_WIFI_STATE_PUSH → ipc_sensorhub_wifi_connected()), the clock appears after NTP, and the network appears in the saved list. On failure the page shows "Connect failed" (wifi_connect_native.c:437). Only if the BLE glue drove the connect (an ENABLE_PAGE_BENTO_BUDDY=1 build with a desktop-driven radio switch, Chapter I1) will you additionally see the [wifi-glue] lines from chapter C1.

Step 3 — Watch the list load asynchronously

Leave the page and come back. The saved list fills in over a few ticks rather than appearing all at once — that is the probe-then-read slot machine running at one slot per 50 ms tick.

What you should observe. Entries appearing progressively; the topbar clock keeps ticking (the GFX task was never blocked). If a slot read fails you see a shorter list, not a stuck page.

Step 4 — Erase the second entry in the list

Delete the second saved network.

What you should observe. That network, and only that network, disappears after a refresh. If a different network disappeared, someone passed the UI index to wifi_saved_erase() instead of resolving it through wifi_saved_find() — Trap 1.

Step 5 — Reboot and check both stores

Power-cycle. The network you connected to in Step 2 auto-connects (it was staged into the LFS store by the CM33 worker — chapter C1) and still shows in the list (it is in OPTIGA).

What you should observe. mtb-only: [HB] t=lus tasks=u then glyph and clock. mtb-mpy: [MPY] GC heap u KB @ p in s then glyph and clock — unless /main.py loops, in which case the list still shows the network but the board does not auto-connect (C1's credential-loss hazard, now visible as a disagreement between the two stores).

Traps

Trap 1 — UI list index is not the OPTIGA slot index.
The list is built from whichever slots answered, in the order they answered, after LRU eviction may have shuffled them. wifi_saved_find() is the only correct bridge from "the row the user tapped" to "the slot to erase". (Appendix X, item 10.)
Trap 2 — probe_finish() twice, or before probe_ready().
Once, and only after ready. The page deletes its own poll timer before advancing to the read phase; copy that discipline.
Trap 3 — Re-issuing read_start() before read_ready().
One in-flight read. The shipped machine is an explicit SEND → POLL pair, one slot per tick; overlapping reads corrupt the response slot.
Trap 4 — The page can die while a response is in flight.
lv_screen_load_anim(auto_del) frees the page's objects; a callback that fires afterwards dereferences freed LVGL memory. Every callback in the shipped page re-checks s_ctx.ui == NULL. Do the same in yours.
Trap 5 — Dropping the barrier.
__DMB(); resp->ready = 1; on the producer and __DMB() before reading ready on the consumer. Both halves, always.
Trap 6 — Looking for [WiFiIPC] SCAN cmd received.
Muted. See chapter C1, Trap 1.

Variant

Variant
mtb-mpy and mtb-only

CM55 is variant-agnostic; the page, wifi_manager.c, and the wifi_saved_* machinery are identical on both. The CM33_NS worker is the same file on both variants; only the credential save at the end of the connect case forks (chapter C1). The OPTIGA store is reached through the HSM IPC credential commands, which means chapter D1's chip-access discipline applies underneath every wifi_saved_* call — optiga_manager_acquire()/release() run on CM33_NS for each slot read and write.