เป้าหมายของหัวข้อนี้
3 เรื่องที่ลูกค้าซึ่งจะต่อยอดเส้นทางฝั่ง cloud ต้องรู้:
- การตั้งค่าเป็น ไฟล์ key=value ที่อ่านตอนทำงานบน LittleFS ไม่ใช่ Kconfig และไม่ใช่ #define — และหนึ่งในฟิลด์ของไฟล์นี้คือ port ซึ่งถูกเก็บไว้แต่ ไม่เคยถูกใช้ กับ MQTT เลย พอร์ตมาจาก tls_mode
- MQTT task ถูกสร้าง แบบเลื่อนเวลา (lazy) เมื่อมีคำขอเชื่อมต่อครั้งแรก ไม่ใช่ตอนบูต
- ส่วนของ topic ที่จะขยายต่อคือชุดมาโครรูปแบบข้อความ บวกกับตัวแยกเส้นทางตาม suffix (suffix router) ที่ทำงานบน thread ของเหตุการณ์ MQTT
ลำดับการทำงานจริงของเฟิร์มแวร์
Config: /.tesaiot_config
TESAIOT_CONFIG_FILE_PATH มีค่าเป็น "/.tesaiot_config" (tesaiot_config_defaults.h:83) ค่าเริ่มต้น: tls_mode = TESAIOT_MODE_SERVER_TLS, broker mqtt.tesaiot.dev, port 8884 (:18, :36-37) มี backend สำหรับ I/O 2 แบบแต่มี parser เดียว — bento_storage_read_file() บน mtb-only และ open() ของ Python บน mtb-mpy — เลือกด้วย BENTO_HAS_MPY (tesaiot_config_store.c:38-54)
bool tesaiot_config_init(void)
{
if (s_initialized) return true;
s_mutex = xSemaphoreCreateMutexStatic(&s_mutex_buf);
config_set_defaults(&s_config);
s_initialized = true;
if (!tesaiot_config_load()) {
printf("[TESAIOT_CFG] no config file, creating defaults\r\n");
tesaiot_config_save();
}
return true;
}
ข้อสังเกตเรื่องลำดับการโหลด: บน mtb-only tesaiot_config_init() ทำงานจาก main() ก่อน ipc_tesaiot_handler_init() ส่วนบน mtb-mpy จะทำงานภายใน task ของ VM หลังจากนั้น ซึ่งเป็นเหตุผลที่ mpy_main.c เรียก ipc_tesaiot_refresh_status() ตามหลัง บท B1 อธิบายทางแยกนี้ไว้
จุดเริ่มสามทาง แต่ปลายทางฟังก์ชันเดียว
ทั้งสามทางไปถึง tesaiot_mqtt_connect() (tesaiot_mqtt.c:83-103) ได้แก่ tesaiot.connect() ของ Python (modtesaiot.c:692-695) ปุ่ม Connect ของหน้า TESAIoT บน CM55 ผ่าน IPC_CMD_TESAIOT_CONNECT (ipc_tesaiot_handler.c:303-307) และ worker ที่ทำ provisioning ของ HSM ก่อนที่จะแตะชิป (บท D2) tesaiot_mqtt_connect() ส่งสถานะ TESAIOT_CONN_CONNECTING ไปยังหน้าจอ แล้วเรียก mqtt_request_start()
การสร้าง task แบบเลื่อนเวลา
bool mqtt_request_start(void)
{
if (s_mqtt_task_handle == NULL) {
BaseType_t r = xTaskCreate(
mqtt_client_task, "MQTT",
MQTT_CLIENT_TASK_STACK_SIZE,
NULL, MQTT_CLIENT_TASK_PRIORITY, &s_mqtt_task_handle);
if (r != pdPASS) {
printf("[MQTT] Task creation failed\n");
return false;
}
for (int i = 0; i < 20 && mqtt_control_events == NULL; i++) {
vTaskDelay(pdMS_TO_TICKS(50));
}
}
if (mqtt_control_events == NULL) return false;
if (mqtt_start_requested) return true;
mqtt_start_requested = true;
(void)xEventGroupSetBits(mqtt_control_events, MQTT_CTRL_START_BIT);
return true;
}
การสร้าง task ตั้งแต่ตอนบูตถูกถอดออก เพราะ stack ของ task นั้นแย่งทรัพยากรกับ cy_wcm_init() (mqtt_task.c:100-102) ตัว task รอ WiFi (นานสุด 60 s โดยวนถาม app_wifi_is_ready() — ไม่ใช่ cy_wcm_is_connected_to_ap() ซึ่ง HardFault ก่อน WCM จะเริ่มทำงาน) จากนั้นรอบิตสั่งเริ่ม แล้วจึง mqtt_init() → mqtt_connect()
จุดที่ตัดสินว่า host พอร์ต และ client-id คืออะไร
switch (cfg.tls_mode) {
case TESAIOT_MODE_MTLS:
case TESAIOT_MODE_MTLS_SW:
broker_info.port = 8883;
break;
case TESAIOT_MODE_SERVER_TLS:
default:
broker_info.port = 8884;
break;
}
ให้อ่าน switch นั้น 2 รอบ cfg.port ไม่ถูกนำมาพิจารณา เลย โหมด 0/1 (mTLS และ mTLS แบบซอฟต์แวร์) → 8883 ส่วนโหมด 2 (server TLS) และค่าอื่นใด → 8884 คอมเมนต์ในโค้ดระบุไว้ตรง ๆ ว่า config stores only one port field และ header ของ config ก็ตรงกัน — tesaiot_config_defaults.h:34-35: "Port stays mode-driven … this default only applies to the latter."
สิ่งที่ตัดสินที่นี่ด้วยเช่นกัน: SNI ย้อนกลับไปใช้ชื่อ broker เมื่อไม่ได้ระบุ ส่วน client-id คือ factory_uid (UID ฮาร์ดแวร์ของ Trust M) ในโหมด mTLS และเป็น device_id ในกรณีอื่น (mqtt_client_config.c:137-153) root CA คือ TESAIOT_ROOT_CA_CERTIFICATE และ ALPN ไม่ถูกใช้อย่างชัดแจ้ง ที่ท้ายฟังก์ชัน เฉพาะในโหมด mTLS เท่านั้น จะส่งงานต่อให้ mqtt_mtls_setup_optiga() — บท C4
Connect
printf("[MQTT] Connecting to '%s:%u' as '%s'...\n",
broker_info.hostname, broker_info.port,
connection_info.client_id);
printf("[MQTT] root_ca=%p size=%u flags=0x%lX\n",
security_info ? security_info->root_ca : NULL,
security_info ? (unsigned)security_info->root_ca_size : 0,
(unsigned long)status_flag);
for (uint32_t retry = 0; retry < cfg.max_retries; retry++) {
if (!app_wifi_is_ready()) {
printf("[MQTT] WiFi disconnected — waiting for reconnect...\n");
for (int w = 0; w < 100 && !app_wifi_is_ready(); w++) {
vTaskDelay(pdMS_TO_TICKS(500));
}
if (!app_wifi_is_ready()) {
printf("[MQTT] WiFi reconnect timeout\n");
return ~CY_RSLT_SUCCESS;
}
}
printf("[MQTT] cy_mqtt_connect attempt %lu...\n", (unsigned long)(retry + 1));
result = cy_mqtt_connect(mqtt_connection, &connection_info);
printf("[MQTT] cy_mqtt_connect returned: 0x%08X\n", (unsigned int)result);
if (result == CY_RSLT_SUCCESS) {
printf("[MQTT] Connected to broker\n");
ลองใหม่ได้สูงสุด cfg.max_retries ครั้ง โดยตรวจ app_wifi_is_ready() ซ้ำในทุกรอบ เมื่อสำเร็จ โค้ดเรียก tesaiot_bridge_mqtt_connected() โดยตรง แทนที่จะรอให้ task ของ config มาวนถาม เพราะ task นั้นทำงานที่ priority 1 ซึ่งต่ำกว่า 2 ของ MQTT และอาจไม่ได้ CPU เลย (mqtt_task.c:359-362)
ส่วนของ topic และตัวแยกเส้นทางตาม suffix
มาโครรูปแบบข้อความ (mqtt_client_config.h:84-91):
MQTT_TOPIC_FMT_TELEMETRY "device/%s/telemetry"
MQTT_TOPIC_FMT_TELEMETRY_SENSOR "device/%s/telemetry/sensor"
MQTT_TOPIC_FMT_COMMANDS "device/%s/commands"
MQTT_TOPIC_FMT_COMMANDS_WILDCARD "device/%s/commands/#"
MQTT_TOPIC_FMT_COMMAND_CSR "device/%s/commands/csr"
MQTT_TOPIC_FMT_COMMAND_REQUEST "device/%s/commands/request"
MQTT_TOPIC_FMT_COMMAND_STATUS "device/%s/commands/status"
MQTT_TOPIC_FMT_COMMAND_ACK "device/%s/commands/ack"
ฝั่งผู้รับสมัครสมาชิก subscribe ที่ device/<device_id>/commands/# เพียงครั้งเดียว แล้วแยกเส้นทางข้อความขาเข้าตาม suffix บน thread ของเหตุการณ์ MQTT:
static const char REQ_SUFFIX[] = "/commands/request";
static const char CSR_SUFFIX[] = "/commands/csr";
static const char PU_SUFFIX[] = "/commands/protected_update";
static const char ST_SUFFIX[] = "/commands/status";
static const char CT_SUFFIX[] = "/commands/certificate";
if (topic != NULL) {
if ((tlen >= sizeof(REQ_SUFFIX) - 1 &&
0 == strncmp(topic + tlen - (sizeof(REQ_SUFFIX) - 1),
REQ_SUFFIX, sizeof(REQ_SUFFIX) - 1)) ||
(tlen >= sizeof(CSR_SUFFIX) - 1 &&
0 == strncmp(topic + tlen - (sizeof(CSR_SUFFIX) - 1),
CSR_SUFFIX, sizeof(CSR_SUFFIX) - 1))) {
vPortFree(data_copy);
return;
}
if (tlen >= sizeof(PU_SUFFIX) - 1 &&
0 == strncmp(topic + tlen - (sizeof(PU_SUFFIX) - 1),
PU_SUFFIX, sizeof(PU_SUFFIX) - 1)) {
cmd = HANDLE_PROTECTED_UPDATE_BUNDLE;
} else if (tlen >= sizeof(CT_SUFFIX) - 1 &&
0 == strncmp(topic + tlen - (sizeof(CT_SUFFIX) - 1),
CT_SUFFIX, sizeof(CT_SUFFIX) - 1)) {
cmd = HANDLE_DEVICE_CERTIFICATE;
} else if (tlen >= sizeof(ST_SUFFIX) - 1 &&
0 == strncmp(topic + tlen - (sizeof(ST_SUFFIX) - 1),
ST_SUFFIX, sizeof(ST_SUFFIX) - 1)) {
cmd = HANDLE_COMMAND_STATUS;
การเพิ่มคำสั่งใหม่ทำได้โดยเพิ่ม suffix ที่นี่และเพิ่ม case ใน handler ส่วน handler ปริยายคือบรรทัด (printf)("[Subscriber] %.*ss\n", …) ที่ subscriber_task.c:186 — สังเกตรูปแบบการเรียกที่มีวงเล็บครอบชื่อ ซึ่งเป็นเหตุผล เดียว ที่ทำให้บรรทัดนั้นพิมพ์ออกมาได้ (ดูกับดัก)
Publish
bool tesaiot_mqtt_connect(void)
{
if (mqtt_is_connected()) {
return true;
}
printf("[TESAIoT-MQTT] Requesting connection...\n");
tesaiot_bridge_mqtt(TESAIOT_CONN_CONNECTING);
if (!mqtt_request_start()) {
printf("[TESAIoT-MQTT] Failed to request start\n");
tesaiot_bridge_mqtt(TESAIOT_CONN_FAILED);
return false;
}
printf("[TESAIoT-MQTT] MQTT task started (async)\n");
return true;
}
bool tesaiot_mqtt_disconnect(void)
{
extern bool mqtt_request_stop(void);
if (!mqtt_is_connected()) {
return true;
}
printf("[TESAIoT-MQTT] Disconnecting...\n");
bool ok = mqtt_request_stop();
if (ok) {
tesaiot_bridge_mqtt(TESAIOT_CONN_OFF);
printf("[TESAIoT-MQTT] Disconnected\n");
} else {
printf("[TESAIoT-MQTT] Disconnect failed\n");
}
return ok;
}
bool tesaiot_mqtt_publish(const char *topic, const char *payload, size_t payload_len)
{
extern QueueHandle_t publisher_task_q;
if (!mqtt_is_connected()) {
printf("[TESAIoT-MQTT] Cannot publish: not connected\n");
return false;
}
if (publisher_task_q == NULL) {
printf("[TESAIoT-MQTT] Cannot publish: publisher queue not ready\n");
return false;
}
publisher_data_t msg;
memset(&msg, 0, sizeof(msg));
msg.cmd = PUBLISH_MQTT_MSG;
if (topic != NULL && topic[0] != '\0') {
size_t tlen = strlen(topic);
if (tlen >= sizeof(msg.topic)) {
printf("[TESAIoT-MQTT] Topic too long (%u >= %u)\n",
(unsigned)tlen, (unsigned)sizeof(msg.topic));
return false;
}
memcpy(msg.topic, topic, tlen + 1);
msg.topic_len = tlen;
} else {
const tesaiot_config_t *cfg = tesaiot_config_get_ptr();
if (!cfg || cfg->device_id[0] == '\0') {
printf("[TESAIoT-MQTT] No device_id configured\n");
return false;
}
int n = snprintf(msg.topic, sizeof(msg.topic),
TESAIOT_TOPIC_FMT_TELEMETRY, cfg->device_id);
if (n < 0 || (size_t)n >= sizeof(msg.topic)) {
printf("[TESAIoT-MQTT] Topic build failed\n");
return false;
}
msg.topic_len = (size_t)n;
}
if (payload != NULL && payload_len > 0) {
char *payload_copy = pvPortMalloc(payload_len);
if (payload_copy == NULL) {
printf("[TESAIoT-MQTT] Payload alloc failed (%u bytes)\n",
(unsigned)payload_len);
return false;
}
memcpy(payload_copy, payload, payload_len);
msg.data = payload_copy;
msg.payload_len = payload_len;
msg.free_after_publish = true;
} else {
msg.data = NULL;
msg.payload_len = 0;
msg.free_after_publish = false;
}
msg.max_attempts = 3;
msg.retry_delay_ticks = pdMS_TO_TICKS(1000);
if (xQueueSend(publisher_task_q, &msg, 0) != pdTRUE) {
tesaiot_mqtt_publish(topic, payload, len) ใช้ค่าปริยายของ topic เป็น device/<device_id>/telemetry และส่งเข้าคิว publisher_task_q ด้วย timeout เป็นศูนย์ ดังนั้นคิวเต็มเมื่อใดข้อความจะถูกทิ้ง task ของผู้เผยแพร่เป็นตัวระบายคิว:
void publisher_task(void *pvParameters)
{
publisher_data_t publisher_q_data;
(void)pvParameters;
publisher_task_q = xQueueCreate(PUBLISHER_TASK_QUEUE_LENGTH,
sizeof(publisher_data_t));
printf("[Publisher] Task started\n");
while (true) {
(printf)("[PUB-TASK] stack_free=%u words\n",
(unsigned)uxTaskGetStackHighWaterMark(NULL));
if (pdTRUE == xQueueReceive(publisher_task_q, &publisher_q_data,
portMAX_DELAY)) {
switch (publisher_q_data.cmd) {
case PUBLISH_MQTT_MSG: {
publish_info.topic = publisher_q_data.topic;
publish_info.topic_len = publisher_q_data.topic_len;
publish_info.payload = publisher_q_data.data;
publish_info.payload_len = publisher_q_data.payload_len;
cy_rslt_t result = ~CY_RSLT_SUCCESS;
uint8_t max_retries = publisher_q_data.max_attempts > 0 ?
publisher_q_data.max_attempts :
PUBLISH_RETRY_LIMIT;
for (uint8_t retry = 0; retry < max_retries; retry++) {
result = cy_mqtt_publish(mqtt_connection, &publish_info);
if (result == CY_RSLT_SUCCESS) {
#if TESAIOT_DEBUG_PUBLISHER_ENABLED
printf("[Publisher] Published to %.*s (%u bytes)\n",
(int)publish_info.topic_len,
publish_info.topic,
(unsigned)publish_info.payload_len);
จากฝั่ง MicroPython: tesaiot.publish() (modtesaiot.c:772)
ทีละขั้น
ขั้นที่ 1 — เขียนไฟล์ config
สร้าง /.tesaiot_config โดยอย่างน้อยต้องมี tls_mode=2, broker=…, device_id=… และข้อมูลรับรองที่แพลตฟอร์มออกให้ บน mtb-mpy เข้าถึงไฟล์นี้ได้จาก REPL (open('/.tesaiot_config','w')) ส่วนบน mtb-only ให้เขียนจากหน้าตั้งค่า TESAIoT บน CM55 หรือใส่มาพร้อมระบบไฟล์ในอิมเมจ
สิ่งที่ควรสังเกต ไม่มีอะไรบน UART ข้อความ [TESAIOT_CFG] loaded: … มีอยู่ในซอร์สแต่ถูกคอมไพล์ทิ้งไป (tesaiot_config_store.c:26) บน mtb-only สัญญาณเชิงบวกคือการ ไม่ปรากฏ ของ ERROR: tesaiot_config_init failed ตอนบูต (proj_cm33_ns/main.c:294-296)
ขั้นที่ 2 — เชื่อมต่อ WiFi
บท C1 หรือ C2 MQTT task จะไม่เดินหน้าต่อหากไม่มีการเชื่อมต่อ
สิ่งที่ควรสังเกต glyph WiFi บน topbar แล้วตามด้วยนาฬิกา
ขั้นที่ 3 — ขอให้เชื่อมต่อ
เรียก tesaiot.connect() จาก REPL หรือแตะปุ่ม Connect บนหน้า TESAIoT
สิ่งที่ควรสังเกต บน UART ตามลำดับนี้ — พิมพ์จริงทุกบรรทัด (การปิดเสียงใน mqtt_task.c:39 ถูกคอมเมนต์ทิ้งไว้ ส่วน mqtt_client_config.c ไม่มีการปิดเสียง):
[MQTT] Waiting for WiFi...
[MQTT] WiFi connected
[MQTT] Waiting for start request...
[MQTT] Start request received
[MQTT-Config] Mode=%d, Broker=%s:%u, Client=%s, User=%s, PassLen=%u
[MQTT] Instance created
[MQTT] Connecting to '%s:%u' as '%s'...
[MQTT] Connected to broker
ให้อ่านค่า Broker=s:u ในบรรทัด [MQTT-Config]: เมื่อ tls_mode=2 ค่าจะเป็น :8884 เสมอ ไม่ว่า port= ในไฟล์จะเขียนไว้อย่างไร นั่นคือกฎที่ว่า cfg.port ไม่ถูกใช้ ซึ่งมองเห็นได้ตรงนี้
บนหน้าจอ: สถานะของหน้า TESAIoT เปลี่ยนเป็นเชื่อมต่อแล้ว และแสดง URL ของ broker — tesaiot_bridge_mqtt_connected() ตั้งค่า mqtt_state และ broker_url (tesaiot_mqtt.c:74-77; ในโหมด mTLS จะตั้ง optiga_state และ cert_state ด้วย :60-64)
รูปแบบความล้มเหลว ซึ่งพิมพ์จริงเช่นกัน: [MQTT] Connect failed: 0x%08X (retry lu/u), [MQTT] Exceeded max retries (u), [MQTT] WiFi not ready after 60s — aborting, [MQTT] Configuration failed — not attempting to connect
ขั้นที่ 4 — พิสูจน์การ publish ตั้งแต่ต้นจนจบ
จากเครื่องที่เข้าถึง broker ได้ ให้ subscribe ก่อนแล้วจึง publish:
mosquitto_sub -h <broker> -p 8884 --cafile <tesaiot-root-ca.pem> \
-u <user> -P <pass> -t 'device/+/telemetry' -v
จากนั้นเรียก tesaiot.publish('{"hello":1}') จาก REPL หรือสั่งจากหน้าจอ
สิ่งที่ควรสังเกต payload มาถึงที่ device/<device_id>/telemetry ใน mosquitto_sub นั่นคือหลักฐาน ห้ามรอบรรทัด [Publisher] Published to … บน UART เพราะไม่มีวันพิมพ์ (ดูกับดัก) บรรทัดเดียวใน publisher_task.c ที่พิมพ์จริงคือข้อมูลวินิจฉัยของ stack (printf)("[PUB-TASK] stack_free=u words\n", …) ที่ :77
ขั้นที่ 5 — พิสูจน์การ subscribe
publish จากเครื่อง host ไปยัง topic ฝั่งคำสั่ง:
mosquitto_pub -h <broker> -p 8884 --cafile <tesaiot-root-ca.pem> \
-u <user> -P <pass> -t 'device/<device_id>/commands/anything' -m 'ping'
สิ่งที่ควรสังเกต [Subscriber] %.*ss บน UART ซึ่งมาจาก handler ปริยายที่ subscriber_task.c:186 ที่ใช้รูปแบบ (printf) จึงพิมพ์จริง หากแยกเส้นทาง suffix ใหม่ไปยัง handler ของตนเอง ให้พิมพ์ผ่าน (printf) เช่นกัน มิฉะนั้นบรรทัดนั้นจะหายไปใต้การปิดเสียงของไฟล์
กับดัก
- กับดัก 1 — การแก้ port= ใน /.tesaiot_config ไม่เปลี่ยนอะไรเลยสำหรับ MQTT
- mqtt_client_config.c อนุมานพอร์ตจาก tls_mode หาก broker ของตนฟังที่พอร์ตนอกมาตรฐาน ต้องแก้ที่ switch เพราะไม่มีเส้นทางตั้งค่าใด ๆ ให้ (ภาคผนวก X ข้อ 15)
- กับดัก 2 — [Subscriber] Subscribing to: และ [Subscriber] Subscribed (QoS…) ตายแล้ว
- subscriber_task.c:46 ปิดเสียง printf แบบธรรมดาทั้งไฟล์ และ 2 บรรทัดนั้น (:98, :104) เป็น printf ธรรมดา จึงถูกคอมไพล์ทิ้ง บรรทัดที่ พิมพ์จริง ในไฟล์นั้นคือการเรียกแบบมีวงเล็บครอบ (printf)(…) ที่ :169, :186, :205, :211, :217, :223, :229 เพราะมาโครที่มีรูปแบบเหมือนฟังก์ชันไม่จับคู่กับชื่อที่มีวงเล็บครอบ เรื่องเดียวกันนี้ใช้กับ [Publisher] Published to … ด้วย (publisher_task.c:100 ภายใต้การปิดเสียงที่ :28) Tutorial ใดที่ยกข้อความเหล่านี้เป็นหมุดหมายคือคำแนะนำที่ผิด
- กับดัก 3 — [TESAIOT_IPC] connect requested ตายแล้ว
- ipc_tesaiot_handler.c:26 ปิดเสียงไฟล์นั้น สิ่งเดียวที่สังเกตได้จากปุ่ม Connect คือลำดับข้อความ [MQTT] ที่มันทำให้เกิดขึ้น
- กับดัก 4 — cy_wcm_is_connected_to_ap() ก่อน WCM เริ่มทำงานทำให้เกิด HardFault
- MQTT task จึงใช้ app_wifi_is_ready() ทุกจุดด้วยเหตุนี้ (mqtt_task.c:340-343) ให้ใช้ตัวเข้าถึงตัวเดียวกันนี้ในส่วนขยายของตน
- กับดัก 5 — คิวของการ publish ทิ้งข้อความเมื่อเต็ม
- xQueueSend(publisher_task_q, &msg, 0) — รอเป็นศูนย์ การ publish รัว ๆ ทำให้ข้อความหายไปเงียบ ๆ ให้คุมจังหวะข้อมูล telemetry หรือตรวจค่าที่คืนกลับมา
- กับดัก 6 — ตัวแยกเส้นทางทำงานบน thread ของเหตุการณ์ MQTT
- handler ที่บล็อก ที่ยึดชิป หรือที่ malloc บัฟเฟอร์ขนาดใหญ่ ควรอยู่หลังคิวที่ส่งต่อไปยัง task ของตนเอง ซึ่งเป็นสิ่งที่ handler ของ Protected Update ที่ส่งมอบมาทำอยู่พอดี (บท D2)
Variant
- variant ที่ใช้ได้
- mtb-mpy และ mtb-only
โมดูล tesaiot_mqtt/ ทั้งโมดูลและที่เก็บ config ส่งมอบมาเป็นซอร์ส และคอมไพล์ได้ทั้งสอง variant สิ่งที่ต่างกันคือ backend สำหรับ I/O ของไฟล์ config (bento_storage_read_file เทียบกับ open() ของ Python) ลำดับการโหลดเทียบกับ ipc_tesaiot_handler_init() (B1) และจุดเริ่มฝั่ง Python คือ tesaiot.connect() / tesaiot.publish() ซึ่งมีเฉพาะบน mtb-mpy บน mtb-only ให้สั่งการเชื่อมต่อจากหน้า TESAIoT บน CM55 หรือเรียก tesaiot_mqtt_connect() จาก task ฝั่ง C ที่เขียนขึ้นเอง