ST67W611M1 T01 HTTPS 實戰

前言

本篇繼續使用 STM32H7R7 + ST67W611M1 T01 + FreeRTOS,將實際網路需求拆成兩部分:

  1. 上傳: 使用 TLS Socket 向 HTTPS API 發送 JSON POST,確認 HTTP 與業務層回應。
  2. 下載: 使用 HTTPS GET 取得 Manifest,再以 HTTP Range 分段抓取 Binary,完成 ETag、長度、SHA-256 / Image 驗證。

上傳與下載流程

STM32H7R7 App
   │
   ├── 取得 Wi-Fi IP(上一篇)
   │
   ├── 上傳 JSON
   │     ├─ DNS 解析 Server
   │     ├─ 建立 TLS 1.2 Socket
   │     ├─ 設定 CA / SNI / ALPN
   │     ├─ 連線 TCP/443 + TLS
   │     ├─ HTTP POST + JSON Body
   │     ├─ 接收 HTTP 回應並解析 JSON
   │     └─ Close / release credential
   │
   └── 下載 Binary
         ├─ HTTP GET 取得 Manifest
         ├─ 解析 URL、size、SHA-256 等
         ├─ 重複 HTTP GET + Range
         ├─ 逐段寫入 PSRAM staging
         ├─ 檢查 206 / Content-Range / ETag
         ├─ 計算整體 SHA-256 與映像有效性
         └─ 通過驗證後交給後續 OTA 流程

上傳資料:HTTPS POST JSON

上傳使用 T01 的 W6X_Net_* Socket API。由於應用要帶自訂 X-Device-Type、X-Source 等 HTTP header,因此採用自己組 HTTP/1.1 request

Step 1:DNS → TLS Socket → CA / SNI / ALPN

/* 原始專案 https_client.c 的核心 API 呼叫順序。 */
uint8_t ip[4] = {0};
int32_t sock = -1;
uint8_t tag;
struct sockaddr_in addr = {0};

if (W6X_Net_ResolveHostAddress(HTTPS_HOST, ip) != W6X_STATUS_OK)
    goto fail;

sock = W6X_Net_Socket(AF_INET, SOCK_STREAM, IPPROTO_TLS_1_2);
if (sock < 0) goto fail;
tag = (uint8_t)sock;

/* demo_ca_pem / demo_ca_pem_len 由應用程式提供。 */
if (W6X_Net_TLS_Credential_AddByContent(
        tag, W6X_NET_TLS_CREDENTIAL_CA_CERTIFICATE,
        "demo_ca.pem", demo_ca_pem, demo_ca_pem_len) != 0)
    goto fail;

if (W6X_Net_Setsockopt(sock, SOL_TLS, TLS_SEC_TAG_LIST,
                       &tag, sizeof(tag)) != 0) goto fail;
if (W6X_Net_Setsockopt(sock, SOL_TLS, TLS_HOSTNAME,
                       HTTPS_HOST, strlen(HTTPS_HOST)) != 0) goto fail;
if (W6X_Net_Setsockopt(sock, SOL_TLS, TLS_ALPN_LIST,
                       "http/1.1", strlen("http/1.1")) != 0) goto fail;

addr.sin_family = AF_INET;
addr.sin_port = PP_HTONS(443);
addr.sin_addr.s_addr = ATON(ip);
if (W6X_Net_Connect(sock, (struct sockaddr *)&addr,
                    sizeof(addr)) != 0) goto fail;

/* 成功後開始 HTTP send / recv。
   fail: 路徑必須 Close socket 並釋放已註冊 credential。 */

HTTPS_HOST、demo_ca_pem 與 demo_ca_pem_len 是本文公開示意的占位符,並不是 ST SDK 自帶的全域變數。實際 OGA 程式使用專案自己的 Server macro 及 CA PEM 陣列。若要完整部署,應加入 socket timeout、credential_added 旗標與所有錯誤出口清理。TLS_HOSTNAME 提供伺服器名稱,但不可單憑該設定就宣稱憑證鏈與 hostname 已通過驗證,必須實測驗證失效/錯誤憑證時會拒絕連線

Step 2:建立 HTTP POST 與 JSON Body

HTTP request header 要與 Body 明確分開,Content-Length 是Body 位元組數,不是 sizeof 整個 buffer

#define HTTPS_HOST  "api.example.com"  /* 公開示意:替換自己的 API Server */
#define HTTPS_PATH  "/v1/events"

static const char json_body[] =
    "{\"deviceId\":\"demo-001\",\"event\":\"startup\"}";

char header[384];
int n = snprintf(header, sizeof(header),
    "POST %s HTTP/1.1\r\n"
    "Host: %s\r\n"
    "Content-Type: application/json\r\n"
    "Accept: application/json\r\n"
    "Accept-Encoding: identity\r\n"
    "Content-Length: %u\r\n"
    "Connection: close\r\n\r\n",
    HTTPS_PATH, HTTPS_HOST, (unsigned)(sizeof(json_body) - 1U));

if (n <= 0 || (size_t)n >= sizeof(header)) {
    /* Header 超出 buffer:停止傳送 */
    return -1;
}

/* send_all 是本專案封裝,用來處理 W6X_Net_Send() 的 partial send。 */
if (send_all(sock, header, (size_t)n) != 0 ||
    send_all(sock, json_body, sizeof(json_body) - 1U) != 0) {
    /* 回應未確認,不能判定 API 已寫入成功 */
    return -1;
}

其中 send_all() 的核心寫法如下(實際應加上整體 deadline,避免卡在持續回傳 0 的情況):

static int send_all(int32_t sock, const char *data, size_t length)
{
    while (length > 0U) {
        ssize_t sent = W6X_Net_Send(sock, data, length, 0);
        if (sent <= 0 || (size_t)sent > length) return -1;
        data   += sent;
        length -= (size_t)sent;
    }
    return 0;
}

Step 3:讀取 HTTP Response 並確認成功

W6X_Net_Send() 成功不等於 Server 已接受資料。 最低限度應解析:HTTP status、完整的 Response Body,以及應用 API 自己定義的成功條件

/* 取自 https_client.c 的核心控制流程;
   response buffer、timeout、HTTP_ResponseDecode 均為專案自備。 */
ssize_t count = W6X_Net_Recv(sock, response + received,
                              response_capacity - received, 0);
if (count > 0)
    received += (size_t)count;

int parsed = HTTP_ResponseDecode(response, received,
                                 count < 0, &http_status,
                                 &body, &body_length);
if (parsed > 0) {
    /* 實際專案還會解析 JSON status / data.inserted。 */
    if (http_status == 200)
        LogInfo("HTTP 200 received; validate business payload next\n");
}

Note:回應除了 HTTP 200,還要求 JSON 中的 status == 0、data.inserted == true,才視為上傳成功

下載資料:HTTPS GET

下載先從最基本的 GET 開始;小型 JSON 可以完整累積在固定 buffer,再做格式解析。大型 Binary 則不適合整包塞進 SRAM,而應分段處理

Step 1:GET 取得 Manifest

int n = snprintf(request_text, sizeof(request_text),
    "GET %s HTTP/1.1\r\n"
    "Host: %s\r\n"
    "Accept: application/json\r\n"
    "Accept-Encoding: identity\r\n"
    "Connection: close\r\n\r\n",
    url->path, url->host);

/* 然後以 W6X_Net_Send() 傳送,W6X_Net_Recv() 累積 HTTP header/body。
   專案使用 BWIFI_HttpHeader() / BWIFI_HttpFeed() 進行邊界檢查。
   取回 Manifest 後,由 BWIFI_ManifestParse() 驗證 URL、size、hash。 */

Step 2:用 HTTP Range 分段下載 Binary

推薦在 Server 支援 Range 且可保證檔案版本穩定時,利用 Range: bytes=start-end 分段下載
目前專案設定:

  • 應用 HTTP Range:4096 bytes / request,定義於 BWIFI_RANGE_BYTES。
  • STM32 一次 W6X_Net_Recv() 的暫存 buffer:1024 bytes。
  • HTTP Header buffer:2048 bytes。
  • T01 NCP Receive buffer:9216 bytes(W6X_NET_RECV_BUFFER_SIZE)。
  • 本次測試專案的 OTA staging 配置上限為2 MiB;它不是模組對所有下載檔案的通用上限,也不能直接代表其他產品的 24 MB OTA 需求。

Range request 組裝與 Receive Loop

/* 這段位於 FetchRange() 中;
   url、sock、offset、size、file_size、out、etag 等由外層提供。 */
int length = snprintf(request_text, sizeof(request_text),
    "GET %s HTTP/1.1\r\nHost: %s\r\n"
    "Accept-Encoding: identity\r\n"
    "Range: bytes=%lu-%lu\r\n"
    "%s%s%sConnection: close\r\n\r\n",
    url->path, url->host,
    (unsigned long)offset,
    (unsigned long)(offset + size - 1U),
    etag[0] ? "If-Range: " : "", etag,
    etag[0] ? "\r\n" : "");

/* 實際程式依 length 逐步傳送,支援 partial send 與 deadline。 */
for (uint32_t sent = 0U; sent < (uint32_t)length;) {
    ssize_t n = W6X_Net_Send(sock,
                              request_text + sent,
                              (uint32_t)length - sent, 0);
    if (n < 0) goto failed;
    if (n == 0) continue;
    sent += (uint32_t)n;
}

/* Read path 使用 1024-byte scratch buffer,不能假設每次 Recv 都回 4096。 */
while (received < size) {
    ssize_t n = W6X_Net_Recv(sock, rx, sizeof(rx), 0);
    if (n == -2 || n == 0) {
        /* 專案會延遲並持續檢查總 timeout */
        continue;
    }
    if (n < 0 || n > (ssize_t)sizeof(rx)) goto failed;
    /* 先收集 HTTP Header 並驗證,再把 Body 複製至 staging。 */
}

Step 3:每段驗證 + 最終 SHA-256

正確的 Range download 不只是反覆呼叫 W6X_Net_Recv()。每段還應檢查:

  • HTTP status 是 206 Partial Content,而不是被 Server 忽略 Range 後回傳的 200。
  • Content-Range 的起點、終點與總長度對應預期的檔案區間。
  • Content-Length 等於要求的區塊大小。
  • 整個下載使用同一個 ETag / 同一個檔案版本,避免新舊版本混合。
  • 每段實際寫入的長度、PSRAM 讀回資料一致;下載完成後做 SHA-256 和映像檢查。
for (uint32_t pos = 0U; pos < image->size;) {
    uint32_t n = image->size - pos;
    if (n > BWIFI_RANGE_BYTES)
        n = BWIFI_RANGE_BYTES;

    if (!FetchRange(&url, ip, pos, n,
                    image->size, destination + pos))
        return 0;

    pos += n;
    vTaskDelay(1U);
}

if (!HashMatches(destination, image->size, image->sha256)) {
    LogError("[OTA] SHA256 mismatch\n");
    return 0;
}
if (!BWIFI_ImageValid(destination, image->size)) {
    LogError("[OTA] image validation failed\n");
    return 0;
}

T01 的 6144-byte TLS 限制

ST 官方文件指出,T01 模組內 SSL socket 的 TLS plaintext fragment 上限為 6144 bytes。HTTP Range 的 4096 bytes 與 Recv() 的 1024 bytes 是應用端設定,不能證明任何 HTTPS Server 都會配合輸出相容的 TLS records。

狀況說明建議
Server / TLS 行為相容T01 可逐段收資料,應用檔案可大於 6144 bytes驗證長時間下載、Range / ETag、timeout
TLS handshake 失敗可能與 CA、hostname、cipher、TLS fragment、ALPN 或網路有關先收集 TLS / socket error,而不是只縮小 Recv()
Range 不支援Server 回 200 而非 206更換 API 策略或讓伺服器支援 Range
CDN 改寫壓縮或快取內容可能造成 byte range 或 hash 不一致Accept-Encoding: identity、檔案版本鎖定、檢查 ETag
T01 TLS 相容性無法排除無法控制 Server TLS 行為評估 T02 Host LwIP / MbedTLS,並做 Host RAM / CPU / PSRAM 壓力測試

如果 Host 同時在使用 TouchGFX、PSRAM 及其他即時任務,不要把 T02 視為零成本替代方案。需要先測記憶體配置、Task stack / Heap、DMA / Cache、TLS 尖峰使用量及多次斷線後資源釋放。

官方參考資料

Leave a Comment

Your email address will not be published. Required fields are marked *

Shopping Cart