diff --git a/OTA_IMPLEMENTATION_ROADMAP.md b/OTA_IMPLEMENTATION_ROADMAP.md new file mode 100644 index 0000000..e322d93 --- /dev/null +++ b/OTA_IMPLEMENTATION_ROADMAP.md @@ -0,0 +1,441 @@ +# OTA 稳定性改造实施路线 + +## 1. 目标与原则 + +本路线的核心目标是:先利用现有 App OTA 发布一个风险尽可能小的桥接版本,建立可靠的升级控制、状态上报和灰度发布能力;随后验证系统级 OTA,修复 Kernel 和 Mini FOTA 中无法通过 App OTA 解决的问题;最后根据 Flash 空间和硬件条件决定是否实施 A/B 分区。 + +实施过程中遵循以下原则: + +- 第一次桥接升级尽量小,不同时引入大量业务改动。 +- 固件升级和音频资源更新相互独立,任何一方失败都不影响另一方。 +- 每个阶段先小批量验证,通过验收门槛后再扩大范围。 +- 所有升级必须可追踪、可暂停,并准备串口烧录救援方案。 +- 在系统 OTA 和回滚能力验证完成前,不假设设备具备可靠的自动恢复能力。 + +## 2. 当前能力边界 + +当前生成的 `fota.bin` 只包含 `customer_app.bin`,属于 App-only OTA,可更新正常业务程序,但不能更新: + +- Kernel; +- Mini FOTA 系统; +- Bootloader; +- 分区表; +- `customer_fs` 文件系统镜像; +- GNSS、DSP、RF 等底层固件。 + +当前设备没有明确的标准 A/B 应用分区。Mini FOTA 会下载并写入 `customer_app`,底层备份和回滚能力尚未得到完整验证。设备进入 FOTA 后,如果下载地址长期不可用,存在持续重启和无法进入正常业务程序的风险。 + +## 3. 推荐实施顺序 + +```text +阶段 0:建设发布基础设施和救援能力 + ↓ +阶段 1:发布最小 App 桥接版本 + ↓ +阶段 2:独立上线音频资源同步 + ↓ +阶段 3:实验室验证系统级 OTA + ↓ +阶段 4:更新 Kernel 和 Mini FOTA + ↓ +阶段 5:建立启动健康确认和失败恢复 + ↓ +阶段 6:评估并实施 A/B 或备份恢复方案 +``` + +--- + +## 4. 阶段 0:发布基础设施和救援准备 + +### 目标 + +在不升级设备的前提下,降低第一次桥接 OTA 的发布风险。 + +### 工作内容 + +1. 固定 OTA 文件地址,发布后禁止覆盖、删除或改名。 +2. 对固件下载服务器增加可用性、响应时间和错误率监控。 +3. 为每个固件保存以下发布信息: + - 产品型号; + - 硬件版本范围; + - 软件版本; + - 构建时间; + - Git commit; + - 文件大小; + - SHA-256; + - 下载地址。 +4. 服务端支持: + - 测试设备白名单; + - 分批灰度; + - 暂停下发; + - 防止重复下发; + - 成功率和失败率统计。 +5. 准备专用测试设备和串口烧录救援流程。 +6. 使用当前旧 FOTA 测试以下场景: + - 弱网和网络切换; + - DNS 失败; + - 下载前断网; + - 下载过程中断网; + - HTTP 404、500 和超时; + - 重复下发相同升级命令; + - 下载服务器临时不可用。 + +### 验收门槛 + +- 可以只向指定测试设备下发 OTA。 +- 可以随时停止后续批次。 +- 固件文件能够长期稳定访问。 +- 能通过设备 ID、任务 ID 和版本号追踪升级结果。 +- 已验证串口烧录恢复流程。 + +--- + +## 5. 阶段 1:最小 App 桥接版本 + +### 目标 + +整理正常 App 内的 OTA 控制逻辑,为后续系统 OTA、资源同步和结果追踪建立可靠入口。 + +### 5.1 统一 OTA 启动入口 + +- OTA 只允许由一个明确的业务入口启动。 +- `parse_command()` 只解析和校验消息,不执行延时、删除文件、重启或调用 FOTA。 +- 删除当前解析阶段和处理阶段可能重复启动 OTA 的设计。 +- 检查并处理 `ql_fullfota_app_start()` 的返回值。 +- OTA 准备期间拒绝或合并重复升级命令。 + +### 5.2 建立 OTA 状态机 + +建议至少包含以下状态: + +```text +IDLE +RECEIVED +VALIDATED +ACCEPTED +PRECHECKING +STARTING +REBOOTING +FIRST_BOOT +SUCCEEDED +FAILED +``` + +状态变化和任务信息应持久化,避免重启后丢失上下文。 + +### 5.3 校验升级命令 + +升级前至少校验: + +- OTA 任务 ID; +- 目标版本; +- 产品型号; +- 硬件版本; +- 固件类型; +- URL 协议; +- 域名白名单; +- 允许的端口和路径; +- 声明的文件大小; +- 声明的 SHA-256; +- 是否为重复升级; +- 是否属于未授权降级。 + +说明:正常 App 可以校验命令和服务器声明,但实际固件由重启后的旧 Mini FOTA 下载。在 Mini FOTA 支持 SHA-256 前,App 不能替代它对最终下载内容进行完整哈希校验。 + +### 5.4 OTA 前置检查 + +调用 FOTA 前检查: + +- SIM 和网络注册状态; +- PDP 是否正常; +- DNS 是否能够解析; +- 下载地址是否可以建立连接; +- HTTP 是否返回 200; +- `Content-Length` 是否合理; +- 电池、电压或外部供电是否满足升级条件; +- 当前是否正在执行通话等关键业务。 + +推荐执行顺序: + +```text +解析命令 +→ 校验任务 +→ 网络和 URL 预检查 +→ 持久化任务信息 +→ 向服务器上报已接受 +→ 确认消息发送完成 +→ 调用 FOTA 接口 +→ 重启进入 Mini FOTA +``` + +预检查只能过滤明显的 URL、404 和服务器故障,不能保证重启后的 Mini FOTA 网络仍然正常。 + +### 5.5 升级结果补报 + +新版本第一次启动后读取持久化的 OTA 任务信息,上报: + +- 任务 ID; +- 原版本; +- 目标版本; +- 当前实际版本; +- 开始和结束时间; +- 最终结果; +- 最后错误码; +- 是否发生异常重启。 + +暂时无法联网时,应保留结果并在下次连接后补报。 + +### 5.6 音频与 OTA 解耦 + +- 删除 OTA 前调用 `app_file_check_del()` 的行为。 +- FBF 下载和 updater 安装期间不清空现有音频。 +- 新 APP 启动、版本确认等于 OTA 目标且网络就绪后,才删除服务器管理的音频并立即补下载。 +- `barge_in_cute.mp3` 纳入删除和补下载范围;发布前必须确认固定资源地址可用。 +- 音频下载失败不得影响固件升级结果。 +- 固件升级失败不得破坏原有可用音频。 + +### 第一版明确不做 + +- 不修改分区表; +- 不更新 Kernel 或 Mini FOTA; +- 不修改 Bootloader; +- 不替换整个 `customer_fs`; +- 不同时进行大规模业务重构; +- 不引入独立音频 manifest、SHA-256 或签名协议。 + +### 验收门槛 + +- 单条 OTA 命令只触发一次 FOTA。 +- 无效 URL、错误型号和重复版本不会导致重启。 +- 设备重启前能够上报已接受状态。 +- 新版本启动后能够补报最终结果。 +- OTA 下载失败时原有音频不被删除。 +- 新版本确认成功后,服务器管理的音频被刷新且缺失文件能够在后续联网时继续补下载。 +- 小批量设备升级成功率达到预定标准后才能扩大灰度。 + +--- + +## 6. 阶段 2:独立音频资源同步 + +### 目标 + +在不更新整个 `customer_fs.bin` 的情况下,安全地下发新增音频,并确保资源失败不会影响设备核心业务。 + +### 6.1 资源清单 + +每个资源至少记录: + +- 资源版本; +- 文件名; +- 下载 URL; +- 文件大小; +- SHA-256; +- 是否必需; +- 最低 App 版本。 + +### 6.2 单文件下载流程 + +```text +检查 LittleFS 剩余空间 +→ 下载到临时文件 +→ 检查实际文件大小 +→ 校验 SHA-256 +→ 校验文件基本格式 +→ 原子重命名为正式文件 +→ 更新本地资源状态 +``` + +### 6.3 可靠性要求 + +- 不能仅以“文件存在”判断下载成功。 +- 下载中断后不能留下被当作有效资源的半文件。 +- 新文件校验失败时保留原文件。 +- 音频同步在后台低优先级执行,不阻塞 MQTT 上线。 +- 每个文件独立记录重试次数,采用退避重试。 +- 写入前计算 LittleFS 剩余空间并保留安全余量。 +- 7 条超时提示音只从已校验有效的文件中随机选择。 +- 新增资源全部不可用时,继续使用原 `tts_error.mp3` 作为兜底。 + +### 验收门槛 + +- 断网、空间不足或校验失败不会破坏现有音频。 +- 半文件在重启后能够被识别并重新下载。 +- 资源服务器不可用时设备仍能正常连接 MQTT。 +- 新增 6 条音频全部写入后,LittleFS 仍保留规定的安全空间。 + +--- + +## 7. 阶段 3:实验室验证系统级 OTA + +### 目标 + +确认 SDK 的系统 FOTA 是否能够安全更新 Kernel、Mini FOTA 和相关底层组件。 + +### 验证内容 + +1. 确认系统升级包的生成工具、输入文件和版本基线。 +2. 确认升级包实际包含和修改的分区。 +3. 确认是整包升级还是指定旧版本到新版本的差分升级。 +4. 验证暂存空间、RAM 和 Flash 空间是否满足要求。 +5. 验证以下异常场景: + - 下载前断网; + - 下载中断网; + - Flash 写入期间断电; + - URL 返回 404 或 500; + - 文件被截断; + - checksum 或签名错误; + - 固件型号不匹配; + - 版本基线不匹配; + - 新系统无法正常启动; + - 连续多次升级失败。 +6. 确认失败后设备是否能恢复旧系统或进入明确的恢复模式。 + +此阶段可以与桥接版本研发并行,但系统 OTA 的正式上线必须在桥接版本稳定之后。 + +### 验收门槛 + +- 明确系统包的生成、签名、校验和发布流程。 +- 明确升级会修改哪些分区。 +- 关键断电和断网测试不会造成设备永久失联。 +- 失败原因可以被持久化和上报。 +- 已具备批量发布所需的恢复方案。 + +--- + +## 8. 阶段 4:更新 Kernel 和 Mini FOTA + +### 目标 + +解决当前 App OTA 无法修复的底层下载、重试、恢复和安全问题。 + +### 工作内容 + +1. 持久化 FOTA 失败次数。 +2. 设置单次启动和跨重启的最大重试次数。 +3. 网络错误使用指数退避,不再主要依靠重启设备进行重试。 +4. 区分并记录以下错误: + - 网络注册失败; + - PDP 激活失败; + - DNS 失败; + - TCP/TLS 连接失败; + - HTTP 非 200; + - 下载超时; + - 文件格式错误; + - Flash 擦除或写入失败; + - checksum 或签名失败。 +5. 超过重试上限后清除 FOTA 状态,恢复进入旧 App。 +6. 正常 App 启动后上报失败阶段和错误码。 +7. 评估并启用可靠的断点续传。 +8. 支持备用下载地址或可靠的服务端容灾。 +9. 使用 HTTPS,并正确校验证书。 +10. 对升级包进行数字签名验证。 +11. 验证产品型号、硬件版本和升级基线。 + +“超过重试上限后恢复旧 App”应优先于 A/B 实施,用于直接解决当前可能无限重启的问题。 + +### 验收门槛 + +- 永久不可用的 URL 不会导致设备无限重启。 +- 达到重试上限后能够恢复正常 App 或进入明确恢复模式。 +- 异常原因能够被持久化并在联网后上报。 +- 非法、损坏或型号不匹配的固件不会被安装。 + +--- + +## 9. 阶段 5:启动健康确认与失败恢复 + +### 目标 + +不再把“固件写入完成”直接等同于“升级成功”。 + +新版本启动后至少检查: + +- 文件系统挂载成功; +- 关键配置读取成功; +- 关键线程正常运行; +- 看门狗状态正常; +- 网络能够注册; +- MQTT 能够连接; +- 设备版本和 OTA 任务可以正常上报; +- 没有连续崩溃或反复重启。 + +只有达到健康条件后才确认升级成功。 + +如果多次启动仍不能确认健康: + +- 具备 A/B 时回滚到旧分区; +- 不具备 A/B 时恢复备份 App 或进入恢复模式。 + +--- + +## 10. 阶段 6:A/B 或备份恢复方案 + +### 优先方案:标准 A/B + +```text +app_a +app_b +boot metadata +持久化升级状态 +启动健康确认 +自动回滚 +``` + +A/B 实施前必须确认: + +- Flash 是否能够容纳两个完整 App; +- Bootloader 是否支持分区选择和回滚; +- 分区表如何迁移; +- 现有设备是否能够在线完成分区变更; +- 工厂烧录包和售后工具是否同步支持新布局。 + +### 空间不足时的次优方案 + +```text +单 App 分区 ++ 下载暂存区 ++ 旧 App 备份区 ++ 持久化失败计数 ++ 启动健康确认 ++ 明确恢复模式 +``` + +如果现有设备必须修改分区表才能实现上述方案,不应默认可以通过当前 App OTA 安全完成。部分设备可能需要工厂重新烧录。 + +--- + +## 11. 灰度发布建议 + +每个阶段建议按以下顺序发布: + +1. 开发板和可串口救援设备; +2. 内部测试设备; +3. 少量真实网络环境设备; +4. 1% 灰度; +5. 5% 灰度; +6. 20% 灰度; +7. 全量发布。 + +每一批次至少观察: + +- 命令接收率; +- 开始升级率; +- 下载成功率; +- 平均重试次数; +- 最终版本到达率; +- 异常重启率; +- 设备重新上线时间; +- 音频和文件系统异常率。 + +出现以下任一情况应暂停后续批次: + +- 设备持续重启; +- 升级后无法上线; +- 失败率超过预设阈值; +- 出现无法自动恢复的设备; +- 固件文件、下载服务或 MQTT 服务异常; +- 发现型号、版本或升级包对应错误。 + +## 12. 最终结论 + +当前最合适的路线不是立即实现 A/B,而是先发布 OTA-ready 工厂基线和最小 FULLOTA2,修复 OTA 入口、命令校验、预检查、状态持久化和结果补报。音频删除已从 OTA 前移到新 APP 版本确认成功且网络就绪之后,并由现有缺失文件下载器补回;后续再升级为带版本、SHA-256 和原子替换的独立资源清单。然后在实验设备上验证系统级 OTA,通过系统 OTA 修复 Mini FOTA 的恢复和安全校验问题;最后根据未来 Flash 空间决定是否采用标准 A/B。 diff --git a/docs/OTA_READY_FACTORY_BASELINE.md b/docs/OTA_READY_FACTORY_BASELINE.md index c7ca8bf..cac985d 100644 --- a/docs/OTA_READY_FACTORY_BASELINE.md +++ b/docs/OTA_READY_FACTORY_BASELINE.md @@ -127,8 +127,12 @@ ql-cross-tool\win32\owtoolchain\Python\python.exe tools\verify_ota_ready_baselin 任一步在设置升级标志前失败,都会关闭 HTTP/FOTA context、删除暂存包、撤销固定升级模式、 清除 pending 状态并把 `fota_start` 恢复为 0。旧 APP 未被擦写,可继续运行并接受下一次命令。 -旧实现会在 OTA 前删除服务器管理的提示音。fs576 基线有足够空间后,这版不再主动删除 -业务资源;若空间不足则明确失败,不通过牺牲业务文件强行升级。 +旧实现会在 OTA 下载前删除服务器管理的提示音,下载失败时会提前损失业务资源。当前实现 +在下载和 updater 安装阶段保留全部音频;只有新 APP 启动、当前版本确认等于 OTA 目标版本且 +数据网络就绪后,才删除服务器管理的提示音并立即调用缺失文件补下载。删除完成标志复用 +pending OTA 状态中的 flags 保存,避免 MQTT 重连时反复删除。`barge_in_cute.mp3` 也参与该 +刷新流程;固定资源地址必须在发布前确认返回 HTTP 200。若 FBF 暂存空间不足则直接拒绝 +OTA,不再通过升级前删除音频强行腾出空间。 `http_aofei.c` 中未被当前业务调用的旧 HTTP 轮询 OTA 包装函数原本仍可进入 FULLOTA1, 本基线将其显式禁用。生产固件只有 MQTT `version + url` 命令可以启动 OTA,避免两个入口 diff --git a/ql-application/threadx/interface/custom_app/http_downfile.c b/ql-application/threadx/interface/custom_app/http_downfile.c index 8036404..c307f6e 100644 --- a/ql-application/threadx/interface/custom_app/http_downfile.c +++ b/ql-application/threadx/interface/custom_app/http_downfile.c @@ -182,13 +182,12 @@ void app_file_check_download(void) int index = 1; char url_buff[128]={0x00}; char filename_buff[64]={0x00}; - int ret = 0, cnt = 0; + int ret = 0; for(index=1;index<20;index++){ + int cnt = 0; - /* barge_in_cute.mp3 is shipped in customer_fs and is not hosted by the - * legacy prompt download service. */ - if(index==4||index==5||index==6||index==7||index==8||index==19){ + if(index==4||index==5||index==6||index==7||index==8){ continue; } @@ -213,9 +212,7 @@ void app_file_check_del(void) for(index=1;index<20;index++){ - /* Keep the bundled barge-in prompt across app-only FOTA. Unlike the - * server-managed prompts, it cannot be restored by the downloader. */ - if(index==4||index==5||index==6||index==7||index==8||index==19){ + if(index==4||index==5||index==6||index==7||index==8){ continue; } diff --git a/ql-application/threadx/interface/custom_app/inc/http_downfile.h b/ql-application/threadx/interface/custom_app/inc/http_downfile.h new file mode 100644 index 0000000..75c4668 --- /dev/null +++ b/ql-application/threadx/interface/custom_app/inc/http_downfile.h @@ -0,0 +1,7 @@ +#ifndef BANBAN_HTTP_DOWNFILE_H +#define BANBAN_HTTP_DOWNFILE_H + +void app_file_check_download(void); +void app_file_check_del(void); + +#endif diff --git a/ql-application/threadx/interface/custom_app/inc/ota_post_update.h b/ql-application/threadx/interface/custom_app/inc/ota_post_update.h new file mode 100644 index 0000000..0e08a3f --- /dev/null +++ b/ql-application/threadx/interface/custom_app/inc/ota_post_update.h @@ -0,0 +1,11 @@ +#ifndef BANBAN_OTA_POST_UPDATE_H +#define BANBAN_OTA_POST_UPDATE_H + +/* + * Prepare server-managed audio refresh only after the running APP version + * matches the persisted OTA target. Returns 1 when files were removed for + * refresh, 0 when no refresh is pending, and a negative value on error. + */ +int ota_prepare_post_update_audio_refresh(void); + +#endif diff --git a/ql-application/threadx/interface/custom_app/mqtt.c b/ql-application/threadx/interface/custom_app/mqtt.c index 16cc6b3..fa565c2 100644 --- a/ql-application/threadx/interface/custom_app/mqtt.c +++ b/ql-application/threadx/interface/custom_app/mqtt.c @@ -21,7 +21,9 @@ #include "cJSON.h" #include "build_macro.h" #include "ql_log.h" +#include "http_downfile.h" #include "ota_fullfota2.h" +#include "ota_post_update.h" #include "ota_url_policy.h" @@ -45,6 +47,7 @@ #define OTA_STATE_TMP_FILE "U:/ota_state.tmp" #define OTA_STATE_MAGIC 0x4F544131U #define OTA_STATE_FORMAT_VERSION 1U +#define OTA_STATE_FLAG_AUDIO_REFRESH_PREPARED 0x0001U #define OTA_PROMPT_PLAY_TIMEOUT_MS 10000 #define OTA_PROMPT_POLL_MS 50 @@ -110,7 +113,7 @@ int xiaoice_get_play_counter(void); typedef struct { uint32_t magic; uint16_t format_version; - uint16_t reserved; + uint16_t flags; char previous_version[16]; char target_version[16]; uint32_t checksum; @@ -147,12 +150,56 @@ static uint32_t ota_state_checksum(const ota_state_t *state) return hash; } + +static int ota_state_write(const ota_state_t *state) +{ + ota_state_t persisted; + QFILE *fp; + int ret; + + if (state == NULL) { + return -1; + } + + persisted = *state; + persisted.checksum = 0; + persisted.checksum = ota_state_checksum(&persisted); + + fp = ql_fopen(OTA_STATE_TMP_FILE, "wb+"); + if (fp == NULL) { + RLOGE("OTA state open failed: %s", OTA_STATE_TMP_FILE); + return -1; + } + + ret = ql_fwrite(&persisted, sizeof(persisted), 1, fp); + if (ret != (int)sizeof(persisted)) { + RLOGE("OTA state write failed: %d", ret); + ql_fclose(fp); + ql_remove(OTA_STATE_TMP_FILE); + return -1; + } + + ret = ql_fsync(fp); + ql_fclose(fp); + if (ret != 0) { + RLOGE("OTA state sync failed: %d", ret); + ql_remove(OTA_STATE_TMP_FILE); + return -1; + } + + ret = ql_rename(OTA_STATE_TMP_FILE, OTA_STATE_FILE); + if (ret != 0) { + RLOGE("OTA state rename failed: %d", ret); + ql_remove(OTA_STATE_TMP_FILE); + return -1; + } + + return 0; +} static int ota_state_save(const char *previous_version, const char *target_version) { ota_state_t state; - QFILE *fp; - int ret; if (previous_version == NULL || target_version == NULL) { return -1; @@ -163,38 +210,7 @@ static int ota_state_save(const char *previous_version, const char *target_versi state.format_version = OTA_STATE_FORMAT_VERSION; strncpy(state.previous_version, previous_version, sizeof(state.previous_version) - 1); strncpy(state.target_version, target_version, sizeof(state.target_version) - 1); - state.checksum = ota_state_checksum(&state); - - fp = ql_fopen(OTA_STATE_TMP_FILE, "wb+"); - if (fp == NULL) { - RLOGE("OTA state open failed: %s", OTA_STATE_TMP_FILE); - return -1; - } - - ret = ql_fwrite(&state, sizeof(state), 1, fp); - if (ret != (int)sizeof(state)) { - RLOGE("OTA state write failed: %d", ret); - ql_fclose(fp); - ql_remove(OTA_STATE_TMP_FILE); - return -1; - } - - ret = ql_fsync(fp); - ql_fclose(fp); - if (ret != 0) { - RLOGE("OTA state sync failed: %d", ret); - ql_remove(OTA_STATE_TMP_FILE); - return -1; - } - - ret = ql_rename(OTA_STATE_TMP_FILE, OTA_STATE_FILE); - if (ret != 0) { - RLOGE("OTA state rename failed: %d", ret); - ql_remove(OTA_STATE_TMP_FILE); - return -1; - } - - return 0; + return ota_state_write(&state); } static int ota_state_load(ota_state_t *state) @@ -296,6 +312,39 @@ static int ota_compare_version(const char *left, const char *right, int *result) return 0; } + +int ota_prepare_post_update_audio_refresh(void) +{ + ota_state_t state; + char current_version[64] = {0}; + int compare_result; + + if (ota_state_load(&state) != 0) { + return 0; + } + + hal_os_get_build_ver(current_version); + if (ota_compare_version(current_version, state.target_version, + &compare_result) != 0 || + compare_result != 0) { + return 0; + } + + if ((state.flags & OTA_STATE_FLAG_AUDIO_REFRESH_PREPARED) != 0) { + return 0; + } + + printf("OTA target APP is running; refreshing managed audio files\r\n"); + app_file_check_del(); + + state.flags |= OTA_STATE_FLAG_AUDIO_REFRESH_PREPARED; + if (ota_state_write(&state) != 0) { + RLOGE("mark post-update audio refresh prepared failed"); + return -1; + } + + return 1; +} /* ==================== 解析函数 ==================== */ diff --git a/ql-application/threadx/interface/custom_app/net_mgt.c b/ql-application/threadx/interface/custom_app/net_mgt.c index 459a771..8987699 100644 --- a/ql-application/threadx/interface/custom_app/net_mgt.c +++ b/ql-application/threadx/interface/custom_app/net_mgt.c @@ -19,6 +19,8 @@ #include "ql_ping.h" #include "ql_nw.h" #include "ql_atcmd.h" +#include "http_downfile.h" +#include "ota_post_update.h" //#define NTP_SERVER_DOMAIN "ntp.aliyun.com" @@ -104,6 +106,7 @@ static void ql_data_call_cb(int profile_idx, int nw_status) void ql_datacall_demo_thread(void *arvg) { int ret = 0; + int refresh_ret = 0; int file_valid = 0; char ip4_addr_str[16] = {0}; struct ql_data_call_info info; @@ -155,7 +158,11 @@ void ql_datacall_demo_thread(void *arvg) ql_set_lwip_dns(1, "223.5.5.5","114.114.114.144"); ip4_addr = info.v4.addr.ip; - + + refresh_ret = ota_prepare_post_update_audio_refresh(); + if(refresh_ret < 0){ + printf("post-update audio refresh preparation failed\r\n"); + } app_file_check_download(); if(g_file_valid==0){ diff --git a/tools/verify_ota_ready_baseline.py b/tools/verify_ota_ready_baseline.py index 401000e..8e0d362 100644 --- a/tools/verify_ota_ready_baseline.py +++ b/tools/verify_ota_ready_baseline.py @@ -29,6 +29,8 @@ BASELINE_HEADER = ROOT / "ql-application/threadx/interface/custom_app/inc/ota_ba 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" +NET_MGT_SOURCE = ROOT / "ql-application/threadx/interface/custom_app/net_mgt.c" +HTTP_DOWNFILE_SOURCE = ROOT / "ql-application/threadx/interface/custom_app/http_downfile.c" FULLOTA2_SOURCE = ROOT / "ql-application/threadx/interface/custom_app/ota_fullfota2.c" URL_POLICY_SOURCE = ROOT / "ql-application/threadx/interface/custom_app/ota_url_policy.c" LEGACY_HTTP_SOURCE = ROOT / "ql-application/threadx/interface/custom_app/http_aofei.c" @@ -204,6 +206,17 @@ def verify_fota_contract() -> None: fail("MQTT OTA command still invokes the legacy FULLOTA1 path") if "banban_ota_url_is_allowed" not in mqtt: fail("MQTT OTA command does not enforce the shared URL policy") + if "OTA_STATE_FLAG_AUDIO_REFRESH_PREPARED" not in mqtt or \ + "ota_prepare_post_update_audio_refresh" not in mqtt: + fail("post-update audio refresh is not tied to the verified OTA target version") + net_mgt = NET_MGT_SOURCE.read_text(encoding="utf-8", errors="replace") + refresh_position = net_mgt.find("ota_prepare_post_update_audio_refresh()") + download_position = net_mgt.find("app_file_check_download()") + if refresh_position < 0 or download_position < 0 or refresh_position > download_position: + fail("post-update audio deletion must run immediately before missing-file download") + audio_source = HTTP_DOWNFILE_SOURCE.read_text(encoding="utf-8", errors="replace") + if re.search(r"index\s*==\s*19", audio_source): + fail("barge_in_cute.mp3 is still excluded from managed audio refresh") url_policy = URL_POLICY_SOURCE.read_text(encoding="utf-8", errors="replace") if 'BANBAN_OTA_BASE_DOMAIN "talkingq.com"' not in url_policy: fail("OTA URL policy no longer allows the talkingq.com domain family")