ESP32S3连接ONENET无线OTA教程(跳过mqtt连接onenet平台)
照搬的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我也会分享在群里哦
更多推荐
所有评论(0)