From f607d8b9f708174eabc0731fd3f7442c08bfe97d Mon Sep 17 00:00:00 2001 From: chenjintao Date: Sun, 27 Sep 2026 18:28:05 +0800 Subject: [PATCH] feat: add local-staging FULLOTA2 flow --- docs/OTA_READY_FACTORY_BASELINE.md | 86 ++++-- .../threadx/interface/custom_app/Makefile | 1 + .../threadx/interface/custom_app/http_aofei.c | 4 +- .../interface/custom_app/inc/ota_fullfota2.h | 25 ++ .../threadx/interface/custom_app/mqtt.c | 22 +- .../interface/custom_app/ota_fullfota2.c | 272 ++++++++++++++++++ tools/verify_ota_ready_baseline.py | 31 ++ 7 files changed, 407 insertions(+), 34 deletions(-) create mode 100644 ql-application/threadx/interface/custom_app/inc/ota_fullfota2.h create mode 100644 ql-application/threadx/interface/custom_app/ota_fullfota2.c diff --git a/docs/OTA_READY_FACTORY_BASELINE.md b/docs/OTA_READY_FACTORY_BASELINE.md index f224efe..64177fd 100644 --- a/docs/OTA_READY_FACTORY_BASELINE.md +++ b/docs/OTA_READY_FACTORY_BASELINE.md @@ -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 { diff --git a/ql-application/threadx/interface/custom_app/Makefile b/ql-application/threadx/interface/custom_app/Makefile index eaa1846..05508c0 100644 --- a/ql-application/threadx/interface/custom_app/Makefile +++ b/ql-application/threadx/interface/custom_app/Makefile @@ -86,6 +86,7 @@ SRC_FILES+= \ sleep_wake.c \ battery.c \ gnss.c \ + ota_fullfota2.c \ #------------------------------------------------------------------------------- diff --git a/ql-application/threadx/interface/custom_app/http_aofei.c b/ql-application/threadx/interface/custom_app/http_aofei.c index 7330d12..8f00975 100644 --- a/ql-application/threadx/interface/custom_app/http_aofei.c +++ b/ql-application/threadx/interface/custom_app/http_aofei.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 diff --git a/ql-application/threadx/interface/custom_app/inc/ota_fullfota2.h b/ql-application/threadx/interface/custom_app/inc/ota_fullfota2.h new file mode 100644 index 0000000..2277d93 --- /dev/null +++ b/ql-application/threadx/interface/custom_app/inc/ota_fullfota2.h @@ -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 diff --git a/ql-application/threadx/interface/custom_app/mqtt.c b/ql-application/threadx/interface/custom_app/mqtt.c index ce8febe..028f054 100644 --- a/ql-application/threadx/interface/custom_app/mqtt.c +++ b/ql-application/threadx/interface/custom_app/mqtt.c @@ -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(); } diff --git a/ql-application/threadx/interface/custom_app/ota_fullfota2.c b/ql-application/threadx/interface/custom_app/ota_fullfota2.c new file mode 100644 index 0000000..64915ff --- /dev/null +++ b/ql-application/threadx/interface/custom_app/ota_fullfota2.c @@ -0,0 +1,272 @@ +#include +#include +#include +#include + +#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); + } +} diff --git a/tools/verify_ota_ready_baseline.py b/tools/verify_ota_ready_baseline.py index 0093ac1..e5bc966 100644 --- a/tools/verify_ota_ready_baseline.py +++ b/tools/verify_ota_ready_baseline.py @@ -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():