SDK สำหรับ TESAIoT Dev Kit
คู่มืออ้างอิง API และ Tutorial (MTB & µPython)
Loading...
Searching...
No Matches
C1 — WiFi: ที่เก็บข้อมูลรับรอง 2 แห่งกับการเชื่อมต่ออัตโนมัติตอนบูต

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

เมื่อจบบทนี้ ผู้อ่านจะระบุได้ว่าข้อมูลรับรอง WiFi แต่ละชุดบนบอร์ดอยู่ในที่เก็บใด คอร์ใดเป็นผู้เขียน และจะอยู่รอดข้ามการรีบูตหรือไม่

ที่เก็บข้อมูลรับรองมี 2 แห่ง และการสับสนระหว่าง 2 แห่งนี้คือความผิดพลาดที่มีโอกาสเกิดมากที่สุดใน Tutorial ทั้งชุดของ SDK นี้:

ที่เก็บบูต LFS ที่เก็บฝั่ง UI ของ OPTIGA
ตระกูล API lfs_wifi_creds_* wifi_saved_*
อยู่ที่ใด ไฟล์ LittleFS /.wifi_creds บนหน่วยความจำแฟลช QSPI NVM ของ OPTIGA Trust M, data object แบบ Type 3, slot 5, 6, 8, 9, 10, 11 (wifi_saved.h:5-10)
คอร์ CM33_NS CM55 (เข้าถึงผ่าน IPC CRED_READ/WRITE/ERASE)
ใครอ่านตอนบูต wifi_boot_auto_connect() — บทนี้ ไม่มีใครอ่าน ที่เก็บนี้ไม่ได้อยู่บนเส้นทางการบูต
ใครแสดงบนหน้าจอ ไม่มี รายการเครือข่ายที่บันทึกไว้ของหน้า WiFi Connect — บท C2
รูปแบบข้อมูล magic "WIFI", version 1, สูงสุด 6 × รายการละ 100 ไบต์, CRC32 (lfs_wifi_creds_c.c:10-18) หนึ่งรายการขนาด 106 ไบต์ต่อหนึ่ง OPTIGA object, การขับออกแบบ LRU

เครือข่ายที่เพิ่มจากจอ LCD จะลงในที่เก็บ OPTIGA และ ลงในที่เก็บ LFS ด้วย (ผ่าน worker บน CM33) ส่วนเครือข่ายที่เพิ่มจาก Python ด้วย wifi.connect() จะลงในที่เก็บ LFS เท่านั้น บอร์ดเชื่อมต่ออัตโนมัติจากที่เก็บ LFS ให้จำประโยคนี้ไว้ตลอดทุกขั้นด้านล่าง

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

เจ้าของเส้นทางนี้คือ WiFiIPC worker task (sensor_auto_task.c, wifi_ipc_worker_task) ไม่ใช่ task ที่ตั้งขึ้นเฉพาะสำหรับ WiFi สิ่งแรกที่ task นี้ทำก่อนเข้าสู่ลูปคิวของตนคือ wifi_boot_auto_connect() โดยเรียกแบบ inline เพราะการแยกเป็นอีก task หนึ่งจะกินแรม 4 KB (คอมเมนต์ที่ sensor_auto_task.c:1101-1104)

การเชื่อมต่ออัตโนมัติตอนบูต

static void wifi_boot_auto_connect(void)
{
#if BENTO_HAS_MPY
/* Wait for mpy_main.c to load credentials from LFS into globals.
* g_boot_wifi_creds_count is set by MicroPython task after VFS mount. */
printf("[WiFi-Boot] Waiting for credentials from MicroPython...\r\n");
for (int wait = 0; wait < 100; wait++) { /* 100 x 100ms = 10s */
if (g_boot_wifi_creds_count != 0) break;
vTaskDelay(pdMS_TO_TICKS(100));
}
#else
/* No VM, so no mpy_main.c to fill the globals. Read the same file
* through the C store; bento_storage_init() ran from main(). */
{
extern int lfs_wifi_creds_read(qspi_wifi_entry_t *entries, int max_entries);
int n = lfs_wifi_creds_read(g_boot_wifi_creds, QSPI_WIFI_CREDS_MAX);
if (n > 0) g_boot_wifi_creds_count = n;
}
#endif
int count = (int)g_boot_wifi_creds_count;
if (count <= 0) {
printf("[WiFi-Boot] No saved credentials — skipping\r\n");
return;
}
printf("[WiFi-Boot] %d saved network(s)\r\n", count);
if (!wifi_ensure_ready()) {
printf("[WiFi-Boot] SDIO/WCM init failed (0x%08lX)\r\n",
(unsigned long)s_wifi_last_error);
return;
}
/* Targeted scan per saved SSID: send a directed probe request for each
* saved network. This verifies the AP exists before attempting connect,
* avoiding 25+ second timeouts on unreachable networks.
*
* Timeout: 8s per SSID to cover 5GHz channels (iPhone hotspots etc.)
* where directed probe responses take longer than 2.4GHz. */
for (int i = 0; i < count; i++) {
/* Snapshot the credential into stack locals under the lock so
* the rest of the iteration body (scan, connect, retry loop,
* NTP sync) runs against an immutable view — concurrent writers
* on the BLE worker / IPC handler can't tear the strncpy of a
* password mid-copy. ~100 bytes of stack per iteration; the
* SensorAuto task has plenty of headroom. */
char ssid_local[33];
char pass_local[65];
bool slot_empty;
wifi_creds_lock();
slot_empty = (g_boot_wifi_creds[i].ssid[0] == '\0');
if (!slot_empty) {
strncpy(ssid_local, g_boot_wifi_creds[i].ssid,
sizeof(ssid_local) - 1);
ssid_local[sizeof(ssid_local) - 1] = '\0';
strncpy(pass_local, g_boot_wifi_creds[i].password,
sizeof(pass_local) - 1);
pass_local[sizeof(pass_local) - 1] = '\0';
}
wifi_creds_unlock();
if (slot_empty) continue;
/* Targeted SSID scan: probe for this specific network */
static wifi_scan_ctx_t boot_scan;
memset(&boot_scan, 0, sizeof(boot_scan));
boot_scan.done = xSemaphoreCreateBinary();
bool found = false;
if (boot_scan.done != NULL) {
memset(&filter, 0, sizeof(filter));
filter.mode = CY_WCM_SCAN_FILTER_TYPE_SSID;
strncpy((char *)filter.param.SSID, ssid_local,
sizeof(filter.param.SSID) - 1);
cy_rslt_t sr = cy_wcm_start_scan(wifi_scan_callback, &boot_scan, &filter);
if (sr == CY_RSLT_SUCCESS) {
/* 8s timeout — covers 5GHz channel scan for iPhone hotspots */
if (xSemaphoreTake(boot_scan.done, pdMS_TO_TICKS(8000)) == pdTRUE) {
found = (boot_scan.count > 0);
} else {
cy_wcm_stop_scan();
found = (boot_scan.count > 0);
}
}
vSemaphoreDelete(boot_scan.done);
boot_scan.done = NULL;
}
if (!found) {
printf("[WiFi-Boot] '%s' not found in scan — skipped\r\n",
ssid_local);
continue;
}
printf("[WiFi-Boot] '%s' found (RSSI=%d) — connecting...\r\n",
ssid_local,
(boot_scan.count > 0) ? boot_scan.entries[0].rssi : 0);
cy_wcm_connect_params_t params;
cy_wcm_ip_address_t ip;
memset(&params, 0, sizeof(params));
memset(&ip, 0, sizeof(ip));
strncpy((char *)params.ap_credentials.SSID, ssid_local,
sizeof(params.ap_credentials.SSID) - 1);
strncpy((char *)params.ap_credentials.password, pass_local,
sizeof(params.ap_credentials.password) - 1);
/* WPA3_WPA2_PSK (SAE + PMF) — see IPC_CMD_WIFI_CONNECT handler above. */
params.ap_credentials.security = (pass_local[0] == '\0')
: CY_WCM_SECURITY_WPA3_WPA2_PSK;
cy_rslt_t r = wifi_connect_robust(&params, &ip, "WiFi-Boot");
if (CY_RSLT_SUCCESS == r) {
#ifdef BOOT_VERBOSE
uint32_t v4 = ip.ip.v4;
printf("[WiFi-Boot] Connected to '%s'! IP=%lu.%lu.%lu.%lu\r\n",
ssid_local,
(unsigned long)(v4 & 0xFF), (unsigned long)((v4 >> 8) & 0xFF),
(unsigned long)((v4 >> 16) & 0xFF), (unsigned long)((v4 >> 24) & 0xFF));
#endif
s_wifi_state.mode = AUTO_WIFI_MODE_STA;
s_wifi_state.connected = true;
strncpy(s_wifi_state.ssid, ssid_local,
sizeof(s_wifi_state.ssid) - 1);
wifi_update_ip_from_wcm();
push_wifi_state_to_cm55(true);
if (ntp_sync_rtc()) {
s_ntp_synced = true;
push_time_to_cm55();
tesaiot_bridge_ntp_synced();
#ifdef BOOT_VERBOSE
printf("[WiFi-Boot] NTP synced + pushed to CM55\r\n");
#endif
}
return; /* Connected — done */

ให้อ่านทางแยกที่ต้นฟังก์ชันนั้นอย่างละเอียด lfs_wifi_creds_read() เป็นตัวอ่านใน ทั้งสอง variant สิ่งที่ต่างกันคือ ใครเป็นผู้เรียก และ implementation ใดเป็นผู้ตอบ:

  • mtb-mpy — MicroPython task เป็นผู้เรียกจาก mpy_main.c หลังเมานต์ VFS แล้ว (ที่เก็บนี้เรียก open() ของ Python อยู่ภายใน จึงต้องใช้ task ของ VM และต้องมี GC heap ว่างราว 16 KB) จากนั้นเติมค่าลง g_boot_wifi_creds ส่วน WiFi worker จะรอนานสุด 10 s (100 × 100 ms) ให้ g_boot_wifi_creds_count มีค่าไม่เป็นศูนย์

    การอ่านตอนบูตอยู่ที่ mpy_main.c:633-661 marker [mpy_lfs_wifi_creds_boot_read] (มีเฉพาะใน zip ของ mtb-mpy — ไฟล์นี้ไม่อยู่ในแพ็กเกจ mtb-only) หลังจาก gc_collect() และ lfs_wifi_creds_init() แกนของมันคือ:

wifi_creds_lock();
int n = lfs_wifi_creds_read(g_boot_wifi_creds, QSPI_WIFI_CREDS_MAX);
g_boot_wifi_creds_count = n;
int lfs_wifi_creds_read(qspi_wifi_entry_t *entries, int max_entries)
เติมอาร์เรย์ของผู้เรียก; คืนค่าจำนวนรายการ และคืน 0 เมื่อการตรวจความถูกต้องล้มเหลวไม่ว่ากรณีใด
#define QSPI_WIFI_CREDS_MAX
Definition wifi_creds_types.h:23
  • mtb-only — ไม่มี VM ดังนั้น WiFi worker จึงเรียก lfs_wifi_creds_read() เอง และ implementation คือที่เก็บฝั่ง C storage_c/lfs_wifi_creds_c.c (:69) ซึ่งวางอยู่บน bento_storage (บท G1) header เดียวกัน ชื่อเดียวกัน และไบต์บนแฟลชชุดเดียวกัน
void lfs_wifi_creds_init(void) { /* bento_storage_init() owns bring-up */ }
void lfs_wifi_creds_deinit(void) { }
bool lfs_wifi_creds_ready(void) { return bento_storage_ready(); }
/* The mtb-mpy store can inherit a pre-LFS sector image that wants rewriting;
* this store writes only the current format, so never. */
bool lfs_wifi_creds_needs_resave(void) { return false; }
int lfs_wifi_creds_read(qspi_wifi_entry_t *entries, int max_entries) {
if (entries == NULL || max_entries <= 0) return 0;
if (!bento_storage_ready()) return 0;
int n = bento_storage_read_file(CREDS_FILE, s_buf, sizeof(s_buf));
if (n < CREDS_TOTAL_SIZE) return 0;
uint32_t magic; memcpy(&magic, &s_buf[0], 4);
uint16_t version; memcpy(&version, &s_buf[4], 2);
uint16_t count; memcpy(&count, &s_buf[6], 2);
if (magic != QSPI_WIFI_CREDS_MAGIC) return 0;
if (version != QSPI_WIFI_CREDS_VERSION) return 0;
if (count > QSPI_WIFI_CREDS_MAX) return 0;
uint32_t stored; memcpy(&stored, &s_buf[CREDS_HEADER_SIZE + CREDS_ENTRIES_SIZE], 4);
if (stored != calc_crc32(s_buf, CREDS_HEADER_SIZE + CREDS_ENTRIES_SIZE)) {
/* Legacy XOR-32 fallback, exactly as the mtb-mpy reader does. */
if (stored != calc_xor32(s_buf, CREDS_HEADER_SIZE + CREDS_ENTRIES_SIZE)) return 0;
}
int out = (count < (uint16_t)max_entries) ? count : max_entries;
memcpy(entries, &s_buf[CREDS_HEADER_SIZE], (size_t)out * sizeof(qspi_wifi_entry_t));
/* Defensive termination, same as the mtb-mpy reader. */
for (int i = 0; i < out; i++) {
entries[i].ssid[32] = '\0';
entries[i].password[64] = '\0';
}
return out;
}
bool lfs_wifi_creds_write(const qspi_wifi_entry_t *entries, int count) {
if (count < 0 || count > QSPI_WIFI_CREDS_MAX) return false;
if (count > 0 && entries == NULL) return false;
if (!bento_storage_ready()) return false;
memset(s_buf, 0, sizeof(s_buf));
uint32_t magic = QSPI_WIFI_CREDS_MAGIC;
uint16_t version = QSPI_WIFI_CREDS_VERSION;
uint16_t cnt = (uint16_t)count;
memcpy(&s_buf[0], &magic, 4);
memcpy(&s_buf[4], &version, 2);
memcpy(&s_buf[6], &cnt, 2);
if (count > 0) {
memcpy(&s_buf[CREDS_HEADER_SIZE], entries,
(size_t)count * sizeof(qspi_wifi_entry_t));
}
uint32_t crc = calc_crc32(s_buf, CREDS_HEADER_SIZE + CREDS_ENTRIES_SIZE);
memcpy(&s_buf[CREDS_HEADER_SIZE + CREDS_ENTRIES_SIZE], &crc, 4);
return bento_storage_write_file(CREDS_FILE, s_buf, sizeof(s_buf));
}

จากนั้นทำทีละ SSID ที่บันทึกไว้: คัดลอกรายการนั้นเป็น snapshot ภายใต้ wifi_creds_lock() (ผู้เขียนอีกรายบน BLE worker หรือใน IPC handler ต้องไม่ทำให้รหัสผ่านขาดกลางระหว่างคัดลอก — sensor_auto_task.c:989-994) แล้วสแกนแบบ เจาะจง เฉพาะ SSID นั้นด้วย timeout 8 s — ช่อง 5 GHz โดยเฉพาะ hotspot ของ iPhone ตอบ probe ที่ระบุปลายทางช้า (:981-986) — ข้าม SSID นั้นไปหากไม่พบในผลสแกน แล้วจึงค่อยพยายามเข้าร่วมเครือข่าย

ตัวการเข้าร่วมเครือข่าย

static bool wifi_ensure_ready(void)
{
if (app_wifi_is_ready()) {
return true;
}
cy_rslt_t r = app_wifi_init();
if (r != CY_RSLT_SUCCESS) {
s_wifi_last_error = r;
return false;
}
s_wifi_last_error = CY_RSLT_SUCCESS;
return true;
}
/*******************************************************************************
* Robust WCM connect — rides out the marginal COLD first-join after boot.
* See BENTO-TESAIoT-Game-libraries/docs/WIFI_NTP_COLD_JOIN_KNOWHOW.md for the full
* root-cause writeup. In short: on a cold boot the CYW55500 RX buffer pool
* (WHD_DEF_MAX_RXBUFPOST=14 x ~2KB) is malloc'd fresh from the ~81KB shared newlib
* heap during the association burst; a momentary buffer shortfall drops the frame
* that sets JOIN_SSID_SET (WLC_E_SET_SSID) and the join returns WHD_JOIN_IN_PROGRESS
* (0x020003FF). Each cy_wcm_connect_ap() tears its half-join down on failure, so
* re-issuing (with disconnect + settle between) is the only recovery. Warm reconnect
* succeeds on the first try. Applies to every STA connect path.
******************************************************************************/
#define WIFI_CONNECT_MAX_ATTEMPTS (6)
#define WIFI_CONNECT_SETTLE_MS (1500)
static cy_rslt_t wifi_connect_robust(cy_wcm_connect_params_t *params,
cy_wcm_ip_address_t *ip, const char *tag)
{
cy_rslt_t r = CY_RSLT_TYPE_ERROR;
for (int attempt = 1; attempt <= WIFI_CONNECT_MAX_ATTEMPTS; attempt++) {
r = cy_wcm_connect_ap(params, ip);
if (CY_RSLT_SUCCESS == r || cy_wcm_is_connected_to_ap()) {
return CY_RSLT_SUCCESS;
}
printf("[%s] attempt %d/%d failed (0x%08lX)\r\n",
tag, attempt, WIFI_CONNECT_MAX_ATTEMPTS, (unsigned long)r);
if (attempt < WIFI_CONNECT_MAX_ATTEMPTS) {
cy_wcm_disconnect_ap(); /* clear WHD join FSM for a clean retry */
vTaskDelay(pdMS_TO_TICKS(WIFI_CONNECT_SETTLE_MS));
}
}
return r;
}

พยายาม 6 ครั้ง ห่างกันครั้งละ 1500 ms และคั่นด้วย cy_wcm_disconnect_ap() ทุกครั้ง คอมเมนต์เหนือฟังก์ชันบันทึกเหตุผลไว้: ในการบูตเย็น (cold boot) พูล RX buffer ของ WHD ยังมีไม่พอ WLC_E_SET_SSID จึงถูกทิ้ง และการเข้าร่วมครั้งแรกคืนค่า 0x020003FF ห้าม "แก้" ลูปนี้ให้เหลือครั้งเดียว

การบันทึกข้อมูลรับรอง — จุดที่สอง variant แยกทางกัน

เส้นทางการบันทึกสองเส้นเขียนไฟล์ /.wifi_creds ไฟล์เดียวกัน แต่จังหวะเวลาตรงข้ามกัน

mtb-only: เขียนทันที จาก WiFi worker

/* ...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. */
{
int n;
wifi_creds_lock();
n = (int)g_boot_wifi_creds_count;
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

คัดลอกเป็น snapshot ภายใต้ lock แล้วเขียน นอก lock — การเขียนแฟลชใช้เวลาระดับมิลลิวินาที และ BLE worker ใช้ตัวแปร global ชุดเดียวกันนี้ร่วมด้วย

mtb-mpy: เลื่อนไปให้ flusher ซึ่งทำงานเฉพาะตอน REPL ว่าง

บน mtb-mpy worker เขียนเองไม่ได้ เพราะ lfs_wifi_creds_write() ต้องอยู่ใน task context ของ MicroPython (sensor_auto_task.c:799) worker จึงเพียงปรับค่า g_boot_wifi_creds และตั้ง g_boot_wifi_creds_dirty ส่วนการเขียนจริงเกิดใน wifi_creds_flush_if_dirty() ที่ mpy_main.c:169-192 marker [mpy_lfs_wifi_creds_flush_if_dirty] (มีเฉพาะใน zip ของ mtb-mpy — ไฟล์นี้ไม่อยู่ในแพ็กเกจ mtb-only) ส่วนท้ายของฟังก์ชัน ได้แก่ การเขียน นโยบายของแฟล็ก และการปลด lock:

if (lfs_wifi_creds_write(g_boot_wifi_creds, count)) {
g_boot_wifi_creds_dirty = false;
} else {
/* Silently retry on next REPL iteration */
}
wifi_creds_unlock();
bool lfs_wifi_creds_write(const qspi_wifi_entry_t *entries, int count)
เขียนทับ /.wifi_creds ด้วย CRC32; ทำ snapshot ภายใต้ lock แล้วเขียนนอก lock

wifi_creds_flush_if_dirty() ถูกเรียกจาก 2 จุดเท่านั้นใน mpy_main.c คือต้นลูป REPL และขั้นตอนรื้อระบบก่อน soft reboot (:680, :699 ตามไฟล์ที่ส่งมอบจริง) ข้อสังเกต: ที่นี่การเขียนทั้งหมดอยู่ ภายใน lock ซึ่งเป็นนโยบายตรงข้ามกับเส้นทาง mtb-only ข้างต้น ทั้ง 2 แบบถูกต้องตามบริบทของตน

Warning
อันตราย (hazard) เรื่องข้อมูลรับรองสูญหายเมื่อ /main.py วนไม่รู้จบ (เฉพาะ mtb-mpy) หาก /main.py ทำงานไม่สิ้นสุด ไม่ว่าจะเป็นลูปอ่านเซนเซอร์ เกม หรือ while True: VM จะไม่กลับไปที่พรอมป์ REPL เลย flusher จึงไม่ได้ทำงาน และข้อมูลรับรองที่บันทึกจากหน้า WiFi บนจอ LCD จะไม่ถูกเขียนลงจริง บอร์ดเชื่อมต่อได้ในตอนนั้น แล้วลืมเครือข่ายนั้นในการตัดไฟแล้วจ่ายไฟใหม่ครั้งถัดไป mtb-only ไม่มีอันตรายข้อนี้เพราะเขียนทันที ถ้า /main.py ของผลิตภัณฑ์วนไม่รู้จบ ให้เลือกอย่างใดอย่างหนึ่ง: กลับไปที่ REPL เป็นระยะ หรือบันทึกข้อมูลรับรองจาก Python ด้วย wifi.connect() ซึ่งเขียนไฟล์โดยตรง (ดูกับดักท้ายบท) (ภาคผนวก X ข้อ 11)

ทีละขั้น

ขั้นที่ 1 — ใส่ข้อมูลรับรองลงในที่เก็บ LFS

เลือกทำ อย่างใดอย่างหนึ่ง ต่อไปนี้ และต้องรู้ว่าเพิ่งเขียนลงที่เก็บใด:

  • จากจอ LCD (ได้ทั้งสอง variant): Home → WiFi → scan → เลือกเครือข่าย → ใส่รหัสผ่าน → Connect วิธีนี้เขียนที่เก็บฝั่ง UI ของ OPTIGA (บท C2) และ ส่งรายการ LFS ต่อให้ worker บน CM33 เตรียมไว้
  • จาก Python (เฉพาะ mtb-mpy): import wifi; wifi.connect("ssid", "password") วิธีนี้เขียนที่เก็บ LFS โดยตรง โดยไม่จับ lock และไม่แตะ g_boot_wifi_creds (modwifi.c:261-285)

สิ่งที่ควรสังเกต

  • mtb-mpy เส้นทาง Python บน UART: [WiFi] Connecting to 's'... แล้วตามด้วย [WiFi] Connected! IP=lu.lu.lu.lu แล้ว [WiFi] Credentials saved to QSPI (d entries) (modwifi.c:243, :253, :285 — ไฟล์ที่ยังพิมพ์จริง ไม่ถูกปิดเสียง)
  • ทั้งสอง variant บนหน้าจอ: glyph WiFi บน topbar เลิกซ่อน ตัวขับคือ IPC_CMD_WIFI_STATE_PUSH จาก push_wifi_state_to_cm55(true) ของ worker และฝั่ง CM55 อ่านด้วย ipc_sensorhub_wifi_connected() พร้อมการตรวจจับขอบสัญญาณ (sensorhub_ui.c:114-124)
  • ทั้งสอง variant บนหน้าจอ ในอีกไม่กี่วินาทีถัดมา: นาฬิกาบน topbar ปรากฏขึ้นเมื่อ NTP ลงตัวแล้ว (ntp_sync_rtc()push_time_to_cm55()) จัดรูปแบบเป็น Thu 28 Aug 14:06 โดย sensorhub_ui.c:127-142

ขั้นที่ 2 — ตรวจให้แน่ว่าการเขียนถึงแฟลชจริง

  • mtb-only: ไม่ต้องทำอะไร การเขียนเกิดขึ้นแล้วใน worker
  • mtb-mpy: ถ้า /main.py กำลังทำงานอยู่ ให้หยุดแล้วกลับไปที่พรอมป์ REPL (กด Ctrl-C จากคอนโซลอนุกรม (serial console) หรือปล่อยให้จบเอง) flusher ทำงานที่ต้นลูป REPL และ ไม่มี บรรทัด UART สำหรับการ flush นี้ เพราะข้อความ [WiFiIPC] Credentials persisted ในไฟล์นั้นถูกคอมไพล์ทิ้งไป (ดูกับดัก)

สิ่งที่ควรสังเกต ไม่มีอะไรบน UART หลักฐานอยู่ที่ขั้นที่ 3

ขั้นที่ 3 — ตัดไฟแล้วจ่ายไฟใหม่ แล้วดูการเชื่อมต่ออัตโนมัติ

ถอดสายบอร์ดแล้วเสียบกลับ (การรีเซ็ตจากดีบักเกอร์ไม่ใช่การตัดไฟแล้วจ่ายไฟใหม่ — บท A1/A2) การเชื่อมต่ออัตโนมัติตอนบูตเริ่มทันทีที่ WiFi worker task ได้ทำงาน

สิ่งที่ควรสังเกต

  • mtb-only: [HB] t=lus tasks=u ทุก 10 s (proj_cm33_ns/main.c:107-109) เป็นหลักฐานว่า CM33_NS ยังจัดคิวงานอยู่ ภายในราว 8–20 s glyph WiFi บน topbar จะปรากฏตามด้วยนาฬิกา
  • mtb-mpy: [MPY] GC heap u KB @ p in s (mpy_main.c:552-554) เป็นหลักฐานว่า VM ขึ้นแล้ว WiFi worker รอให้ VM เติมค่าตัวแปร global อยู่ จากนั้นจึงเห็น glyph และนาฬิกาเช่นเดียวกัน
  • ทั้งสอง variant หากตัวที่สั่งเชื่อมต่อคือส่วนเชื่อม BLE/เดสก์ท็อปแทน (build ที่ตั้ง ENABLE_PAGE_BENTO_BUDDY=1 และมีการสลับคลื่นวิทยุจากเดสก์ท็อป บท I1 — เป็นเส้นทางเดียวที่เรียก app_wifi_connect_direct()): [wifi-glue] connecting SSID=s sec=d[wifi-glue] connected, IP=lu.lu.lu.lu หรือ [wifi-glue] attempt d/3 failed (0x%08lX) (wifi_init.c:228, :237, :244) ใน build ปริยายบรรทัดเหล่านี้ไม่พิมพ์เลย
  • ความล้มเหลวตอนเปิดใช้ฮาร์ดแวร์นั้นดังและไม่ถูกปิดเสียง: [WiFi] ERROR: SDIO interrupt init failed, [WiFi] ERROR: SDIO setup failed (0x%08lX), [WiFi] ERROR: Host wake interrupt init failed, [WiFi] ERROR: WCM init failed (0x%08lX) (wifi_init.c:84, :94-95, :122, :146-147)

หาก glyph ไม่ปรากฏเลยและไม่มีบรรทัด ERROR ใด ๆ พิมพ์ออกมา แสดงว่าที่เก็บ LFS ว่าง (ข้ามขั้นที่ 2 ไปบน mtb-mpy) หรือไม่พบ SSID นั้นในการสแกนแบบเจาะจง

ขั้นที่ 4 — พิสูจน์ว่าอ่านจากที่เก็บใด

ย้ายบอร์ด ตัวเดิม ข้าม variant: แฟลช mtb-only ทับอิมเมจ mtb-mpy ที่เคยบันทึกเครือข่ายไว้ บอร์ดจะเชื่อมต่อกลับได้ ที่เก็บฝั่ง C และที่เก็บฝั่ง VM อ่านและเขียนไบต์ชุดเดียวกัน (lfs_wifi_creds_c.c:18 — "so a board can move between variants and keep its saved networks") ส่วนรายการฝั่ง UI ของ OPTIGA ไม่ได้รับผลกระทบจากการแฟลชไม่ว่าทางใด เพราะอยู่ใน secure element (ชิปนิรภัยแยกส่วน)

กับดัก

กับดัก 1 — ทุกบรรทัด [WiFi-Boot] และ [WiFiIPC] ตายหมด
sensor_auto_task.c:36 นิยาม printf(...) ให้เป็น ((void)0) ทั้งไฟล์ ดังนั้น [WiFi-Boot] 2 saved network(s), [WiFi-Boot] 's' not found in scan — skipped, [WiFiIPC] Credentials persisted (d entries), [WiFiIPC] ERROR: credential save failed — kept dirty และบรรทัดพี่น้องทั้งหมดถูกคอมไพล์ทิ้งไป Tutorial หรือตั๋วสนับสนุนใดที่บอกให้เฝ้าดูบรรทัดเหล่านี้คือคำแนะนำที่ผิด หากต้องการเห็นระหว่างเปิดใช้ระบบของตนเอง ให้คอมเมนต์ sensor_auto_task.c:36 ทิ้ง สร้างใหม่ แล้วใส่กลับก่อนส่งมอบ — การปิดเสียงนี้มีอยู่เพราะการแย่งใช้ UART ทำให้เฟรมข้อมูลไบนารีเสียหาย
กับดัก 2 — wifi_saved_* ไม่ใช่ที่เก็บสำหรับบูต
wifi_saved_load(), wifi_saved_load_all(), wifi_saved_add() อยู่บน CM55 และคุยกับ slot ของ OPTIGA ไม่มีอะไรบนเส้นทางการบูตเรียกฟังก์ชันเหล่านี้ ถ้าผลิตภัณฑ์ "บันทึกเครือข่าย" ผ่าน wifi_saved_add() เพียงอย่างเดียว (เช่น จากหน้า CM55 ที่เขียนขึ้นเอง) บอร์ดจะแสดงเครือข่ายนั้นในรายการ และจะไม่เชื่อมต่ออัตโนมัติไปยังเครือข่ายนั้นเลย
กับดัก 3 — ผู้เขียน 2 รายไม่จับ lock
modwifi.c:261-285 (wifi.connect() ของ Python) และ wifi_init.c:261-302 (lfs_save_wifi_creds() ซึ่งเป็นส่วนเชื่อม BLE) ทำ read-modify-write กับ /.wifi_creds โดยตรง และไม่แตะ g_boot_wifi_creds เลย ทั้งสองยังคงปฏิบัติตามข้อกำหนดใน header (sensor_auto_task.h:92-95 — ให้จับ lock ทุกครั้งที่เข้าถึงตัวแปร global 3 ตัวนั้นแบบหลายไบต์) ด้วยการไม่แตะตัวแปร global แต่ยังเกิด race condition กับตัว ไฟล์ ได้อยู่ดีเมื่อมี lfs_wifi_creds_write() ทำงานพร้อมกัน ไม่มี lock ระดับไฟล์อยู่นอก bs_lock() ของ bento_storage.c และตัวนั้นมีเฉพาะบน mtb-only (ภาคผนวก X ข้อ 18)
กับดัก 4 — เมื่อเขียนล้มเหลว แฟล็ก dirty ถูกคงไว้โดยเจตนา
wifi_creds_flush_if_dirty() ล้าง g_boot_wifi_creds_dirty ต่อเมื่อ lfs_wifi_creds_write() คืนค่า true เท่านั้น การเขียนที่ล้มเหลวจะถูกลองใหม่ในรอบที่ REPL ว่างครั้งถัดไป ห้ามเพิ่มบรรทัดที่ "ล้างแฟล็กทิ้งไปเลย"
กับดัก 5 — lfs_wifi_creds_init() ต้องใช้ VM และ heap (mtb-mpy)
ฟังก์ชันนี้ต้องทำงานจาก MicroPython task หลังเมานต์ VFS แล้ว และโค้ดที่ส่งมอบเรียก gc_collect() ทันทีก่อนหน้านั้น บน mtb-only ชื่อเดียวกันนี้เป็น no-op (lfs_wifi_creds_c.c:60) เพราะ bento_storage_init() เป็นเจ้าของการเปิดใช้งานจาก main()

Variant

variant ที่ใช้ได้
mtb-mpy และ mtb-only
mtb-mpy mtb-only
ใครเรียก lfs_wifi_creds_read() ตอนบูต MicroPython task, mpy_main.c WiFi worker, sensor_auto_task.c:961
Implementation libbento_mpy.a (อาศัย VFS ของ Python) storage_c/lfs_wifi_creds_c.c
จังหวะการบันทึก เลื่อนไปที่ flusher — ตอน REPL ว่าง หรือตอน soft reboot เขียนทันทีใน worker
นโยบาย lock ขณะเขียน เขียนทั้งหมดภายใน wifi_creds_lock() คัดลอก snapshot ภายใน เขียนภายนอก
/main.py ที่วนไม่รู้จบทำให้การบันทึกหาย ใช่ ไม่เกี่ยวข้อง
ฟอร์แมตวอลุมเมื่อเมานต์ไม่สำเร็จ ใช่ (mpy_main.c, except: เปล่า → mkfs) ไม่เลย (bento_storage.c ปล่อยวอลุมไว้อย่างเดิม)
ข้อความ [WiFi] … จากเส้นทาง Python มี ไม่มี Python จึงไม่มีข้อความเหล่านี้
ข้อความ [wifi-glue] … คอมไพล์รวมไว้ แต่มีเฉพาะการเชื่อมต่อที่ขับด้วย BLE บน build ที่ตั้ง ENABLE_PAGE_BENTO_BUDDY=1 เท่านั้นที่ไปถึง เหมือนกัน

ฟังก์ชัน lfs_wifi_creds_* ทั้ง 6 ตัวปรากฏใน mpy_secure/api.txt แต่บน mtb-only ตัวที่ให้บริการคือซอร์ส C ที่ส่งมอบมา ไม่ใช่ archive เพราะ libbento_mpy.a ไม่ถูกลิงก์ภายใต้ BENTO_HAS_MPY=0 (proj_cm33_ns/Makefile:458-462)