feat: add local-staging FULLOTA2 flow
This commit is contained in:
@@ -12,22 +12,22 @@ OTA 所依赖的底层条件。
|
||||
- 保持 `customer_app` 为 460 KiB;
|
||||
- 将 APP 物理/XIP 地址固定为 `0x002C2000` / `0x802C2000`;
|
||||
- 保留 SDK 已有的 `QUEC_APP_FULL_FOTA` 与 FULLOTA2 公共 API;
|
||||
- 将现有 OTA 命令切换为最小本地暂存式 FULLOTA2;
|
||||
- 为设备和构建产物加入唯一布局身份;
|
||||
- 在构建前后执行 fail-closed 校验。
|
||||
|
||||
本基线暂不完成:
|
||||
|
||||
- 不把当前 `ql_fullfota_app_start()` 在线升级流程切换成 FULLOTA2;
|
||||
- 不增加签名 manifest、SHA-256 下载校验、断点续传或后台灰度;
|
||||
- 不提供 A/B APP 分区,也不承诺自动版本回滚;
|
||||
- 不在线更新 kernel、分区表或 `customer_fs`。
|
||||
|
||||
这样安排是为了先冻结量产后难以改变的 Flash 布局,同时避免在生产前仓促替换业务 OTA
|
||||
状态机。后续 FULLOTA2 逻辑只需通过 `customer_app` 版本迭代即可加入。
|
||||
这版保留现有后端的 `version + url` 命令格式,只改变设备端执行方式。网络包会完整写入
|
||||
`U:/FotaFile.bin`,通过 SDK FBF 校验并设置升级标志后才重启。
|
||||
|
||||
> 生产门禁:从本工厂基线开始量产,到 FULLOTA2 业务 APP 完成真机验收并正式发布之前,
|
||||
> 后端必须暂停向生产设备下发 OTA 命令。当前 `ql_fullfota_app_start()` 入口仍是原有
|
||||
> FULLOTA1 流程,新的分区基线不会自动消除其覆盖下载风险。
|
||||
> 生产门禁:完成本文的真机 FULLOTA2 正向、失败注入和断电测试前,后端不得向生产设备
|
||||
> 下发 OTA 命令。验收通过后可继续使用原协议下发;这条门禁针对测试状态,不再是因为
|
||||
> 设备仍调用 FULLOTA1。
|
||||
|
||||
## 2. 固定身份与 Flash 布局
|
||||
|
||||
@@ -55,7 +55,7 @@ ota_ready_fs640
|
||||
| fota_param | `0x003F7000` | `0x003FA000` | 12 KiB |
|
||||
| factory_a | `0x003FA000` | `0x00400000` | 24 KiB |
|
||||
|
||||
`customer_app` 是单槽。FULLOTA2 能保护下载阶段:文件未下载完整、哈希错误或 FBF 校验
|
||||
`customer_app` 是单槽。FULLOTA2 能保护下载阶段:文件未下载完整或 SDK FBF 校验
|
||||
失败时不会设置升级标志;但 updater 开始擦写 `customer_app` 后,没有旧 APP 槽可供回滚。
|
||||
|
||||
## 3. 本次修改
|
||||
@@ -90,6 +90,8 @@ ota_ready_fs640
|
||||
- kernel 必须继续定义 `QUEC_APP_FULL_FOTA`;
|
||||
- FULLOTA2 所需的 SDK API 必须存在;
|
||||
- `app.bin + 8192` 不得超过 460 KiB。
|
||||
- MQTT OTA 入口必须调用 `banban_fullfota2_start()`,不得回退到
|
||||
`ql_fullfota_app_start()`。
|
||||
|
||||
可独立执行:
|
||||
|
||||
@@ -99,6 +101,34 @@ ql-cross-tool\win32\owtoolchain\Python\python.exe tools\verify_ota_ready_baselin
|
||||
--app-bin ql-application\threadx\build\app.bin
|
||||
```
|
||||
|
||||
### 3.4 最小 FULLOTA2 执行路径
|
||||
|
||||
`ota_fullfota2.c` 使用独立任务完成同步 HTTP 下载。现有 MQTT 工作线程只负责参数校验、
|
||||
保存目标版本、回复 `accepted` 和启动任务,不会承担长时间网络读取。
|
||||
|
||||
执行顺序:
|
||||
|
||||
1. 删除上一次残留的 `U:/FotaFile.bin`;
|
||||
2. HTTP GET 必须返回 200 和非零 `Content-Length`;
|
||||
3. FBF 必须大于 8 KiB 且不超过 460 KiB;
|
||||
4. `U:` 空闲空间必须不小于 `Content-Length + 64 KiB`;
|
||||
5. 设置内部 Flash 固定暂存路径,初始化 FOTA context;
|
||||
6. 以 4096 字节缓冲顺序读取并调用 `ql_fota_image_write()`;
|
||||
7. 实际接收字节数必须严格等于 `Content-Length`;
|
||||
8. 调用 `ql_fota_image_flush()`,再用 `ql_stat()` 复核落盘文件长度;
|
||||
9. 调用 `ql_fota_image_verify()` 校验 FBF 并设置升级标志;
|
||||
10. deinit 后重启,由 updater 写入单槽 `customer_app`。
|
||||
|
||||
任一步在设置升级标志前失败,都会关闭 HTTP/FOTA context、删除暂存包、撤销固定升级模式、
|
||||
清除 pending 状态并把 `fota_start` 恢复为 0。旧 APP 未被擦写,可继续运行并接受下一次命令。
|
||||
|
||||
旧实现会在 OTA 前删除服务器管理的提示音。fs640 基线有足够空间后,这版不再主动删除
|
||||
业务资源;若空间不足则明确失败,不通过牺牲业务文件强行升级。
|
||||
|
||||
`http_aofei.c` 中未被当前业务调用的旧 HTTP 轮询 OTA 包装函数原本仍可进入 FULLOTA1,
|
||||
本基线将其显式禁用。生产固件只有 MQTT `version + url` 命令可以启动 OTA,避免两个入口
|
||||
使用不同的升级语义。
|
||||
|
||||
## 4. 构建和生产烧录
|
||||
|
||||
在 Windows SDK 构建环境中执行:
|
||||
@@ -156,39 +186,39 @@ ASR1605/CRANELRH,但量产包必须由 `EG800AKCN_11LG` 配置生成。
|
||||
4. 在写入 10%、50%、90% 时分别断电,重启后确认业务 APP 仍可运行并可清理残留文件。
|
||||
5. 验证正常业务文件占用后,仍满足 `FBF size + 64 KiB` 空闲门槛。
|
||||
|
||||
### 5.4 最小 FULLOTA2 冒烟测试
|
||||
### 5.4 最小 FULLOTA2 验收测试
|
||||
|
||||
正式量产前至少做一次 A 到 B 的本地暂存升级。B 可以只修改版本号:
|
||||
|
||||
1. 将 B 版 `customer_app.fbf` 顺序写入 `U:/FotaFile.bin`;
|
||||
2. 调用 `ql_fota_fixed_internal_flash_upgrade(1)`;
|
||||
3. 调用 `ql_fota_set_package_path()`、`ql_fota_init()`、`ql_fota_image_write()`、
|
||||
`ql_fota_image_flush()` 和 `ql_fota_image_verify()`;
|
||||
4. 重启,确认 updater 安装 B,且 `U:/FotaFile.bin` 被正确处理;
|
||||
5. 确认 NVM、设备身份、业务配置和预置音频未损坏;
|
||||
6. 下载损坏、截断和旧地址 FBF,确认均不能设置有效升级标志。
|
||||
1. 后端按现有协议下发 B 版 `version + url`;
|
||||
2. 串口必须看到 HTTP 长度、下载进度、FBF verify 成功和重启日志;
|
||||
3. 重启后确认 updater 安装 B,且 `U:/FotaFile.bin` 被正确处理;
|
||||
4. 确认 NVM、设备身份、业务配置和 14 个预置音频未损坏;
|
||||
5. 分别测试 404、无 Content-Length、空间不足、传输中断、截断 FBF、损坏 FBF 和旧地址
|
||||
FBF,确认旧 APP 保持可运行且下一次 OTA 可以重新开始;
|
||||
6. 在下载 10%、50%、90% 时断电,确认旧 APP 启动后清理残留状态并可再次升级;
|
||||
7. 在 `ql_fota_image_verify()` 成功后的 updater 擦写阶段断电,记录底层 updater 的实际恢复
|
||||
行为;当前单槽设计不能承诺该阶段自动回滚。
|
||||
|
||||
## 6. 后续 FULLOTA2 业务实现
|
||||
## 6. 后续 OTA 加固
|
||||
|
||||
后续 APP 版本建议按以下顺序加入,不再改变本基线分区:
|
||||
后续 APP 版本建议按以下顺序加固,不再改变本基线分区:
|
||||
|
||||
1. 下载所有权收敛到单一 OTA task,其他任务只投递数据或事件。
|
||||
2. 下载前停止对话、录音、播放和非必要 Flash 写入,保持 watchdog 正常喂狗。
|
||||
3. 删除确认不再使用的旧暂存文件。
|
||||
4. 检查 `free >= manifest.size + 65536`。
|
||||
5. 从偏移 0 顺序写入 `U:/FotaFile.bin`;首版不要宣称支持断点续传。
|
||||
6. 同时计算 SHA-256,并在下载完成后重新打开本地文件复核大小和 SHA-256。
|
||||
7. 在设置升级标志前持久化 `PENDING_REBOOT` 状态。
|
||||
8. 只有 `ql_fota_image_verify()` 成功后才重启。
|
||||
9. 新 APP 启动后核对版本、release sequence 和 layout,再上报健康状态。
|
||||
10. 失败时 deinit、删除残留文件、恢复业务,并上报明确错误码。
|
||||
1. 后端增加 size、SHA-256、layout ID 和单调递增 release sequence;
|
||||
2. 下载过程中计算 SHA-256,完成后重新打开本地文件复核;
|
||||
3. manifest 增加数字签名、公钥轮换和允许的 HTTPS host;
|
||||
4. 将单 pending 文件升级为双时隙、CRC 和递增 generation;
|
||||
5. 增加下载进度、稳定失败码和升级阶段上报;
|
||||
6. 增加按设备灰度、暂停和失败率熔断;
|
||||
7. 若未来硬件 Flash 扩容,再评估真正 A/B APP。
|
||||
|
||||
建议使用两个带 CRC 和递增 generation 的状态文件,例如 `U:/ota_state.a`、
|
||||
`U:/ota_state.b`。这能抵抗状态文件写入中断,但不构成固件 A/B 回滚。
|
||||
|
||||
## 7. 后端联调契约
|
||||
|
||||
现有 OTA 命令仅有 URL 和版本不足以可靠激活 FULLOTA2。建议后端增加:
|
||||
最小 FULLOTA2 继续兼容现有 URL 和版本协议,不要求后端立即修改。完成首轮生产验收后,
|
||||
建议后端增加:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -86,6 +86,7 @@ SRC_FILES+= \
|
||||
sleep_wake.c \
|
||||
battery.c \
|
||||
gnss.c \
|
||||
ota_fullfota2.c \
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
@@ -517,7 +517,9 @@ exit:
|
||||
|
||||
int http_ota_fw_download(char *url)
|
||||
{
|
||||
return ql_fullfota_app_start(url);
|
||||
(void)url;
|
||||
RLOGE("legacy HTTP polling FULLOTA1 is disabled; use MQTT FULLOTA2 command\r\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
typedef struct recv_buf
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#ifndef BANBAN_OTA_FULLOTA2_H
|
||||
#define BANBAN_OTA_FULLOTA2_H
|
||||
|
||||
typedef enum {
|
||||
BANBAN_FULLOTA2_OK = 0,
|
||||
BANBAN_FULLOTA2_ERR_ARGUMENT = -1,
|
||||
BANBAN_FULLOTA2_ERR_BUSY = -2,
|
||||
BANBAN_FULLOTA2_ERR_TASK = -3,
|
||||
BANBAN_FULLOTA2_ERR_HTTP = -4,
|
||||
BANBAN_FULLOTA2_ERR_LENGTH = -5,
|
||||
BANBAN_FULLOTA2_ERR_SPACE = -6,
|
||||
BANBAN_FULLOTA2_ERR_INIT = -7,
|
||||
BANBAN_FULLOTA2_ERR_WRITE = -8,
|
||||
BANBAN_FULLOTA2_ERR_FLUSH = -9,
|
||||
BANBAN_FULLOTA2_ERR_VERIFY = -10
|
||||
} banban_fullfota2_result_t;
|
||||
|
||||
typedef void (*banban_fullfota2_finished_cb_t)(int result);
|
||||
|
||||
int banban_fullfota2_start(const char *url,
|
||||
banban_fullfota2_finished_cb_t finished_cb);
|
||||
int banban_fullfota2_is_running(void);
|
||||
void banban_fullfota2_discard_staged_package(void);
|
||||
|
||||
#endif
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "cJSON.h"
|
||||
#include "build_macro.h"
|
||||
#include "ql_log.h"
|
||||
#include "ota_fullfota2.h"
|
||||
|
||||
|
||||
|
||||
@@ -242,6 +243,17 @@ static void ota_state_clear(void)
|
||||
}
|
||||
}
|
||||
|
||||
static void ota_fullfota2_finished(int result)
|
||||
{
|
||||
if (result == BANBAN_FULLOTA2_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
RLOGE("FULLOTA2 failed before activation: %d", result);
|
||||
ota_state_clear();
|
||||
fota_start = 0;
|
||||
}
|
||||
|
||||
static int ota_parse_version(const char *version, unsigned int parts[3])
|
||||
{
|
||||
char tail;
|
||||
@@ -1246,6 +1258,8 @@ static void ota_report_pending_success(void)
|
||||
compare_result != 0) {
|
||||
RLOGE("OTA pending state version mismatch: current=%s target=%s",
|
||||
current_version, state.target_version);
|
||||
banban_fullfota2_discard_staged_package();
|
||||
ota_state_clear();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1369,12 +1383,10 @@ static void handle_ota_upgrade(command_t *cmd)
|
||||
}
|
||||
}
|
||||
|
||||
/* Product requirement: keep deleting the configured audio files before OTA. */
|
||||
app_file_check_del();
|
||||
|
||||
ret = ql_fullfota_app_start(cmd->params.ota.url);
|
||||
ret = banban_fullfota2_start(cmd->params.ota.url,
|
||||
ota_fullfota2_finished);
|
||||
if (ret != 0) {
|
||||
RLOGE("start OTA failed: %d", ret);
|
||||
RLOGE("start FULLOTA2 task failed: %d", ret);
|
||||
fota_start = 0;
|
||||
ota_state_clear();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ql_fota.h"
|
||||
#include "ql_fs.h"
|
||||
#include "ql_power.h"
|
||||
#include "ql_rtos.h"
|
||||
#include "webclient.h"
|
||||
|
||||
#include "ota_baseline.h"
|
||||
#include "ota_fullfota2.h"
|
||||
|
||||
#define BANBAN_FULLOTA2_PACKAGE_PATH "U:/FotaFile.bin"
|
||||
#define BANBAN_FULLOTA2_URL_MAX 256U
|
||||
#define BANBAN_FULLOTA2_READ_BUFFER_SIZE 4096U
|
||||
#define BANBAN_FULLOTA2_TASK_STACK_SIZE (12U * 1024U)
|
||||
#define BANBAN_FULLOTA2_TASK_PRIORITY 100U
|
||||
#define BANBAN_FULLOTA2_HTTP_TIMEOUT_MS 30000
|
||||
#define BANBAN_FULLOTA2_FBF_OVERHEAD 8192U
|
||||
|
||||
typedef struct {
|
||||
char url[BANBAN_FULLOTA2_URL_MAX];
|
||||
banban_fullfota2_finished_cb_t finished_cb;
|
||||
} banban_fullfota2_request_t;
|
||||
|
||||
static ql_task_t g_fullfota2_task = NULL;
|
||||
static volatile int g_fullfota2_running = 0;
|
||||
static banban_fullfota2_request_t g_fullfota2_request;
|
||||
|
||||
static void banban_fullfota2_finish(int result,
|
||||
struct webclient_session *session,
|
||||
uint8_t *buffer,
|
||||
qlFotaImgProcCtxPtr fota)
|
||||
{
|
||||
banban_fullfota2_finished_cb_t finished_cb =
|
||||
g_fullfota2_request.finished_cb;
|
||||
|
||||
if (session != NULL) {
|
||||
webclient_close(session);
|
||||
}
|
||||
if (buffer != NULL) {
|
||||
web_free(buffer);
|
||||
}
|
||||
if (fota != NULL) {
|
||||
ql_fota_deinit(fota);
|
||||
}
|
||||
|
||||
if (result != BANBAN_FULLOTA2_OK) {
|
||||
ql_fota_fixed_internal_flash_upgrade(0);
|
||||
if (ql_access(BANBAN_FULLOTA2_PACKAGE_PATH, 0) == 0) {
|
||||
ql_remove(BANBAN_FULLOTA2_PACKAGE_PATH);
|
||||
}
|
||||
}
|
||||
|
||||
memset(&g_fullfota2_request, 0, sizeof(g_fullfota2_request));
|
||||
g_fullfota2_running = 0;
|
||||
g_fullfota2_task = NULL;
|
||||
|
||||
if (finished_cb != NULL) {
|
||||
finished_cb(result);
|
||||
}
|
||||
}
|
||||
|
||||
static void banban_fullfota2_task(void *argument)
|
||||
{
|
||||
struct webclient_session *session = NULL;
|
||||
qlFotaImgProcCtxPtr fota = NULL;
|
||||
uint8_t *buffer = NULL;
|
||||
int status;
|
||||
int content_length;
|
||||
int free_size;
|
||||
int read_length;
|
||||
int written = 0;
|
||||
int next_progress = 10;
|
||||
int result = BANBAN_FULLOTA2_ERR_HTTP;
|
||||
struct lfs_info staged_info;
|
||||
|
||||
(void)argument;
|
||||
|
||||
printf("[FULLOTA2] start url=%s path=%s\r\n",
|
||||
g_fullfota2_request.url,
|
||||
BANBAN_FULLOTA2_PACKAGE_PATH);
|
||||
|
||||
if (ql_access(BANBAN_FULLOTA2_PACKAGE_PATH, 0) == 0) {
|
||||
ql_remove(BANBAN_FULLOTA2_PACKAGE_PATH);
|
||||
}
|
||||
|
||||
session = webclient_session_create(2048);
|
||||
if (session == NULL) {
|
||||
result = BANBAN_FULLOTA2_ERR_HTTP;
|
||||
goto failed;
|
||||
}
|
||||
webclient_set_timeout(session, BANBAN_FULLOTA2_HTTP_TIMEOUT_MS);
|
||||
|
||||
status = webclient_get(session, g_fullfota2_request.url);
|
||||
if (status != 200) {
|
||||
printf("[FULLOTA2] HTTP GET failed status=%d\r\n", status);
|
||||
result = BANBAN_FULLOTA2_ERR_HTTP;
|
||||
goto failed;
|
||||
}
|
||||
|
||||
content_length = session->content_length;
|
||||
if (content_length <= (int)BANBAN_FULLOTA2_FBF_OVERHEAD ||
|
||||
content_length > (int)BANBAN_OTA_APP_PARTITION_SIZE) {
|
||||
printf("[FULLOTA2] invalid Content-Length=%d\r\n", content_length);
|
||||
result = BANBAN_FULLOTA2_ERR_LENGTH;
|
||||
goto failed;
|
||||
}
|
||||
|
||||
free_size = ql_fs_free_size('U');
|
||||
if (free_size < 0 ||
|
||||
(uint32_t)free_size <
|
||||
(uint32_t)content_length + BANBAN_OTA_STAGING_RESERVE_BYTES) {
|
||||
printf("[FULLOTA2] insufficient space free=%d required=%lu\r\n",
|
||||
free_size,
|
||||
(unsigned long)((uint32_t)content_length +
|
||||
BANBAN_OTA_STAGING_RESERVE_BYTES));
|
||||
result = BANBAN_FULLOTA2_ERR_SPACE;
|
||||
goto failed;
|
||||
}
|
||||
|
||||
ql_fota_fixed_internal_flash_upgrade(1);
|
||||
if (ql_fota_set_package_path(BANBAN_FULLOTA2_PACKAGE_PATH) != 0) {
|
||||
printf("[FULLOTA2] set package path failed\r\n");
|
||||
result = BANBAN_FULLOTA2_ERR_INIT;
|
||||
goto failed;
|
||||
}
|
||||
|
||||
fota = ql_fota_init();
|
||||
if (fota == NULL) {
|
||||
printf("[FULLOTA2] ql_fota_init failed\r\n");
|
||||
result = BANBAN_FULLOTA2_ERR_INIT;
|
||||
goto failed;
|
||||
}
|
||||
|
||||
buffer = web_malloc(BANBAN_FULLOTA2_READ_BUFFER_SIZE);
|
||||
if (buffer == NULL) {
|
||||
printf("[FULLOTA2] allocate read buffer failed\r\n");
|
||||
result = BANBAN_FULLOTA2_ERR_INIT;
|
||||
goto failed;
|
||||
}
|
||||
|
||||
while (written < content_length) {
|
||||
int request_length = content_length - written;
|
||||
int progress;
|
||||
|
||||
if (request_length > (int)BANBAN_FULLOTA2_READ_BUFFER_SIZE) {
|
||||
request_length = BANBAN_FULLOTA2_READ_BUFFER_SIZE;
|
||||
}
|
||||
|
||||
read_length = webclient_read(session, buffer, request_length);
|
||||
if (read_length <= 0) {
|
||||
printf("[FULLOTA2] body read failed ret=%d received=%d expected=%d\r\n",
|
||||
read_length, written, content_length);
|
||||
result = BANBAN_FULLOTA2_ERR_HTTP;
|
||||
goto failed;
|
||||
}
|
||||
|
||||
if (ql_fota_image_write(fota, buffer, read_length) != 0) {
|
||||
printf("[FULLOTA2] image write failed received=%d chunk=%d\r\n",
|
||||
written, read_length);
|
||||
result = BANBAN_FULLOTA2_ERR_WRITE;
|
||||
goto failed;
|
||||
}
|
||||
|
||||
written += read_length;
|
||||
progress = (written * 100) / content_length;
|
||||
if (progress >= next_progress || written == content_length) {
|
||||
printf("[FULLOTA2] download progress=%d%% bytes=%d/%d\r\n",
|
||||
progress, written, content_length);
|
||||
while (next_progress <= progress) {
|
||||
next_progress += 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (written != content_length) {
|
||||
result = BANBAN_FULLOTA2_ERR_LENGTH;
|
||||
goto failed;
|
||||
}
|
||||
|
||||
if (ql_fota_image_flush(fota) != 0) {
|
||||
printf("[FULLOTA2] image flush failed\r\n");
|
||||
result = BANBAN_FULLOTA2_ERR_FLUSH;
|
||||
goto failed;
|
||||
}
|
||||
|
||||
memset(&staged_info, 0, sizeof(staged_info));
|
||||
if (ql_stat(BANBAN_FULLOTA2_PACKAGE_PATH, &staged_info) != 0 ||
|
||||
staged_info.size != (uint32_t)content_length) {
|
||||
printf("[FULLOTA2] staged length mismatch file=%lu expected=%d\r\n",
|
||||
(unsigned long)staged_info.size, content_length);
|
||||
result = BANBAN_FULLOTA2_ERR_LENGTH;
|
||||
goto failed;
|
||||
}
|
||||
|
||||
if (ql_fota_image_verify(fota) != 0) {
|
||||
printf("[FULLOTA2] FBF verify/setflag failed\r\n");
|
||||
result = BANBAN_FULLOTA2_ERR_VERIFY;
|
||||
goto failed;
|
||||
}
|
||||
|
||||
printf("[FULLOTA2] package verified; rebooting into updater\r\n");
|
||||
banban_fullfota2_finish(BANBAN_FULLOTA2_OK, session, buffer, fota);
|
||||
ql_rtos_task_sleep_ms(200);
|
||||
ql_power_reset();
|
||||
|
||||
/* ql_power_reset() is not expected to return. Keep the task quiesced if it does. */
|
||||
while (1) {
|
||||
ql_rtos_task_sleep_s(60);
|
||||
}
|
||||
|
||||
failed:
|
||||
printf("[FULLOTA2] failed result=%d\r\n", result);
|
||||
banban_fullfota2_finish(result, session, buffer, fota);
|
||||
ql_rtos_task_delete(NULL);
|
||||
}
|
||||
|
||||
int banban_fullfota2_start(const char *url,
|
||||
banban_fullfota2_finished_cb_t finished_cb)
|
||||
{
|
||||
int ret;
|
||||
size_t url_length;
|
||||
|
||||
if (url == NULL || url[0] == '\0') {
|
||||
return BANBAN_FULLOTA2_ERR_ARGUMENT;
|
||||
}
|
||||
|
||||
url_length = strlen(url);
|
||||
if (url_length >= sizeof(g_fullfota2_request.url)) {
|
||||
return BANBAN_FULLOTA2_ERR_ARGUMENT;
|
||||
}
|
||||
|
||||
if (g_fullfota2_running) {
|
||||
return BANBAN_FULLOTA2_ERR_BUSY;
|
||||
}
|
||||
|
||||
memset(&g_fullfota2_request, 0, sizeof(g_fullfota2_request));
|
||||
memcpy(g_fullfota2_request.url, url, url_length + 1);
|
||||
g_fullfota2_request.finished_cb = finished_cb;
|
||||
g_fullfota2_running = 1;
|
||||
|
||||
ret = ql_rtos_task_create(&g_fullfota2_task,
|
||||
BANBAN_FULLOTA2_TASK_STACK_SIZE,
|
||||
BANBAN_FULLOTA2_TASK_PRIORITY,
|
||||
"ota_fullfota2",
|
||||
banban_fullfota2_task,
|
||||
NULL);
|
||||
if (ret != 0) {
|
||||
memset(&g_fullfota2_request, 0, sizeof(g_fullfota2_request));
|
||||
g_fullfota2_running = 0;
|
||||
g_fullfota2_task = NULL;
|
||||
return BANBAN_FULLOTA2_ERR_TASK;
|
||||
}
|
||||
|
||||
return BANBAN_FULLOTA2_OK;
|
||||
}
|
||||
|
||||
int banban_fullfota2_is_running(void)
|
||||
{
|
||||
return g_fullfota2_running ? 1 : 0;
|
||||
}
|
||||
|
||||
void banban_fullfota2_discard_staged_package(void)
|
||||
{
|
||||
if (!g_fullfota2_running &&
|
||||
ql_access(BANBAN_FULLOTA2_PACKAGE_PATH, 0) == 0) {
|
||||
ql_remove(BANBAN_FULLOTA2_PACKAGE_PATH);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,10 @@ KERNEL_FLAGS = ROOT / "ql-kernel/threadx/config/common/default_dflags_1602.mk"
|
||||
FOTA_HEADER = ROOT / "ql-application/threadx/common/include/ql_fota.h"
|
||||
BASELINE_HEADER = ROOT / "ql-application/threadx/interface/custom_app/inc/ota_baseline.h"
|
||||
BUILD_MACRO = ROOT / "ql-application/threadx/interface/custom_app/lib/hal/build_macro.h"
|
||||
APP_MAKEFILE = ROOT / "ql-application/threadx/interface/custom_app/Makefile"
|
||||
MQTT_SOURCE = ROOT / "ql-application/threadx/interface/custom_app/mqtt.c"
|
||||
FULLOTA2_SOURCE = ROOT / "ql-application/threadx/interface/custom_app/ota_fullfota2.c"
|
||||
LEGACY_HTTP_SOURCE = ROOT / "ql-application/threadx/interface/custom_app/http_aofei.c"
|
||||
|
||||
FLASH_SIZE = 0x400000
|
||||
EXPECTED = {
|
||||
@@ -139,6 +143,33 @@ def verify_fota_contract() -> None:
|
||||
if '#define CUSTOM_APP_OTA_BUILD_PROFILE "ota_ready_fs640"' not in generated:
|
||||
fail("generated build macro has the wrong OTA build profile")
|
||||
|
||||
makefile = APP_MAKEFILE.read_text(encoding="utf-8", errors="replace")
|
||||
if "ota_fullfota2.c" not in makefile:
|
||||
fail("minimal FULLOTA2 implementation is not part of the application build")
|
||||
mqtt = MQTT_SOURCE.read_text(encoding="utf-8", errors="replace")
|
||||
if "banban_fullfota2_start" not in mqtt:
|
||||
fail("MQTT OTA command does not start the local-staging FULLOTA2 path")
|
||||
if "ql_fullfota_app_start(cmd->params.ota.url)" in mqtt:
|
||||
fail("MQTT OTA command still invokes the legacy FULLOTA1 path")
|
||||
legacy_http = LEGACY_HTTP_SOURCE.read_text(encoding="utf-8", errors="replace")
|
||||
if "return ql_fullfota_app_start(url)" in legacy_http:
|
||||
fail("legacy HTTP polling path can still invoke FULLOTA1")
|
||||
implementation = FULLOTA2_SOURCE.read_text(encoding="utf-8", errors="replace")
|
||||
required_steps = (
|
||||
"ql_fs_free_size('U')",
|
||||
"ql_fota_fixed_internal_flash_upgrade(1)",
|
||||
"ql_fota_set_package_path",
|
||||
"ql_fota_init",
|
||||
"ql_fota_image_write",
|
||||
"ql_fota_image_flush",
|
||||
"ql_stat",
|
||||
"ql_fota_image_verify",
|
||||
"ql_power_reset",
|
||||
)
|
||||
positions = [implementation.find(step) for step in required_steps]
|
||||
if any(position < 0 for position in positions) or positions != sorted(positions):
|
||||
fail("minimal FULLOTA2 prepare/verify/activate sequence is incomplete or reordered")
|
||||
|
||||
|
||||
def verify_app(path: Path) -> None:
|
||||
if not path.is_file():
|
||||
|
||||
Reference in New Issue
Block a user