OTA更新流程
固件更新流程如下:
1、裝置要使用OTA服務, 必須先以aiot_ota_report_version 向雲端上報目前的固件版本号,否則無法接收到固件資訊推送。
2、裝置下載下傳固件時, 裝置自行用aiot_download_send_request 從雲端擷取,SDK不會自動下載下傳固件。(Demo中有示例)
3、裝置下載下傳中或者下載下傳完成後, 裝置需自行将記憶體buffer中的固件内容寫到Flash上,SDK不含有固件燒錄的邏輯。
4、裝置更新完成後, 需以aiot_ota_report_version 接口, 向雲端上報更新後的新固件版本号,否則雲端不會認為更新完成
如何表示更新成功呢?裝置端需要
彙報更新進度
IOT_OTA_ReportProgress
可選API, OTA下載下傳階段, 調用此函數向服務端彙報已經下載下傳了全部固件内容的百分之多少
彙報版本号
IOT_OTA_ReportVersion
OTA會話階段, 向服務端彙報目前的固件版本号
先彙報更新進度,然後再彙報版本号才表示更新完成
上報更新進度到Topic:/ota/device/progress/${YourProductKey}/${YourDeviceName}。
上報進度消息格式:
{
"id": 1,
"params": {
"step":"1",
"desc":" xxxxxxxx "
}
}
更新進度到100時,再上報更新後的固件版本到Topic:/ota/device/inform/${YourProductKey}/${YourDeviceName}。
上報固件版本的格式:
{
"id": 1,
"params": {
"version": "1.0.0"
}
}
Step By Step
1、上傳更新包到控制台
2、準備好裝置端代碼
此處我們用到的是C-SDK-4.x Demo 中的fota_posix_demo.c
sdk_source/demos/fota_posix_demo.c
(1)替換三元組
(2)修改版本号為2.0.0
3、開始更新
點選驗證,選擇2.0.0版本(注:這個版本如果沒有,可以先執行一下裝置端程式,進行上報作為2.0.0版本的記錄)
表示待更新的裝置版本号是2.0.0
點選确定後可以檢視更新包詳情,已經處于更新中
4、執行裝置端程式
裝置開始運作,上報版本号2.0.0,正好符合更新條件,
然後裝置端收到平台推送的固件url更新包
裝置端上報更新進度
更新進度到達100
看到控制台中批次詳情狀态已經是100,但此時還并不表示更新完成。
5、修改Demo版本号,再次執行裝置端程式
再次執行程式,裝置端上報版本号為3.0.0
控制台中批次詳情顯示更新成功
裝置端源碼
/*
* 這個例程适用于`Linux`這類支援pthread的POSIX裝置, 它示範了用SDK配置MQTT參數并建立連接配接, 之後建立3個線程
*
* + 一個線程用于保活長連接配接
* + 一個線程用于接收消息, 并在有消息到達時進入預設的資料回調, 在連接配接狀态變化時進入事件回調
* + 一個線程用于從網絡上HTTP下載下傳待更新的固件, 這個線程由接收消息線程得到OTA更新的MQTT消息後啟動
*
* 需要使用者關注或修改的部分, 已用 `TODO` 在注釋中标明
*
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include "aiot_state_api.h"
#include "aiot_sysdep_api.h"
#include "aiot_ota_api.h"
#include "aiot_mqtt_api.h"
/* TODO: 替換為自己裝置的三元組 */
char *product_key = "a16h***pRCl";
char *device_name = "IoTD***emo1";
char *device_secret = "a3b15********1439b7bd4bc4952";
/* 位于portfiles/aiot_port檔案夾下的系統适配函數集合 */
extern aiot_sysdep_portfile_t g_aiot_sysdep_portfile;
/* 位于external/ali_ca_cert.c中的伺服器證書 */
extern const char *ali_ca_cert;
static pthread_t g_mqtt_process_thread; /* 用于MQTT的長連接配接保活線程 */
static pthread_t g_mqtt_recv_thread; /* 用于MQTT的循環收消息線程 */
static pthread_t g_download_thread; /* 用于HTTP的固件下載下傳線程 */
/* TODO: 如果要關閉日志, 就把這個函數實作為空, 如果要減少日志, 可根據code選擇不列印
*
* 例如: [1578463098.611][LK-0309] pub: /ota/device/upgrade/a13FN5TplKq/ota_demo
*
* 上面這條日志的code就是0309(十六進制), code值的定義見core/aiot_state_api.h
*
*/
/* 日志回調函數, SDK的日志會從這裡輸出 */
int32_t demo_state_logcb(int32_t code, char *message)
{
/* 下載下傳固件的時候會有大量的HTTP收包日志, 通過code篩選出來關閉 */
if (STATE_HTTP_LOG_RECV_CONTENT != code) {
printf("%s", message);
}
return 0;
}
/* 下載下傳收包回調, 使用者調用 aiot_download_recv() 後, SDK收到資料會進入這個函數, 把下載下傳到的資料交給使用者 */
/* TODO: 一般來說, 裝置更新時, 會在這個回調中, 把下載下傳到的資料寫到Flash上 */
void demo_download_recv_handler(void *handle, const aiot_download_recv_t *packet, void *userdata)
{
uint32_t data_buffer_len = 0;
uint32_t last_percent = 0;
int32_t percent = 0;
/* 目前隻支援 packet->type 為 AIOT_DLRECV_HTTPBODY 的情況 */
if (!packet || AIOT_DLRECV_HTTPBODY != packet->type) {
return;
}
percent = packet->data.percent;
/* userdata可以存放 demo_download_recv_handler() 的不同次進入之間, 需要共享的資料 */
/* 這裡用來存放上一次進入本回調函數時, 下載下傳的固件進度百分比 */
if (userdata) {
last_percent = *((uint32_t *)(userdata));
}
data_buffer_len = packet->data.len;
printf("__bollg__ test begin3....\n");
/* 如果 percent 為負數, 說明發生了收包異常或者digest校驗錯誤 */
if (percent < 0) {
printf("exception: percent = %d\r\n", percent);
if (userdata) {
free(userdata);
}
return;
}
printf("new yeay percernt is %d\n",percent);
/*
* TODO: 下載下傳一段固件成功, 這個時候, 使用者應該将
* 起始位址為 packet->data.buffer, 長度為 packet->data.len 的記憶體, 儲存到flash上
*
* 如果燒寫flash失敗, 還應該調用 aiot_download_report_progress(handle, -4) 将失敗上報給雲平台
* 備注:協定中, 與雲平台商定的錯誤碼在 aiot_ota_protocol_errcode_t 類型中, 例如
* -1: 表示更新失敗
* -2: 表示下載下傳失敗
* -3: 表示校驗失敗
* -4: 表示燒寫失敗
*
* 詳情可見 https://help.aliyun.com/document_detail/85700.html
*/
/* percent 入參的值為 100 時, 說明SDK已經下載下傳固件内容全部完成 */
if (percent == 100) {
/*
* TODO: 這個時候, 一般使用者就應該完成所有的固件燒錄, 儲存目前工作, 重新開機裝置, 切換到新的固件上啟動了
并且, 新的固件必須要以
aiot_ota_report_version(ota_handle, new_version);
這樣的操作, 将更新後的新版本号(比如1.0.0升到1.1.0, 則new_version的值是"1.1.0")上報給雲平台
雲平台收到了新的版本号上報後, 才會判定更新成功, 否則會認為本次更新失敗了
如果下載下傳成功後更新失敗, 還應該調用 aiot_download_report_progress(handle, -1) 将失敗上報給雲平台
*/
}
/* 簡化輸出, 隻有距離上次的下載下傳進度增加5%以上時, 才會列印進度, 并向伺服器上報進度 */
if (percent - last_percent >= 5 || percent == 100) {
printf("download %03d%% done, +%d bytes\r\n", percent, data_buffer_len);
aiot_download_report_progress(handle, percent);
if (userdata) {
*((uint32_t *)(userdata)) = percent;
}
if (percent == 100 && userdata) {
free(userdata);
}
}
}
/* 執行aiot_download_recv的線程, 實作固件内容的請求和接收 */
void *demo_ota_download_thread(void *dl_handle)
{
int32_t ret = 0;
printf("starting download thread in 2 seconds ......\r\n");
sleep(2);
/* 向固件伺服器請求下載下傳 */
/*
* TODO: 下面這樣的寫法, 就是以1個請求, 擷取全部的固件内容
* 裝置資源比較少, 或者網絡較差時, 也可以分段下載下傳, 需要組合
*
* aiot_download_setopt(dl_handle, AIOT_DLOPT_RANGE_START, ...);
* aiot_download_setopt(dl_handle, AIOT_DLOPT_RANGE_END, ...);
* aiot_download_send_request(dl_handle);
*
* 實作, 這種情況下, 需要把以上組合語句放置到循環中, 多次 send_request 和 recv
*
*/
aiot_download_send_request(dl_handle);
while (1) {
/* 從網絡收取伺服器回應的固件内容 */
ret = aiot_download_recv(dl_handle);
/* 固件全部下載下傳完時, aiot_download_recv() 的傳回值會等于 STATE_DOWNLOAD_FINISHED, 否則是當次擷取的位元組數 */
if (STATE_DOWNLOAD_FINISHED == ret) {
printf("download completed\r\n");
break;
}
if (STATE_DOWNLOAD_RENEWAL_REQUEST_SENT == ret) {
printf("download renewal request has been sent successfully\r\n");
continue;
}
if (ret <= STATE_SUCCESS) {
printf("download failed, error code is %d, try to send renewal request\r\n", ret);
continue;
}
}
/* 下載下傳所有固件内容完成, 銷毀下載下傳會話, 線程自行退出 */
aiot_download_deinit(&dl_handle);
printf("download thread exit\r\n");
return NULL;
}
/* 使用者通過 aiot_ota_setopt() 注冊的OTA消息處理回調, 如果SDK收到了OTA相關的MQTT消息, 會自動識别, 調用這個回調函數 */
void demo_ota_recv_handler(void *ota_handle, aiot_ota_recv_t *ota_msg, void *userdata)
{
switch (ota_msg->type) {
case AIOT_OTARECV_FOTA: {
uint32_t res = 0;
uint16_t port = 443;
uint32_t max_buffer_len = (8 * 1024);
aiot_sysdep_network_cred_t cred;
void *dl_handle = NULL;
void *last_percent = NULL;
if (NULL == ota_msg->task_desc) {
break;
}
dl_handle = aiot_download_init();
if (NULL == dl_handle) {
break;
}
printf("OTA target firmware version: %s, size: %u Bytes \r\n", ota_msg->task_desc->version,
ota_msg->task_desc->size_total);
if (NULL != ota_msg->task_desc->extra_data) {
printf("extra data: %s\r\n", ota_msg->task_desc->extra_data);
}
memset(&cred, 0, sizeof(aiot_sysdep_network_cred_t));
cred.option = AIOT_SYSDEP_NETWORK_CRED_SVRCERT_CA;
cred.max_tls_fragment = 16384;
cred.x509_server_cert = ali_ca_cert;
cred.x509_server_cert_len = strlen(ali_ca_cert);
/* 設定下載下傳時為TLS下載下傳 */
aiot_download_setopt(dl_handle, AIOT_DLOPT_NETWORK_CRED, (void *)(&cred));
/* 設定下載下傳時通路的伺服器端口号 */
aiot_download_setopt(dl_handle, AIOT_DLOPT_NETWORK_PORT, (void *)(&port));
/* 設定下載下傳的任務資訊, 通過輸入參數 ota_msg 中的 task_desc 成員得到, 内含下載下傳位址, 固件大小, 固件簽名等 */
aiot_download_setopt(dl_handle, AIOT_DLOPT_TASK_DESC, (void *)(ota_msg->task_desc));
/* 設定下載下傳内容到達時, SDK将調用的回調函數 */
aiot_download_setopt(dl_handle, AIOT_DLOPT_RECV_HANDLER, (void *)(demo_download_recv_handler));
/* 設定單次下載下傳最大的buffer長度, 每當這個長度的記憶體讀滿了後會通知使用者 */
aiot_download_setopt(dl_handle, AIOT_DLOPT_BODY_BUFFER_MAX_LEN, (void *)(&max_buffer_len));
/* 設定 AIOT_DLOPT_RECV_HANDLER 的不同次調用之間共享的資料, 比如例程把進度存在這裡 */
last_percent = malloc(sizeof(uint32_t));
if (NULL == last_percent) {
aiot_download_deinit(&dl_handle);
break;
}
memset(last_percent, 0, sizeof(uint32_t));
aiot_download_setopt(dl_handle, AIOT_DLOPT_USERDATA, (void *)last_percent);
/* 啟動專用的下載下傳線程, 去完成固件内容的下載下傳 */
res = pthread_create(&g_download_thread, NULL, demo_ota_download_thread, dl_handle);
if (res != 0) {
printf("pthread_create demo_ota_download_thread failed: %d\r\n", res);
aiot_download_deinit(&dl_handle);
free(last_percent);
} else {
/* 下載下傳線程被設定為 detach 類型, 固件内容擷取完畢後可自主退出 */
pthread_detach(g_download_thread);
}
break;
}
default:
break;
}
}
/* MQTT事件回調函數, 當網絡連接配接/重連/斷開時被觸發, 事件定義見core/aiot_mqtt_api.h */
void demo_mqtt_event_handler(void *handle, const aiot_mqtt_event_t *event, void *userdata)
{
switch (event->type) {
/* SDK因為使用者調用了aiot_mqtt_connect()接口, 與mqtt伺服器建立連接配接已成功 */
case AIOT_MQTTEVT_CONNECT: {
printf("AIOT_MQTTEVT_CONNECT\r\n");
/* TODO: 處理SDK建連成功, 不可以在這裡調用耗時較長的阻塞函數 */
}
break;
/* SDK因為網絡狀況被動斷連後, 自動發起重連已成功 */
case AIOT_MQTTEVT_RECONNECT: {
printf("AIOT_MQTTEVT_RECONNECT\r\n");
/* TODO: 處理SDK重連成功, 不可以在這裡調用耗時較長的阻塞函數 */
}
break;
/* SDK因為網絡的狀況而被動斷開了連接配接, network是底層讀寫失敗, heartbeat是沒有按預期得到服務端心跳應答 */
case AIOT_MQTTEVT_DISCONNECT: {
char *cause = (event->data.disconnect == AIOT_MQTTDISCONNEVT_NETWORK_DISCONNECT) ? ("network disconnect") :
("heartbeat disconnect");
printf("AIOT_MQTTEVT_DISCONNECT: %s\r\n", cause);
/* TODO: 處理SDK被動斷連, 不可以在這裡調用耗時較長的阻塞函數 */
}
break;
default: {
}
}
}
/* MQTT預設消息處理回調, 當SDK從伺服器收到MQTT消息時, 且無對應使用者回調處理時被調用 */
void demo_mqtt_default_recv_handler(void *handle, const aiot_mqtt_recv_t *packet, void *userdata)
{
switch (packet->type) {
case AIOT_MQTTRECV_HEARTBEAT_RESPONSE: {
printf("heartbeat response\r\n");
/* TODO: 處理伺服器對心跳的回應, 一般不處理 */
}
break;
case AIOT_MQTTRECV_SUB_ACK: {
printf("suback, res: -0x%04X, packet id: %d, max qos: %d\r\n",
-packet->data.sub_ack.res, packet->data.sub_ack.packet_id, packet->data.sub_ack.max_qos);
/* TODO: 處理伺服器對訂閱請求的回應, 一般不處理 */
}
break;
case AIOT_MQTTRECV_PUB: {
printf("pub, qos: %d, topic: %.*s\r\n", packet->data.pub.qos, packet->data.pub.topic_len, packet->data.pub.topic);
printf("pub, payload: %.*s\r\n", packet->data.pub.payload_len, packet->data.pub.payload);
/* TODO: 處理伺服器下發的業務封包 */
}
break;
case AIOT_MQTTRECV_PUB_ACK: {
printf("puback, packet id: %d\r\n", packet->data.pub_ack.packet_id);
/* TODO: 處理伺服器對QoS1上報消息的回應, 一般不處理 */
}
break;
default: {
}
}
}
/* 執行aiot_mqtt_process的線程, 包含心跳發送和QoS1消息重發 */
void *demo_mqtt_process_thread(void *args)
{
while (1) {
aiot_mqtt_process(args);
sleep(1);
}
return NULL;
}
/* 執行aiot_mqtt_recv的線程, 包含網絡自動重連和從伺服器收取MQTT消息 */
void *demo_mqtt_recv_thread(void *args)
{
int32_t res = STATE_SUCCESS;
while (1) {
res = aiot_mqtt_recv(args);
if (res < STATE_SUCCESS) {
sleep(1);
}
}
return NULL;
}
int main(int argc, char *argv[])
{
int32_t res = STATE_SUCCESS;
void *mqtt_handle = NULL;
int8_t public_instance =
1; /* 用公共執行個體, 該參數要設定為1. 若用獨享執行個體, 要将該參數設定為0 */
char *url = "iot-as-mqtt.cn-shanghai.aliyuncs.com"; /* 阿裡雲平台上海站點的域名字尾 */
char host[100] = {0}; /* 用這個數組拼接裝置連接配接的雲平台站點全位址, 規則是 ${productKey}.iot-as-mqtt.cn-shanghai.aliyuncs.com */
uint16_t port = 443; /* 無論裝置是否使用TLS連接配接阿裡雲平台, 目的端口都是443 */
aiot_sysdep_network_cred_t cred; /* 安全憑據結構體, 如果要用TLS, 這個結構體中配置CA憑證等參數 */
void *ota_handle = NULL;
char *cur_version = NULL;
/* 配置SDK的底層依賴 */
aiot_sysdep_set_portfile(&g_aiot_sysdep_portfile);
/* 配置SDK的日志輸出 */
aiot_state_set_logcb(demo_state_logcb);
/* 建立SDK的安全憑據, 用于建立TLS連接配接 */
memset(&cred, 0, sizeof(aiot_sysdep_network_cred_t));
cred.option = AIOT_SYSDEP_NETWORK_CRED_SVRCERT_CA; /* 使用RSA證書校驗MQTT服務端 */
cred.max_tls_fragment = 16384; /* 最大的分片長度為16K, 其它可選值還有4K, 2K, 1K, 0.5K */
cred.sni_enabled = 1; /* TLS建連時, 支援Server Name Indicator */
cred.x509_server_cert = ali_ca_cert; /* 用來驗證MQTT服務端的RSA根證書 */
cred.x509_server_cert_len = strlen(ali_ca_cert); /* 用來驗證MQTT服務端的RSA根證書長度 */
/* 建立1個MQTT用戶端執行個體并内部初始化預設參數 */
mqtt_handle = aiot_mqtt_init();
if (NULL == mqtt_handle) {
printf("aiot_mqtt_init failed\r\n");
return -1;
}
if (1 == public_instance) {
snprintf(host, 100, "%s.%s", product_key, url);
} else {
snprintf(host, 100, "%s", url);
}
/* 配置MQTT伺服器位址 */
aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_HOST, (void *)host);
/* 配置MQTT伺服器端口 */
aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_PORT, (void *)&port);
/* 配置裝置productKey */
aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_PRODUCT_KEY, (void *)product_key);
/* 配置裝置deviceName */
aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_DEVICE_NAME, (void *)device_name);
/* 配置裝置deviceSecret */
aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_DEVICE_SECRET, (void *)device_secret);
/* 配置網絡連接配接的安全憑據, 上面已經建立好了 */
aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_NETWORK_CRED, (void *)&cred);
/* 配置MQTT預設消息接收回調函數 */
aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_RECV_HANDLER, (void *)demo_mqtt_default_recv_handler);
/* 配置MQTT事件回調函數 */
aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_EVENT_HANDLER, (void *)demo_mqtt_event_handler);
/* 與MQTT例程不同的是, 這裡需要增加建立OTA會話執行個體的語句 */
ota_handle = aiot_ota_init();
if (NULL == ota_handle) {
printf("aiot_ota_init failed\r\n");
aiot_mqtt_deinit(&mqtt_handle);
return -2;
}
/* 用以下語句, 把OTA會話和MQTT會話關聯起來 */
aiot_ota_setopt(ota_handle, AIOT_OTAOPT_MQTT_HANDLE, mqtt_handle);
/* 用以下語句, 設定OTA會話的資料接收回調, SDK收到OTA相關推送時, 會進入這個回調函數 */
aiot_ota_setopt(ota_handle, AIOT_OTAOPT_RECV_HANDLER, demo_ota_recv_handler);
/* 與伺服器建立MQTT連接配接 */
res = aiot_mqtt_connect(mqtt_handle);
if (res < STATE_SUCCESS) {
/* 嘗試建立連接配接失敗, 銷毀MQTT執行個體, 回收資源 */
aiot_mqtt_deinit(&mqtt_handle);
aiot_ota_deinit(&ota_handle);
printf("aiot_mqtt_connect failed: -0x%04X\r\n", -res);
return -3;
}
/* TODO: 非常重要!!!
*
* cur_version 要根據使用者實際情況, 改成從裝置的配置區擷取, 要反映真實的版本号, 而不能像示例這樣寫為固定值
*
* 1. 如果裝置從未上報過版本号, 在控制台網頁将無法部署更新任務
* 2. 如果裝置更新完成後, 上報的不是新的版本号, 在控制台網頁将會顯示更新失敗
*
*/
/* 示範MQTT連接配接建立起來之後, 就可以上報目前裝置的版本号了 */
cur_version = "3.0.0";
printf("__bollg__ 88888((((((9999900999999999999999999\n");
res = aiot_ota_report_version(ota_handle, cur_version);
if (res < STATE_SUCCESS) {
printf("aiot_ota_report_version failed: -0x%04X\r\n", -res);
}
/* 建立一個單獨的線程, 專用于執行aiot_mqtt_process, 它會自動發送心跳保活, 以及重發QoS1的未應答封包 */
res = pthread_create(&g_mqtt_process_thread, NULL, demo_mqtt_process_thread, mqtt_handle);
if (res != 0) {
printf("pthread_create demo_mqtt_process_thread failed: %d\r\n", res);
return -1;
}
/* 建立一個單獨的線程用于執行aiot_mqtt_recv, 它會循環收取伺服器下發的MQTT消息, 并在斷線時自動重連 */
res = pthread_create(&g_mqtt_recv_thread, NULL, demo_mqtt_recv_thread, mqtt_handle);
if (res != 0) {
printf("pthread_create demo_mqtt_recv_thread failed: %d\r\n", res);
return -1;
}
/* 主循環進入休眠 */
while (1) {
sleep(1);
}
/* 斷開MQTT連接配接, 一般不會運作到這裡 */
res = aiot_mqtt_disconnect(mqtt_handle);
if (res < STATE_SUCCESS) {
aiot_mqtt_deinit(&mqtt_handle);
aiot_ota_deinit(&ota_handle);
printf("aiot_mqtt_disconnect failed: -0x%04X\r\n", -res);
return -1;
}
/* 銷毀MQTT執行個體, 一般不會運作到這裡 */
res = aiot_mqtt_deinit(&mqtt_handle);
if (res < STATE_SUCCESS) {
printf("aiot_mqtt_deinit failed: -0x%04X\r\n", -res);
aiot_ota_deinit(&ota_handle);
return -1;
}
/* 銷毀OTA執行個體, 一般不會運作到這裡 */
aiot_ota_deinit(&ota_handle);
return 0;
}