照搬的b站大佬宸芯IOT的文档,只做记录,如有侵权,联系速删

传送门:【ESP32S3+ESP-IDF开发应用详细教程 提高必备 中文字幕 持续更新中】 https://www.bilibili.com/video/BV1K3HvzEEDj/?p=24&share_source=copy_web&vd_source=ac13dabfddf63f849e12295d318fa57d

前言

最近在学esp32s3,跟着宸芯IOT,大佬教程,为加深理解并记录自己的探索过程,写一篇此教程,以便后续再使用到的时候返回来接着看

本教程只讲明应该怎么做以及为什么这么做。

教程

原理

OTA原理是在ESP32的存储空间中开辟出来一些分区来存放程序,这些区域功能不同,有的相当于开机自检,有的是用户app主程序,而OTA是开辟出来两块存储用户app的地方,称之为OTA_1和OTA_2然后由开机的bootload程序来自检审查当前版本与网络端发来的程序版本号,然后将新的版本程序存储到OTA_2中,重启再运行OTA_2以此来实现程序无线更新

OTA的两种模式

无回滚功能比较简单,启动OTA后,选取一个App分区,也就是ota0-15这些分区,将程序数据下载进去之后校验,然后把新分区状态改为ESP_OTA_IMG_UNDEFINED,这个状态刚才介绍了,和ESP_OTA_IMG_VALID是一样的效果,之后重启就跳转到新分区上运行程序,没什么验证功能。

带有回滚功能的程序流程稍微复杂一点,所谓的回滚功能,简单来说,就是完成OTA更新并跳到新的App分区上运行程序,如果我们的程序自检发现有问题,我们认为这个版本不能运行,那就可以回滚到上一个App分区运行,新的App程序标记为非法。启动回滚功能后,接收完数据后,我们的程序会将这个新分区标记为ESP_OTA_IMG_NEW状态,说明这是一个新分区,然后重启后,Bootloader会判断新分区状态,如果是ESP_OTA_IMG_NEW状态,就会立即改成ESP_OTA_IMG_PENDING_VERIFY状态,然后我们新分区程序运行的时候,需要显式的标记这个程序是否可用,如果标记成可用,则后续都将运行此程序,如果标记成不可用,则将重启,然后回滚的上个分区运行。还有,如果上电后bootloader判断到新分区是ESP_OTA_IMG_PENDING_VERIFY,则会直接标记成ESP_OTA_IMG_ABORT状态,ESP_OTA_IMG_ABORT这个状态就是不可用的,不会再启用,然后回滚到上一个APP分区运行,所以大家一定记得,如果启用了回滚功能,如果程序没问题,就一定要显式的标记此程序可用,否则重启后又会回到上一个分区。

程序实现(esp32s3端操作)

处理OTA通知整体流程

如下:加粗的在esp32端实现

1、用户在onenet平台添加一个升级任务

2、设备上电后上报版本号

3、onenet平台根据设备上报的版本号选择是否下发ota升级通知

4、设备收到OTA升级通知,接下来去查询升级任务信息,获取任务id以及要升级到的版本号信息

5、设备上报一个升级任务状态(也就是升级进度百分比)

6、设备开始下载升级数据

7、再上报一个升级任务状态(升级进度百分百)

8、设备重启

9、连上onenet平台后,上报版本号,调用set_app_vaild函数标记程序合法,可以运行

 

在espidf中进入SDK配置编辑器,勾选Enable app rollback support

搜索par,选择 如图分区选项

勾选 Allow HTTP for OTA 

更改主频240MHz

flash size 选16MB(我用的是esp32s3n16r8)

速度选80MHz

 

勾选使用PSRAM

然后下面选Octal Mode PSRAM

clock speed 选 80MKz

 

2,写代码

由于esp的app镜像也就是你写的用户主程序有一个属于它自己的信息表,是一个结构体,然后在esp32的函数中对这两个操作分别有函数去执行

获取当前app的版本号函数

const char* get_app_version(void) //获取当前应用版本号
{
    static char app_version[32] = {0};
    if(app_version[0] != 0)
    {
        const esp_partition_t * running =  esp_ota_get_running_partition();//获取当前运行分区
        esp_app_desc_t app_desc;
        esp_ota_get_partition_description(running ,&app_desc);  //获取分区描述信息
        snprintf(app_version, sizeof(app_version), "%s", app_desc.version); //获取版本号
    }
    return app_version;
}

接着设置当前app分区状态的函数

app分区状态共有以下几种

    ESP_OTA_IMG_NEW:说明这个OTA分区是新下载的程序,启动后bootloader会马上把这个状态修改为下面的PENDING_VERIFY状态

    ESP_OTA_IMG_PENDING:待校验,处于这个状态的App分区程序待验证,需要在App代码里面显式修改成这个分区是否可用,这两个状态,都是回滚功能启用的时候才会有用,什么是回滚功能,一会再介绍。

    ESP_OTA_IMG_VALID:合法的OTA分区,说明这个App分区程序没有问题,可以使用

    ESP_OTA_IMG_INVALID:非法分区,说明这个App分区程序存在问题,bootloader不会用此分区进行启动

    ESP_OTA_IMG_ABORTED:和上述类似,无法确认是否存在问题,也不会选用此状态的分区启动。

    ESP_OTA_IMG_UNDEFINED:这个和VALID类似,可以启用、

参考esp_ota_img_states_t这一枚举量

typedef enum {
    ESP_OTA_IMG_NEW             = 0x0U,         /*!< Monitor the first boot. In bootloader this state is changed to ESP_OTA_IMG_PENDING_VERIFY. */
    ESP_OTA_IMG_PENDING_VERIFY  = 0x1U,         /*!< First boot for this app was. If while the second boot this state is then it will be changed to ABORTED. */
    ESP_OTA_IMG_VALID           = 0x2U,         /*!< App was confirmed as workable. App can boot and work without limits. */
    ESP_OTA_IMG_INVALID         = 0x3U,         /*!< App was confirmed as non-workable. This app will not selected to boot at all. */
    ESP_OTA_IMG_ABORTED         = 0x4U,         /*!< App could not confirm the workable or non-workable. In bootloader IMG_PENDING_VERIFY state will be changed to IMG_ABORTED. This app will not selected to boot at all. */
    ESP_OTA_IMG_UNDEFINED       = 0xFFFFFFFFU,  /*!< Undefined. App can boot and work without limits. */
} esp_ota_img_states_t;

下面是函数实现

void set_app_valid(int valid) //
{
    const esp_partition_t * running =  esp_ota_get_running_partition(); //获取当前运行分区
    esp_ota_img_states_t ota_state;
    //获取当前分区OTA状态
    if(esp_ota_get_state_partition(running, &ota_state)== ESP_OK)
    {
        if(ota_state == ESP_OTA_IMG_PENDING_VERIFY) //如果当前状态是待验证且需要设置为有效
        {
            if(valid)
            {
                esp_ota_mark_app_valid_cancel_rollback(); //设置当前分区为有效,取消回滚
                ESP_LOGE("Set app valid success\r\n");
            }
            else
            {
                esp_ota_mark_app_invalid_rollback_and_reboot(); //设置当前分区为无效,回滚到上一个分区并重启
                ESP_LOGE("Set app invalid and reboot\r\n");
            }
        }
    }
}

2.上报版本号

#define     MAX_DATA_BUFF   1024
//ota基础url
#define     ONENET_OTA_URL  "http://iot-api.heclouds.com/fuse-ota"
//token合法时间戳
#define     TOKEN_TIMESTAMP     1924833600
//接收到的http 数据
static uint8_t data_buff[MAX_DATA_BUFF];
//接收到的http数据长度
static size_t   data_buff_len = 0;
/**
 * http事件回调函数
 * @param evt 包含http的数据
 * @return 错误码
 */
static esp_err_t http_client_event_handler(esp_http_client_event_t *evt)
{
    switch(evt->event_id) {
        case HTTP_EVENT_ERROR:    //错误事件
            //ESP_LOGI(TAG, "HTTP_EVENT_ERROR");
            break;
        case HTTP_EVENT_ON_CONNECTED:    //连接成功事件
            //ESP_LOGI(TAG, "HTTP_EVENT_ON_CONNECTED");
            break;
        case HTTP_EVENT_HEADER_SENT:    //发送头事件
            //ESP_LOGI(TAG, "HTTP_EVENT_HEADER_SENT");
            break;
        case HTTP_EVENT_ON_HEADER:    //接收头事件
            //ESP_LOGI(TAG, "HTTP_EVENT_ON_HEADER");
            printf("%.*s", evt->data_len, (char*)evt->data);
            break;
        case HTTP_EVENT_ON_DATA:    //接收数据事件
            {
                size_t copy_len = 0;
                ESP_LOGI(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
                printf("HTTP_EVENT_ON_DATA data=%.*s\r\n", evt->data_len,(char*)evt->data);
                if(evt->data_len > MAX_DATA_BUFF - data_buff_len)
                {
                    copy_len = MAX_DATA_BUFF - data_buff_len;
                }
                else
                {
                    copy_len = evt->data_len;
                }
                //将数据存到data_buff里面
                memcpy(&data_buff[data_buff_len],evt->data,copy_len);
                data_buff_len += copy_len;
            }
            break;
        case HTTP_EVENT_ON_FINISH:    //会话完成事件
            data_buff_len = 0;
            break;
        case HTTP_EVENT_DISCONNECTED:    //断开事件
            //ESP_LOGI(TAG, "HTTP_EVENT_DISCONNECTED");
            data_buff_len = 0;
            break;
        case HTTP_EVENT_REDIRECT:
            //ESP_LOGI(TAG, "HTTP_EVENT_REDIRECT");
            break;
    }
    return ESP_OK;
}
/**
 * 发起http请求
 * @param url 请求地址
 * @param method 请求方法
 * @param payload 消息体内容
 * @return 错误码
 */
static esp_err_t onenet_ota_http_connect(const char* url,esp_http_client_method_t method,char* post_data)
{
    esp_http_client_config_t config =
    {
        .url = url,
        .event_handler = http_client_event_handler,
    };
    //初始化结构体
    esp_http_client_handle_t http_client = esp_http_client_init(&config);   //初始化http连接
    if(!http_client)
    {
        ESP_LOGI(TAG,"http_client init fail!");
        return ESP_FAIL;
    }

    char* token = (char*)malloc(256);
    memset(token,0,256);
    //计算token
    dev_token_generate(token,SIG_METHOD_SHA256,TOKEN_TIMESTAMP,ONENET_PRODUCT_ID,NULL,ONENET_ACCESS_KEY);
    ESP_LOGI(TAG,"user token:%s",token);
    //设置发送请求头
    esp_http_client_set_method(http_client, method);
    esp_http_client_set_header(http_client,"Content-Type","application/json");
    esp_http_client_set_header(http_client,"Authorization",token);
    esp_http_client_set_header(http_client,"host","iot-api.heclouds.com");
    if(post_data)
    {
        ESP_LOGI(TAG,"post data:%s",post_data);
        esp_http_client_set_post_field(http_client,post_data,strlen(post_data));
    }
    data_buff_len = 0;
    memset(data_buff,0,sizeof(data_buff));
    //esp_http_client_perform这句函数会阻塞,直到完整的http请求结束才返回
    esp_err_t err  = esp_http_client_perform(http_client);
    free(token);
    //清理操作
    esp_http_client_cleanup(http_client);
    return err;
}

/**
 * 上报版本号
 * @param 无
 * @return 错误码
 */
esp_err_t onenet_ota_upload_version(void)
{
    //格式:{"s_version":"V1.3", "f_version": "V2.0"}
    char version_info[128];
    char url[256];
    esp_err_t ret = ESP_FAIL;
    //获取版本号
    const char* version = get_app_verion();
    //生成消息体内容(版本号)
    snprintf(version_info,sizeof(version_info),"{\"s_version\":\"%s\", \"f_version\": \"%s\"}",version,version);
    //计算url
    snprintf(url,256,ONENET_OTA_URL"/%s/%s/version",ONENET_PRODUCT_ID,ONENET_DEVICE_NAME);
    if(ESP_OK == onenet_ota_http_connect(url,HTTP_METHOD_POST,version_info))
    {
        cJSON *root = cJSON_Parse((const char*)data_buff);
        if(root)
        {
            cJSON* code_js =  cJSON_GetObjectItem(root,"code");
            if(code_js && cJSON_GetNumberValue(code_js) == 0)
                ret = ESP_OK;
            cJSON_Delete(root);
        }
    }
    else
    {
        ESP_LOGI(TAG,"Upload version fail!");
        return ret;
    }
    return ret;
}

查询升级任务状态我们先不做,这个查询升级任务状态主要是查这个升级任务是否被取消了。

4.1设备收到OTA升级通知后,需要给平台回复的消息,我们主要回复这个code:200就行了

/**
 * 返回OTA确认
 * @param code 错误码
 * @param message 信息
 * @return mqtt连接参数
 */
static void onenet_ota_ack(const char* id,int code,const char* message)
{
    char topic[128];
    snprintf(topic,sizeof(topic),"$sys/%s/%s/ota/inform_reply",ONENET_PRODUCT_ID,ONENET_DEVICE_NAME);

    cJSON *reply_js = cJSON_CreateObject();
    cJSON_AddStringToObject(reply_js,"id",id);
    cJSON_AddNumberToObject(reply_js,"code",code);
    cJSON_AddStringToObject(reply_js,"message",message);
    char* data = cJSON_PrintUnformatted(reply_js);
    esp_mqtt_client_publish(s_onenet_client,topic,data,strlen(data),1,0); 
    cJSON_free(data);
    cJSON_Delete(reply_js);
}

4.2查询升级任务信息获取任务id以及要升级到的版本号信息程序

这是我们要解析的数据,其中我只看code , data里面的target和tid

/**
 * 查询升级任务状态
 * @param type = 1,说明是完整包,type=2,说明是差分包
 * @param version 当前设备版本
 * @return 错误码
 */
esp_err_t  onenet_ota_check_task(const char* type,const char* version)
{
    char url[256];
    esp_err_t ret = ESP_FAIL;
    snprintf(url,256,ONENET_OTA_URL"/%s/%s/check?type=%s&version=%s",ONENET_PRODUCT_ID,ONENET_DEVICE_NAME,type,version);
    if(ESP_OK == onenet_ota_http_connect(url,HTTP_METHOD_GET,NULL))
    {
        cJSON *root =  cJSON_Parse((const char*)data_buff);
        if(root)
        {
            cJSON* code_js =  cJSON_GetObjectItem(root,"code");    //错误代码
            cJSON *data_js = cJSON_GetObjectItem(root,"data");
            cJSON* target_js = cJSON_GetObjectItem(data_js,"target");
            cJSON* tid_js = cJSON_GetObjectItem(data_js,"tid");
            if(code_js && cJSON_GetNumberValue(code_js) == 0)
            {
                if(target_js && tid_js)    //我们感兴趣的只有任务id和目标版本号
                {
                    snprintf(target_version,sizeof(target_version),"%s",cJSON_GetStringValue(target_js));
                    task_id = cJSON_GetNumberValue(tid_js);    //取出任务id
                    ret = ESP_OK;
                }
            }
            else 
            {
                ESP_LOGI(TAG,"Check ota task invaild code");
            }
            cJSON_Delete(root);
        }
        else
        {
            ESP_LOGI(TAG,"Check ota task fail!");
            return ret;
        }
    }
    else
    {
        return ret;
    }
    return ret;
}

5、设备上报一个升级任务状态(也就是升级进度百分比)

/*
设备可以根据升级包的下载情况,上报下载进度和下载结果。
下面是具体内容

POST http://iot-api.heclouds.com/fuse-ota/{pro_id}/{dev_name}/{tid}/status


Content-Type: application/json

Authorization:version=2022-05-01&res=userid%2F112&et=1662515432&method=sha1&sign=Pd14JLeTo77e0FOpKN8bR1INPLA%3D 

host:iot-api.heclouds.com

Content-Length:20


{"step":10} 

回复:
{
	"code": 0,
	"msg": "succ",
	"request_id": "**********"
}
这里依然只关注code字段,0表示成功,非0表示失败。
*/


esp_err_t onenet_ota_upload_status(int tid,int step)
{
    char url[256];
    char payload[32];
    esp_err_t ret = ESP_FAIL;
    snprintf(url,256,ONENET_OTA_URL"/%s/%s/%d/status",ONENET_PRODUCT_ID,ONENET_DEVICE_NAME,tid);
    snprintf(payload,sizeof(payload),"{\"step\":%d}",step);
    if(ESP_OK == onenet_ota_http_connect(url,HTTP_METHOD_POST,payload))
    {
        cJSON *root = cJSON_Parse((const char*)data_buff);
        if(root)
        {
            cJSON* code_js =  cJSON_GetObjectItem(root,"code");
            if(code_js && cJSON_GetNumberValue(code_js) == 0)
                ret = ESP_OK;
            cJSON_Delete(root);
        }
    }
    else
    {
        ESP_LOGI(TAG,"Upload status fail!");
        return ret;
    }
    return ret;
}

6、设备开始下载升级数据

/**
 * 启动ota下载
 * @param tid 升级任务id,通过查询升级任务可获取
 * @return 错误码
 */
/*
检查到升级任务后,设备根据任务信息进行升级包下载.

GET 
http://iot-api.heclouds.com/fuse-ota/{pro_id}/{dev_name}/{tid}/download

Authorization:version=2022-05-01&res=userid%2F112&et=1662515432&method=sha1&sign=Pd14JLeTo77e0FOpKN8bR1INPLA%3D

host:iot-api.heclouds.com

*/
esp_err_t onenet_ota_download(int tid)
{
    esp_err_t ota_finish_err = ESP_OK;
    char url[256];
    snprintf(url,sizeof(url),ONENET_OTA_URL"/%s/%s/%d/download",ONENET_PRODUCT_ID,ONENET_DEVICE_NAME,tid);
    esp_http_client_config_t config =
    {
        .url = url,
        .crt_bundle_attach = esp_crt_bundle_attach,
    };

    esp_https_ota_config_t ota_config = {
        .http_config = &config,
        .http_client_init_cb = http_ota_init_callback,
    };
    ota_finish_err = esp_https_ota(&ota_config);
    if(ota_finish_err == ESP_OK)
    {
        ESP_LOGI(TAG, "ESP_HTTPS_OTA upgrade successful. Rebooting ...");

    }
    else
    {
        ESP_LOGE(TAG, "ESP_HTTPS_OTA upgrade failed 0x%x", ota_finish_err);
    }
    return ota_finish_err;
}

9、连上onenet平台后,上报版本号,调用set_app_vaild函数标记程序合法,可以运行

升级完之后,我们定义,在连上onenet平台那一刻起即可验证我们的程序合法,因此后续在mqtt的回调函数中写入

case MQTT_EVENT_CONNECTED:
    {
        ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");
        
        onenet_subscribe();
        cJSON* property_js = onenet_property_upload(); //上传属性数据
        char * data = cJSON_PrintUnformatted(property_js); //转换为字符串
        onenet_post_property_data(data); //设备上线后,上传属性数据
        onenet_ota_upload_version(); //连接成功后上传固件版本号
        set_app_vaild(1); //设置当前应用为有效,取消回滚
        free(data);
        cJSON_Delete(property_js);
        break;
    }

ONENET平台操作

1.

2.改成1.0.2,生成一个1.0.2的版本用于我们升级测试,改完编译后,我们找到build目录下的onenet.bin文件,这个是我们应用的APP文件,我们一会要把这个文件上传到Onenet。

3.现在我们需要把ESP32S3的版本改为1.0.0,烧录到我们的设备上。

这样,设备上报的版本号就是1.0.0,平台检测到设备的版本号是1.0.0,而平台上面有一个更新的版本1.0.2,就判断出设备需要升级。

控制台操作验证升级

等一会设备就会收到Onenet平台的ota/inform通知了

大家可以留意ESP32S3的监控,如果有重启并且完成升级后上报的是1.0.2版本,则说明实验成功

结语

非原创仅供参考。

对单片机感兴趣但时常因为一个bug解决不了的,建议加群,我喜欢聚在一起唠嗑,有写好的有趣的小demo我也会分享在群里哦

 

更多推荐