diff --git a/docs/factory_test.md b/docs/factory_test.md index 4ff0d84..5fc4ea8 100644 --- a/docs/factory_test.md +++ b/docs/factory_test.md @@ -7,20 +7,21 @@ 业务按键或业务 NFC 线程。 产测固件复用 SDK 底层接口、ES8311 配置、DP1312EA 驱动和 Type A 读卡实现。 -测试结果通过 USB CDC 和调试串口输出。上位机只需解析以 `FT1 ` 开头的行, +测试结果通过 DBG UART 输出。上位机只需解析以 `FT1 ` 开头的行, 其他 SDK 调试信息可以忽略。 默认通信参数: -- USB CDC,115200 8N1; -- 同一条协议也通过 `printf` 输出到调试日志; +- DBG UART(GPIO29/30),115200 8N1; +- 协议与 SDK 调试日志共用 `printf` 输出; - 每条协议消息为一行 UTF-8/ASCII JSON,以 `\r\n` 结束; -- 协议前缀固定为 `FT1 `,协议版本为 `1`。 +- 协议前缀固定为 `FT1 `,协议版本为 `2`。 ## 2. 测试流程 ```text 长按电源键开机 + -> 立即在后台打开 GNSS,持续累计 NMEA、卫星数和定位状态 -> 等待音频子系统就绪 -> ES8311 ID/初始化,设置所有播放音量为 100% -> 播放开机提示音 @@ -31,7 +32,9 @@ -> 4G 注册、PDP 激活、获取 IPv4 -> 前面全部通过后启用 NFC 测试 -> 读取 NTAG213 UID - -> 播放“全部测试通过” + -> 播放一次 GNSS 测试提示音并读取后台累计结果 + -> 未达到门槛时保持呼吸灯并持续等待 + -> GNSS 通过后结束搜索 -> 播放关机提示音 -> 输出最终报告 -> 正常关机 @@ -47,29 +50,29 @@ | 0 | `BOOT_AUDIO` | 读取 ES8311 ID,初始化外置 Codec,播放 `U:/factory_boot_zh.mp3` | Codec ID 为 `0x83`,文件存在且播放结束 | 15 秒播放超时 | | 1 | `POWER_ADC` | 读取 6 次 VBAT,去掉最大值和最小值后平均 | `3500mV <= 平均值 <= 4200mV` | - | | 2 | `AUDIO_LOOPBACK` | 按住 GPIO84 录音,松开后按原音量回放 | 录音和回放 API 成功且至少有一帧 PCM | 等待按键 60 秒,录音最长 5 秒 | -| 3 | `ESIM` | 查询 SIM 状态并读取 ICCID | `READY`,ICCID 为 18-22 位十进制数字 | 30 秒 | +| 3 | `ESIM` | 查询 SIM 状态并读取 ICCID | `READY`,且能读到 18-22 位 ICCID | 30 秒 | | 4 | `DATA_CALL` | 等待注册、激活 CID 1 IPv4 PDP | 返回非 `0.0.0.0` 的 IPv4 | 注册等待 120 秒 | | 5 | `NFC` | 检查 DP1312EA 版本并轮询 NTAG213 | 版本 `0x82`,成功读取 UID | 60 秒 | +| 6 | `GNSS` | 开机后后台搜索,到节点后循环读取累计值 | `open_ret=0`、`nmea_count>=1`,且 `sat_count>=1` 或 `fix_status>0` | 无限等待直到通过;打开失败时每 2 秒重试 | 说明:`AUDIO_LOOPBACK` 使用 GPIO84 按键,软件通过仅代表录放数据链路完成。 最终声学判断由工人完成。 +GPIO70 蓝色状态灯在普通流程中长亮,Loopback 使用 250ms 慢闪,NFC 使用 100ms +快闪,GNSS 使用 PWM1 做约 1.4 秒一轮的呼吸效果。如果 PWM 配置或使能失败, +GNSS 会自动降级为两次 80ms 短闪后熄灭 520ms,不影响测试判定。GNSS 未达到 +基础硬件门槛时不会报失败或退出节点,指示灯持续呼吸;通过后才恢复常亮并进入关机流程。 + ## 4. 音频和文件系统接口 固件只通过以下固定路径访问提示音: ```text U:/factory_boot_zh.mp3 -U:/factory_pass_zh.mp3 -U:/factory_shutdown_zh.mp3 -``` - -文件系统同时预留三段快速节点提示音,供后续接入测试节点时使用: - -```text U:/factory_audio_loopback_zh.mp3 U:/factory_nfc_test_zh.mp3 U:/factory_gnss_test_zh.mp3 +U:/factory_shutdown_zh.mp3 ``` 对应资源源目录为: @@ -79,8 +82,8 @@ audio_prompts/factory_test/customer_fs/ ``` 资源为 16kHz、单声道、48kbps MP3。代码不会下载提示音,也不会在运行时回退到 -业务网络资源。缺少开机提示音会返回 `1002`;最终提示音缺失或播放失败会返回 -`1801`,并禁止自动关机。 +业务网络资源。节点提示音缺失或播放失败会返回 `1002`/`1003`;关机提示音播放 +失败会返回 `1801`,并禁止自动关机。本固件没有“全部测试通过”音频。 本分支不生成、不覆盖 `ql-config/config/EG800AKCN_11LG/customer_fs.bin`。 Windows 打包方法见 [factory_test_windows_fs.md](factory_test_windows_fs.md)。 @@ -91,7 +94,7 @@ Windows 打包方法见 [factory_test_windows_fs.md](factory_test_windows_fs.md) | 字段 | 说明 | |---|---| -| `v` | 协议版本,当前为 1 | +| `v` | 协议版本,当前为 2 | | `type` | `session_start`、`node_start`、`node_progress`、`node_result`、`session_result` 或 `power_off` | | `seq` | 本次开机内单调递增的消息序号 | | `session` | RTC 有效时使用时间生成,否则为 `BOOT-` | @@ -106,6 +109,7 @@ Windows 打包方法见 [factory_test_windows_fs.md](factory_test_windows_fs.md) ### 5.2 会话开始 ```text +FT1 {"v":2,"type":"session_start","seq":1,"session":"20260923-163010","uptime_ms":2050,"imei":"860123456789012","iccid":"","nfc_uid":"","fw":"FACTORY_TEST_1.1.0","start_time":"2026-09-23T16:30:10"} ``` RTC 无效时 `start_time` 为 `null`。RTC 无效不会导致产测失败;测试耗时始终以 @@ -114,40 +118,38 @@ RTC 无效时 `start_time` 为 `null`。RTC 无效不会导致产测失败;测 ### 5.3 节点开始 ```text -FT1 {"v":1,"type":"node_start","seq":2,"session":"20260923-163010","uptime_ms":2100,"imei":"860123456789012","iccid":"","nfc_uid":"","node":1,"name":"POWER_ADC"} +FT1 {"v":2,"type":"node_start","seq":2,"session":"20260923-163010","uptime_ms":2100,"imei":"860123456789012","iccid":"","nfc_uid":"","node":1,"name":"POWER_ADC"} ``` ### 5.4 ADC 通过 ```text -FT1 {"v":1,"type":"node_result","seq":3,"session":"20260923-163010","uptime_ms":2450,"imei":"860123456789012","iccid":"","nfc_uid":"","node":1,"name":"POWER_ADC","result":"PASS","code":0,"elapsed_ms":350,"data":{"voltage_mv":3865,"sample_min_mv":3858,"sample_max_mv":3873,"limit_min_mv":3500,"limit_max_mv":4200}} +FT1 {"v":2,"type":"node_result","seq":3,"session":"20260923-163010","uptime_ms":2450,"imei":"860123456789012","iccid":"","nfc_uid":"","node":1,"name":"POWER_ADC","result":"PASS","code":0,"elapsed_ms":350,"data":{"voltage_mv":3865,"sample_min_mv":3858,"sample_max_mv":3873,"limit_min_mv":3500,"limit_max_mv":4200}} ``` -### 5.5 NFC 和最终报告 +### 5.5 数据连接 ```text -FT1 {"v":1,"type":"node_result","seq":14,"session":"20260923-163010","uptime_ms":28000,"imei":"860123456789012","iccid":"89860xxxxxxxxxxxxxxx","nfc_uid":"","node":5,"name":"DATA_CALL","result":"PASS","code":0,"elapsed_ms":8200,"data":{"ip":"10.23.45.67","ip_type":"IPv4"}} +FT1 {"v":2,"type":"node_result","seq":14,"session":"20260923-163010","uptime_ms":28000,"imei":"860123456789012","iccid":"89860xxxxxxxxxxxxxxx","nfc_uid":"","node":4,"name":"DATA_CALL","result":"PASS","code":0,"elapsed_ms":8200,"data":{"ip":"10.23.45.67","ip_type":"IPv4"}} ``` -```text -``` - -### 5.6 NFC 和最终报告 +### 5.6 NFC、GNSS 和最终报告 ```text -FT1 {"v":1,"type":"node_result","seq":18,"session":"20260923-163010","uptime_ms":34000,"imei":"860123456789012","iccid":"89860xxxxxxxxxxxxxxx","nfc_uid":"04A1B2C3D4E5F6","node":5,"name":"NFC","result":"PASS","code":0,"elapsed_ms":1900,"data":{"chip_version":"0x82","uid":"04A1B2C3D4E5F6"}} +FT1 {"v":2,"type":"node_result","seq":18,"session":"20260923-163010","uptime_ms":34000,"imei":"860123456789012","iccid":"89860xxxxxxxxxxxxxxx","nfc_uid":"04A1B2C3D4E5F6","node":5,"name":"NFC","result":"PASS","code":0,"elapsed_ms":1900,"data":{"chip_version":"0x82","uid":"04A1B2C3D4E5F6"}} +FT1 {"v":2,"type":"node_result","seq":20,"session":"20260923-163010","uptime_ms":35200,"imei":"860123456789012","iccid":"89860xxxxxxxxxxxxxxx","nfc_uid":"04A1B2C3D4E5F6","node":6,"name":"GNSS","result":"PASS","code":0,"elapsed_ms":950,"data":{"open_ret":0,"nmea_count":65,"sat_count":1,"fix_status":0,"search_ms":35100,"required_nmea_count":1,"required_sat_count":1}} ``` 最终报告包含 IMEI、ICCID、NFC UID、失败码、开始时间、总耗时和全部节点状态: ```text -FT1 {"v":1,"type":"session_result","seq":19,"session":"20260923-163010","uptime_ms":40120,"result":"PASS","fail_code":0,"imei":"860123456789012","iccid":"89860xxxxxxxxxxxxxxx","nfc_uid":"04A1B2C3D4E5F6","start_time":"2026-09-23T16:30:10","duration_ms":38070,"nodes":{"BOOT_AUDIO":"PASS","POWER_ADC":"PASS","AUDIO_LOOPBACK":"PASS","ESIM":"PASS","DATA_CALL":"PASS","NFC":"PASS"}} +FT1 {"v":2,"type":"session_result","seq":21,"session":"20260923-163010","uptime_ms":40120,"result":"PASS","fail_code":0,"imei":"860123456789012","iccid":"89860xxxxxxxxxxxxxxx","nfc_uid":"04A1B2C3D4E5F6","start_time":"2026-09-23T16:30:10","duration_ms":38070,"nodes":{"BOOT_AUDIO":"PASS","POWER_ADC":"PASS","AUDIO_LOOPBACK":"PASS","ESIM":"PASS","DATA_CALL":"PASS","NFC":"PASS","GNSS":"PASS"}} ``` 上位机收到 `power_off` 后应立即落盘本次记录: ```text -FT1 {"v":1,"type":"power_off","seq":20,"session":"20260923-163010","uptime_ms":40150,"imei":"860123456789012","iccid":"89860xxxxxxxxxxxxxxx","nfc_uid":"04A1B2C3D4E5F6","code":0} +FT1 {"v":2,"type":"power_off","seq":22,"session":"20260923-163010","uptime_ms":40150,"imei":"860123456789012","iccid":"89860xxxxxxxxxxxxxxx","nfc_uid":"04A1B2C3D4E5F6","code":0} ``` ## 6. 失败码 @@ -156,8 +158,8 @@ FT1 {"v":1,"type":"power_off","seq":20,"session":"20260923-163010","uptime_ms":4 |---:|---| | 0 | 成功 | | 1001 | ES8311 初始化或设备 ID 检查失败 | -| 1002 | 开机提示音文件不存在 | -| 1003 | 开机提示音播放失败或超时 | +| 1002 | 当前节点提示音文件不存在 | +| 1003 | 当前节点提示音播放失败或超时 | | 1101 | ADC 初始化或读取失败 | | 1102 | 电压低于 3500mV | | 1103 | 电压高于 4200mV | @@ -167,10 +169,13 @@ FT1 {"v":1,"type":"power_off","seq":20,"session":"20260923-163010","uptime_ms":4 | 1204 | 60 秒内未按下 Loopback 按键 | | 1401 | eSIM 未进入 READY | | 1402 | ICCID 读取失败 | -| 1403 | ICCID 长度或字符格式错误 | +| 1403 | ICCID 长度不在 18-22 位范围内 | | 1501 | 4G 网络注册超时 | | 1502 | IPv4 PDP 激活失败 | | 1503 | 未获取有效 IPv4 地址 | +| 1601 | GNSS 打开失败 | +| 1602 | GNSS 打开成功但未收到 NMEA | +| 1603 | GNSS 已收到 NMEA,但未发现卫星且没有有效定位 | | 1701 | 前置节点未全部通过,NFC 锁定 | | 1702 | DP1312EA 版本寄存器不是 `0x82` | | 1703 | 60 秒内没有成功刷卡 | @@ -182,6 +187,7 @@ FT1 {"v":1,"type":"power_off","seq":20,"session":"20260923-163010","uptime_ms":4 - `interface/factory_test/factory_test.c`:顺序状态机和具体测试; - `interface/factory_test/factory_audio.c`:ES8311、100% 音量、MP3、录音回放; +- `interface/factory_test/factory_gnss.c`:开机后台搜星、NMEA/GSV/GGA 累计; - `interface/factory_test/factory_protocol.c`:FT1 JSON 日志; - `interface/factory_test/factory_test.h`:阈值、超时、业务域名、GPIO 和文件路径; - `interface/factory_test/Makefile`:独立产测编译单元。 @@ -193,6 +199,6 @@ ADC 阈值、按键超时和 NFC 超时集中在 `factory_test.h`。GPIO84 是 1. 确认 PCB 上对话键确实连接模组 PWRKEY,第二按键连接 GPIO84。 2. 确认 GPIO68 为 NS4150B `PA_CTL`,GPIO86 为 NFC Reset。 -3. 将三段 MP3 打入 U 盘文件系统,不能只更新 APP。 -4. 首板先观察 ES8311 ID、ADC 原始值、IP 和 DP1312EA 版本日志。 +3. 将五段 MP3 打入 U 盘文件系统,不能只更新 APP。 +4. 首板先观察 ES8311 ID、ADC 原始值、IP、DP1312EA 版本和 GNSS 累计值。 5. 上位机只把 `FT1 ` 行作为协议消息,SDK 的普通 `printf` 不应进入数据库。 diff --git a/factory_host/.gitignore b/factory_host/.gitignore index 8dde840..c041eac 100644 --- a/factory_host/.gitignore +++ b/factory_host/.gitignore @@ -2,4 +2,5 @@ __pycache__/ *.pyc build/ dist/ +dist-*/ vendor/ diff --git a/factory_host/README.md b/factory_host/README.md index dfd1453..d54f0c9 100644 --- a/factory_host/README.md +++ b/factory_host/README.md @@ -38,7 +38,7 @@ factory_host/ - 持续检测烧录串口,支持多工位自动等待设备上电后烧录。 - 同一设备烧录成功后锁定,端口断开并重新出现后才允许烧录下一台。 - 自动识别 DBG UART,解析设备每 500 ms 广播的 FT1 状态。 -- 显示六个测试节点、IMEI、ICCID、NFC UID、失败码和实时日志。 +- 显示七个测试节点、IMEI、ICCID、NFC UID、失败码和实时日志。 - 自动保存汇总 CSV 与单机 JSON,烧录和扫描过程不弹出控制台黑框。 ## 快速验证 diff --git a/factory_host/config/factory_host_config.json b/factory_host/config/factory_host_config.json index 3a7a96b..85574bd 100644 --- a/factory_host/config/factory_host_config.json +++ b/factory_host/config/factory_host_config.json @@ -33,7 +33,7 @@ }, "protocol": { "prefix": "FT1 ", - "version": 1, + "version": 2, "auto_save": true } } diff --git a/factory_host/docs/DEVELOPMENT.md b/factory_host/docs/DEVELOPMENT.md index 5466400..1625b22 100644 --- a/factory_host/docs/DEVELOPMENT.md +++ b/factory_host/docs/DEVELOPMENT.md @@ -135,7 +135,7 @@ firmware/ ``` 还应在一台真实设备上验证:自动等待上电烧录、断开后才允许下一台、DBG UART 自动识别、 -六项 FT1 状态、JSON/CSV 保存,以及产测固件和正式固件切换。 +七项 FT1 状态、GNSS 累计数据、JSON/CSV 保存,以及产测固件和正式固件切换。 ## 7. 固件替换规则 diff --git a/factory_host/docs/USAGE.md b/factory_host/docs/USAGE.md index d6fcd92..e7da6c8 100644 --- a/factory_host/docs/USAGE.md +++ b/factory_host/docs/USAGE.md @@ -50,13 +50,13 @@ factory_host/dist/ }, "protocol": { "prefix": "FT1 ", - "version": 1, + "version": 2, "auto_save": true } } ``` -- `production`:产测固件,包含 FT1 测试程序、开机/Loopback/NFC/关机音频,以及暂不参与流程的 GNSS 音频。 +- `production`:产测固件,包含 FT1 测试程序以及开机、Loopback、NFC、GNSS、关机五段音频。 - `official`:正式业务固件,供产测完成后换烧。 - 固件路径可以使用相对于 EXE 所在发布目录的路径,也可以填写绝对路径。 - 必须选择可直接烧录的内层 FT1 ZIP 或 `*-fac.zip`,不要选择外层 `*-target.zip` 归档。 @@ -92,7 +92,11 @@ factory_host/dist/ - IMEI、ICCID、NFC UID; - 失败码、结构化测试数据和实时串口日志。 -产测固件当前依次执行:开机音频、电源 ADC、音频 Loopback、eSIM、数据连接和 NFC。 +产测固件开机后立即在后台启动 GNSS 搜索,同时依次执行:开机音频、电源 ADC、音频 Loopback、eSIM、数据连接和 NFC。最后的 GNSS 节点只播放一次提示音,然后循环读取此前累计的 NMEA、卫星数和定位状态,直到达到基础硬件判定门槛。 + +GNSS 的基础硬件判定门槛为:GNSS 成功打开、累计收到至少 1 条 NMEA,并且至少发现 1 颗卫星或已取得有效定位。经纬度、定位精度、TTFF 和 1PPS 不参与判定。 + +指示灯状态:普通流程长亮,Loopback 慢闪,NFC 快闪,GNSS 为呼吸灯。如果设备运行时无法启用 PWM,GNSS 会自动改为“双短闪 + 长灭”。GNSS 未通过时会无限等待并持续呼吸;通过后才播放关机音并自动关机。 ## 5. 测试结果与日志 @@ -178,4 +182,4 @@ python -B -m unittest discover -s .\factory_host\tests -p "test_*.py" -v powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\factory_host\scripts\build_release.ps1 ``` -发布前至少使用一台设备验证:产测固件烧录、调试 UART 自动识别、START 握手、六项测试结果、JSON/CSV 保存,以及正式固件换烧。 +发布前至少使用一台设备验证:产测固件烧录、调试 UART 自动识别、七项测试结果、GNSS 室外或窗边搜星、JSON/CSV 保存,以及正式固件换烧。 diff --git a/factory_host/firmware/test/EG800AKCN_11LG-FT1-factory-test.zip b/factory_host/firmware/test/EG800AKCN_11LG-FT1-factory-test.zip index b098821..fb8023b 100644 --- a/factory_host/firmware/test/EG800AKCN_11LG-FT1-factory-test.zip +++ b/factory_host/firmware/test/EG800AKCN_11LG-FT1-factory-test.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:799adedf6b0ff6ad3f6adab281f8a206b99689fb394a198130f51b46e5c01848 -size 6222215 +oid sha256:d124f130a3a45cf8b95d931970ec2ec70aed2b85c9472ce42291b1c2bc2198e8 +size 6224194 diff --git a/factory_host/src/factory_host.py b/factory_host/src/factory_host.py index 0f5b60d..02d4274 100644 --- a/factory_host/src/factory_host.py +++ b/factory_host/src/factory_host.py @@ -94,7 +94,7 @@ DEFAULT_CONFIG = { "official": "firmware/EG800AKCN_11LG-official-fac.zip", }, "serial": {"baudrate": 115200, "encoding": "utf-8", "line_ending": "\\r\\n"}, - "protocol": {"prefix": "FT1 ", "version": 1, "auto_save": True}, + "protocol": {"prefix": "FT1 ", "version": 2, "auto_save": True}, } @@ -550,7 +550,7 @@ class FactoryHost(Tk): number = str(int(number) + 1) if number.isdigit() else f"{number}_new" station = Station(number=number, flash_port=flash_port, test_port=test_port, firmware_kind=self.firmware_kind.get()) self.stations[number] = station - self.table.insert("", END, iid=number, values=(number, flash_port, test_port, station.status, "", "", "0/6", "产测固件", "")) + self.table.insert("", END, iid=number, values=(number, flash_port, test_port, station.status, "", "", f"0/{len(NODE_NAMES)}", "产测固件", "")) return station def remove_station(self) -> None: @@ -592,7 +592,7 @@ class FactoryHost(Tk): protocol = station.protocol self.table.item(station.number, values=( station.number, station.flash_port, station.test_port, station.status, - protocol.session, protocol.imei, f"{protocol.passed_count}/6", + protocol.session, protocol.imei, f"{protocol.passed_count}/{len(NODE_NAMES)}", "产测固件" if station.firmware_kind == "production" else "正式固件", station.message, )) diff --git a/factory_host/src/ft1_protocol.py b/factory_host/src/ft1_protocol.py index bec226b..683c2a6 100644 --- a/factory_host/src/ft1_protocol.py +++ b/factory_host/src/ft1_protocol.py @@ -8,7 +8,7 @@ from typing import Any PREFIX = "FT1 " -VERSION = 1 +VERSION = 2 MESSAGE_TYPES = { "session_start", "node_start", @@ -27,12 +27,13 @@ NODE_NAMES = ( "ESIM", "DATA_CALL", "NFC", + "GNSS", ) ERROR_MESSAGES = { 0: "成功", 1001: "ES8311 初始化或设备 ID 检查失败", - 1002: "开机提示音文件不存在", - 1003: "开机提示音播放失败或超时", + 1002: "当前节点提示音文件不存在", + 1003: "当前节点提示音播放失败或超时", 1101: "ADC 初始化或读取失败", 1102: "电压低于 3500mV", 1103: "电压高于 4200mV", @@ -46,6 +47,9 @@ ERROR_MESSAGES = { 1501: "4G 网络注册超时", 1502: "IPv4 PDP 激活失败", 1503: "未获取有效 IPv4 地址", + 1601: "GNSS 打开失败", + 1602: "GNSS 未收到 NMEA", + 1603: "GNSS 已收到 NMEA,但未发现卫星", 1701: "前置节点未全部通过,NFC 锁定", 1702: "DP1312EA 版本不是 0x82", 1703: "60 秒内未成功刷卡", diff --git a/factory_host/tests/test_factory_host.py b/factory_host/tests/test_factory_host.py index e14353d..b5b415c 100644 --- a/factory_host/tests/test_factory_host.py +++ b/factory_host/tests/test_factory_host.py @@ -75,7 +75,7 @@ class FactoryHostTests(unittest.TestCase): def test_ft1_ignores_sdk_log_and_parses_node(self): self.assertIsNone(parse_ft1_line("DP1312EA is OK!")) message = parse_ft1_line( - 'FT1 {"v":1,"type":"node_result","seq":3,"session":"BOOT-1",' + 'FT1 {"v":2,"type":"node_result","seq":3,"session":"BOOT-1",' '"uptime_ms":2450,"imei":"860123456789012","iccid":"","nfc_uid":"",' '"node":1,"name":"POWER_ADC","result":"PASS","code":0,"elapsed_ms":350,' '"data":{"voltage_mv":3865}}' @@ -85,17 +85,17 @@ class FactoryHostTests(unittest.TestCase): def test_ft1_session_result_and_sequence_guard(self): session = FT1Session() start = parse_ft1_line( - 'FT1 {"v":1,"type":"session_start","seq":1,"session":"BOOT-1",' + 'FT1 {"v":2,"type":"session_start","seq":1,"session":"BOOT-1",' '"uptime_ms":2050,"imei":"860123456789012","iccid":"","nfc_uid":"",' - '"fw":"FACTORY_TEST_1.0.0","start_time":null}' + '"fw":"FACTORY_TEST_1.1.0","start_time":null}' ) session.apply(start) result = parse_ft1_line( - 'FT1 {"v":1,"type":"session_result","seq":2,"session":"BOOT-1",' + 'FT1 {"v":2,"type":"session_result","seq":2,"session":"BOOT-1",' '"uptime_ms":40120,"result":"FAIL","fail_code":1203,"imei":"860123456789012",' '"iccid":"","nfc_uid":"","start_time":null,"duration_ms":38070,"nodes":' '{"BOOT_AUDIO":"PASS","POWER_ADC":"PASS","AUDIO_LOOPBACK":"FAIL",' - '"ESIM":"WAIT","DATA_CALL":"WAIT","NFC":"WAIT"}}' + '"ESIM":"WAIT","DATA_CALL":"WAIT","NFC":"WAIT","GNSS":"WAIT"}}' ) session.apply(result) self.assertEqual(session.result, "FAIL") @@ -107,37 +107,37 @@ class FactoryHostTests(unittest.TestCase): def test_ft1_rejects_node_name_mismatch(self): with self.assertRaises(ProtocolError): parse_ft1_line( - 'FT1 {"v":1,"type":"node_start","seq":2,"session":"BOOT-1",' + 'FT1 {"v":2,"type":"node_start","seq":2,"session":"BOOT-1",' '"uptime_ms":2100,"node":1,"name":"NFC"}' ) def test_ft1_control_messages(self): ready = parse_ft1_line( - 'FT1 {"v":1,"type":"device_ready","seq":1,"session":"BOOT-1",' - '"uptime_ms":100,"fw":"FACTORY_TEST_1.0.0"}' + 'FT1 {"v":2,"type":"device_ready","seq":1,"session":"BOOT-1",' + '"uptime_ms":100,"fw":"FACTORY_TEST_1.1.0"}' ) ack = parse_ft1_line( - 'FT1 {"v":1,"type":"command_ack","seq":2,"session":"BOOT-1",' + 'FT1 {"v":2,"type":"command_ack","seq":2,"session":"BOOT-1",' '"uptime_ms":120,"command":"START","accepted":true}' ) session = FT1Session() - self.assertEqual(session.apply(ready), "设备已识别 FACTORY_TEST_1.0.0") + self.assertEqual(session.apply(ready), "设备已识别 FACTORY_TEST_1.1.0") self.assertEqual(session.apply(ack), "START 已确认") self.assertTrue(session.device_ready_seen) self.assertTrue(session.start_ack_seen) def test_ft1_status_snapshot_keeps_passed_nodes(self): first = parse_ft1_line( - 'FT1 {"v":1,"type":"status_snapshot","seq":1,"session":"BOOT-1",' + 'FT1 {"v":2,"type":"status_snapshot","seq":1,"session":"BOOT-1",' '"uptime_ms":100,"active_node":0,"nodes":{"BOOT_AUDIO":"WAIT",' '"POWER_ADC":"WAIT","AUDIO_LOOPBACK":"WAIT","ESIM":"WAIT",' - '"DATA_CALL":"WAIT","NFC":"WAIT"}}' + '"DATA_CALL":"WAIT","NFC":"WAIT","GNSS":"WAIT"}}' ) second = parse_ft1_line( - 'FT1 {"v":1,"type":"status_snapshot","seq":2,"session":"BOOT-1",' + 'FT1 {"v":2,"type":"status_snapshot","seq":2,"session":"BOOT-1",' '"uptime_ms":600,"active_node":1,"nodes":{"BOOT_AUDIO":"PASS",' '"POWER_ADC":"WAIT","AUDIO_LOOPBACK":"WAIT","ESIM":"WAIT",' - '"DATA_CALL":"WAIT","NFC":"WAIT"}}' + '"DATA_CALL":"WAIT","NFC":"WAIT","GNSS":"WAIT"}}' ) session = FT1Session() session.apply(first) @@ -145,6 +145,20 @@ class FactoryHostTests(unittest.TestCase): self.assertEqual(session.nodes["BOOT_AUDIO"], "PASS") self.assertEqual(session.current_node, "POWER_ADC") + def test_ft1_parses_low_threshold_gnss_result(self): + message = parse_ft1_line( + 'FT1 {"v":2,"type":"node_result","seq":9,"session":"BOOT-1",' + '"uptime_ms":30600,"imei":"860123456789012","iccid":"8986000000000000000",' + '"nfc_uid":"04A1B2C3D4E5F6","node":6,"name":"GNSS","result":"PASS",' + '"code":0,"elapsed_ms":800,"data":{"open_ret":0,"nmea_count":1,' + '"sat_count":1,"fix_status":0,"search_ms":28500,' + '"required_nmea_count":1,"required_sat_count":1}}' + ) + session = FT1Session() + session.apply(message) + self.assertEqual(session.nodes["GNSS"], "PASS") + self.assertEqual(session.node_details["GNSS"]["data"]["sat_count"], 1) + if __name__ == "__main__": unittest.main() diff --git a/ql-application/threadx/interface/factory_test/Makefile b/ql-application/threadx/interface/factory_test/Makefile index 0d18af2..93c1a21 100644 --- a/ql-application/threadx/interface/factory_test/Makefile +++ b/ql-application/threadx/interface/factory_test/Makefile @@ -11,6 +11,7 @@ vpath %.c ../custom_app ../custom_app/lib/libnfc SRC_FILES+= \ factory_test.c \ factory_protocol.c \ + factory_gnss.c \ factory_audio.c \ hal_i2c.c \ DP1312EA.c \ diff --git a/ql-application/threadx/interface/factory_test/factory_gnss.c b/ql-application/threadx/interface/factory_test/factory_gnss.c new file mode 100644 index 0000000..40cd41b --- /dev/null +++ b/ql-application/threadx/interface/factory_test/factory_gnss.c @@ -0,0 +1,118 @@ +#include +#include + +#include "ql_gnss.h" +#include "ql_rtos.h" + +#include "factory_gnss.h" + +#define FT_GNSS_NMEA_GGA_GSV ((1 << 0) | (1 << 2)) + +typedef struct { + volatile int open_ret; + volatile u32 nmea_count; + volatile int sat_count; + volatile int fix_status; + volatile int started; + u32 start_ms; +} factory_gnss_context_t; + +static factory_gnss_context_t g_factory_gnss; + +static u32 factory_gnss_now_ms(void) +{ + return ql_rtos_get_systicks() * 5U; +} + +static int nmea_field_int(const char *line, int line_len, int wanted_field, + int *value) +{ + int field = 0; + int i; + int parsed = 0; + int number = 0; + + for (i = 0; i < line_len; ++i) { + char ch = line[i]; + if (ch == ',') { + if (field == wanted_field) break; + ++field; + continue; + } + if (field == wanted_field) { + if (ch < '0' || ch > '9') break; + number = number * 10 + (ch - '0'); + parsed = 1; + } + } + if (!parsed) return 0; + *value = number; + return 1; +} + +static void factory_gnss_callback(char *data, int len) +{ + int i; + + if (!data || len <= 0) return; + for (i = 0; i + 5 < len; ++i) { + int line_end; + int value; + + if (data[i] != '$') continue; + ++g_factory_gnss.nmea_count; + for (line_end = i; line_end < len; ++line_end) { + if (data[line_end] == '\r' || data[line_end] == '\n') break; + } + if (data[i + 3] == 'G' && data[i + 4] == 'S' && data[i + 5] == 'V' && + nmea_field_int(data + i, line_end - i, 3, &value) && + value > g_factory_gnss.sat_count) { + g_factory_gnss.sat_count = value; + } + if (data[i + 3] == 'G' && data[i + 4] == 'G' && data[i + 5] == 'A' && + nmea_field_int(data + i, line_end - i, 6, &value) && + value > g_factory_gnss.fix_status) { + g_factory_gnss.fix_status = value; + } + i = line_end; + } +} + +void factory_gnss_start(void) +{ + int cfgsys_ret; + int cfgmsg_ret; + int mode_ret; + + memset(&g_factory_gnss, 0, sizeof(g_factory_gnss)); + g_factory_gnss.open_ret = -1; + g_factory_gnss.start_ms = factory_gnss_now_ms(); + cfgsys_ret = ql_gnss_cfgsys_set(1); + cfgmsg_ret = ql_gnss_cfgmsg_set(FT_GNSS_NMEA_GGA_GSV); + mode_ret = ql_set_gnss_nmea_mode(PACKET_MODE); + g_factory_gnss.open_ret = ql_gnss_open(factory_gnss_callback); + g_factory_gnss.started = (g_factory_gnss.open_ret == 0); + printf("[FT_GNSS] start open_ret=%d cfgsys_ret=%d cfgmsg_ret=%d mode_ret=%d\r\n", + g_factory_gnss.open_ret, cfgsys_ret, cfgmsg_ret, mode_ret); +} + +void factory_gnss_snapshot(factory_gnss_result_t *result) +{ + if (!result) return; + result->open_ret = g_factory_gnss.open_ret; + result->nmea_count = g_factory_gnss.nmea_count; + result->sat_count = g_factory_gnss.sat_count; + result->fix_status = g_factory_gnss.fix_status; + result->search_ms = factory_gnss_now_ms() - g_factory_gnss.start_ms; +} + +void factory_gnss_stop(void) +{ + int ret; + if (!g_factory_gnss.started) return; + ret = ql_gnss_close(); + g_factory_gnss.started = 0; + printf("[FT_GNSS] stop ret=%d nmea=%lu sat=%d fix=%d\r\n", ret, + (unsigned long)g_factory_gnss.nmea_count, + g_factory_gnss.sat_count, g_factory_gnss.fix_status); +} diff --git a/ql-application/threadx/interface/factory_test/factory_gnss.h b/ql-application/threadx/interface/factory_test/factory_gnss.h new file mode 100644 index 0000000..49ac10e --- /dev/null +++ b/ql-application/threadx/interface/factory_test/factory_gnss.h @@ -0,0 +1,18 @@ +#ifndef BANBAN_FACTORY_GNSS_H +#define BANBAN_FACTORY_GNSS_H + +#include "ql_type.h" + +typedef struct { + int open_ret; + u32 nmea_count; + int sat_count; + int fix_status; + u32 search_ms; +} factory_gnss_result_t; + +void factory_gnss_start(void); +void factory_gnss_snapshot(factory_gnss_result_t *result); +void factory_gnss_stop(void); + +#endif diff --git a/ql-application/threadx/interface/factory_test/factory_protocol.c b/ql-application/threadx/interface/factory_test/factory_protocol.c index e92691c..f4917e0 100644 --- a/ql-application/threadx/interface/factory_test/factory_protocol.c +++ b/ql-application/threadx/interface/factory_test/factory_protocol.c @@ -42,7 +42,7 @@ const char *ft_node_name(ft_node_t node) { static const char *names[FT_NODE_COUNT] = { "BOOT_AUDIO", "POWER_ADC", "AUDIO_LOOPBACK", - "ESIM", "DATA_CALL", "NFC" + "ESIM", "DATA_CALL", "NFC", "GNSS" }; if (node < 0 || node >= FT_NODE_COUNT) { @@ -249,7 +249,7 @@ void ft_protocol_session_result(int passed, ft_error_t code) "\"duration_ms\":%lu,\"nodes\":{" "\"BOOT_AUDIO\":\"%s\",\"POWER_ADC\":\"%s\"," "\"AUDIO_LOOPBACK\":\"%s\",\"ESIM\":\"%s\"," - "\"DATA_CALL\":\"%s\",\"NFC\":\"%s\"}}", + "\"DATA_CALL\":\"%s\",\"NFC\":\"%s\",\"GNSS\":\"%s\"}}", FT_PROTOCOL_VERSION, (unsigned long)++g_protocol.seq, g_protocol.session, (unsigned long)ft_now_ms(), passed ? "PASS" : "FAIL", code, g_protocol.imei, g_protocol.iccid, g_protocol.nfc_uid, @@ -258,7 +258,8 @@ void ft_protocol_session_result(int passed, ft_error_t code) (unsigned long)(ft_now_ms() - g_protocol.start_ms), ft_state_name(g_protocol.nodes[0]), ft_state_name(g_protocol.nodes[1]), ft_state_name(g_protocol.nodes[2]), ft_state_name(g_protocol.nodes[3]), - ft_state_name(g_protocol.nodes[4]), ft_state_name(g_protocol.nodes[5])); + ft_state_name(g_protocol.nodes[4]), ft_state_name(g_protocol.nodes[5]), + ft_state_name(g_protocol.nodes[6])); } void ft_protocol_power_off(void) diff --git a/ql-application/threadx/interface/factory_test/factory_test.c b/ql-application/threadx/interface/factory_test/factory_test.c index 95c1a58..9a5dfe7 100644 --- a/ql-application/threadx/interface/factory_test/factory_test.c +++ b/ql-application/threadx/interface/factory_test/factory_test.c @@ -9,12 +9,14 @@ #include "ql_iic.h" #include "ql_nw.h" #include "ql_power.h" +#include "ql_pwm.h" #include "ql_rtos.h" #include "ql_sim.h" #include "sockets.h" #include "DP1312EA.h" #include "factory_audio.h" +#include "factory_gnss.h" #include "factory_protocol.h" #include "factory_test.h" #include "nfc.h" @@ -34,24 +36,96 @@ static char g_ip_address[64]; static volatile int g_led_mode; static ql_task_t g_led_task; +enum { + FT_LED_OFF = 0, + FT_LED_ON, + FT_LED_LOOPBACK, + FT_LED_NFC, + FT_LED_GNSS +}; + +static int factory_led_pwm_set(unsigned short duty, int *enabled) +{ + QL_PWM_CONFIG_EX config; + + config.pwm_psc = 1; + config.high_one_cycle_duration = duty; + config.total_one_cycle_duration = 320; + config.clk_src = QL_PWM_CLK_SRC_32K; + if (ql_pwm_config_plus_ex(FT_STATUS_LED_PIN, &config) != 0) return -1; + if (!*enabled) { + if (ql_pwm_enable(FT_STATUS_LED_PIN) != 0) { + ql_pwm_disable(FT_STATUS_LED_PIN); + return -1; + } + *enabled = 1; + } + return 0; +} + +static void factory_led_pwm_stop(int *enabled) +{ + if (*enabled) ql_pwm_disable(FT_STATUS_LED_PIN); + *enabled = 0; +} + static void factory_led_task(void *arg) { int phase = 0; + int last_mode = -1; + int pwm_enabled = 0; + int pwm_failed = 0; + int breath_duty = 16; + int breath_step = 16; (void)arg; ql_gpio_init(FT_STATUS_LED_PIN, PIN_DIRECTION_OUT, PIN_PULL_DISABLE, PIN_LEVEL_HIGH); while (1) { - if (g_led_mode == 0) { + int mode = g_led_mode; + if (mode != last_mode) { + phase = 0; + breath_duty = 16; + breath_step = 16; + pwm_failed = 0; + last_mode = mode; + } + if (mode == FT_LED_GNSS && !pwm_failed) { + if (factory_led_pwm_set((unsigned short)breath_duty, + &pwm_enabled) == 0) { + breath_duty += breath_step; + if (breath_duty >= 304 || breath_duty <= 16) { + breath_step = -breath_step; + } + ql_rtos_task_sleep_ms(40); + continue; + } + pwm_failed = 1; + factory_led_pwm_stop(&pwm_enabled); + printf("[FT_LED] GPIO70 PWM unavailable, use GNSS double-flash\r\n"); + } + if (pwm_enabled) { + factory_led_pwm_stop(&pwm_enabled); + ql_gpio_init(FT_STATUS_LED_PIN, PIN_DIRECTION_OUT, + PIN_PULL_DISABLE, PIN_LEVEL_HIGH); + } + if (mode == FT_LED_OFF) { ql_gpio_set_level(FT_STATUS_LED_PIN, PIN_LEVEL_LOW); ql_rtos_task_sleep_ms(100); - } else if (g_led_mode == 1) { + } else if (mode == FT_LED_ON) { ql_gpio_set_level(FT_STATUS_LED_PIN, PIN_LEVEL_HIGH); ql_rtos_task_sleep_ms(100); + } else if (mode == FT_LED_GNSS) { + static const int levels[] = {1, 0, 1, 0}; + static const int delays[] = {80, 80, 80, 520}; + ql_gpio_set_level(FT_STATUS_LED_PIN, + levels[phase] ? PIN_LEVEL_HIGH : PIN_LEVEL_LOW); + ql_rtos_task_sleep_ms(delays[phase]); + phase = (phase + 1) % 4; } else { phase = !phase; ql_gpio_set_level(FT_STATUS_LED_PIN, phase ? PIN_LEVEL_HIGH : PIN_LEVEL_LOW); - ql_rtos_task_sleep_ms(g_led_mode == 2 ? 250 : 100); + ql_rtos_task_sleep_ms(mode == FT_LED_LOOPBACK ? 250 : 100); } } } @@ -123,9 +197,9 @@ static ft_error_t run_audio_loopback(char *data, unsigned int data_size) if (ret != 0) { return FT_ERR_AUDIO_PLAY; } - factory_led_mode(2); + factory_led_mode(FT_LED_LOOPBACK); ret = factory_audio_wait_and_loopback(FT_DIALOG_KEY_TIMEOUT_MS, &recorded_ms); - factory_led_mode(1); + factory_led_mode(FT_LED_ON); snprintf(data, data_size, "{\"recorded_ms\":%lu,\"max_ms\":%d,\"volume_percent\":100}", (unsigned long)recorded_ms, FT_AUDIO_MAX_MS); @@ -211,12 +285,12 @@ static ft_error_t run_nfc(char *data, unsigned int data_size) if (audio_ret != 0) { return FT_ERR_AUDIO_PLAY; } - factory_led_mode(3); + factory_led_mode(FT_LED_NFC); DP1312EA_Reset(); version = DP1312EA_ReadRawRC(VersionReg); if (version != FT_NFC_VERSION_EXPECTED) { snprintf(data, data_size, "{\"chip_version\":\"0x%02X\"}", version); - factory_led_mode(1); + factory_led_mode(FT_LED_ON); return FT_ERR_NFC_CHIP; } @@ -227,16 +301,54 @@ static ft_error_t run_nfc(char *data, unsigned int data_size) snprintf(data, data_size, "{\"chip_version\":\"0x%02X\",\"uid\":\"%s\"}", version, (char *)output_uuid); - factory_led_mode(1); + factory_led_mode(FT_LED_ON); return FT_OK; } ql_rtos_task_sleep_ms(200); } while (ft_now_ms() - start < FT_NFC_TIMEOUT_MS); - factory_led_mode(1); + factory_led_mode(FT_LED_ON); return FT_ERR_NFC_TIMEOUT; } +static ft_error_t run_gnss(char *data, unsigned int data_size) +{ + factory_gnss_result_t result; + int audio_ret; + + factory_led_mode(FT_LED_GNSS); + audio_ret = factory_audio_play_prompt(FT_AUDIO_GNSS_PATH); + if (audio_ret != 0) { + factory_gnss_stop(); + factory_led_mode(FT_LED_ON); + return audio_ret == -2 ? FT_ERR_AUDIO_FILE_MISSING : FT_ERR_AUDIO_PLAY; + } + + while (1) { + factory_gnss_snapshot(&result); + if (result.open_ret == 0 && result.nmea_count >= 1 && + (result.sat_count >= 1 || result.fix_status > 0)) { + snprintf(data, data_size, + "{\"open_ret\":%d,\"nmea_count\":%lu,\"sat_count\":%d," + "\"fix_status\":%d,\"search_ms\":%lu," + "\"required_nmea_count\":1,\"required_sat_count\":1}", + result.open_ret, (unsigned long)result.nmea_count, + result.sat_count, result.fix_status, + (unsigned long)result.search_ms); + factory_gnss_stop(); + factory_led_mode(FT_LED_ON); + return FT_OK; + } + + if (result.open_ret != 0) { + ql_rtos_task_sleep_s(2); + factory_gnss_start(); + } else { + ql_rtos_task_sleep_ms(500); + } + } +} + static ft_error_t run_node(ft_node_t node, char *data, unsigned int data_size) { data[0] = '\0'; @@ -247,6 +359,7 @@ static ft_error_t run_node(ft_node_t node, char *data, unsigned int data_size) case FT_NODE_ESIM: return run_esim(data, data_size); case FT_NODE_DATA_CALL: return run_data_call(data, data_size); case FT_NODE_NFC: return run_nfc(data, data_size); + case FT_NODE_GNSS: return run_gnss(data, data_size); default: return FT_ERR_NFC_LOCKED; } } @@ -261,10 +374,11 @@ static void factory_test_task(void *arg) int audio_ret; (void)arg; + factory_gnss_start(); ql_rtos_task_sleep_s(2); ft_protocol_init(); ql_rtos_task_create(&g_led_task, 2048, 15, "ft_led", factory_led_task, NULL); - factory_led_mode(1); + factory_led_mode(FT_LED_ON); if (ql_dev_get_imei(imei, sizeof(imei)) == 0) { ft_protocol_set_imei(imei); } diff --git a/ql-application/threadx/interface/factory_test/factory_test.h b/ql-application/threadx/interface/factory_test/factory_test.h index c80082d..0cf5b88 100644 --- a/ql-application/threadx/interface/factory_test/factory_test.h +++ b/ql-application/threadx/interface/factory_test/factory_test.h @@ -4,8 +4,8 @@ #include "ql_type.h" #include "ql_gpio.h" -#define FT_PROTOCOL_VERSION 1 -#define FT_FIRMWARE_VERSION "FACTORY_TEST_1.0.0" +#define FT_PROTOCOL_VERSION 2 +#define FT_FIRMWARE_VERSION "FACTORY_TEST_1.1.0" #define FT_ADC_MIN_MV 3500 #define FT_ADC_MAX_MV 4200 @@ -37,6 +37,7 @@ typedef enum { FT_NODE_ESIM, FT_NODE_DATA_CALL, FT_NODE_NFC, + FT_NODE_GNSS, FT_NODE_COUNT } ft_node_t; @@ -58,6 +59,9 @@ typedef enum { FT_ERR_NETWORK_REGISTER = 1501, FT_ERR_PDP_ACTIVATE = 1502, FT_ERR_IP_INVALID = 1503, + FT_ERR_GNSS_OPEN = 1601, + FT_ERR_GNSS_NO_NMEA = 1602, + FT_ERR_GNSS_NO_SATELLITE = 1603, FT_ERR_NFC_LOCKED = 1701, FT_ERR_NFC_CHIP = 1702, FT_ERR_NFC_TIMEOUT = 1703,