基本流程
说到IAP,其最核心的部分就是bootloader,bootloader的存在使得程序空间的更新可以脱离外部下载器,从而无需人为操作的介入。在我们这个项目中,由于W601已经内置了 RT-Thread 提供的 bootloader程序,因此我们仅需实现新固件的下载功能即可。
其自带的 bootloader 程序会在每次上电时检测 NOR FLASH 的 download 分区的数据,一旦该分区的数据满足其校验条件,则 bootloader 会提取其中的 APP 数据并且写入到应用程序空间(即W601片上FLASH 的 APP 数据区,写入完成后 bootloader 就会执行程序跳转,跳转到 APP 程序开始运行,至此一个完整的固件升级流程就完成了。其流程可参考下方流程图:
环境搭建
由于官方W601_IoT_Board SDK 中的 demo 已经为我们实现了很多基础功能,所以我们可以基于这些 demo 进行二次开发。而其中的 web_server (23_iot_web_server) 例程已经帮实现了绝大部分本次实践的基础功能如 文件系统、 FAL分区、SD卡、WIFI、Webserver,我们仅需实现对接到前端 Upload Tool 的握手、固件上传与显示版本号这三个接口即可,因此我们选择以该demo为基础进行开发。
其中握手和获取版本号这个功能使用到了Webnet的CGI功能,固件上传使用到了 UPLOAD 功能。因此我们需要将Webnet配置的 CGI 和 UPLOAD 功能开启。
同时在与前端的数据交互中使用到了Json数据结构,因此我们也需要加入 cJSON 这个软件包。
其余软件包或组件在官方 demo 中均已开启,因此我们无需再做任何配置。
基本原理
Upload Tool
Upload Tool 提供了可以给后台传输文件的前端的交互框架,其完整功能有如下几点:
- 后台版本号显示
- 固件上传
- 文件上传
- 文件夹上传
- 清空存储器
- 存储器容量显示
- 上传文件校验
后端可根据自身需求进行功能裁剪,并通过握手协议(下文会进行解释)通知到前端,前端会解析握手协议中的字段,将后端支持的功能打开并隐藏后端不支持功能对应的UI及逻辑。
上述接口的实现都是通过 HTTP 的 GET 或 POST 接口与后端进行交互。
握手、版本号显示、存储器容量显示这几个接口都是前端向对应的api路径发送GET请求,后端回复对应的JSON格式数据给前端进行解析及显示。
清空存储器、上传文件校验这两个接口则是使用POST请求,并且会带有对应的请求参数,后端解析前端发送的参数并处理,随后回复对应的JSON格式数据给前端进行解析及显示。
固件上传、文件上传与文件夹上传则是使用POST请求直接发送二进制数据,后端对这些二进制数据进行相应的文件逻辑操作,并在操作完成后发送JSON格式的响应给前端。
后端处理
后端基于 RT-Thread Webnet 软件包,仅需实现前端需要的几个接口即可。
在本项目中我们需要实现的接口有下面两种:
握手协议接口
该协议接口的对接需要使用 webnet 的 CGI 注册函数注册一个回调处理函数:
webnet_cgi_register("handshake", cgi_handshake);
当 webnet 截获前端对 /cgi-bin/handshake 这个接口的请求时会调用我们注册的 cgi_handshake 这个函数。这个函数就是将我们目前支持的功能打包成 JSON 格式的响应数据并发送给 Upload Tool 。
接口具体实现代码如下:
void cgi_handshake(struct webnet_session *session) {
char *json_data = NULL;
cJSON *root = cJSON_CreateObject();
cJSON *handshake = cJSON_CreateObject();
cJSON_AddItemToObject(root, "code", cJSON_CreateNumber(0));
cJSON_AddItemToObject(root, "handshake", handshake);
cJSON_AddItemToObject(handshake, "version", cJSON_CreateString(VERSION));
cJSON_AddItemToObject(handshake, "support_firmwareupload", cJSON_CreateBool(1));
cJSON_AddItemToObject(handshake, "support_fileupload", cJSON_CreateBool(0));
cJSON_AddItemToObject(handshake, "support_directoryupload", cJSON_CreateBool(0));
cJSON_AddItemToObject(handshake, "support_diskclean", cJSON_CreateBool(0));
cJSON_AddItemToObject(handshake, "support_diskfree", cJSON_CreateBool(0));
cJSON_AddItemToObject(handshake, "support_filecheck", cJSON_CreateBool(0));
json_data = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
cgi_head("json", 200, strlen(json_data));
webnet_session_printf(session, json_data);
if (json_data) rt_free(json_data);
}
Upload Tool 通过该接口来确认后端支持的功能,该接口需要后端回复一个JSON结构的数据,如下例所示:
{
"code": 0,
"handshake": {
"version": "V1.0.2",
"support_firmwareupload": true,
"support_fileupload": false,
"support_directoryupload": false,
"support_diskclean": false,
"support_diskfree": false,
"support_filecheck": false
}
}
各字段解析参考下表:
字段名 | 描述 |
code | 0 表示无异常 |
version | 固件版本号 |
support_firmwareupload | 是否支持固件升级 |
support_fileupload | 是否支持文件上传 |
support_directoryupload | 是否支持文件夹上传 |
support_diskclean | 是否支持存储器清空 |
support_diskfree | 是否支持存储器容量查询 |
support_filecheck | 是否支持上传文件校验 |
在打包响应数据时需要注意以下几点:
- 正常响应时 code 必须为 0,任何其他值都会被 Upload Tool 认为是异常,从而显示 read failed。
- 如果某项特性不被后端支持,除了将对应特性字段的值写为 false ,也可在响应握手数据时不包含对应字段,Upload Tool 会默认关闭握手数据中不存在的特性。
固件上传接口
固件上传功能需要对接 WebNet upload 模块的接口,将 upload 处理结构注册到 WebNet 内核中:
webnet_upload_add(&upload_bin_upload);
upload 处理结构如下所示:
const struct webnet_module_upload_entry upload_bin_upload = {
"/upload/app",
upload_open,
upload_close,
upload_write,
upload_done
};
其中第一个成员 /upload/app 为固件上传接口名, Upload Tool 在执行固件升级操作时会向该接口发送固件数据。
upload_open、upload_close、upload_write、upload_done 分别是 升级文件打开、升级文件关闭、升级中、升级结束的回调函数,WebNet 会在这些对应的固件上传阶段调用这些回调函数。
根据之前的分析可以了解到固件上传功能在后端实现的核心逻辑就是获取到前端传输的固件数据并且将其写入 SPI FLASH 的 download 分区。而上面四个接口就是用来实现这一核心功能。
upload_open
static int upload_open(struct webnet_session *session) {
const struct fal_partition *part = RT_NULL;
const char *file_name = RT_NULL;
file_name = get_file_name(session);
rt_kprintf("Upload file: %s\n", file_name);
if (webnet_upload_get_filename(session) != RT_NULL) {
part = fal_partition_find("download");
if (part == RT_NULL) {
// webnet_session_close(session);
goto _exit;
}
fal_partition_erase_all(part);
}
file_size = 0;
_exit:
return (int)part;
}
该接口会在文件上传开始前被调用,我们在该接口中获取了上传的文件名并打印出来,同时尝试寻找 download 分区,若分区存在则擦除整个分区待后续写入固件数据。
upload_write
static int upload_write(struct webnet_session *session, const void *data, rt_size_t length) {
const struct fal_partition *part = RT_NULL;
part = (const struct fal_partition *)webnet_upload_get_userdata(session);
if (part == RT_NULL) return 0;
fal_partition_write(part, file_size, data, length);
file_size += length;
return length;
}
该接口会在固件上传过程中被调用,我们直接将该接口传入的数据写入到 download 分区即可。须注意的是固件数据会分成多次进行传输,也就意味着该接口会在同一固件上传流程中被调用多次,因此我们需要记录数据偏移,确保每次写入都是在上一个数据的结尾。
upload_done
static int upload_done(struct webnet_session *session) {
const char *mimetype;
static char status[100];
/* get mimetype */
mimetype = mime_get_type(".html");
/* set http header */
session->request->result_code = 200;
rt_memset(status, 0, sizeof(status));
rt_snprintf(status, sizeof(status), "{\"code\":0,\"filesize\":%d}", file_size);
webnet_session_set_header(session, mimetype, 200, "Ok", rt_strlen(status));
webnet_session_printf(session, status, file_size);
firm_upload_done = 1;
return 0;
}
该接口会在数据都上传完成后调用,标志着固件升级已经结束。我们在该接口中会返回固件升级的结果状态以及最终接收到的总固件大小,Upload Tool 会获取这两个数据进行固件升级结果是否成功的判断。返回结果格式如下:
{
"code": 0,
"filesize": 259264
}
若 code 不为 0 或 filesize 与 Upload Tool 识别到的文件大小不同则会提示错误。
upload_close
static int upload_close(struct webnet_session *session) {
rt_kprintf("Upload FileSize: %d\n", file_size);
return 0;
}
该接口与 upload_open 接口成对,同样会在文件传输完成后被调用,处理文件的关闭逻辑,在这里我们并没有打开文件,因此无需关闭文件,在此只将最终接收到的文件大小打印出来。
至此固件上传接口就已经全部实现。但目前为止我们只是完成了从前端接收固件数据并且写到 download 分区,实际上固件并没有真正启动升级,而要触发升级操作我们就需要引导程序进入bootloader,因此可以看到在 upload_done 接口的最后有一行代码:
firm_upload_done = 1;
这个变量就是复位标志位,我们在主程序中会循环检测该标志,一旦发现标志有效则在经过1秒的延时后复位CPU,使其进入 bootloader 程序并完成升级。
while (1) {
if (firm_upload_done) {
LOG_W("catch firmware upload done flag, system will reset in 1 second to upgrade");
rt_thread_mdelay(1000);
rt_hw_cpu_reset();
}
rt_thread_mdelay(1000);
}
我们之所以不在 upload_done 接口中直接进行复位并且在主函数还要进行1秒的延时是因为我们在调用 webnet_session_printf 这个接口给前端发送响应时内核仅仅将待发送的数据拷贝到了缓存,实际并没有发送,我们需要等待我们的后端将响应数据成功发送才能进行复位,否则前端就无法得知本次的固件升级状态,从而影响到交互体验。
结语
至此,整个固件升级后端实现的流程就已讲解完成。虽然在这个项目中只有固件升级的功能,但是大家也可以参照此例程实现其他 Upload Tool 支持的功能。