SDK สำหรับ TESAIoT Dev Kit
คู่มืออ้างอิง API และ Tutorial (ModusToolbox)
Loading...
Searching...
No Matches
I2 — ส่วนที่เรียกใช้ได้ของโปรโตคอล NUS
variant ที่ใช้ได้
mtb-mpy และ mtb-only
Warning
สิ่งที่ต้องอ่านก่อนทั้งกลุ่ม — อ่านก่อนสิ่งอื่นใด โมดูล Bento Buddy / BLE ทั้งหมดคอมไพล์ออกจาก build ปริยาย: ENABLE_PAGE_BENTO_BUDDY ?= 0 (proj_cm33_ns/Makefile:64, :305) และบล็อก BLE ทั้งบล็อกรวมถึงข้อความ [boot] อยู่ภายใน #if ENABLE_PAGE_BENTO_BUDDY (main.c:36, :325) ขั้นแรกคือ make getlibs ใน proj_cm33_ns (ดึง btstack และเฟิร์มแวร์ BT) แล้วจึงสร้างใหม่ด้วย ENABLE_PAGE_BENTO_BUDDY=1 หากไลบรารีขาดหาย build จะหยุดที่ $(error) ของ Makefile ที่ :353-359 — ตัว error นั้นเองคือสิ่งที่สังเกตได้ ซึ่งหัวข้อนี้ใช้แสดงสถานะที่ยังไม่มีสิ่งที่ต้องมีก่อน: ENABLE_PAGE_BENTO_BUDDY=1 requires btstack-integration. Run 'make getlibs' to fetch the AIROC BLE host stack. ไม่มีสิ่งใดในบทนี้หรือใน I1 — การ bring-up BLE และกฎวิทยุเดียว ที่สังเกตได้บน build ปริยาย

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

ส่ง payload ที่ประกอบเป็นเฟรมแล้ว ปล่อย event ตอบรับคำสั่ง ข้ามไปยัง CM55 ผ่านสะพานหน่วยความจำร่วม และรู้ว่าส่วนใดของ API ที่ส่งออกมานั้นใช้งานได้จริงและส่วนใดตายแล้ว symbol ที่ส่งออก 77 จาก 87 ตัวไม่มีผู้เรียกใน template/ และมีไฟล์ .c ของ ble_nus เพียง 3 ไฟล์ที่ส่งมอบเป็นซอร์ส (bento_time.c, character_lottie.c, character_lottie_default.c) ที่เหลือทั้งหมดอยู่ใน libbento_secure.a ส่วนข้อความตัดตอน (excerpt) ที่กำกับว่ามาจาก archive นั้นคัดมาตามตัวอักษรจากซอร์สเหล่านั้น และแต่ละชิ้นระบุที่มาของตัวเองไว้ (file:lines — คอมไพล์เข้าไปใน libbento_secure.a ไม่ได้ส่งมอบเป็นซอร์ส)

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

การประกอบเฟรมเพื่อส่ง ตัวอย่างที่มีอยู่จริงในของที่ส่งมอบคือ ack ของการซิงก์เวลา: ผู้เรียกประกอบ JSON โดยปิดท้ายด้วย \n ตรวจขอบเขตของ snprintf ส่งความยาวไปอย่างชัดแจ้ง และแคสต์ผลลัพธ์เป็น (void):

/* ...context: inside the bento.time.sync ack emitter ... */
int w = snprintf(tx, sizeof(tx),
"{\"ack\":\"bento.time.sync\",\"ok\":true,\"n\":0,"
"\"synced\":true,\"boot_epoch_ms\":%s,\"uptime_ms_at_sync\":%s}\n",
boot_buf, up_buf);
if (w > 0 && w < (int)sizeof(tx)) {
(void)ble_nus_send((const uint8_t *)tx, (size_t)w);
}

ble_nus_send คืนค่า −1 เว้นแต่ init สำเร็จ สถานะเป็น CONNECTED และอุปกรณ์คู่เปิดการแจ้งเตือน TX แล้ว (ble_nus.c:878-885) การแบ่งชิ้นข้อมูลจัดการอยู่ภายใน (MTU−3 จำกัดสูงสุดที่ 180 B) ไลบรารีจองหน่วยความจำแล้วคัดลอกข้อมูลเอง บัฟเฟอร์บน stack จึงใช้ได้ (:893-905) stub ชนิด weak ที่ ble_nus.c:65 ทำให้อ้างถึง symbol นี้ได้อย่างปลอดภัยก่อน init จำนวนผู้เรียกจริงของมันคือราว 80 ขึ้นไปผ่านมาโคร NUS_SEND

การวนถามสถานะ เฟิร์มแวร์เลือกวนถามโดยเจตนา แทนที่จะไว้ใจ callback on_state — พบว่าเส้นทางกลับมาทำงานเร็วของคู่อุปกรณ์ที่ผูกไว้แล้วพลาด GATT_CONNECTION_STATUS_EVT (2026-05-10):

/* ...context: inside the sensor auto-push loop ... */
#if defined(BENTO_HAS_BLE_NUS) && (BENTO_HAS_BLE_NUS == 1)
/* BLE NUS host-link state poll (every 5th cycle = ~500ms).
* Why poll instead of using on_state callback: the callback
* registration window depends on the AIROC stack delivering
* GATT_CONNECTION_STATUS_EVT, which we observed was missing on
* the bonded-pair fast-resume path on 2026-05-10 — the desktop
* was actively serving fw.query verbs over the GATT link but
* ble_nus_get_state() still read ADVERTISING. Polling closes
* the loop deterministically: whatever the stack actually
* thinks the state is, the LCD topbar will reflect it within
* 500 ms of any change. */
if ((now_ms - last_ble_ms) >= 500u) {
last_ble_ms = now_ms;
static int8_t last_pushed_ble = -1; /* −1 = uninitialized */
int8_t now_connected = (ble_nus_get_state() == BLE_NUS_STATE_CONNECTED) ? 1 : 0;
if (now_connected != last_pushed_ble) {
sensor_auto_push_ble_state(now_connected != 0);
last_pushed_ble = now_connected;
}
}

ble_nus_get_state() เป็นการอ่านธรรมดาแบบไม่ใช้ lock ปลอดภัยจากทุก task และปลอดภัยก่อน init (stub ชนิด weak คืน BLE_NUS_STATE_OFF)

การปล่อย event nus_emit_event(json) รับ JSON ที่ปิดท้ายด้วย NUL และ ไม่มีการขึ้นบรรทัดใหม่ต่อท้าย (ตัวมันเติมให้เอง) ต้องมี strlen(json) + 2 <= 256 มิฉะนั้นคืน −1 แล้ว ทิ้ง ข้อมูลนั้นไม่มีการตัดสั้น ค่า −1 ยังครอบคลุมกรณี NULL และลิงก์ล่มด้วย ผู้เรียกที่มีอยู่จริงในของที่ส่งมอบ (mod_dualband.c:196-213 ซอร์สฝั่ง mtb-mpy ประกาศผ่าน extern ในไฟล์ที่ :44-46) จัดรูป {"evt":"bento.net.down","n":lu,"reason":"s"} ตรวจขอบเขตของ snprintf แล้วโยน OSError("nus_emit_event failed (d)") เมื่อค่าที่ได้ไม่เป็นศูนย์

การตอบรับคำสั่ง — ผู้เรียกที่เก็บอยู่ใน archive พร้อมการหนีบช่วงเวลาของ sensor_stream_start ไว้ที่ [10, 5000]:

ที่มา
ยกมาจาก nus_commands.c:770-784 (คอมไพล์รวมอยู่ใน archive สำเร็จรูป ไม่ได้ส่งมอบมาเป็นซอร์ส)

ตัวคำสั่งถูกตัดตายตัวที่ 32 อักขระ ack_cmd_len == 0 หมายถึงให้ใช้ strlen ค่า ok เป็นค่าที่คำนวณขึ้น (ถ้าเป็น NULL ทั้งคู่ ⇒ true ส่วน error ที่ไม่ใช่ NULL จะพลิกเป็น false และต่อท้ายด้วย "error") เป็น void — ความล้มเหลวในการส่งจึงมองไม่เห็น และทำงานบน task ที่กระจายงาน NUS RX ไม่ใช่ context ใดก็ได้

การข้ามไปยัง CM55 ipc_bento_buddy_send เขียนข้อความ .cy_shared_socmem แบบ static เพียงชุดเดียว — ไม่ reentrant ค่า data_len ถูกหนีบเป็น IPC_DATA_MAX_LEN อย่างเงียบ ๆ มีการลองใหม่แบบมีขอบเขต 50 × 1 ms จึง เรียกได้จาก task เท่านั้น ห้ามจาก ISR ให้ส่ง len+1 เพื่อให้ฝั่งรับได้ NUL ไปด้วย และได้จำนวนที่ไม่รวม NUL ใน value:

ที่มา
ยกมาจาก ipc_bento_buddy_bridge.c:43-70 (คอมไพล์รวมอยู่ใน archive สำเร็จรูป ไม่ได้ส่งมอบมาเป็นซอร์ส)
ที่มา
ยกมาจาก nus_agent.c:189-204 (คอมไพล์รวมอยู่ใน archive สำเร็จรูป ไม่ได้ส่งมอบมาเป็นซอร์ส)

สถานะวิทยุบนสายradio_mode_str คืนค่าเป็นสตริงคงที่แบบ static (คืน "invalid" เมื่ออยู่นอกช่วง s จึงปลอดภัยเสมอ) คำศัพท์ที่ใช้คือ unknown | ble_adv | ble_paired | switching_to_wifi | wifi_active | switching_to_ble | wifi_failed:

ที่มา
ยกมาจาก nus_commands.c:1443-1469 (คอมไพล์รวมอยู่ใน archive สำเร็จรูป ไม่ได้ส่งมอบมาเป็นซอร์ส)

Devmode ค่า secret อยู่ใน RAM เท่านั้นและสร้างใหม่ทุกครั้งที่บูต เครื่องเดสก์ท็อปจึงต้องจัดเตรียม (provisioning) ใหม่หลังการรีบูตทุกครั้ง bento_devmode_init เป็น idempotent และจุดเข้าใช้งานสาธารณะทุกจุดเรียกมันเอง:

ขอบเขตที่ challenge/response นี้ปกป้องได้จริง ขั้นตอน HMAC-SHA256 ถูกนำไปใช้อย่างถูกต้อง (nonce มี TTL ใช้ได้ครั้งเดียว และมีตัวจำกัดอัตราการลองผิด) แต่ตัว secret เองถูกส่งออกไปในรูป plaintext: bento_devmode_emit_provision() ส่งเฟรม {"evt":"bento.devmode.provision","secret":"<64 hex>"} ให้ peer ตัวแรกที่เชื่อมต่อสำเร็จ (ble_nus_lazy.c:99-109) ผ่าน TX ที่ยังไม่ได้เข้ารหัส

นอกจากนี้ secret มาจาก xorshift32 ที่ seed ด้วย tick ของ FreeRTOS, แอดเดรสบน stack และค่าคงที่หนึ่งค่า (bento_devmode.c:54-66) จึงเป็นเอนโทรปีระดับต่ำกว่า 32 บิต ไม่ใช่ค่าจาก RNG ของ OPTIGA ผลรวมคือ gate นี้กัน peer ที่มาทีหลังได้ แต่กัน peer ที่เชื่อมต่อเป็นรายแรกไม่ได้ ให้ถือว่าเป็นเครื่องกีดขวาง ไม่ใช่ขอบเขตการบังคับใช้สิทธิ์

ที่มา
ยกมาจาก bento_devmode.c:90-108 (คอมไพล์รวมอยู่ใน archive สำเร็จรูป ไม่ได้ส่งมอบมาเป็นซอร์ส)

สถานะของ agent คือคำถามที่กำลังดำเนินอยู่เพียงรายการเดียว ซึ่ง NUS RX task เดียวเป็นเจ้าของ:

ที่มา
ยกมาจาก nus_agent.c:115-133 (คอมไพล์รวมอยู่ใน archive สำเร็จรูป ไม่ได้ส่งมอบมาเป็นซอร์ส)

ทีละขั้น

ขั้นที่ 1 — จับคู่แล้วเฝ้าดูแถบบน

เชื่อมต่อจาก Bento Desktop Buddy การโฆษณา (advertising) เปิดรับ central ทุกตัว จึงเชื่อมต่อได้โดยไม่ต้องจับคู่ก่อน หากเครื่องเดสก์ท็อปขอจับคู่ จะเป็นแบบ Just Works (BTM_IO_CAPABILITIES_NONE + BTM_LE_AUTH_REQ_SC_BOND ไม่มีบิต MITM) ไม่มี passkey แสดงบนหน้าจอ และไม่มีการป้องกัน MITM ส่วนหน้าจอ passkey 6 หลักแบบ DisplayOnly นั้นยังไม่ได้พัฒนา

สิ่งที่ควรสังเกต glyph BLE บนแถบบนเปลี่ยนภายใน 500 ms หลังการเชื่อมต่อ — นั่นคือจังหวะการวนถามที่กล่าวไว้ข้างต้น

การจับคู่เป็นทางเลือก ไม่ใช่ข้อบังคับ ฐานข้อมูล GATT ที่ส่งมอบจริงไม่ได้ตั้งบิต GATTDB_PERM_AUTH_* บน RX, TX หรือ CCCD เลย (ตรวจใน libbento_secure.a สมาชิก bento_core_11.o เมื่อ 2026-08-29 ดู ข้อมูล GATT) อุปกรณ์คู่ที่ยังไม่ได้ผูกจึงเขียน RX และเปิด notification ได้ตามปกติ และจะไม่ได้รับ GATT_INSUF_AUTHENTICATION ให้ปฏิบัติต่อ link นี้เสมือนยังไม่ผ่านการพิสูจน์ตัวตน

ขั้นที่ 2 — ack ของการซิงก์เวลา

ให้เครื่องเดสก์ท็อปส่ง bento.time.sync

สิ่งที่ควรสังเกต บนเครื่องเดสก์ท็อป: บรรทัด JSON {"ack":"bento.time.sync","ok":true,"n":0,"synced":true,"boot_epoch_ms":…,"uptime_ms_at_sync":…} — เป็นเฟรมที่ snippet สร้างขึ้นพอดี

ขั้นที่ 3 — event สถานะวิทยุ

กระตุ้นให้เปลี่ยนโหมด (bento_buddy.stop() บน mtb-mpy หรือปุ่มบนหน้า CM55)

สิ่งที่ควรสังเกต บนเครื่องเดสก์ท็อป: {"event":"bento.radio.state","mode":"…","ssid":"…","ipv4":"…","ble_paired":…,"wifi_fail_count":…} โดย mode มาจากคำศัพท์ที่ระบุไว้ข้างต้น

ขั้นที่ 4 — สตรีมและ ack

ส่ง bento.sensor.stream.start พร้อม id ที่ไม่รู้จัก

สิ่งที่ควรสังเกต ack ที่มี "ok":false,"error":"unknown_id" ส่วนกรณีที่ id ถูกต้องและ interval_ms อยู่นอกช่วง [10, 5000] สตรีมจะทำงานที่ช่วงเวลาที่หนีบไว้แล้ว

ส่วนที่ส่งออกมาแล้วตาย — 13 รายการ ตัวอย่างเขียนขึ้นเอง

รายการต่อไปนี้ส่งออกจาก libbento_secure.a โดย ไม่มีผู้เรียกในซอร์สทั้ง 2 ชุด ตัวอย่างของแต่ละรายการเขียนขึ้นเองเพื่อเอกสารนี้ (ไม่มี call site ในของที่ส่งมอบ) และกำกับไว้เช่นนั้น ได้แก่ NUS_UUID_CHAR_TX, NUS_UUID_CHAR_RX (ปรากฏเป็นไบต์ดิบภายใน nus_gatt_database[] เท่านั้น, nus_gatt_db.c:83), voice_capture_is_running, radio_scheduler_set_boot_mode, radio_scheduler_get_boot_mode, nus_protocol_tick, nus_protocol_send_permission, nus_fp_is_active, nus_events_pending_ack_count, nus_agent_note_ask (ตัวฟังก์ชันอยู่ที่ nus_agent.c:122-133 ซึ่งเป็นนิยาม ไม่ใช่การเรียก), nus_agent_buffer_len, fw_hash_prefix8, bento_buddy_auto_start_install (ถูกแทนที่ไปแล้ว main.c:344-347) หน้าใดที่แสดงรายการเหล่านี้เป็นส่วนหนึ่งของเส้นทางโปรโตคอลที่ส่งมอบจริงคือหน้าที่ผิด

symbol 5 ตัวใน overridable.txt เป็นกรณีตรงข้าม — เป็น hook ชนิด WEAK ที่ผู้ใช้ไลบรารี ต้องนิยามเอง และนิยามไว้ใน wifi_init.c: :193 app_wifi_connect_direct, :248 app_wifi_disconnect, :256 app_wifi_get_ipv4, :261 lfs_save_wifi_creds, :304 lfs_load_wifi_creds:

int lfs_save_wifi_creds(const char *ssid, const char *password, const char *security)
{
if (!lfs_wifi_creds_ready()) return -1;
qspi_wifi_entry_t saved[QSPI_WIFI_CREDS_MAX];
int count = lfs_wifi_creds_read(saved, QSPI_WIFI_CREDS_MAX);
if (count < 0) count = 0;
/* Upsert: find existing slot for this SSID, else append (LRU eviction
* when full). */
int idx = -1;
for (int i = 0; i < count; i++) {
if (strncmp(saved[i].ssid, ssid, sizeof(saved[i].ssid)) == 0) {
idx = i;
break;
}
}
if (idx < 0) {
idx = (count < QSPI_WIFI_CREDS_MAX) ? count : 0;
if (count < QSPI_WIFI_CREDS_MAX) count++;
}
memset(&saved[idx], 0, sizeof(qspi_wifi_entry_t));
strncpy(saved[idx].ssid, ssid, sizeof(saved[idx].ssid) - 1);
if (password != NULL) {
strncpy(saved[idx].password, password, sizeof(saved[idx].password) - 1);
}
/* security field is uint8_t — store the low byte of the cy_wcm enum so
* we stay binary-compatible with modwifi.c's saved entries (which also
* writes the truncated enum). The low byte distinguishes the common
* cases: 0=OPEN, 4=WPA2_AES (also matches WPA3_SAE's low byte — WPA3
* is treated as WPA2 on the load path for now; explicit WPA3 storage
* requires bumping the struct format). */
bool is_open = (security != NULL && strcmp(security, "OPEN") == 0)
|| (password == NULL || password[0] == '\0');
saved[idx].security = is_open
? (uint8_t)(CY_WCM_SECURITY_OPEN & 0xFFu)
: (uint8_t)(CY_WCM_SECURITY_WPA2_AES_PSK & 0xFFu);
saved[idx].flags = 0x01; /* auto_connect */
return lfs_wifi_creds_write(saved, count) ? 0 : -2;
}

กับดัก

Warning
ผู้เรียก ipc_bento_buddy_send 2 รายพร้อมกันทำให้ข้อมูลของกันและกันเสียหาย — เพราะมีข้อความร่วมเพียงชุดเดียว
nus_emit_event ทิ้ง payload ที่ใหญ่เกินไปอย่างเงียบ ๆ โดยไม่ส่งออกสาย ให้ตรวจค่าที่มันคืนมา
lfs_save_wifi_creds ไม่จับ wifi_creds_lock — มันทำ read-modify-write ลงไฟล์ LFS โดยตรง และชิงกับ lfs_wifi_creds_write ที่ทำงานพร้อมกันได้ (ภาคผนวก X #18)
การเรียก ble_nus_init จาก task อื่นที่ไม่ใช่ task ที่จ่ายไฟให้ชิป ล้มเหลวด้วย state=ERROR

กล่อง variant

mtb-mpy mtb-only
handler ของโปรโตคอล เหมือนกัน (อยู่ใน archive) เหมือนกัน
ผู้เรียก nus_emit_event ที่แสดงไว้ mod_dualband.c (ส่งมอบในแพ็กเกจนี้) ไม่ได้ส่งมอบ แต่ข้อกำหนดการเรียกใช้เหมือนกัน
การดันสถานะขึ้นแถบบน sensor_auto_task.c:1440-1459 ไฟล์เดียวกันนี้ส่งมอบในทั้ง 2 แพ็กเกจ