本文以C Link SDK中的Demo文件./demos/fota_multi_file_demo.c为例,介绍如何调用Link SDK的API,帮助设备端使用HTTPS协议,下载含多个升级文件的OTA升级包,实现设备的OTA升级。

背景信息

说明 仅2021年09月09日(不含当日)以后下载的C Link SDK,支持设备下载含多个文件的OTA升级包功能。

获取SDK的详细信息,请参见获取C Link SDK

步骤一:初始化OTA功能

  1. 添加头文件。
    ……
    ……
    #include "aiot_ota_api.h"
    ……
  2. 配置底层依赖和日志输出。
        aiot_sysdep_set_portfile(&g_aiot_sysdep_portfile);
        aiot_state_set_logcb(demo_state_logcb);
  3. 调用aiot_ota_init,创建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功能

调用aiot_ota_setopt,配置以下功能。

  1. 关联MQTT连接的句柄。
    重要 在配置OTA参数前,请确保已配置设备认证信息等相关参数。具体操作,请参见MQTT配置连接参数
    • 示例代码:
          aiot_ota_setopt(ota_handle, AIOT_OTAOPT_MQTT_HANDLE, mqtt_handle);
    • 相关参数:
      配置项示例说明
      AIOT_OTAOPT_MQTT_HANDLEmqtt_handleOTA功能的请求基于MQTT连接,通过该配置项,关联MQTT连接句柄。
  2. 配置OTA升级指令消息的回调。
    • 示例代码:
          aiot_ota_setopt(ota_handle, AIOT_OTAOPT_RECV_HANDLER, demo_ota_recv_handler);
    • 相关参数:
      配置项示例说明
      AIOT_OTAOPT_MQTT_HANDLERdemo_ota_recv_handler当设备收到来自物联网平台的OTA升级指令,调用该回调函数。

步骤三:上报设备当前版本号

设备建立MQTT连接后,调用aiot_ota_report_version,上报当前设备的版本号。物联网平台根据版本号,判断是否需要升级。

以下示例代码中,OTA升级前设备上报的版本号为1.0.0,在实际业务中,您需从设备的配置区获取实际的版本号,并执行编写代码。

重要

设备进行OTA升级前,需至少上报一次版本号。

    cur_version = "1.0.0";
    res = aiot_ota_report_version(ota_handle, cur_version);
    if (res < STATE_SUCCESS) {
        printf("aiot_ota_report_version failed: -0x%04X\r\n", -res);
    }

步骤四:接收升级指令

  1. 在物联网平台添加升级包,并发起升级任务后,物联网平台向设备端下发升级指令。
    具体操作,请参见添加升级包
  2. 设备端调用aiot_mqtt_recv接收消息,当消息被识别为OTA升级指令后,调用回调函数demo_ota_recv_handler
  3. 编写回调函数的处理逻辑。
    您可以参考以下内容,编写回调函数的处理逻辑:
    • 物联网平台通过Topic /ota/device/upgrade/${ProductKey}/${DeviceName},向设备下发OTA升级包指令。

      ${ProductKey}${DeviceName}的详细说明,请参见获取设备认证信息

    • OTA升级指令的类型为AIOT_OTARECV_FOTA
      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;
                  multi_download_status_t *download_status = NULL;
      
                  if (NULL == ota_msg->task_desc) {
                      break;
                  }
           ……
           ……
      
      }
    • OTA升级指令消息的数据结构类型为aiot_ota_recv_t,Link SDK自动解析收到的升级指令消息。
    • 您可以参考示例代码,编写回调函数的处理逻辑,请参见步骤五:下载升级包并进行OTA升级

步骤五:下载升级包并进行OTA升级

重要 设备端收到物联网平台推送的升级包消息后,不会自动下载升级包,您需要调用Link SDK提供的API启动下载。

触发函数demo_ota_recv_handler后,基于HTTPS协议,下载器发起下载请求,然后接收升级包,实现OTA升级。

  1. 初始化下载器。
    调用aiot_download_init,创建download客户端实例,并初始化默认参数。
    说明

    OTA升级支持含多个文件的OTA升级包,更多信息,请参见添加升级包

    dl_handle = aiot_download_init();
                if (NULL == dl_handle) {
                    break;
                }
    
                if (NULL != ota_msg->task_desc->file_name) {
                    printf("\r\nTotal file number is %d, current file id is %d, with file_name %s\r\n", ota_msg->task_desc->file_num,
                           ota_msg->task_desc->file_id, ota_msg->task_desc->file_name);
                }
    
                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);
  2. 配置下载参数。
    调用aiot_download_setopt,配置下载任务的相关参数。
    说明

    OTA升级支持下载含多个文件的OTA升级包,因此在下载时需注意区分每个文件的ID、数量和下载进度。

                /* 设置下载时为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));
                /* 设置单次下载最大的缓存长度, 每当这个长度的内存读满了后会通知用户 */
                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(download_status, 0, sizeof(multi_download_status_t));
                download_status->file_id = ota_msg->task_desc->file_id;
                download_status->file_num = ota_msg->task_desc->file_num;
                aiot_download_setopt(dl_handle, AIOT_DLOPT_USERDATA, (void *)download_status);
    
                /* 如果是第一个下载任务, 则上报进度0 */
                if (0 == ota_msg->task_desc->file_id) {
                    aiot_download_report_progress(dl_handle, 0);
                }
                            
  3. 发起下载请求
    1. 启动下载线程demo_ota_download_thread
                  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(download_status);
                  } else {
                      /* 下载线程被设置为 detach 类型, 固件内容获取完毕后可自主退出 */
                      pthread_detach(g_download_thread);
                  }
    2. 通过下载线程demo_ota_download_thread,调用aiot_download_send_request,向指定的升级包存储服务器,发起HTTPS协议的GET请求, 请求下载升级包。
      • 示例代码:
        void *demo_ota_download_thread(void *dl_handle)
        {
            int32_t     ret = 0;
        
            printf("\r\nstarting 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_DLOPT_RANGE_STARTAIOT_DLOPT_RANGE_END,可以分段下载升级包。

        例如,一个1024字节的升级包分两次下载,两次的参数可分别设置为:
        • 第一次:AIOT_DLOPT_RANGE_START=0AIOT_DLOPT_RANGE_END=511
        • 第二次:AIOT_DLOPT_RANGE_START=512AIOT_DLOPT_RANGE_END=1023
  4. 接收升级包
    1. 下载请求发送后,通过下载线程demo_ota_download_thread,调用aiot_download_recv, 接收升级包消息。接收的应答消息触发回调函数demo_download_recv_handler, 您需将下载的升级包保存至设备的本地存储或文件系统。
      void *demo_ota_download_thread(void *dl_handle)
      {
           ……
           ……
      
         aiot_download_send_request(dl_handle);
          // while (1) {
          while (should_stop == 0) {
              /* 从网络收取服务器回应的固件内容 */
              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;
              }
          }
           ……
           ……
      }
    2. 定义回调函数demo_download_recv_handler,编写升级包下载后的存放和升级逻辑。
      说明 示例代码仅做打印处理,未配置升级包的存储和烧录逻辑,您需根据环境自行保存升级包,最后运行升级包,完成OTA升级。
      • 如果OTA升级使用单个文件的OTA升级包,您可以将文件烧写到指定的本地存储位置。
      • 如果OTA升级使用多个文件的OTA升级包,每个文件可对应不同的存储地址,您需识别每个固件的file_name字段。
      void demo_download_recv_handler(void *handle, const aiot_download_recv_t *packet, void *userdata)
      {
          uint32_t data_buffer_len = 0;
          int32_t last_percent = 0;
          int32_t  percent = 0;
          multi_download_status_t *download_status = (multi_download_status_t *)userdata;
      
          /* 目前只支持 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 = (download_status->last_percent);
          }
      
          data_buffer_len = packet->data.len;
      
          /* 如果 percent 为负数, 说明发生了收包异常或者digest校验错误 */
          if (percent < 0) {
              printf("exception: percent = %d\r\n", percent);
              if (userdata) {
                  free(userdata);
              }
              return;
          }
           ……
           ……
      
      }
  5. 上报下载进度

    通过定义回调函数demo_download_recv_handler,调用aiot_download_report_progress,向物联网平台上报下载进度,以及升级过程中发生的异常(例如:烧录失败、网络断开等)。

    • 查看上报进度:

      会直接显示在物联网平台控制台。更多信息,请参见查看升级情况

    • 正常和异常下载时上报进度:
      • 如果下载正常,则以整数的形式,向物联网平台上报下载情况。Link SDK会自动计算出percent参数值,并在回调中,将其返回至物联网平台。
      • 如果下载异常,或下载之后固件的烧录异常,则将异常发送至物联网平台。设备和物联网平台的协议错误码,请参见aiot_ota_protocol_errcode_t
    • 上报进度的方式:
      • 如果OTA升级使用单个文件的OTA升级包,上报的升级包进度即为总进度。
      • 如果OTA升级使用多个文件的OTA升级包,为了避免每个升级包上报各自的进度造成混乱,请勿上报单个文件的下载进度。建议您上报进度时,以总文件大小为分母,各文件已下载总字节数为分子,计算百分比,然后再上报下载进度。
        您可根据业务需要自行开发上报升级包进度的场景。例如:
        • 开始下载时上报0%,然后仅当设备下载的总进度达到10%的整数倍时再上报一次进度,直至100%。
        • 为了简化上报进度,您也可以在开始下载时上报0%,完成下载后上报100%,仅分两次上报下载进度。
    void demo_download_recv_handler(void *handle, const aiot_download_recv_t *packet, void *userdata)
    {
         ……
         ……
    
        /*
         * TODO: 下载一段固件成功, 这个时候, 用户应该将
         *       起始地址为 packet->data.buffer, 长度为 packet->data.len 的内存, 保存到本地存储位置上
         *
         *       如果烧写失败, 还应该调用 aiot_download_report_progress(handle, -4) 将失败上报给物联网平台
         *       备注:协议中, 与云平台商定的错误码在 aiot_ota_protocol_errcode_t 类型中, 例如
         *           -1: 表示升级失败
         *           -2: 表示下载失败
         *           -3: 表示校验失败
         *           -4: 表示烧写失败
         *
         */
    
        /* percent 入参的值为 100 时, 说明SDK已经下载固件内容全部完成 */
        if (percent == 100) {
            g_finished_task_num++;
            /*
             * TODO: 此时, 应该完成所有的固件烧录, 保存当前工作, 重启设备, 切换到新的固件上启动。
                     并且, 新的固件必须通过以下代码,将升级后的新版本号(例如从1.0.0升到1.1.0, 则new_version的值为1.1.0)上报至物联网平台。
                     aiot_ota_report_version(ota_handle, new_version);
                     物联网平台收到新的版本号后, 才判定升级成功, 否则认定升级失败。
                     如果下载成功后升级失败, 还应该调用aiot_download_report_progress(handle, -1)将失败类型上报。
             */
        }
    
        /* 简化输出, 只有距离上次的下载进度增加5%以上时, 才会打印进度, 并向服务器上报进度 */
        if (percent - last_percent >= 5 || percent == 100) {
            if (NULL != download_status) {
                printf("file_id %d, download %03d%% done, +%d bytes\r\n", download_status->file_id, percent, data_buffer_len);
                download_status->last_percent = percent;
                if (g_finished_task_num == download_status->file_num) {
                    /* 考虑到多个线程并发下载, 仅仅在所有文件下载完成后才上报100%的进度 */
                    aiot_download_report_progress(handle, 100);
                }
            }
    
            if (percent == 100 && userdata) {
                free(userdata);
            }
        }
    }
  6. 退出下载器。

    升级包内容下载完成后,调用aiot_download_deinit,销毁download会话,下载线程自行退出。

        aiot_download_deinit(&dl_handle);
        printf("download thread exit\n");

步骤六:上报升级后版本号

  • 如果OTA升级使用单个文件的OTA升级包,该升级包仅含有一个固件,升级包携带了该固件版本号,您可以将该版本号上报到物联网平台,从而完成版本号的匹配。
  • 如果OTA升级使用多个文件的OTA升级包,多个文件可对应一个固件,也可对应多个固件。如果每个文件对应不同固件的版本号,例如a1b1c1,那么您上报的版本号必须为该升级包整体的版本号,例如a1b1c1

上报版本号的示例代码,请参见步骤三:上报设备当前版本号

说明
  • 设备完成OTA升级后,需上报最新版本号,否则物联网平台视该OTA升级任务为失败。

  • 如果设备升级后需重启,则设备重启后需上报最新的版本号。

  • 示例代码中不包含升级完成后重新上报版本号的过程,您需自行实现。

步骤七:断开连接

说明

MQTT接入常应用于长连接的设备,程序通常不会运行至此。

例程的主线程任务为配置参数并成功建立连接。连接建立后,主线程可进入休眠。

调用aiot_mqtt_disconnect,向物联网平台发送断开连接的报文,然后断开网络连接。

    res = aiot_mqtt_disconnect(mqtt_handle);
    if (res < STATE_SUCCESS) {
        aiot_mqtt_deinit(&mqtt_handle);
        printf("aiot_mqtt_disconnect failed: -0x%04X\n", -res);
        return -1;
    }

步骤八:退出OTA程序

调用aiot_ota_deinit,销毁OTA客户端实例,释放资源。

    aiot_ota_deinit(&ota_handle);

后续步骤

  • 例程文件配置完成后,需进行编译,生成可执行文件./output/fota-multi-file_demo

    更多信息,请参见编译与运行

  • 关于运行结果的详细说明,请参见运行日志