Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2c78208bf | ||
|
|
c53ab581d0 | ||
|
|
23983ccdc8 | ||
|
|
109a95497a | ||
|
|
00e9509dd0 | ||
|
|
84ce54eeb0 | ||
|
|
886ea58419 | ||
|
|
65029594a3 | ||
|
|
869f5887de | ||
|
|
bbb77137bb | ||
|
|
f3691c8267 | ||
|
|
5399921c3d | ||
|
|
a0796f9a1d | ||
|
|
05522c929f | ||
|
|
79c154579c | ||
|
|
6cdd88e9f4 | ||
|
|
20e5872a75 | ||
|
|
f607d8b9f7 | ||
|
|
b726fe192a | ||
|
|
6fad31c37a | ||
|
|
83ef72839e | ||
|
|
82a419f812 |
@@ -7,6 +7,7 @@
|
||||
*.lib filter=lfs diff=lfs merge=lfs -text
|
||||
*.a filter=lfs diff=lfs merge=lfs -text
|
||||
*.bin filter=lfs diff=lfs merge=lfs -text
|
||||
ql-config/config/EG800AKCN_11LG/customer_fs.bin -filter -diff -merge -text
|
||||
*.img filter=lfs diff=lfs merge=lfs -text
|
||||
*.axf filter=lfs diff=lfs merge=lfs -text
|
||||
*.elf filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
@@ -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。
|
||||
+18
-1
@@ -1,9 +1,12 @@
|
||||
@echo off
|
||||
set CURDIR=%~dp0
|
||||
for /f %%i in ('powershell -NoProfile -Command "Get-Date -Format yyyyMMddHHmmss"') do set appbuildtime=%%i
|
||||
for /f %%i in ('powershell -NoProfile -Command "Get-Date -Format yyyyMMddHHmmss"') do set appbuildtime=%%i
|
||||
set appbuildrev=NULL
|
||||
set appbuildmodel=EG800AKCN_11LG
|
||||
set appbuildver=beta1
|
||||
set otalayoutid=eg800akcn_11lg-hxgnss-fs576-app2e5000
|
||||
set otabuildprofile=ota_ready_hxgnss_fs576
|
||||
set pythonexe=ql-cross-tool\win32\owtoolchain\Python\python.exe
|
||||
|
||||
if /i not "%1" == "" (
|
||||
set appbuildver=%1
|
||||
@@ -18,14 +21,28 @@ echo #define CUSTOM_APP_BUILD_TIME "%appbuildtime%" >> ql-applicati
|
||||
echo #define CUSTOM_APP_BUILD_REV "%appbuildrev%" >> ql-application\threadx\interface\custom_app\lib\hal\build_macro.h
|
||||
echo #define CUSTOM_APP_BUILD_MODEL "%appbuildmodel%" >> ql-application\threadx\interface\custom_app\lib\hal\build_macro.h
|
||||
echo #define CUSTOM_APP_BUILD_VER "%appbuildver%" >> ql-application\threadx\interface\custom_app\lib\hal\build_macro.h
|
||||
echo #define CUSTOM_APP_OTA_LAYOUT_ID "%otalayoutid%" >> ql-application\threadx\interface\custom_app\lib\hal\build_macro.h
|
||||
echo #define CUSTOM_APP_OTA_BUILD_PROFILE "%otabuildprofile%" >> ql-application\threadx\interface\custom_app\lib\hal\build_macro.h
|
||||
echo #endif >> ql-application\threadx\interface\custom_app\lib\hal\build_macro.h
|
||||
|
||||
"%pythonexe%" tools\verify_ota_ready_baseline.py
|
||||
if errorlevel 1 (
|
||||
echo "OTA-ready baseline source verification failed"
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
call build.bat project_name -p EG800AKCN_11LG -b NO_EXTFLASH -x XIP -c %appbuildmodel%
|
||||
call build.bat clean
|
||||
call build.bat app
|
||||
call build.bat kernel
|
||||
call build.bat firmware
|
||||
|
||||
"%pythonexe%" tools\verify_ota_ready_baseline.py --app-bin ql-application\threadx\build\app.bin --kernel-bin ql-kernel\threadx\build\release_kernel.bin
|
||||
if errorlevel 1 (
|
||||
echo "OTA-ready baseline artifact verification failed"
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if exist "target\%appbuildmodel%.zip" (
|
||||
mkdir target\%appbuildmodel%\fota\
|
||||
mkdir target\%appbuildmodel%\fota\a
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
# Banban OTA-ready 工厂基线
|
||||
|
||||
## 1. 目标和边界
|
||||
|
||||
本基线用于尚未量产的 `EG800AKCN_11LG` 设备。目标是在不改变现有对话、音频、
|
||||
MQTT、NFC、GNSS、按键、休眠和 LED 业务流程的前提下,一次性固定后续本地暂存式
|
||||
OTA 所依赖的底层条件。
|
||||
|
||||
本基线完成:
|
||||
|
||||
- 将 HX_GNSS 的 `customer_fs` 从 256 KiB 扩大到 576 KiB;
|
||||
- 将 `customer_app` 从 460 KiB 调整为 384 KiB;
|
||||
- 将 APP 物理/XIP 地址固定为 `0x002E5000` / `0x802E5000`;
|
||||
- 保留 SDK 已有的 `QUEC_APP_FULL_FOTA` 与 FULLOTA2 公共 API;
|
||||
- 将现有 OTA 命令切换为最小本地暂存式 FULLOTA2;
|
||||
- 为设备和构建产物加入唯一布局身份;
|
||||
- 在构建前后执行 fail-closed 校验。
|
||||
|
||||
本基线暂不完成:
|
||||
|
||||
- 不增加签名 manifest、SHA-256 下载校验、断点续传或后台灰度;
|
||||
- 不提供 A/B APP 分区,也不承诺自动版本回滚;
|
||||
- 不在线更新 kernel、分区表或 `customer_fs`。
|
||||
|
||||
这版保留现有后端的 `version + url` 命令格式,只改变设备端执行方式。网络包会完整写入
|
||||
`U:/FotaFile.bin`,通过 SDK FBF 校验并设置升级标志后才重启。
|
||||
|
||||
> 生产门禁:完成本文的真机 FULLOTA2 正向、失败注入和断电测试前,后端不得向生产设备
|
||||
> 下发 OTA 命令。验收通过后可继续使用原协议下发;这条门禁针对测试状态,不再是因为
|
||||
> 设备仍调用 FULLOTA1。
|
||||
|
||||
## 2. 固定身份与 Flash 布局
|
||||
|
||||
设备布局身份:
|
||||
|
||||
```text
|
||||
eg800akcn_11lg-hxgnss-fs576-app2e5000
|
||||
```
|
||||
|
||||
构建 profile:
|
||||
|
||||
```text
|
||||
ota_ready_hxgnss_fs576
|
||||
```
|
||||
|
||||
4 MiB 内部 Flash 的尾部分区固定如下:
|
||||
|
||||
| 分区 | 起始地址 | 结束地址 | 大小 |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| reserved(按当前镜像估算) | `0x002D0000` | `0x002E5000` | 84 KiB |
|
||||
| customer_app | `0x002E5000` | `0x00345000` | 384 KiB |
|
||||
| customer_fs | `0x00345000` | `0x003D5000` | 576 KiB |
|
||||
| nvm | `0x003D5000` | `0x003F5000` | 128 KiB |
|
||||
| erase_rd | `0x003F5000` | `0x003F7000` | 8 KiB |
|
||||
| fota_param | `0x003F7000` | `0x003FA000` | 12 KiB |
|
||||
| factory_a | `0x003FA000` | `0x00400000` | 24 KiB |
|
||||
|
||||
`customer_app` 是单槽。FULLOTA2 能保护下载阶段:文件未下载完整或 SDK FBF 校验
|
||||
失败时不会设置升级标志;但 updater 开始擦写 `customer_app` 后,没有旧 APP 槽可供回滚。
|
||||
|
||||
## 3. 本次修改
|
||||
|
||||
### 3.1 分区和链接地址
|
||||
|
||||
- `CRANELRH_QUEC_FLASH_LAYOUT_SS_04M_HX_GNSS.json`:当前 `CC11XXW_GPS` 构建实际选择的
|
||||
分区表,`customer_app = 384KiB`、`customer_fs = 576KiB`;
|
||||
- `CRANELRH_QUEC_FLASH_LAYOUT_SS_04M_OPEN.json`:同步保持 `customer_fs = 640KiB`,避免
|
||||
无 GNSS variant 回退到旧布局;
|
||||
- `app_linkscript.ld`:ROM 起始地址改为 `0x802E5000`,最大 ROM 大小收紧为 `0x60000`;
|
||||
- `customer_fs.bin`:从原 256 KiB LittleFS 中提取全部 14 个音频文件,以 4096 字节块、
|
||||
144 个块重新生成 576 KiB LittleFS;
|
||||
- 新文件系统镜像 SHA-256:
|
||||
`51ad32f8196aeab36a62ed436ac65f495ce204436f8ef57efccafc87313b0d3c`。
|
||||
|
||||
### 3.2 运行身份
|
||||
|
||||
`ota_baseline.h` 固定布局身份、APP/FS 地址和分区大小。应用启动时打印:
|
||||
|
||||
```text
|
||||
[OTA-BASELINE] layout=eg800akcn_11lg-hxgnss-fs576-app2e5000 ... slot=single
|
||||
[OTA-BASELINE] U: total=... free=... fota2_reserve=65536
|
||||
```
|
||||
|
||||
该日志用于产线和联调确认设备没有混入旧的 fs256 或错误的 fs640 OPEN 基线,不改变任何业务状态。
|
||||
|
||||
### 3.3 构建保护
|
||||
|
||||
`build_rel.bat` 在构建前运行源码校验,在生成 APP 后运行容量校验。任何一项不满足都会中止:
|
||||
|
||||
- HX_GNSS `customer_fs` 必须为 576 KiB;
|
||||
- APP 必须为 384 KiB,地址必须为 `0x002E5000`;
|
||||
- LittleFS 必须为 4096 字节块、144 块且镜像哈希一致;
|
||||
- 当前 kernel、DSP、RF 和 GNSS 镜像到 APP 之间必须至少保留 64 KiB;
|
||||
- kernel 必须继续定义 `QUEC_APP_FULL_FOTA`;
|
||||
- FULLOTA2 所需的 SDK API 必须存在;
|
||||
- `app.bin + 8192` 不得超过 384 KiB。
|
||||
- MQTT OTA 入口必须调用 `banban_fullfota2_start()`,不得回退到
|
||||
`ql_fullfota_app_start()`。
|
||||
|
||||
可独立执行:
|
||||
|
||||
```powershell
|
||||
ql-cross-tool\win32\owtoolchain\Python\python.exe tools\verify_ota_ready_baseline.py
|
||||
ql-cross-tool\win32\owtoolchain\Python\python.exe tools\verify_ota_ready_baseline.py `
|
||||
--app-bin ql-application\threadx\build\app.bin `
|
||||
--kernel-bin ql-kernel\threadx\build\release_kernel.bin
|
||||
```
|
||||
|
||||
### 3.4 最小 FULLOTA2 执行路径
|
||||
|
||||
`ota_fullfota2.c` 使用独立任务完成同步 HTTP 下载。现有 MQTT 工作线程只负责参数校验、
|
||||
保存目标版本、回复 `accepted` 和启动任务,不会承担长时间网络读取。
|
||||
|
||||
执行顺序:
|
||||
|
||||
1. 删除上一次残留的 `U:/FotaFile.bin`;
|
||||
2. HTTP GET 必须返回 200 和非零 `Content-Length`;
|
||||
3. FBF 必须大于 8 KiB 且不超过 384 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 下载前删除服务器管理的提示音,下载失败时会提前损失业务资源。当前实现
|
||||
在下载和 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,避免两个入口
|
||||
使用不同的升级语义。
|
||||
|
||||
## 4. 构建和生产烧录
|
||||
|
||||
在 Windows SDK 构建环境中执行:
|
||||
|
||||
```bat
|
||||
build_rel.bat <生产版本号>
|
||||
```
|
||||
|
||||
量产必须烧录该构建产生的完整 `*-fac.zip`,不能只烧 `customer_app`。完整工厂包需要同时
|
||||
携带新 `partition.bin`、新 `customer_fs.bin`、11LG kernel/system 和新地址 APP。
|
||||
|
||||
量产前冻结并记录:
|
||||
|
||||
- 源码 Git commit;
|
||||
- 工厂 ZIP 文件名、字节数和 SHA-256;
|
||||
- `customer_fs.bin` SHA-256;
|
||||
- `customer_app.bin` 字节数和 SHA-256;
|
||||
- `partition.bin` 字节数和 SHA-256;
|
||||
- 编译器、Aboot/arelease 和 FBFMake 版本;
|
||||
- 首台、末台抽检设备串口中的 OTA baseline 日志。
|
||||
|
||||
不要把 `EG800AKCN_91LC` 的 `cp.bin`、工厂包或射频/GNSS 制品复制给本项目。两个项目同为
|
||||
ASR1605/CRANELRH,但量产包必须由 `EG800AKCN_11LG` 配置生成。
|
||||
|
||||
## 5. 生产前测试
|
||||
|
||||
### 5.1 静态和产物测试
|
||||
|
||||
1. 运行 `verify_ota_ready_baseline.py`,结果必须通过。
|
||||
2. 解包工厂 ZIP,确认其中 `customer_fs.bin` 为 589824 字节且哈希一致。
|
||||
3. 使用 Aboot 工具检查 `partition.bin`,确认 APP/FS 地址与本文件一致。
|
||||
4. 确认 `customer_app.bin + 8192 <= 393216`。
|
||||
5. 确认 FBF 的目标物理地址是 `0x002E5000`,不能是旧地址 `0x00322000`、
|
||||
也不能是错误基线地址 `0x002C2000`。
|
||||
|
||||
### 5.2 全擦工厂烧录测试
|
||||
|
||||
至少准备 3 台板执行完整擦除和工厂烧录:
|
||||
|
||||
1. 下载器无越界、校验或 partition 错误。
|
||||
2. USB 重新枚举,`AT`/`ATI` 正常。
|
||||
3. 串口打印正确 layout、地址和 576 KiB 文件系统容量。
|
||||
4. 14 个预置音频均可读取和播放。
|
||||
5. IMEI、SIM、驻网、数据连接和 GNSS 正常。
|
||||
6. MQTT 注册、命令收发、心跳和状态上报正常。
|
||||
7. 唤醒、对话、打断、录音、播放、音量和休眠正常。
|
||||
8. NFC、按键、LED、电池检测和低电关机正常。
|
||||
9. 连续冷启动、软重启和断电启动各不少于 20 次。
|
||||
10. 连续运行至少 8 小时,观察 watchdog、内存、文件系统和网络重连。
|
||||
|
||||
### 5.3 文件系统压力测试
|
||||
|
||||
1. 启动后记录 `ql_fs_size('U')` 和 `ql_fs_free_size('U')`。
|
||||
2. 写入一个与计划 FBF 等大的测试文件,关闭、重启并逐字节回读。
|
||||
3. 删除测试文件,确认空间恢复。
|
||||
4. 在写入 10%、50%、90% 时分别断电,重启后确认业务 APP 仍可运行并可清理残留文件。
|
||||
5. 验证正常业务文件占用后,仍满足 `FBF size + 64 KiB` 空闲门槛。
|
||||
|
||||
### 5.4 最小 FULLOTA2 验收测试
|
||||
|
||||
正式量产前至少做一次 A 到 B 的本地暂存升级。B 可以只修改版本号:
|
||||
|
||||
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. 后续 OTA 加固
|
||||
|
||||
后续 APP 版本建议按以下顺序加固,不再改变本基线分区:
|
||||
|
||||
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. 后端联调契约
|
||||
|
||||
最小 FULLOTA2 继续兼容现有 URL 和版本协议,不要求后端立即修改。完成首轮生产验收后,
|
||||
建议后端增加:
|
||||
|
||||
```json
|
||||
{
|
||||
"product": "EG800AKCN_11LG",
|
||||
"layout_id": "eg800akcn_11lg-hxgnss-fs576-app2e5000",
|
||||
"version": "1.9.0",
|
||||
"release_seq": 10900,
|
||||
"min_release_seq": 10800,
|
||||
"url": "https://ota.example/releases/1.9.0/customer_app.fbf",
|
||||
"size": 212992,
|
||||
"sha256": "64-lowercase-hex",
|
||||
"signature": "optional-first-stage"
|
||||
}
|
||||
```
|
||||
|
||||
设备校验顺序:产品、layout、版本序号、URL host、大小、SHA-256,最后才允许设置升级标志。
|
||||
|
||||
当前最小版本保留原服务器
|
||||
`http://101.35.224.118:8080/assets/firmware/`,并允许 `talkingq.com` 根域及任意合法
|
||||
子域名通过 HTTP/HTTPS 提供 `.bin` 文件,例如 `ota.talkingq.com` 和
|
||||
`cdn.cn.talkingq.com`。校验基于解析出的完整 host 边界,`eviltalkingq.com`、
|
||||
`talkingq.com.evil.example`、userinfo 绕过、非法端口和路径穿越均会被拒绝。
|
||||
|
||||
设备状态建议至少包含:
|
||||
|
||||
- `ACCEPTED`:命令和布局校验通过;
|
||||
- `DOWNLOADING`:带下载字节数或百分比;
|
||||
- `VERIFIED`:本地文件 SHA-256 和 FBF 校验通过;
|
||||
- `REBOOTING`:升级标志已设置;
|
||||
- `UPDATED`:目标 APP 已启动并通过健康检查;
|
||||
- `FAILED`:带稳定错误码和失败阶段。
|
||||
|
||||
联调测试矩阵必须覆盖:同版本、降级、错误 layout、大小不符、SHA 不符、404、超时、下载
|
||||
中断、下载阶段断电、verify 前断电、重启后无法确认,以及后台重复下发同一命令。
|
||||
|
||||
如果暂时没有动态 OTA 后端,也可以用“不可变 FBF 对象 + 签名 current.json”实现;但灰度、
|
||||
设备级进度和失败统计需要后台服务。
|
||||
|
||||
## 8. 发布和回退策略
|
||||
|
||||
- 新生产设备只允许使用 HX_GNSS fs576 工厂包和 `0x002E5000` APP/FBF。
|
||||
- 禁止向 fs576 设备发布旧地址 `0x00322000` 或错误地址 `0x002C2000` 的 FBF。
|
||||
- 如生产前验证失败,可回退源码分支和工厂包重新全擦烧录;不得混烧新旧布局制品。
|
||||
- 量产后若只是业务问题,可通过 APP OTA 修复;若分区或 kernel 基线有问题,需要返厂或有线
|
||||
完整烧录,不能假定业务 APP OTA 能安全修改分区表。
|
||||
- 当前为单槽。发布新版本必须先小批量灰度,并保持可用的有线恢复工厂包。
|
||||
@@ -0,0 +1,168 @@
|
||||
# OTA 下载进度上报协议说明
|
||||
|
||||
## 1. 目的
|
||||
|
||||
在当前 OTA 协议基础上增加下载进度上报。
|
||||
|
||||
本次只增加进度,不修改以下内容:
|
||||
|
||||
- OTA 下发主题不变;
|
||||
- OTA 下发报文不变;
|
||||
- OTA 响应主题不变;
|
||||
- `msg_id` 继续使用 `003`;
|
||||
- 当前的 `accepted` 和 `success` 响应保持不变;
|
||||
- 不增加任务 ID、错误码、新主题或新的 OTA 状态机。
|
||||
|
||||
设备端可以先发布。后端尚未处理进度消息时,不影响设备执行 OTA;后端后续只需增加对 `status=updating` 的解析。
|
||||
|
||||
## 2. 当前 OTA 下发协议
|
||||
|
||||
下发主题:
|
||||
|
||||
```text
|
||||
device/{imei}/command
|
||||
```
|
||||
|
||||
下发报文保持不变:
|
||||
|
||||
```json
|
||||
{
|
||||
"msg_id": "003",
|
||||
"time": "1791853200000",
|
||||
"params": {
|
||||
"url": "http://banban.api.talkingq.com:8080/path/fota.bin",
|
||||
"version": "2.0.4"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. OTA 上报主题
|
||||
|
||||
设备继续使用当前响应主题:
|
||||
|
||||
```text
|
||||
device/{imei}/response
|
||||
```
|
||||
|
||||
## 4. 接受 OTA
|
||||
|
||||
设备接受 OTA 命令后,继续发送当前报文:
|
||||
|
||||
```json
|
||||
{
|
||||
"msg_id": "003",
|
||||
"status": "accepted",
|
||||
"data": {
|
||||
"current_version": "2.0.3",
|
||||
"target_version": "2.0.4"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
该报文表示设备已经接受 OTA 命令,不表示下载已经完成。
|
||||
|
||||
## 5. 新增下载进度上报
|
||||
|
||||
设备下载 FOTA 包时,每增加 10% 上报一次:
|
||||
|
||||
```json
|
||||
{
|
||||
"msg_id": "003",
|
||||
"status": "updating",
|
||||
"data": {
|
||||
"target_version": "2.0.4",
|
||||
"progress": 30
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `msg_id` | string | 固定为 `003` |
|
||||
| `status` | string | 下载进度消息固定为 `updating` |
|
||||
| `data.target_version` | string | 本次 OTA 目标版本 |
|
||||
| `data.progress` | number | FOTA 包下载百分比,范围为 `0~100` |
|
||||
|
||||
设备建议上报以下进度值:
|
||||
|
||||
```text
|
||||
10、20、30、40、50、60、70、80、90、100
|
||||
```
|
||||
|
||||
不需要每下载一个数据块就上报,避免产生过多 MQTT 消息。
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{"msg_id":"003","status":"updating","data":{"target_version":"2.0.4","progress":10}}
|
||||
```
|
||||
|
||||
```json
|
||||
{"msg_id":"003","status":"updating","data":{"target_version":"2.0.4","progress":50}}
|
||||
```
|
||||
|
||||
```json
|
||||
{"msg_id":"003","status":"updating","data":{"target_version":"2.0.4","progress":100}}
|
||||
```
|
||||
|
||||
注意:`progress=100` 只表示 FOTA 包下载完成,不表示新固件已经启动成功。
|
||||
|
||||
## 6. OTA 成功
|
||||
|
||||
设备下载、校验、重启并进入新 APP 后,继续发送当前成功报文:
|
||||
|
||||
```json
|
||||
{
|
||||
"msg_id": "003",
|
||||
"status": "success",
|
||||
"data": {
|
||||
"new_version": "2.0.4"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
后端只有收到 `status=success` 时,才能将本次 OTA 判断为最终成功。
|
||||
|
||||
## 7. 后端需要修改的内容
|
||||
|
||||
后端只需要在现有 `003` 响应解析中增加一种状态:
|
||||
|
||||
```text
|
||||
status = updating
|
||||
```
|
||||
|
||||
解析逻辑:
|
||||
|
||||
1. 收到 `accepted`:记录设备已接受 OTA。
|
||||
2. 收到 `updating`:读取 `data.progress` 并更新下载进度。
|
||||
3. 收到 `success`:记录设备已经进入目标版本,OTA 最终成功。
|
||||
|
||||
MQTT 当前使用 QoS 1,同一条进度消息可能重复到达。后端收到重复进度时直接覆盖原进度即可,不要重复创建 OTA 任务。
|
||||
|
||||
如果后端收到的进度小于已经保存的进度,可以忽略较小的进度,避免进度倒退。
|
||||
|
||||
## 8. 设备端实现约定
|
||||
|
||||
1. 当前 `accepted` 和 `success` 报文不修改。
|
||||
2. 下载任务每达到一个新的 10% 档位,上报一次 `updating`。
|
||||
3. 进度上报仍发送到当前 `response` 主题。
|
||||
4. 进度上报失败只记录日志,不中止 OTA 下载。
|
||||
5. 下载完成后上报 `progress=100`。
|
||||
6. `progress=100` 后设备继续执行文件校验和重启。
|
||||
7. 新 APP 启动后仍使用当前 `success` 报文确认最终成功。
|
||||
|
||||
## 9. 完整交互示例
|
||||
|
||||
```text
|
||||
后端 -> 设备:003 OTA 命令
|
||||
设备 -> 后端:003 accepted
|
||||
设备 -> 后端:003 updating progress=10
|
||||
设备 -> 后端:003 updating progress=20
|
||||
设备 -> 后端:003 updating progress=30
|
||||
...
|
||||
设备 -> 后端:003 updating progress=100
|
||||
设备重启并安装新 APP
|
||||
设备 -> 后端:003 success
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
__ROM_START_ADDR = 0x80322000;
|
||||
__MAX_ROM_SIZE = 0x100000;
|
||||
__ROM_START_ADDR = 0x802E5000;
|
||||
__MAX_ROM_SIZE = 0x60000;
|
||||
|
||||
__RAM_START_ADDR = 0x7e040800;
|
||||
__MAX_RAM_SIZE = 0x100000;
|
||||
|
||||
@@ -86,6 +86,8 @@ SRC_FILES+= \
|
||||
sleep_wake.c \
|
||||
battery.c \
|
||||
gnss.c \
|
||||
ota_fullfota2.c \
|
||||
ota_url_policy.c \
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "ql_log.h"
|
||||
#include "hal_api.h"
|
||||
#include "nfc.h"
|
||||
#include "ota_baseline.h"
|
||||
|
||||
|
||||
#define APP_ENABLE_SLEEP
|
||||
@@ -146,6 +147,17 @@ static void custom_app_task(void * argv)
|
||||
|
||||
hal_os_get_build_time(tempbuf);
|
||||
printf("BUILD TIME:%s\r\n",tempbuf);
|
||||
printf("[OTA-BASELINE] layout=%s profile=%s app_phys=0x%08lX app_xip=0x%08lX app_size=%lu fs_size=%lu slot=single\r\n",
|
||||
BANBAN_OTA_LAYOUT_ID,
|
||||
BANBAN_OTA_BUILD_PROFILE,
|
||||
(unsigned long)BANBAN_OTA_APP_PHYSICAL_ADDRESS,
|
||||
(unsigned long)BANBAN_OTA_APP_XIP_ADDRESS,
|
||||
(unsigned long)BANBAN_OTA_APP_PARTITION_SIZE,
|
||||
(unsigned long)BANBAN_OTA_FS_PARTITION_SIZE);
|
||||
printf("[OTA-BASELINE] U: total=%d free=%d fota2_reserve=%lu\r\n",
|
||||
ql_fs_size('U'),
|
||||
ql_fs_free_size('U'),
|
||||
(unsigned long)BANBAN_OTA_STAGING_RESERVE_BYTES);
|
||||
|
||||
|
||||
//app_uartsh_init();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -37,7 +37,7 @@ const char *mp3_file[] ={
|
||||
"barge_in_cute.mp3",//19 对话打断提示音
|
||||
};
|
||||
|
||||
//http://banban.api.talkingq.com:8080/assets/roles/banban/tts_error.mp3
|
||||
//http://banban.api.talkingq.com:8080/assets/roles/banban/tts_error.mp3
|
||||
|
||||
//#define PCM_FILE "http://47.113.99.138:7050/output_16k.pcm"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -197,7 +196,7 @@ void app_file_check_download(void)
|
||||
if(ql_access(filename_buff, 0)!=0){
|
||||
printf("downing mp3 file %d,%s\r\n", index,filename_buff);
|
||||
down_loop:
|
||||
sprintf(url_buff, "http://banban.api.talkingq.com:8080/assets/roles/banban/%s", mp3_file[index]);
|
||||
sprintf(url_buff, "http://banban.api.talkingq.com:8080/assets/roles/banban/%s", mp3_file[index]);
|
||||
|
||||
ret = http_file_download(url_buff);
|
||||
if(ret!=0&&cnt++<3) goto down_loop;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,21 @@
|
||||
#ifndef BANBAN_OTA_BASELINE_H
|
||||
#define BANBAN_OTA_BASELINE_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "build_macro.h"
|
||||
|
||||
#define BANBAN_OTA_EXPECTED_LAYOUT_ID "eg800akcn_11lg-hxgnss-fs576-app2e5000"
|
||||
#define BANBAN_OTA_LAYOUT_ID CUSTOM_APP_OTA_LAYOUT_ID
|
||||
#define BANBAN_OTA_BUILD_PROFILE CUSTOM_APP_OTA_BUILD_PROFILE
|
||||
#define BANBAN_OTA_APP_PHYSICAL_ADDRESS UINT32_C(0x002E5000)
|
||||
#define BANBAN_OTA_APP_XIP_ADDRESS UINT32_C(0x802E5000)
|
||||
#define BANBAN_OTA_APP_PARTITION_SIZE UINT32_C(0x00060000)
|
||||
#define BANBAN_OTA_FS_PHYSICAL_ADDRESS UINT32_C(0x00345000)
|
||||
#define BANBAN_OTA_FS_PARTITION_SIZE UINT32_C(0x00090000)
|
||||
#define BANBAN_OTA_STAGING_RESERVE_BYTES UINT32_C(0x00010000)
|
||||
|
||||
#if !defined(CUSTOM_APP_OTA_LAYOUT_ID) || !defined(CUSTOM_APP_OTA_BUILD_PROFILE)
|
||||
#error "Use build_rel.bat to build the OTA-ready production baseline"
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,28 @@
|
||||
#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);
|
||||
/* Download percentage in 10% steps; reporting must not cancel the download. */
|
||||
typedef void (*banban_fullfota2_progress_cb_t)(int progress);
|
||||
|
||||
int banban_fullfota2_start(const char *url,
|
||||
banban_fullfota2_progress_cb_t progress_cb,
|
||||
banban_fullfota2_finished_cb_t finished_cb);
|
||||
int banban_fullfota2_is_running(void);
|
||||
void banban_fullfota2_discard_staged_package(void);
|
||||
|
||||
#endif
|
||||
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef BANBAN_OTA_URL_POLICY_H
|
||||
#define BANBAN_OTA_URL_POLICY_H
|
||||
|
||||
int banban_ota_url_is_allowed(const char *url);
|
||||
|
||||
#endif
|
||||
@@ -4,4 +4,6 @@
|
||||
#define CUSTOM_APP_BUILD_REV "NULL"
|
||||
#define CUSTOM_APP_BUILD_MODEL "EG800AKCN_11LG"
|
||||
#define CUSTOM_APP_BUILD_VER "1.8.0"
|
||||
#define CUSTOM_APP_OTA_LAYOUT_ID "eg800akcn_11lg-hxgnss-fs576-app2e5000"
|
||||
#define CUSTOM_APP_OTA_BUILD_PROFILE "ota_ready_hxgnss_fs576"
|
||||
#endif
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
#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"
|
||||
|
||||
|
||||
|
||||
@@ -43,9 +47,9 @@
|
||||
#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
|
||||
#define OTA_ALLOWED_URL_PREFIX "http://101.35.224.118:8080/assets/firmware/"
|
||||
|
||||
static ql_timer_t mqtt_event_task_timer = NULL;
|
||||
|
||||
@@ -104,12 +108,13 @@ static char payload[MQTT_PAYLOAD_SIZE] = {0};
|
||||
|
||||
extern int fota_start;
|
||||
int app_play_ota_tips_file(void);
|
||||
int app_role_notice_handle_url(const char *url);
|
||||
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;
|
||||
@@ -118,6 +123,7 @@ typedef struct {
|
||||
int app_mqtt_publish(char *buf, int len, int type);
|
||||
void build_register(void);
|
||||
static void ota_report_pending_success(void);
|
||||
static char ota_progress_target_version[16] = {0};
|
||||
|
||||
enum{
|
||||
MQTT_WORK_CONNECT,
|
||||
@@ -146,12 +152,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;
|
||||
@@ -162,38 +212,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)
|
||||
@@ -242,6 +261,49 @@ static void ota_state_clear(void)
|
||||
}
|
||||
}
|
||||
|
||||
static void ota_fullfota2_progress(int progress)
|
||||
{
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON *data = cJSON_CreateObject();
|
||||
char *payload;
|
||||
|
||||
if (root == NULL || data == NULL) {
|
||||
cJSON_Delete(root);
|
||||
cJSON_Delete(data);
|
||||
RLOGE("allocate OTA progress response failed");
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON_AddStringToObject(root, "msg_id", "003");
|
||||
cJSON_AddStringToObject(root, "status", "updating");
|
||||
cJSON_AddStringToObject(data, "target_version", ota_progress_target_version);
|
||||
cJSON_AddNumberToObject(data, "progress", progress);
|
||||
cJSON_AddItemToObject(root, "data", data);
|
||||
payload = cJSON_PrintUnformatted(root);
|
||||
cJSON_Delete(root);
|
||||
if (payload == NULL) {
|
||||
RLOGE("build OTA progress response failed");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Progress is best effort; a publish failure must not abort FULLOTA2. */
|
||||
if (app_mqtt_publish(payload, strlen(payload), 0) != 0) {
|
||||
RLOGE("OTA progress publish failed: %d%%", progress);
|
||||
}
|
||||
free(payload);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -284,25 +346,40 @@ static int ota_compare_version(const char *left, const char *right, int *result)
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int ota_url_is_allowed(const char *url)
|
||||
{
|
||||
size_t prefix_len = strlen(OTA_ALLOWED_URL_PREFIX);
|
||||
|
||||
if (url == NULL || url[0] == '\0') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strncmp(url, OTA_ALLOWED_URL_PREFIX, prefix_len) != 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (url[prefix_len] == '\0' || strstr(url + prefix_len, "..") != NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return strstr(url + prefix_len, ".bin") != NULL;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/* ==================== 解析函数 ==================== */
|
||||
|
||||
/**
|
||||
@@ -1246,6 +1323,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;
|
||||
}
|
||||
|
||||
@@ -1304,7 +1383,7 @@ static void handle_ota_upgrade(command_t *cmd)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ota_url_is_allowed(cmd->params.ota.url)) {
|
||||
if (!banban_ota_url_is_allowed(cmd->params.ota.url)) {
|
||||
RLOGE("OTA URL is not allowed: %s", cmd->params.ota.url);
|
||||
return;
|
||||
}
|
||||
@@ -1369,12 +1448,13 @@ 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);
|
||||
snprintf(ota_progress_target_version, sizeof(ota_progress_target_version),
|
||||
"%s", cmd->params.ota.version);
|
||||
ret = banban_fullfota2_start(cmd->params.ota.url,
|
||||
ota_fullfota2_progress,
|
||||
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();
|
||||
}
|
||||
@@ -1467,7 +1547,9 @@ void handle_command(command_t *cmd)
|
||||
app_mqtt_publish(resp, strlen(resp), 0);
|
||||
free(resp);
|
||||
|
||||
add_file_to_play_player_queue(cmd->params.nfc.url, 0);
|
||||
if (!app_role_notice_handle_url(cmd->params.nfc.url)) {
|
||||
add_file_to_play_player_queue(cmd->params.nfc.url, 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
#include <string.h>
|
||||
#include "ql_rtos.h"
|
||||
#include "ql_application.h"
|
||||
#include "ql_audio.h"
|
||||
#include "ql_iic.h"
|
||||
#include "ql_audio.h"
|
||||
#include "ql_dev.h"
|
||||
#include "ql_iic.h"
|
||||
#include "ql_gpio.h"
|
||||
#include "app_inc.h"
|
||||
#include "ql_fs.h"
|
||||
@@ -48,6 +49,7 @@ static volatile u32 g_nfc_tx_mode_deadline = 0;
|
||||
#define NFC_TYPE2_READ_SIZE 16
|
||||
#define NFC_TYPE2_MEMORY_SIZE 160
|
||||
#define NFC_TYPE2_NDEF_OFFSET 16
|
||||
#define NFC_IMEI_DIGITS 15
|
||||
|
||||
#define NFC_IRQ_TX 0x40
|
||||
#define NFC_IRQ_RX 0x20
|
||||
@@ -173,6 +175,7 @@ static void nfc_type2_build_ndef(void)
|
||||
{
|
||||
char device_id[32] = {0};
|
||||
char serial_number[32] = {0};
|
||||
char imei[32] = {0};
|
||||
char json[128];
|
||||
int json_len;
|
||||
u8 *ndef;
|
||||
@@ -184,10 +187,17 @@ static void nfc_type2_build_ndef(void)
|
||||
if (hal_os_get_device_sn(serial_number) != 0) {
|
||||
strcpy(serial_number, "unknown");
|
||||
}
|
||||
if (ql_dev_get_imei(imei, sizeof(imei)) != QL_DEV_SUCCESS ||
|
||||
imei[0] == '\0') {
|
||||
strcpy(imei, "unknown");
|
||||
} else {
|
||||
imei[NFC_IMEI_DIGITS] = '\0';
|
||||
}
|
||||
|
||||
json_len = snprintf(json, sizeof(json),
|
||||
"{\"device_id\":\"%s\",\"serial_number\":\"%s\"}",
|
||||
device_id, serial_number);
|
||||
"{\"device_id\":\"%s\",\"serial_number\":\"%s\","
|
||||
"\"imei\":\"%s\"}",
|
||||
device_id, serial_number, imei);
|
||||
if (json_len < 0) {
|
||||
json_len = 0;
|
||||
} else if (json_len >= (int)sizeof(json)) {
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
#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_progress_cb_t progress_cb;
|
||||
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) {
|
||||
if (g_fullfota2_request.progress_cb != NULL) {
|
||||
g_fullfota2_request.progress_cb(next_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_progress_cb_t progress_cb,
|
||||
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.progress_cb = progress_cb;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ota_url_policy.h"
|
||||
|
||||
#define BANBAN_OTA_LEGACY_URL_PREFIX \
|
||||
"http://101.35.224.118:8080/assets/firmware/"
|
||||
#define BANBAN_OTA_BASE_DOMAIN "talkingq.com"
|
||||
|
||||
static int banban_ascii_equal_nocase(const char *left,
|
||||
const char *right,
|
||||
size_t length)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < length; ++i) {
|
||||
unsigned char a = (unsigned char)left[i];
|
||||
unsigned char b = (unsigned char)right[i];
|
||||
|
||||
if (a >= 'A' && a <= 'Z') {
|
||||
a = (unsigned char)(a + ('a' - 'A'));
|
||||
}
|
||||
if (b >= 'A' && b <= 'Z') {
|
||||
b = (unsigned char)(b + ('a' - 'A'));
|
||||
}
|
||||
if (a != b) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int banban_hostname_is_valid(const char *host, size_t host_length)
|
||||
{
|
||||
size_t i;
|
||||
int label_has_character = 0;
|
||||
int previous_was_hyphen = 0;
|
||||
|
||||
if (host_length == 0 || host_length > 253) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (i = 0; i < host_length; ++i) {
|
||||
unsigned char character = (unsigned char)host[i];
|
||||
|
||||
if ((character >= 'A' && character <= 'Z') ||
|
||||
(character >= 'a' && character <= 'z') ||
|
||||
(character >= '0' && character <= '9')) {
|
||||
label_has_character = 1;
|
||||
previous_was_hyphen = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character == '-') {
|
||||
if (!label_has_character) {
|
||||
return 0;
|
||||
}
|
||||
previous_was_hyphen = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character == '.') {
|
||||
if (!label_has_character || previous_was_hyphen) {
|
||||
return 0;
|
||||
}
|
||||
label_has_character = 0;
|
||||
previous_was_hyphen = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
return label_has_character && !previous_was_hyphen;
|
||||
}
|
||||
|
||||
static int banban_host_is_talkingq(const char *host, size_t host_length)
|
||||
{
|
||||
const size_t base_length = sizeof(BANBAN_OTA_BASE_DOMAIN) - 1;
|
||||
|
||||
if (!banban_hostname_is_valid(host, host_length)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (host_length == base_length) {
|
||||
return banban_ascii_equal_nocase(host,
|
||||
BANBAN_OTA_BASE_DOMAIN,
|
||||
base_length);
|
||||
}
|
||||
|
||||
if (host_length <= base_length + 1 ||
|
||||
host[host_length - base_length - 1] != '.') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return banban_ascii_equal_nocase(host + host_length - base_length,
|
||||
BANBAN_OTA_BASE_DOMAIN,
|
||||
base_length);
|
||||
}
|
||||
|
||||
static int banban_port_is_valid(const char *port, size_t port_length)
|
||||
{
|
||||
size_t i;
|
||||
unsigned long value = 0;
|
||||
|
||||
if (port_length == 0 || port_length > 5) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (i = 0; i < port_length; ++i) {
|
||||
if (port[i] < '0' || port[i] > '9') {
|
||||
return 0;
|
||||
}
|
||||
value = value * 10 + (unsigned long)(port[i] - '0');
|
||||
}
|
||||
|
||||
return value > 0 && value <= 65535;
|
||||
}
|
||||
|
||||
static int banban_path_is_fbf(const char *path)
|
||||
{
|
||||
const char *path_end;
|
||||
const char *query;
|
||||
|
||||
if (path == NULL || path[0] != '/' || path[1] == '\0' ||
|
||||
strstr(path, "..") != NULL || strchr(path, '\\') != NULL ||
|
||||
strchr(path, '#') != NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
query = strchr(path, '?');
|
||||
path_end = query != NULL ? query : path + strlen(path);
|
||||
if ((size_t)(path_end - path) < 4) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return banban_ascii_equal_nocase(path_end - 4, ".bin", 4);
|
||||
}
|
||||
|
||||
int banban_ota_url_is_allowed(const char *url)
|
||||
{
|
||||
const char *authority;
|
||||
const char *path;
|
||||
const char *host_end;
|
||||
const char *colon;
|
||||
const size_t legacy_prefix_length =
|
||||
sizeof(BANBAN_OTA_LEGACY_URL_PREFIX) - 1;
|
||||
|
||||
if (url == NULL || url[0] == '\0') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strncmp(url, BANBAN_OTA_LEGACY_URL_PREFIX,
|
||||
legacy_prefix_length) == 0) {
|
||||
return banban_path_is_fbf(url + legacy_prefix_length - 1);
|
||||
}
|
||||
|
||||
if (strncmp(url, "http://", 7) == 0) {
|
||||
authority = url + 7;
|
||||
} else if (strncmp(url, "https://", 8) == 0) {
|
||||
authority = url + 8;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
|
||||
path = strchr(authority, '/');
|
||||
if (path == NULL || path == authority) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (memchr(authority, '@', (size_t)(path - authority)) != NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
colon = memchr(authority, ':', (size_t)(path - authority));
|
||||
host_end = colon != NULL ? colon : path;
|
||||
if (!banban_host_is_talkingq(authority,
|
||||
(size_t)(host_end - authority))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (colon != NULL &&
|
||||
!banban_port_is_valid(colon + 1, (size_t)(path - colon - 1))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return banban_path_is_fbf(path);
|
||||
}
|
||||
@@ -102,9 +102,13 @@ void enter_sleep_mode(void)
|
||||
if (g_in_sleep_mode) return;
|
||||
|
||||
printf("enter_sleep_mode\r\n");
|
||||
/* 先排入休眠提示音,再设置交互休眠标志;提示音入口会拦截休眠状态。
|
||||
* 播放线程会唤醒音频芯片,播放结束后由原有空闲计时进入低功耗。 */
|
||||
int prompt_ret = app_play_tips_file(12);
|
||||
g_in_sleep_mode = 1;
|
||||
|
||||
app_play_tips_file(12);
|
||||
if (prompt_ret != 0) {
|
||||
printf("sleep prompt queue failed:ret=%d\r\n", prompt_ret);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
// 在文件顶部添加状态变量
|
||||
static uint8_t speech_detect_count = 0; // 连续语音帧计数器
|
||||
#define SPEECH_DETECT_THRESHOLD 20 // 连续检测到语音的阈值
|
||||
#define PLAYBACK_VAD_GUARD_MS 800 // 屏蔽播放结束后的尾音和音频通路切换噪声
|
||||
#define VAD_FRAME_BYTES 640 // 20ms @ 16kHz, 16-bit mono
|
||||
#define VAD_PREROLL_FRAMES 25 // 保留500ms预录音,避免吞掉用户首字
|
||||
uint8_t no_speech_timeout = 0;
|
||||
extern int g_mic_working;
|
||||
|
||||
@@ -40,7 +43,10 @@ static uint8_t vad_leave_message_flag = 0;//留言模式vad只做单次
|
||||
static Fvad *fvad_inst = NULL;
|
||||
extern struct ringbuffer *ring_sender;
|
||||
extern ql_sem_t sender_sem;
|
||||
extern unsigned char wss_connect_start; //表示vad拾音结束 开始等待后台返回 如果超过5s没有返回 则报超时语音
|
||||
extern void conversation_no_speech(void);
|
||||
extern void conversation_record_begin(void);
|
||||
extern void conversation_cancel_wait(void);
|
||||
extern int app_role_notice_before_finish(void);
|
||||
|
||||
// 配置
|
||||
static int sample_rate = 16000; // FVAD 支持的采样率:8k/16k/32k/48k
|
||||
@@ -57,6 +63,9 @@ static int64_t no_sound_start_time = 0;
|
||||
static int64_t cmd_wait_start_time = 0;
|
||||
static bool first_sound_detected = false;
|
||||
static int64_t recording_start_time = 0; // 记录录音开始时间(首次检测到声音时)
|
||||
static uint8_t vad_preroll[VAD_PREROLL_FRAMES][VAD_FRAME_BYTES];
|
||||
static size_t vad_preroll_next = 0;
|
||||
static size_t vad_preroll_count = 0;
|
||||
|
||||
static int vad_tts_playback_finished_status = 0;
|
||||
// 统计
|
||||
@@ -83,6 +92,12 @@ void vad_tts_playback_finished()
|
||||
vad_tts_playback_finished_status = 1;
|
||||
}
|
||||
|
||||
void vad_reset_conversation_flags(void)
|
||||
{
|
||||
no_speech_timeout = 0;
|
||||
vad_tts_playback_finished_status = 0;
|
||||
}
|
||||
|
||||
void vad_set_speech_timeout_flag(uint8_t value)
|
||||
{
|
||||
no_speech_timeout = value;
|
||||
@@ -101,6 +116,9 @@ void set_state(system_state_t new_state) {
|
||||
no_sound_start_time = now;
|
||||
first_sound_detected = false;
|
||||
speech_frame_count = 0;
|
||||
speech_detect_count = 0;
|
||||
vad_preroll_next = 0;
|
||||
vad_preroll_count = 0;
|
||||
g_mic_working = 0;
|
||||
break;
|
||||
case STATE_RECORDING_ACTIVE:
|
||||
@@ -207,7 +225,7 @@ int vad_audio_stop(void) {
|
||||
|
||||
ql_audio_record_close(record_hd, 1);
|
||||
|
||||
wss_connect_start = 1;//用于判断后台响应超时
|
||||
/* Reply timeout starts in the sender only after END is submitted. */
|
||||
|
||||
key_start_leave = 0;
|
||||
//ql_rtos_semaphore_release(sender_sem);
|
||||
@@ -217,6 +235,7 @@ int vad_audio_stop(void) {
|
||||
|
||||
// 停止录音
|
||||
int vad_audio_stop_no_wait_resp(void) {
|
||||
conversation_cancel_wait();
|
||||
|
||||
printf("Audio recording stopped no_wait_resp, frames: %d, speech: %d",
|
||||
frame_count, speech_frame_count);
|
||||
@@ -225,7 +244,7 @@ int vad_audio_stop_no_wait_resp(void) {
|
||||
|
||||
ql_audio_record_close(record_hd, 1);
|
||||
|
||||
wss_connect_start = 0;
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -233,6 +252,7 @@ int vad_audio_stop_no_wait_resp(void) {
|
||||
|
||||
// 触发录音(按键触发)
|
||||
void vad_audio_trigger_record(void) {
|
||||
conversation_record_begin();
|
||||
g_triggered = true;
|
||||
printf("Record triggered");
|
||||
}
|
||||
@@ -290,26 +310,63 @@ void vad_leave_message(void)
|
||||
audio_packet_type = 4;
|
||||
|
||||
vad_leave_message_flag = 1;
|
||||
/* A previous conversation may still be parked in WAITING_COMMAND. NFC
|
||||
* leave-message recording must enter IDLE first, otherwise g_triggered is
|
||||
* left pending forever and its 10-second no-speech timer never starts. */
|
||||
set_state(STATE_IDLE);
|
||||
vad_audio_trigger_record();
|
||||
}
|
||||
|
||||
int vad_leave_message_is_active(void)
|
||||
{
|
||||
return vad_leave_message_flag != 0 || audio_packet_type == 4;
|
||||
}
|
||||
|
||||
/* The conversation key has priority over an unfinished NFC leave-message
|
||||
* transaction. Clear every local latch before the normal conversation prompt
|
||||
* is queued so that its recording can never inherit packetType=4. */
|
||||
int vad_cancel_leave_message(void)
|
||||
{
|
||||
int was_active = vad_leave_message_is_active();
|
||||
|
||||
if (!was_active) return 0;
|
||||
|
||||
printf("cancel pending leave message\r\n");
|
||||
vad_leave_message_flag = 0;
|
||||
g_triggered = false;
|
||||
audio_packet_type = 0;
|
||||
no_speech_timeout = 0;
|
||||
speech_detect_count = 0;
|
||||
vad_preroll_next = 0;
|
||||
vad_preroll_count = 0;
|
||||
key_start_leave = 0;
|
||||
|
||||
if (vad_audio_is_recording()) {
|
||||
set_state(STATE_IDLE);
|
||||
vad_audio_stop_no_wait_resp();
|
||||
} else {
|
||||
set_state(STATE_IDLE);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 主处理函数 - 需要在循环中定期调用
|
||||
void vad_audio_process(void) {
|
||||
size_t bytes_read = 0;
|
||||
int64_t now = hal_os_up_time_ms();
|
||||
|
||||
static size_t buffer_offset = 0;
|
||||
uint8_t mic_buf[640]={0};
|
||||
int record_len = 640;
|
||||
uint8_t mic_buf[VAD_FRAME_BYTES]={0};
|
||||
int record_len = VAD_FRAME_BYTES;
|
||||
uint16 frame[320];
|
||||
int index = 0, i = 0;
|
||||
|
||||
// 状态机处理
|
||||
switch (g_state) {
|
||||
case STATE_IDLE:
|
||||
no_speech_timeout = 0;
|
||||
vad_leave_message_flag = 0;
|
||||
speech_detect_count = 0; // 空闲时清零计数器
|
||||
case STATE_IDLE:
|
||||
no_speech_timeout = 0;
|
||||
speech_detect_count = 0; // 空闲时清零计数器
|
||||
vad_tts_playback_finished_status = 0;
|
||||
|
||||
if (g_triggered) {
|
||||
@@ -326,15 +383,19 @@ void vad_audio_process(void) {
|
||||
//每次读取640字节音频数据
|
||||
ql_audio_record_read(record_hd, mic_buf, &record_len);
|
||||
|
||||
//如果检测到声音再去传输
|
||||
// if(first_sound_detected){
|
||||
//实际上每次编码需要输入1152字节的PCM数据
|
||||
if(hal_os_rb_write(ring_sender, (uint8*)mic_buf, record_len, 25)>0){
|
||||
|
||||
//quec_usbcdc_write((uint8*)mic_buf, record_len);
|
||||
ql_rtos_semaphore_release(sender_sem); //告诉线程去获取录音数据
|
||||
if (g_state == STATE_RECORDING_WAIT_SOUND) {
|
||||
/* 首次开口确认前不向服务端发送静音。只在本地保留最近500ms,
|
||||
* 确认开口时再回填,防止截掉用户首字。 */
|
||||
memcpy(vad_preroll[vad_preroll_next], mic_buf, VAD_FRAME_BYTES);
|
||||
vad_preroll_next = (vad_preroll_next + 1) % VAD_PREROLL_FRAMES;
|
||||
if (vad_preroll_count < VAD_PREROLL_FRAMES) {
|
||||
vad_preroll_count++;
|
||||
}
|
||||
} else {
|
||||
if(hal_os_rb_write(ring_sender, (uint8*)mic_buf, record_len, 25)>0){
|
||||
ql_rtos_semaphore_release(sender_sem);
|
||||
}
|
||||
}
|
||||
// }
|
||||
#if 1
|
||||
for(index=0; index<320; index++)
|
||||
{
|
||||
@@ -347,6 +408,12 @@ void vad_audio_process(void) {
|
||||
|
||||
// 首次声音检测
|
||||
if (!first_sound_detected) {
|
||||
/* 播放刚结束时扬声器尾音和PA切换噪声容易被FVAD认为用户开口。 */
|
||||
if ((now - no_sound_start_time) < PLAYBACK_VAD_GUARD_MS) {
|
||||
speech_detect_count = 0;
|
||||
buffer_offset = 0;
|
||||
break;
|
||||
}
|
||||
if (current_vad == VAD_STATE_ACTIVE) {
|
||||
speech_detect_count++;
|
||||
printf("Speech frame count: %d\r\n", speech_detect_count);
|
||||
@@ -363,8 +430,23 @@ void vad_audio_process(void) {
|
||||
}
|
||||
#endif
|
||||
|
||||
first_sound_detected = true;
|
||||
set_state(STATE_RECORDING_ACTIVE);
|
||||
first_sound_detected = true;
|
||||
{
|
||||
size_t first = (vad_preroll_next + VAD_PREROLL_FRAMES - vad_preroll_count) % VAD_PREROLL_FRAMES;
|
||||
size_t n;
|
||||
int preroll_queued = 0;
|
||||
for (n = 0; n < vad_preroll_count; n++) {
|
||||
size_t frame_index = (first + n) % VAD_PREROLL_FRAMES;
|
||||
if (hal_os_rb_write(ring_sender, vad_preroll[frame_index], VAD_FRAME_BYTES, 25) > 0) {
|
||||
preroll_queued = 1;
|
||||
}
|
||||
}
|
||||
if (preroll_queued) {
|
||||
ql_rtos_semaphore_release(sender_sem);
|
||||
}
|
||||
vad_preroll_count = 0;
|
||||
}
|
||||
set_state(STATE_RECORDING_ACTIVE);
|
||||
printf("First speech confirmed by FVAD (continuous %d frames)", SPEECH_DETECT_THRESHOLD);
|
||||
}
|
||||
} else {
|
||||
@@ -378,6 +460,7 @@ void vad_audio_process(void) {
|
||||
if ((now - no_sound_start_time) > VAD_NO_SOUND_TIMEOUT_MS) {
|
||||
printf("No speech for timeout, stopping");
|
||||
no_speech_timeout = 1;
|
||||
conversation_no_speech();
|
||||
set_state(STATE_WAITING_COMMAND);
|
||||
vad_audio_stop();
|
||||
buffer_offset = 0;
|
||||
@@ -418,15 +501,21 @@ void vad_audio_process(void) {
|
||||
buffer_offset = 0;
|
||||
break;
|
||||
|
||||
case STATE_WAITING_COMMAND:
|
||||
if(vad_tts_playback_finished_status == 1) {
|
||||
printf("finished_status:%d, no_speech_timeout:%d\n", vad_tts_playback_finished_status, no_speech_timeout);
|
||||
set_state(STATE_IDLE);
|
||||
ql_rtos_task_sleep_ms(50);
|
||||
if(no_speech_timeout == 0&&vad_leave_message_flag==0) {
|
||||
vad_audio_trigger_record();
|
||||
}
|
||||
}
|
||||
case STATE_WAITING_COMMAND:
|
||||
if(vad_tts_playback_finished_status == 1) {
|
||||
if (app_role_notice_before_finish()) break;
|
||||
int leave_message_finished = vad_leave_message_flag != 0;
|
||||
printf("finished_status:%d, no_speech_timeout:%d\n", vad_tts_playback_finished_status, no_speech_timeout);
|
||||
set_state(STATE_IDLE);
|
||||
ql_rtos_task_sleep_ms(50);
|
||||
if(no_speech_timeout == 0&&vad_leave_message_flag==0) {
|
||||
vad_audio_trigger_record();
|
||||
}
|
||||
if(leave_message_finished) {
|
||||
vad_leave_message_flag = 0;
|
||||
audio_packet_type = 0;
|
||||
}
|
||||
}
|
||||
|
||||
key_start_leave = 0;
|
||||
break;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ Number_of_Images = 1
|
||||
1_Image_Enable = 1
|
||||
1_Image_Image_ID = 0x30
|
||||
1_Image_Path = customer_app.bin
|
||||
1_Image_Flash_Entry_Address = 0x0000322000
|
||||
1_Image_Flash_Entry_Address = 0x00002E5000
|
||||
1_Image_ID_Name = 1
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
+2
-2
@@ -61,12 +61,12 @@
|
||||
{
|
||||
"name": "customer_app",
|
||||
"format": "raw",
|
||||
"size": "460KiB"
|
||||
"size": "384KiB"
|
||||
},
|
||||
{
|
||||
"name": "customer_fs",
|
||||
"format": "raw",
|
||||
"size": "256KiB"
|
||||
"size": "576KiB"
|
||||
},
|
||||
{
|
||||
"name": "nvm",
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@
|
||||
{
|
||||
"name": "customer_fs",
|
||||
"format": "raw",
|
||||
"size": "256KiB"
|
||||
"size": "640KiB"
|
||||
},
|
||||
{
|
||||
"name": "nvm",
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
"""Run timeout, round and role-notice scheduling with scripted RTOS and transport."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
source = (root / 'ql-application/threadx/interface/custom_app/ws_talkingq.c').read_text()
|
||||
|
||||
def function(name):
|
||||
start = source.index(name + '(')
|
||||
start = source.rfind('\n', 0, start) + 1
|
||||
opening = source.index('{', start)
|
||||
depth = 1
|
||||
end = opening + 1
|
||||
while depth:
|
||||
depth += (source[end] == '{') - (source[end] == '}')
|
||||
end += 1
|
||||
return source[start:end] + '\n'
|
||||
|
||||
state_start = source.index('static ql_mutex_t conversation_mutex')
|
||||
state_end = source.index('static volatile int player_mq_couter')
|
||||
harness = r'''
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
typedef void *ql_mutex_t;
|
||||
static int g_mic_working, recording, shine_init, fota_start;
|
||||
enum { STATE_IDLE, STATE_RECORDING_WAIT_SOUND, STATE_RECORDING_ACTIVE, STATE_WAITING_COMMAND };
|
||||
static int tts_playing, tts_play_status, audio_packet_type, no_speech_timeout;
|
||||
static int power_down_status, sleeping, vad_state;
|
||||
static int queue_failure, last_queue_type;
|
||||
static char last_queue_url[256];
|
||||
static int get_sleep_mode(void) { return sleeping; }
|
||||
static int vad_audio_get_state(void) { return vad_state; }
|
||||
static int queued, disconnected, flags_reset, mutex_locked, player_mq_couter, vad_starts;
|
||||
static uint64_t now;
|
||||
static unsigned int gSequenceNumber;
|
||||
static int end_result;
|
||||
typedef struct { unsigned int sequenceNumber; int packetType, dataSize; } AudioPacket;
|
||||
static int ql_rtos_mutex_lock(ql_mutex_t m, unsigned int t) {
|
||||
(void)m; (void)t; assert(!mutex_locked); mutex_locked = 1; return 0;
|
||||
}
|
||||
static void ql_rtos_mutex_unlock(ql_mutex_t m) { (void)m; assert(mutex_locked); mutex_locked = 0; }
|
||||
static int vad_audio_is_recording(void) { return recording; }
|
||||
static uint64_t hal_os_up_time_ms(void) { return now; }
|
||||
static void vad_set_speech_timeout_flag(int value) { no_speech_timeout = value; }
|
||||
static void vad_reset_conversation_flags(void) { no_speech_timeout = 0; flags_reset++; }
|
||||
static int build_random_tts_error_url(char *p, size_t n) { (void)p; (void)n; return 1; }
|
||||
static int add_file_to_play_player_queue(char *p, int type) {
|
||||
assert(type == 3 || type == 5);
|
||||
if (queue_failure) return -1;
|
||||
queued++; player_mq_couter++; last_queue_type = type;
|
||||
snprintf(last_queue_url, sizeof(last_queue_url), "%s", p);
|
||||
return 0;
|
||||
}
|
||||
static void vad_start_func(void) { vad_starts++; recording = 1; vad_state = STATE_RECORDING_WAIT_SOUND; g_mic_working = 0; }
|
||||
static void app_play_tips_file(int n) { (void)n; assert(0); }
|
||||
static int conversation_try_begin(void);
|
||||
static void talkingq_ws_disconnect(void) {
|
||||
assert(!mutex_locked); disconnected++;
|
||||
/* A key racing a committed timeout cannot start inside disconnect. */
|
||||
assert(conversation_try_begin() == -1);
|
||||
}
|
||||
static int sendRecordingAudioPacket(AudioPacket *p) { assert(p->packetType == 2); return end_result; }
|
||||
'''
|
||||
harness += source[state_start:state_end]
|
||||
for name in ['conversation_try_begin', 'sendEndPacket', 'conversation_check_timeout', 'conversation_accept_voice_message', 'conversation_finish_startup_prompt', 'conversation_find_session_id', 'role_notice_is_url', 'role_notice_start_locked', 'role_notice_poll_locked', 'app_role_notice_handle_url', 'app_role_notice_before_finish', 'role_notice_finished']:
|
||||
harness += function(name)
|
||||
harness += r'''
|
||||
static void reset(void) {
|
||||
g_mic_working = recording = shine_init = fota_start = 0;
|
||||
conversation_round = wait_round = sender_round = 0;
|
||||
conversation_wait = WAIT_NONE; timeout_action_busy = round_response_received = 0;
|
||||
queued = disconnected = flags_reset = no_speech_timeout = mutex_locked = 0;
|
||||
power_down_status = sleeping = queue_failure = 0; vad_state = STATE_IDLE;
|
||||
role_notice_pending = role_notice_playing = role_notice_consumed = 0;
|
||||
role_notice_round = 0; last_queue_type = 0; last_queue_url[0] = 0;
|
||||
now = 1000; end_result = 0; player_mq_couter = vad_starts = 0; active_session_valid = 0; startup_start_ms = 0; tts_playing = 0;
|
||||
}
|
||||
int main(void) {
|
||||
char id[33];
|
||||
const char *tagged = "PROMPT_SOUND_URL:http://host/26101309423304_EFGHIJKLMNOPQRSTUV.mp3";
|
||||
const char *plain = "PROMPT_SOUND_URL:http://host/tts_error.mp3";
|
||||
assert(conversation_find_session_id(tagged, (unsigned int)strlen(tagged), id));
|
||||
assert(strcmp(id, "26101309423304EFGHIJKLMNOPQRSTUV") == 0);
|
||||
assert(!conversation_find_session_id(plain, (unsigned int)strlen(plain), id));
|
||||
assert(!conversation_find_session_id(tagged, 20, id));
|
||||
/* No confirmed speech: prompt remains, without disconnect or reply wait. */
|
||||
reset(); assert(conversation_try_begin() == 0); g_mic_working = 0;
|
||||
conversation_no_speech(); conversation_check_timeout();
|
||||
assert(queued == 1 && disconnected == 0 && no_speech_timeout == 1);
|
||||
conversation_check_timeout(); assert(queued == 1);
|
||||
/* New key cancels pending old no-speech prompt. */
|
||||
reset(); conversation_no_speech(); assert(conversation_try_begin() == 0);
|
||||
g_mic_working = 0; now += 20000; conversation_check_timeout();
|
||||
assert(!queued && !disconnected && !no_speech_timeout);
|
||||
/* Repeated keys during either waiting-for-speech or active speech are ignored. */
|
||||
reset(); recording = 1; conversation_wait = WAIT_REPLY; wait_round = 0;
|
||||
assert(conversation_try_begin() == -1);
|
||||
assert(conversation_round == 0 && conversation_wait == WAIT_REPLY && !flags_reset);
|
||||
/* END starts reply deadline; stale round and early URL cannot arm it. */
|
||||
reset(); sendEndPacket(); assert(conversation_wait == WAIT_REPLY);
|
||||
now += 5000; conversation_check_timeout(); assert(!queued);
|
||||
now++; conversation_check_timeout(); assert(queued == 1 && disconnected == 1);
|
||||
reset(); sendEndPacket(); assert(conversation_try_begin() == 0);
|
||||
g_mic_working = 0; now += 20000; conversation_check_timeout(); assert(!queued && !disconnected);
|
||||
reset(); conversation_round = 1; sendEndPacket(); assert(conversation_wait == WAIT_NONE);
|
||||
reset(); round_response_received = 1; sendEndPacket(); assert(conversation_wait == WAIT_NONE);
|
||||
/* END queue failure still produces an error instead of waiting forever. */
|
||||
reset(); end_result = -1; sendEndPacket(); assert(conversation_wait == WAIT_SEND_FAILURE);
|
||||
conversation_check_timeout(); assert(queued == 1 && disconnected == 1);
|
||||
/* Prompt completion preserves the key-reserved round; automatic listening advances. */
|
||||
reset(); assert(conversation_try_begin() == 0); conversation_record_begin(); assert(conversation_round == 1);
|
||||
g_mic_working = 0; conversation_record_begin(); assert(conversation_round == 2);
|
||||
/* Force-stop cancels waits; stale timeout owner cannot affect a new round. */
|
||||
reset(); conversation_no_speech(); conversation_cancel_wait(); conversation_check_timeout(); assert(!queued);
|
||||
reset(); conversation_wait = WAIT_REPLY; wait_round = 9; now += 20000;
|
||||
conversation_check_timeout(); assert(!queued && !disconnected);
|
||||
/* Reproduce ai_active followed by a late untagged backend error. */
|
||||
reset(); active_session_valid = 1; active_session_round = 0;
|
||||
assert(conversation_try_begin() == 0);
|
||||
assert(!conversation_accept_voice_message("PROMPT_SOUND_URL:http://host/tts_error.mp3"));
|
||||
assert(!conversation_accept_voice_message("TTS_START"));
|
||||
assert(conversation_accept_voice_message("NFC_SOUND_URL:http://host/card.mp3"));
|
||||
/* A following queue entry cannot consume the startup action. */
|
||||
player_mq_couter = 1;
|
||||
conversation_finish_startup_prompt(conversation_round, 0);
|
||||
assert(vad_starts == 1 && recording && !g_mic_working);
|
||||
assert(!conversation_accept_voice_message("PROMPT_SOUND_URL:http://host/tts_error.mp3"));
|
||||
active_session_valid = 1; active_session_round = conversation_round;
|
||||
assert(conversation_accept_voice_message("PROMPT_SOUND_URL:http://host/tts_error.mp3"));
|
||||
/* Stale prompt completion must not start another round. */
|
||||
reset(); assert(conversation_try_begin() == 0);
|
||||
conversation_finish_startup_prompt(conversation_round - 1, 0); assert(!vad_starts);
|
||||
conversation_finish_startup_prompt(conversation_round, -1); assert(vad_starts == 1);
|
||||
/* Lost startup releases reservation; active work is never recovered. */
|
||||
reset(); assert(conversation_try_begin() == 0); now += 30001;
|
||||
conversation_check_timeout(); assert(!g_mic_working);
|
||||
assert(conversation_try_begin() == 0);
|
||||
now += 30001; player_mq_couter = 1; conversation_check_timeout(); assert(g_mic_working);
|
||||
player_mq_couter = 0; recording = 1; conversation_check_timeout(); assert(g_mic_working);
|
||||
/* The exact resource alone is routed; unrelated notices retain original path. */
|
||||
reset();
|
||||
assert(!app_role_notice_handle_url("http://host/assets/audio/card.mp3"));
|
||||
assert(!app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3.extra"));
|
||||
assert(!role_notice_is_url("http://host/other/switch_role.mp3"));
|
||||
assert(!queued);
|
||||
/* No speech: retain one notice until tts_error + optional local fallback ends. */
|
||||
reset(); recording = 1; vad_state = STATE_RECORDING_WAIT_SOUND;
|
||||
assert(app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3"));
|
||||
assert(!queued && role_notice_pending);
|
||||
recording = 0; vad_state = STATE_WAITING_COMMAND; conversation_wait = WAIT_NO_SPEECH;
|
||||
assert(app_role_notice_before_finish() && !queued);
|
||||
conversation_wait = WAIT_NONE; player_mq_couter = 1; no_speech_timeout = 1;
|
||||
assert(app_role_notice_before_finish() && !queued);
|
||||
player_mq_couter = 0;
|
||||
assert(app_role_notice_before_finish() && queued == 1 && last_queue_type == 5);
|
||||
assert(no_speech_timeout == 1); /* Error flag never becomes auto-listening. */
|
||||
assert(app_role_notice_before_finish() && queued == 1);
|
||||
player_mq_couter = 0; role_notice_finished(conversation_round, 0);
|
||||
assert(!app_role_notice_before_finish() && no_speech_timeout == 1);
|
||||
/* Speech: zero insertion while uploading/waiting or between reply segments. */
|
||||
reset(); recording = 1; vad_state = STATE_RECORDING_ACTIVE;
|
||||
app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
|
||||
recording = 0; vad_state = STATE_WAITING_COMMAND; conversation_wait = WAIT_REPLY;
|
||||
assert(app_role_notice_before_finish() && !queued);
|
||||
conversation_wait = WAIT_NONE; tts_playing = 1;
|
||||
assert(app_role_notice_before_finish() && !queued);
|
||||
tts_playing = 0; player_mq_couter = 2;
|
||||
assert(app_role_notice_before_finish() && !queued);
|
||||
player_mq_couter = 0;
|
||||
assert(app_role_notice_before_finish() && queued == 1);
|
||||
role_notice_finished(conversation_round, 0); player_mq_couter = 0;
|
||||
assert(!app_role_notice_before_finish() && no_speech_timeout == 0);
|
||||
/* Repeated switches produce one latest URL at the end, never six entries. */
|
||||
reset(); vad_state = STATE_WAITING_COMMAND; tts_playing = 1;
|
||||
for (int n = 0; n < 6; n++) {
|
||||
char url[128]; snprintf(url, sizeof(url), "http://host/assets/audio/switch_role.mp3?v=%d", n);
|
||||
assert(app_role_notice_handle_url(url)); assert(!queued);
|
||||
}
|
||||
tts_playing = 0;
|
||||
assert(app_role_notice_before_finish() && queued == 1);
|
||||
assert(strstr(last_queue_url, "v=5"));
|
||||
app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
|
||||
assert(queued == 1 && !role_notice_pending);
|
||||
role_notice_finished(conversation_round, 0); player_mq_couter = 0;
|
||||
app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
|
||||
assert(queued == 1 && !app_role_notice_before_finish());
|
||||
/* Idle plays promptly; playback/queue failure releases completion too. */
|
||||
reset(); app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
|
||||
assert(queued == 1); player_mq_couter = 0;
|
||||
role_notice_finished(conversation_round, -1); assert(!app_role_notice_before_finish());
|
||||
reset(); queue_failure = 1;
|
||||
app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
|
||||
assert(!queued && !app_role_notice_before_finish());
|
||||
/* Accepted restart/force-stop cancels notice; no replay into next round. */
|
||||
reset(); vad_state = STATE_WAITING_COMMAND; tts_playing = 1;
|
||||
app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
|
||||
assert(conversation_try_begin() == 0 && !role_notice_pending);
|
||||
reset(); recording = 1; app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
|
||||
conversation_cancel_wait(); assert(!role_notice_pending && !role_notice_playing);
|
||||
for (int mode = 0; mode < 3; mode++) {
|
||||
reset(); vad_state = STATE_WAITING_COMMAND;
|
||||
app_role_notice_handle_url("http://host/assets/audio/switch_role.mp3");
|
||||
fota_start = mode == 0; power_down_status = mode == 1; sleeping = mode == 2;
|
||||
assert(!app_role_notice_before_finish() && !role_notice_pending && !queued);
|
||||
}
|
||||
puts("PASS: role notices: silent/speech end, latest-only coalescing, idle, failure, restart/OTA/shutdown/sleep isolation; late backend error admission, prompt completion with queued audio, stale prompt, failed playback, stalled-startup recovery; no-speech prompt, new-round cancellation, ignored recording keys, END deadline, stale rounds, early reply, send failure, timeout/key race, auto-listening");
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='banban-timeout-') as directory:
|
||||
cfile = Path(directory) / 'timeout.c'
|
||||
exe = Path(directory) / 'timeout-test'
|
||||
cfile.write_text(harness)
|
||||
subprocess.run(['cc', '-std=c99', '-Wall', '-Wextra', '-Werror', str(cfile), '-o', str(exe)], check=True)
|
||||
subprocess.run([str(exe)], check=True)
|
||||
@@ -0,0 +1,53 @@
|
||||
#include <stdio.h>
|
||||
|
||||
#include "ota_url_policy.h"
|
||||
|
||||
typedef struct {
|
||||
const char *url;
|
||||
int expected;
|
||||
} url_case_t;
|
||||
|
||||
int main(void)
|
||||
{
|
||||
static const url_case_t cases[] = {
|
||||
{"http://101.35.224.118:8080/assets/firmware/app.bin", 1},
|
||||
{"http://101.35.224.118:8080/assets/firmware/release/app.bin?token=x", 1},
|
||||
{"http://talkingq.com/app.bin", 1},
|
||||
{"https://talkingq.com/releases/app.bin", 1},
|
||||
{"https://ota.talkingq.com/releases/app.bin", 1},
|
||||
{"https://cdn.cn.talkingq.com:8443/a/B.BIN?token=x", 1},
|
||||
{"https://OTA.TALKINGQ.COM/app.bin", 1},
|
||||
{"https://eviltalkingq.com/app.bin", 0},
|
||||
{"https://talkingq.com.evil.example/app.bin", 0},
|
||||
{"https://talkingq.com@evil.example/app.bin", 0},
|
||||
{"https://evil.example@talkingq.com/app.bin", 0},
|
||||
{"https://a..talkingq.com/app.bin", 0},
|
||||
{"https://-ota.talkingq.com/app.bin", 0},
|
||||
{"https://ota-.talkingq.com/app.bin", 0},
|
||||
{"https://ota.talkingq.com:0/app.bin", 0},
|
||||
{"https://ota.talkingq.com:65536/app.bin", 0},
|
||||
{"https://ota.talkingq.com:abc/app.bin", 0},
|
||||
{"ftp://ota.talkingq.com/app.bin", 0},
|
||||
{"https://ota.talkingq.com/", 0},
|
||||
{"https://ota.talkingq.com/app.zip", 0},
|
||||
{"https://ota.talkingq.com/app.bin/extra", 0},
|
||||
{"https://ota.talkingq.com/a/../app.bin", 0},
|
||||
{"https://ota.talkingq.com/app.bin#fragment", 0},
|
||||
{"http://101.35.224.118:8080/other/app.bin", 0},
|
||||
{"http://192.168.1.1/app.bin", 0},
|
||||
};
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) {
|
||||
int actual = banban_ota_url_is_allowed(cases[i].url);
|
||||
if (actual != cases[i].expected) {
|
||||
fprintf(stderr, "case %lu failed: expected=%d actual=%d url=%s\n",
|
||||
(unsigned long)i, cases[i].expected, actual, cases[i].url);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
printf("OTA URL policy tests passed: %lu cases\n",
|
||||
(unsigned long)(sizeof(cases) / sizeof(cases[0])));
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fail-closed checks for the EG800AKCN_11LG OTA-ready factory baseline."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
LAYOUT = ROOT / "ql-config/quec-project/aboot/config/partition/CRANELRH_QUEC_FLASH_LAYOUT_SS_04M_HX_GNSS.json"
|
||||
FLASH_TYPE = ROOT / "ql-config/config/QuecFLashType.ini"
|
||||
CHIP_PLATFORM = ROOT / "ql-config/config/CHIP_PLAT.ini"
|
||||
LINKER = ROOT / "ql-application/threadx/config/common/app_linkscript.ld"
|
||||
FS_IMAGE = ROOT / "ql-config/config/EG800AKCN_11LG/customer_fs.bin"
|
||||
DSP_IMAGE = ROOT / "ql-config/soc_platform/CRANELRH/images/DSP_Reduce_SuLog_Buffer/dsp.bin"
|
||||
RF_IMAGE = ROOT / "ql-config/config/EG800AKCN_11LG/rf.bin"
|
||||
GNSS_BOOT_IMAGE = ROOT / "ql-config/config/EG800AKCN_11LG/gnss_boot.pkg"
|
||||
GNSS_FIRM_IMAGE = ROOT / "ql-config/config/EG800AKCN_11LG/gnss_firm.pkg"
|
||||
KERNEL_FLAGS = ROOT / "ql-kernel/threadx/config/common/default_dflags_1602.mk"
|
||||
FOTA_HEADER = ROOT / "ql-application/threadx/common/include/ql_fota.h"
|
||||
FBF_CONFIG = ROOT / "ql-bootloader/updater/tool/config"
|
||||
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"
|
||||
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"
|
||||
|
||||
FLASH_SIZE = 0x400000
|
||||
EXPECTED = {
|
||||
"app_start": 0x002E5000,
|
||||
"app_size": 0x00060000,
|
||||
"fs_start": 0x00345000,
|
||||
"fs_size": 0x00090000,
|
||||
"xip_start": 0x802E5000,
|
||||
}
|
||||
FBF_HEADER_ALLOWANCE = 8192
|
||||
MINIMUM_SYSTEM_MARGIN = 64 * 1024
|
||||
EXPECTED_FS_SHA256 = "51ad32f8196aeab36a62ed436ac65f495ce204436f8ef57efccafc87313b0d3c"
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
raise SystemExit(f"OTA-ready baseline verification failed: {message}")
|
||||
|
||||
|
||||
def parse_size(value: str) -> int:
|
||||
if value == "-1":
|
||||
return -1
|
||||
match = re.fullmatch(r"(\d+)(KiB|MiB)", value)
|
||||
if not match:
|
||||
fail(f"unsupported partition size {value!r}")
|
||||
scale = 1024 if match.group(2) == "KiB" else 1024 * 1024
|
||||
return int(match.group(1)) * scale
|
||||
|
||||
|
||||
def flattened_tail() -> list[tuple[str, int]]:
|
||||
data = json.loads(LAYOUT.read_text(encoding="utf-8"))
|
||||
qspi = next(item for item in data["partitions"] if item["name"] == "qspi")
|
||||
result: list[tuple[str, int]] = []
|
||||
for item in qspi["partitions"]:
|
||||
if item["name"] == "factory":
|
||||
result.extend((child["name"], parse_size(child["size"])) for child in item["partitions"])
|
||||
elif item["name"] in {"reserved", "customer_app", "customer_fs", "nvm", "erase_rd", "fota_param"}:
|
||||
result.append((item["name"], parse_size(item["size"])))
|
||||
return result
|
||||
|
||||
|
||||
def verify_layout() -> None:
|
||||
if CHIP_PLATFORM.read_text(encoding="utf-8").strip() != "1605":
|
||||
fail("this baseline requires CHIP_PLAT.ini=1605")
|
||||
if FLASH_TYPE.read_text(encoding="utf-8").strip() != "CC11XXW_GPS":
|
||||
fail("this baseline requires QuecFLashType.ini=CC11XXW_GPS")
|
||||
|
||||
tail = flattened_tail()
|
||||
sizes = dict(tail)
|
||||
if sizes.get("customer_app") != EXPECTED["app_size"]:
|
||||
fail("customer_app must be 384 KiB")
|
||||
if sizes.get("customer_fs") != EXPECTED["fs_size"]:
|
||||
fail("customer_fs must be 576 KiB")
|
||||
|
||||
suffix = 0
|
||||
starts: dict[str, int] = {}
|
||||
for name, size in reversed(tail):
|
||||
if size < 0:
|
||||
continue
|
||||
suffix += size
|
||||
starts[name] = FLASH_SIZE - suffix
|
||||
|
||||
if starts.get("customer_app") != EXPECTED["app_start"]:
|
||||
fail(f"customer_app starts at 0x{starts.get('customer_app', 0):08X}, expected 0x{EXPECTED['app_start']:08X}")
|
||||
if starts.get("customer_fs") != EXPECTED["fs_start"]:
|
||||
fail(f"customer_fs starts at 0x{starts.get('customer_fs', 0):08X}, expected 0x{EXPECTED['fs_start']:08X}")
|
||||
|
||||
|
||||
def verify_linker() -> None:
|
||||
text = LINKER.read_text(encoding="utf-8")
|
||||
rom = re.search(r"__ROM_START_ADDR\s*=\s*(0x[0-9A-Fa-f]+)", text)
|
||||
maximum = re.search(r"__MAX_ROM_SIZE\s*=\s*(0x[0-9A-Fa-f]+)", text)
|
||||
if not rom or int(rom.group(1), 16) != EXPECTED["xip_start"]:
|
||||
fail("linker ROM address is not 0x802E5000")
|
||||
if not maximum or int(maximum.group(1), 16) != EXPECTED["app_size"]:
|
||||
fail("linker ROM limit is not the 384 KiB customer_app partition")
|
||||
|
||||
|
||||
def verify_fs_image() -> None:
|
||||
image = FS_IMAGE.read_bytes()
|
||||
if len(image) != EXPECTED["fs_size"]:
|
||||
fail(f"customer_fs.bin is {len(image)} bytes, expected {EXPECTED['fs_size']}")
|
||||
digest = hashlib.sha256(image).hexdigest()
|
||||
if digest != EXPECTED_FS_SHA256:
|
||||
fail(f"customer_fs.bin SHA-256 is {digest}; expected {EXPECTED_FS_SHA256}")
|
||||
magic_at = image.find(b"littlefs", 0, 64)
|
||||
if magic_at < 0:
|
||||
fail("customer_fs.bin has no LittleFS superblock")
|
||||
# The global-state tag follows the magic; geometry starts 12 bytes later.
|
||||
disk_version, block_size, block_count = struct.unpack_from("<III", image, magic_at + 12)
|
||||
if disk_version >> 16 != 2:
|
||||
fail(f"unsupported LittleFS disk version 0x{disk_version:08X}")
|
||||
if block_size != 4096 or block_count != 144:
|
||||
fail(f"LittleFS geometry is block_size={block_size}, block_count={block_count}; expected 4096/144")
|
||||
|
||||
|
||||
def align_up(value: int, alignment: int) -> int:
|
||||
return (value + alignment - 1) // alignment * alignment
|
||||
|
||||
|
||||
def verify_system_fit(kernel_path: Path) -> None:
|
||||
images = (kernel_path, DSP_IMAGE, RF_IMAGE, GNSS_BOOT_IMAGE, GNSS_FIRM_IMAGE)
|
||||
for path in images:
|
||||
if not path.is_file():
|
||||
fail(f"system image does not exist: {path}")
|
||||
|
||||
# bootloader=88 KiB; system begins with ptable=4 KiB and rd=16 KiB.
|
||||
system_end = (88 + 4 + 16) * 1024
|
||||
for path in images:
|
||||
system_end = align_up(system_end, 4096)
|
||||
system_end += path.stat().st_size
|
||||
system_end = align_up(system_end, 4096)
|
||||
|
||||
margin = EXPECTED["app_start"] - system_end
|
||||
if margin < MINIMUM_SYSTEM_MARGIN:
|
||||
fail(
|
||||
f"system ends at 0x{system_end:08X}; margin before customer_app is "
|
||||
f"{margin} bytes, expected at least {MINIMUM_SYSTEM_MARGIN}"
|
||||
)
|
||||
|
||||
|
||||
def verify_fota_contract() -> None:
|
||||
flags = KERNEL_FLAGS.read_text(encoding="utf-8", errors="replace")
|
||||
if "-DQUEC_APP_FULL_FOTA" not in flags:
|
||||
fail("kernel no longer enables QUEC_APP_FULL_FOTA")
|
||||
api = FOTA_HEADER.read_text(encoding="utf-8", errors="replace")
|
||||
for symbol in (
|
||||
"ql_fota_init",
|
||||
"ql_fota_image_write",
|
||||
"ql_fota_image_flush",
|
||||
"ql_fota_image_verify",
|
||||
"ql_fota_set_package_path",
|
||||
"ql_fota_fixed_internal_flash_upgrade",
|
||||
):
|
||||
if symbol not in api:
|
||||
fail(f"SDK FULLOTA2 API {symbol} is missing")
|
||||
|
||||
fbf_config = FBF_CONFIG.read_text(encoding="utf-8", errors="replace")
|
||||
fbf_image_path = re.search(r"(?m)^1_Image_Path\s*=\s*(\S+)\s*$", fbf_config)
|
||||
fbf_entry = re.search(
|
||||
r"(?m)^1_Image_Flash_Entry_Address\s*=\s*(0x[0-9A-Fa-f]+)\s*$",
|
||||
fbf_config,
|
||||
)
|
||||
if not fbf_image_path or fbf_image_path.group(1) != "customer_app.bin":
|
||||
fail("FBF config does not package customer_app.bin")
|
||||
if not fbf_entry or int(fbf_entry.group(1), 16) != EXPECTED["app_start"]:
|
||||
actual = fbf_entry.group(1) if fbf_entry else "missing"
|
||||
fail(
|
||||
f"FBF target address is {actual}; expected "
|
||||
f"0x{EXPECTED['app_start']:08X}"
|
||||
)
|
||||
|
||||
identity = BASELINE_HEADER.read_text(encoding="utf-8")
|
||||
if "eg800akcn_11lg-hxgnss-fs576-app2e5000" not in identity:
|
||||
fail("application layout identity is missing")
|
||||
generated = BUILD_MACRO.read_text(encoding="utf-8", errors="replace")
|
||||
if '#define CUSTOM_APP_OTA_LAYOUT_ID "eg800akcn_11lg-hxgnss-fs576-app2e5000"' not in generated:
|
||||
fail("generated build macro has the wrong OTA layout identity")
|
||||
if '#define CUSTOM_APP_OTA_BUILD_PROFILE "ota_ready_hxgnss_fs576"' 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")
|
||||
if "ota_url_policy.c" not in makefile:
|
||||
fail("OTA URL policy 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")
|
||||
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")
|
||||
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():
|
||||
fail(f"application binary does not exist: {path}")
|
||||
size = path.stat().st_size
|
||||
if size + FBF_HEADER_ALLOWANCE > EXPECTED["app_size"]:
|
||||
fail(
|
||||
f"app.bin ({size}) plus {FBF_HEADER_ALLOWANCE}-byte FBF allowance exceeds "
|
||||
f"customer_app ({EXPECTED['app_size']})"
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--app-bin", type=Path, help="also enforce the application/FBF capacity gate")
|
||||
parser.add_argument("--kernel-bin", type=Path, help="also enforce the GNSS system/customer_app margin")
|
||||
args = parser.parse_args()
|
||||
verify_layout()
|
||||
verify_linker()
|
||||
verify_fs_image()
|
||||
verify_fota_contract()
|
||||
if args.app_bin:
|
||||
verify_app(args.app_bin if args.app_bin.is_absolute() else ROOT / args.app_bin)
|
||||
if args.kernel_bin:
|
||||
verify_system_fit(args.kernel_bin if args.kernel_bin.is_absolute() else ROOT / args.kernel_bin)
|
||||
print("OTA-ready baseline verified: hxgnss fs576, app384@0x002E5000, FULLOTA2 available")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user