feat: automate FT1 factory test status reporting
This commit is contained in:
Binary file not shown.
@@ -1,12 +1,12 @@
|
||||
# EG800 FT1 工厂烧录与产测上位机
|
||||
|
||||
本工具配套 `test/factory-test` 分支的独立产测固件,支持批量烧录、USB CDC
|
||||
本工具配套 `test/factory-test` 分支的独立产测固件,支持批量烧录、DBG UART
|
||||
协议采集、6 个硬件节点判定和结果落盘。
|
||||
|
||||
## 主要功能
|
||||
|
||||
- 产测固件和正式固件分开保存,每个工位可独立选择。
|
||||
- 烧录串口和产测 CDC 串口分开配置,避免把 DIAG/AT/CDC 当成同一个端口。
|
||||
- 烧录串口和产测 DBG UART 分开配置,避免把 DIAG/AT/调试口误认成烧录口。
|
||||
- 使用原厂 `adownload.exe -u -a`,先进入等待状态,再由关机设备上电触发烧录。
|
||||
- 提供“烧录选中”单工位入口;USB 自动等待的批量任务按工位顺序执行,避免抢设备。
|
||||
- 只解析以 `FT1 ` 开头的协议行;普通 SDK 日志只显示,不进入产测数据库。
|
||||
@@ -30,13 +30,13 @@ burn_tool/aboot-tools-2024.08.08-win-x64/adownload.exe
|
||||
factory_host/dist/EG800FactoryHost.exe
|
||||
```
|
||||
|
||||
默认“产测固件”指向已包含六个产测 MP3 的 FT1 整包;默认“正式固件”指向
|
||||
默认“产测固件”指向已包含节点提示音的 FT1 整包;默认“正式固件”指向
|
||||
`version` 目录中的正式内层 `-fac.zip`。两者都可在外置
|
||||
`factory_host_config.json` 中替换,烧录时不要选择 `-target.zip` 外层归档。
|
||||
|
||||
双击表格中的工位可以分别设置烧录口和产测 CDC 口。默认 USB 自动等待模式不需要
|
||||
双击表格中的工位可以分别设置烧录口和产测 DBG UART 口。默认 USB 自动等待模式不需要
|
||||
|
||||
填写烧录口;产测 CDC 口仍需填写。“烧录选中”要求只选择一个工位;“批量烧录”
|
||||
填写烧录口;产测 UART 可留空并自动识别。“烧录选中”要求只选择一个工位;“批量烧录”
|
||||
在未选中行时作用于全部工位,选中后只作用于选中工位。
|
||||
|
||||
单工位烧录时保持设备关机并连接 USB,选择工位后点击“烧录选中”。工位状态显示
|
||||
@@ -69,7 +69,7 @@ python -B .\factory_host\build_factory_audio_package.py
|
||||
|
||||
脚本会执行以下检查:
|
||||
|
||||
1. 源目录必须且只能包含六个固定文件名。
|
||||
1. 源目录必须且只能包含五个固定文件名。
|
||||
2. 每个文件必须为 16 kHz、单声道、48 kbps MP3。
|
||||
3. 使用 SDK `mklfs-v2.exe -m` 生成 262144 字节 `customer_fs.bin`。
|
||||
4. 立即反向解包镜像并逐文件比对 SHA-256。
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
# EG800 产测上位机使用说明
|
||||
|
||||
## 1. 发布目录
|
||||
|
||||
现场使用时至少保留以下内容,并保持相对目录不变:
|
||||
|
||||
```text
|
||||
banban/
|
||||
├─ burn_tool/
|
||||
│ └─ aboot-tools-2024.08.08-win-x64/
|
||||
│ ├─ adownload.exe
|
||||
│ └─ 其他随工具提供的 DLL、配置和驱动文件
|
||||
├─ factory_host/
|
||||
│ └─ dist/
|
||||
│ ├─ EG800FactoryHost.exe
|
||||
│ ├─ factory_host_config.json
|
||||
│ └─ factory_firmware/
|
||||
│ └─ EG800AKCN_11LG-FT1-factory-test.zip
|
||||
└─ version/
|
||||
└─ 正式固件目录/
|
||||
└─ *-fac.zip
|
||||
```
|
||||
|
||||
`adownload.exe` 不能脱离原来的工具目录单独复制,否则可能缺少 DLL、配置或驱动。
|
||||
|
||||
## 2. 首次配置
|
||||
|
||||
上位机读取与 EXE 同目录的 `factory_host_config.json`。推荐配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"flash": {
|
||||
"downloader_path": "burn_tool/aboot-tools-2024.08.08-win-x64/adownload.exe",
|
||||
"args": ["-u", "-a", "-s", "{baudrate}", "-q", "{firmware}"],
|
||||
"timeout_seconds": 900,
|
||||
"workers": 2
|
||||
},
|
||||
"firmwares": {
|
||||
"production": "factory_host/dist/factory_firmware/EG800AKCN_11LG-FT1-factory-test.zip",
|
||||
"official": "version/正式固件目录/EG800AKCN_11LG-版本号-fac.zip"
|
||||
},
|
||||
"serial": {
|
||||
"baudrate": 115200,
|
||||
"encoding": "utf-8",
|
||||
"line_ending": "\\r\\n"
|
||||
},
|
||||
"protocol": {
|
||||
"prefix": "FT1 ",
|
||||
"version": 1,
|
||||
"auto_save": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `production`:产测固件,包含 FT1 测试程序、开机/Loopback/NFC/关机音频,以及暂不参与流程的 GNSS 音频。
|
||||
- `official`:正式业务固件,供产测完成后换烧。
|
||||
- 固件路径可以使用相对于项目根目录的路径,也可以填写绝对路径。
|
||||
- 必须选择可直接烧录的内层 FT1 ZIP 或 `*-fac.zip`,不要选择外层 `*-target.zip` 归档。
|
||||
- 修改配置后重新启动上位机,或使用界面中的重新加载配置功能。
|
||||
|
||||
## 3. 烧录产测固件
|
||||
|
||||
1. 启动 `EG800FactoryHost.exe`,固件类型选择“产测固件”。
|
||||
2. 设备保持关机,连接烧录器和 USB。
|
||||
3. 选择工位后点击单工位烧录;批量生产时使用批量烧录。
|
||||
4. 界面显示“等待上电”后再给设备开机。
|
||||
5. 等待状态变成“烧录成功”,不要在烧录过程中拔线或关闭程序。
|
||||
|
||||
默认参数使用 `adownload.exe -u -a` 自动等待 AROM 下载设备,因此烧录口和产测 UART 是两个不同角色。
|
||||
|
||||
## 4. 自动识别并开始产测
|
||||
|
||||
烧录完成、设备正常启动并连接调试 UART 后:
|
||||
|
||||
1. 点击“扫描串口”可只识别设备并查看工位;直接点击“开始产测”也会先自动扫描。
|
||||
2. 上位机被动监听所有候选 COM 口,不向设备发送 `IDENTIFY` 或 `START` 命令。
|
||||
3. 只有能持续返回合法 FT1 状态广播的 UART 才会自动添加为工位。当前固件使用 `QL_DEBUG_UART_PORT`(GPIO29/30),不使用 USB CDC;AROM、主 UART、DIAG 和 AT 口不会被添加。
|
||||
4. 每发现一台设备,上位机自动创建或复用一个空工位,并在工位表显示对应 COM 口。
|
||||
5. 设备上电后自动开始测试;上位机只接收并解析每 500ms 一次的完整状态广播,不需要发送启动命令。
|
||||
6. 未手工选中工位时,“开始产测”会启动所有已识别工位;选中工位时只启动选中的设备。
|
||||
|
||||
每个工位会实时显示:
|
||||
|
||||
- 当前测试节点和通过数量;
|
||||
- 整机通过或失败状态;
|
||||
- IMEI、ICCID、NFC UID;
|
||||
- 失败码、结构化测试数据和实时串口日志。
|
||||
|
||||
产测固件当前依次执行:开机音频、电源 ADC、音频 Loopback、eSIM、数据连接和 NFC。
|
||||
|
||||
## 5. 测试结果与日志
|
||||
|
||||
默认自动保存位置:
|
||||
|
||||
```text
|
||||
factory_host/dist/logs/factory_results.csv
|
||||
factory_host/dist/logs/sessions/*.json
|
||||
```
|
||||
|
||||
- `factory_results.csv`:每台设备一行,适合生产汇总。
|
||||
- `sessions/*.json`:保存完整协议消息、节点耗时、失败码和各节点数据,适合追查单台设备。
|
||||
- 普通 SDK 日志会显示在实时日志区,但只有 `FT1 ` 开头的合法协议消息会参与结果判定。
|
||||
- 最终判定以设备发送的 `session_result` 为准,上位机不会根据普通文本日志猜测通过或失败。
|
||||
|
||||
## 6. 替换固件包
|
||||
|
||||
### 方法一:修改配置文件
|
||||
|
||||
把新固件复制到稳定目录,然后修改:
|
||||
|
||||
```json
|
||||
"firmwares": {
|
||||
"production": "新产测固件.zip",
|
||||
"official": "新正式固件-fac.zip"
|
||||
}
|
||||
```
|
||||
|
||||
推荐每次替换后记录文件名、版本、文件大小和 SHA-256,避免现场混用旧包。
|
||||
|
||||
### 方法二:在界面选择
|
||||
|
||||
选择“产测固件”或“正式固件”类型,再使用界面的固件选择按钮指定对应 ZIP。选择后检查界面顶部显示的完整路径。
|
||||
|
||||
正式固件通常不包含 FT1 产测协议。换烧正式固件后,上位机不能再自动识别为 FT1 产测工位,这是正常现象。
|
||||
|
||||
## 7. 更新产测音频并生成固件
|
||||
|
||||
音频源目录:
|
||||
|
||||
```text
|
||||
audio_prompts/factory_test/customer_fs/
|
||||
├─ factory_boot_zh.mp3
|
||||
├─ factory_audio_loopback_zh.mp3
|
||||
├─ factory_nfc_test_zh.mp3
|
||||
├─ factory_gnss_test_zh.mp3
|
||||
└─ factory_shutdown_zh.mp3
|
||||
```
|
||||
|
||||
文件必须保持脚本要求的名称和音频格式。更新后在项目根目录执行:
|
||||
|
||||
```powershell
|
||||
python -B .\factory_host\build_factory_audio_package.py
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\factory_host\build_factory_firmware.ps1
|
||||
```
|
||||
|
||||
生成物:
|
||||
|
||||
```text
|
||||
factory_host/dist/factory_audio/customer_fs.bin
|
||||
factory_host/dist/factory_audio/EG800AKCN_11LG-FT1-factory-audio.zip
|
||||
factory_host/dist/factory_firmware/EG800AKCN_11LG-FT1-factory-test.zip
|
||||
factory_host/dist/factory_firmware/firmware_manifest.json
|
||||
```
|
||||
|
||||
固件脚本会临时替换构建目录中的 `customer_fs.bin`,完成后自动恢复业务镜像并核对哈希。若脚本报告恢复失败,不要继续发布固件,应先检查备份和当前业务镜像。
|
||||
|
||||
## 8. 重新打包上位机
|
||||
|
||||
开发环境先安装依赖:
|
||||
|
||||
```powershell
|
||||
python -m pip install -r .\factory_host\requirements.txt
|
||||
python -m pip install pyinstaller
|
||||
```
|
||||
|
||||
运行测试并打包:
|
||||
|
||||
```powershell
|
||||
python -B -m unittest discover -s factory_host -p "test_*.py" -v
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\factory_host\build_release.ps1
|
||||
```
|
||||
|
||||
发布前至少使用一台设备验证:产测固件烧录、调试 UART 自动识别、START 握手、六项测试结果、JSON/CSV 保存,以及正式固件换烧。
|
||||
@@ -18,7 +18,6 @@ EXPECTED_FILES = (
|
||||
"factory_audio_loopback_zh.mp3",
|
||||
"factory_nfc_test_zh.mp3",
|
||||
"factory_gnss_test_zh.mp3",
|
||||
"factory_pass_zh.mp3",
|
||||
"factory_shutdown_zh.mp3",
|
||||
)
|
||||
IMAGE_SIZE = 262144
|
||||
@@ -168,7 +167,7 @@ def build_package(root: Path, output: Path) -> tuple[Path, Path, dict]:
|
||||
readme_path.write_text(
|
||||
"班班 FT1 产测音频包\n\n"
|
||||
"customer_fs.bin 固定为 262144 字节,用于 EG800AKCN_11LG 的 customer_fs 分区。\n"
|
||||
"六个 MP3 必须位于 U:/ 根目录;不要将此镜像混入正式业务固件。\n"
|
||||
"五个 MP3 必须位于 U:/ 根目录;不要将此镜像混入正式业务固件。\n"
|
||||
"烧录后以 FT1 BOOT_AUDIO 和最终 session_result 作为设备侧验证。\n",
|
||||
encoding="utf-8-sig",
|
||||
)
|
||||
|
||||
@@ -158,6 +158,32 @@ def list_serial_ports() -> list[str]:
|
||||
return sorted(set(ports), key=lambda item: int(item[3:]) if item[3:].isdigit() else item)
|
||||
|
||||
|
||||
def identify_ft1_port(port: str, baudrate: int = 115200, timeout: float = 1.2) -> bool:
|
||||
"""Passively probe a DBG UART without assigning unrelated ports."""
|
||||
try:
|
||||
import serial # type: ignore
|
||||
with serial.Serial(port, baudrate, timeout=0.15) as stream:
|
||||
stream.reset_input_buffer()
|
||||
deadline = time.monotonic() + timeout
|
||||
pending = bytearray()
|
||||
while time.monotonic() < deadline:
|
||||
chunk = stream.read(256)
|
||||
if not chunk:
|
||||
continue
|
||||
pending.extend(chunk)
|
||||
while b"\n" in pending:
|
||||
raw, _, pending = pending.partition(b"\n")
|
||||
try:
|
||||
message = parse_ft1_line(raw.rstrip(b"\r").decode("utf-8", errors="replace"))
|
||||
except ProtocolError:
|
||||
continue
|
||||
if message and message.get("type") in {"device_ready", "session_start", "status_snapshot", "node_start", "node_result", "session_result"}:
|
||||
return True
|
||||
except (ImportError, OSError, ValueError):
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
class SerialReader:
|
||||
"""Small Windows serial reader using pyserial when available, file handles otherwise."""
|
||||
|
||||
@@ -170,6 +196,7 @@ class SerialReader:
|
||||
self._thread: threading.Thread | None = None
|
||||
self._stream = None
|
||||
self._pyserial = None
|
||||
self._opened = threading.Event()
|
||||
|
||||
def start(self) -> None:
|
||||
if self._thread and self._thread.is_alive():
|
||||
@@ -189,6 +216,7 @@ class SerialReader:
|
||||
self._thread.join(timeout=1.0)
|
||||
|
||||
def send(self, text: str, line_ending: str = "\r\n") -> None:
|
||||
self._opened.wait(timeout=2.0)
|
||||
payload = (text + line_ending).encode(self.encoding, errors="replace")
|
||||
if self._pyserial is not None:
|
||||
self._pyserial.write(payload)
|
||||
@@ -207,6 +235,7 @@ class SerialReader:
|
||||
subprocess.run(["mode", self.port, f"BAUD={self.baudrate}", "PARITY=n", "DATA=8", "STOP=1"], capture_output=True, check=False)
|
||||
device = self.port if self.port.startswith("\\\\.\\") else "\\\\.\\" + self.port
|
||||
self._stream = open(device, "r+b", buffering=0)
|
||||
self._opened.set()
|
||||
buffer = bytearray()
|
||||
while not self._stop.is_set():
|
||||
chunk = self._stream.read(256)
|
||||
@@ -222,6 +251,7 @@ class SerialReader:
|
||||
except Exception as exc: # serial backends surface OS-specific errors
|
||||
self.on_line(f"[SERIAL_ERROR] {exc}")
|
||||
finally:
|
||||
self._opened.clear()
|
||||
try:
|
||||
if self._stream is not None:
|
||||
self._stream.close()
|
||||
@@ -342,11 +372,9 @@ class FactoryHost(Tk):
|
||||
self._refresh_tool_status()
|
||||
|
||||
def _load_default_rows(self) -> None:
|
||||
ports = list_serial_ports()
|
||||
for index, port in enumerate(ports, 1):
|
||||
self.add_station(number=str(index), flash_port=port, test_port=port)
|
||||
if not ports:
|
||||
self.add_station(number="1")
|
||||
# COM enumeration is intentionally deferred until FT1 identification;
|
||||
# AROM/DIAG/AT ports must not become fake stations.
|
||||
self.add_station(number="1")
|
||||
|
||||
def _refresh_tool_status(self) -> None:
|
||||
downloader = find_downloader(self.config_data["flash"].get("downloader_path", ""), self.config_data["flash"].get("aboot_path", ""))
|
||||
@@ -364,14 +392,34 @@ class FactoryHost(Tk):
|
||||
self._refresh_tool_status()
|
||||
|
||||
def scan_ports(self) -> None:
|
||||
ports = list_serial_ports()
|
||||
self._log("系统", f"检测到串口: {', '.join(ports) if ports else '无'}")
|
||||
for station in self.stations.values():
|
||||
if not station.flash_port and ports:
|
||||
station.flash_port = ports.pop(0)
|
||||
if not station.test_port:
|
||||
station.test_port = station.flash_port
|
||||
self._update_row(station)
|
||||
found = self._auto_add_ft1_stations()
|
||||
self._log("系统", f"FT1 自动识别完成,新增/确认工位: {', '.join(found) if found else '无'}")
|
||||
|
||||
def _auto_add_ft1_stations(self) -> list[str]:
|
||||
"""Probe every COM port and create one visible station per FT1 device."""
|
||||
baudrate = int(self.config_data["serial"].get("baudrate", 115200))
|
||||
used = {station.test_port for station in self.stations.values() if station.test_port}
|
||||
found: list[str] = []
|
||||
for candidate in list_serial_ports():
|
||||
if candidate in used:
|
||||
continue
|
||||
if not identify_ft1_port(candidate, baudrate):
|
||||
continue
|
||||
station = next((item for item in self.stations.values()
|
||||
if not item.test_port and item.reader is None), None)
|
||||
if station is None:
|
||||
number = 1
|
||||
while str(number) in self.stations:
|
||||
number += 1
|
||||
self.add_station(number=str(number))
|
||||
station = self.stations[str(number)]
|
||||
station.test_port = candidate
|
||||
station.status = "已识别"
|
||||
station.message = "FT1 UART 已连接,等待状态广播"
|
||||
used.add(candidate)
|
||||
found.append(f"工位 {station.number}={candidate}")
|
||||
self._update_row(station)
|
||||
return found
|
||||
|
||||
def open_config(self) -> None:
|
||||
try:
|
||||
@@ -417,7 +465,7 @@ class FactoryHost(Tk):
|
||||
ttk.Label(dialog, text="烧录串口(自动 USB 模式可空):").grid(row=0, column=0, padx=8, pady=8)
|
||||
flash_port_var = StringVar(value=station.flash_port)
|
||||
ttk.Entry(dialog, textvariable=flash_port_var, width=20).grid(row=0, column=1, padx=8, pady=8)
|
||||
ttk.Label(dialog, text="产测 CDC 串口:").grid(row=1, column=0, padx=8, pady=8)
|
||||
ttk.Label(dialog, text="产测 DBG UART:").grid(row=1, column=0, padx=8, pady=8)
|
||||
test_port_var = StringVar(value=station.test_port)
|
||||
ttk.Entry(dialog, textvariable=test_port_var, width=20).grid(row=1, column=1, padx=8, pady=8)
|
||||
ttk.Label(dialog, text="固件:").grid(row=2, column=0, padx=8, pady=8)
|
||||
@@ -576,20 +624,24 @@ class FactoryHost(Tk):
|
||||
self.events.put(("station", station.number, result, message))
|
||||
|
||||
def start_tests(self) -> None:
|
||||
selected = self.table.selection() or tuple(self.stations.keys())
|
||||
had_selection = bool(self.table.selection())
|
||||
self._auto_add_ft1_stations()
|
||||
selected = self.table.selection() if had_selection else tuple(
|
||||
number for number, station in self.stations.items() if station.test_port
|
||||
)
|
||||
for iid in selected:
|
||||
if iid not in self.stations:
|
||||
continue
|
||||
station = self.stations[iid]
|
||||
if not station.test_port:
|
||||
station.status, station.message = "失败", "未设置产测 CDC 串口"
|
||||
station.status, station.message = "失败", "未发现 FT1 产测 DBG UART"
|
||||
self._update_row(station)
|
||||
continue
|
||||
station.protocol.reset()
|
||||
station.report_path = None
|
||||
station.report_saved = False
|
||||
station.status = "等待设备"
|
||||
station.message = "等待 FT1 session_start"
|
||||
station.message = "等待 FT1 状态广播"
|
||||
if station.reader:
|
||||
station.reader.stop()
|
||||
station.reader = SerialReader(
|
||||
@@ -601,7 +653,7 @@ class FactoryHost(Tk):
|
||||
station.reader.start()
|
||||
self._update_row(station)
|
||||
self._refresh_node_table()
|
||||
self._log("系统", "FT1 串口产测已启动;只保存以 FT1 开头的协议行")
|
||||
self._log("系统", "已自动识别 FT1 UART;设备自动开始产测并每500ms广播状态")
|
||||
|
||||
def _serial_line(self, station: Station, line: str) -> None:
|
||||
self.events.put(("serial", station.number, line))
|
||||
@@ -624,6 +676,26 @@ class FactoryHost(Tk):
|
||||
self._log(station.number, f"[SDK] {line}")
|
||||
return
|
||||
|
||||
if message["type"] in {"device_ready", "command_ack"}:
|
||||
station.message = station.protocol.apply(message)
|
||||
station.status = "设备已识别" if message["type"] == "device_ready" else ("开始产测" if message.get("accepted") else "START 被拒绝")
|
||||
self._log(station.number, f"[FT1] {station.message}")
|
||||
self._update_row(station)
|
||||
return
|
||||
|
||||
if message["type"] == "status_snapshot":
|
||||
station.protocol.apply(message)
|
||||
station.status = "产测中" if message["active_node"] >= 0 else "产测已启动"
|
||||
if station.protocol.session_result_seen:
|
||||
station.status = "产测通过" if station.protocol.result == "PASS" else "产测失败"
|
||||
station.message = station.protocol.current_node or "状态广播"
|
||||
self._log(station.number, f"[FT1] {station.message}: " + ", ".join(
|
||||
f"{name}={station.protocol.nodes[name]}" for name in NODE_NAMES
|
||||
))
|
||||
self._update_row(station)
|
||||
self._refresh_node_table()
|
||||
return
|
||||
|
||||
previous_session = station.protocol.session
|
||||
try:
|
||||
description = station.protocol.apply(message)
|
||||
|
||||
@@ -16,6 +16,9 @@ MESSAGE_TYPES = {
|
||||
"node_result",
|
||||
"session_result",
|
||||
"power_off",
|
||||
"device_ready",
|
||||
"command_ack",
|
||||
"status_snapshot",
|
||||
}
|
||||
NODE_NAMES = (
|
||||
"BOOT_AUDIO",
|
||||
@@ -126,6 +129,22 @@ def parse_ft1_line(line: str) -> dict | None:
|
||||
raise ProtocolError("节点汇总无效: " + ", ".join(missing))
|
||||
if msg_type == "power_off":
|
||||
_require_int(message, "code", 0)
|
||||
if msg_type == "device_ready":
|
||||
if not isinstance(message.get("fw"), str):
|
||||
raise ProtocolError("device_ready 缺少 fw")
|
||||
if msg_type == "command_ack":
|
||||
if message.get("command") != "START" or not isinstance(message.get("accepted"), bool):
|
||||
raise ProtocolError("command_ack 字段无效")
|
||||
if msg_type == "status_snapshot":
|
||||
active_node = message.get("active_node")
|
||||
if isinstance(active_node, bool) or not isinstance(active_node, int) or active_node < -1 or active_node >= len(NODE_NAMES):
|
||||
raise ProtocolError("status_snapshot active_node 无效")
|
||||
nodes = message.get("nodes")
|
||||
if not isinstance(nodes, dict):
|
||||
raise ProtocolError("status_snapshot 缺少 nodes")
|
||||
missing = [name for name in NODE_NAMES if nodes.get(name) not in {"WAIT", "PASS", "FAIL"}]
|
||||
if missing:
|
||||
raise ProtocolError("status_snapshot 节点状态缺失: " + ", ".join(missing))
|
||||
return message
|
||||
|
||||
|
||||
@@ -145,6 +164,8 @@ class FT1Session:
|
||||
session_start_seen: bool = False
|
||||
session_result_seen: bool = False
|
||||
power_off_seen: bool = False
|
||||
device_ready_seen: bool = False
|
||||
start_ack_seen: bool = False
|
||||
nodes: dict[str, str] = field(default_factory=lambda: {name: "WAIT" for name in NODE_NAMES})
|
||||
node_details: dict[str, dict] = field(default_factory=dict)
|
||||
messages: list[dict] = field(default_factory=list)
|
||||
@@ -182,6 +203,20 @@ class FT1Session:
|
||||
self.firmware = str(message.get("fw", ""))
|
||||
self.start_time = message.get("start_time")
|
||||
return f"会话开始 {self.firmware or self.session}"
|
||||
if msg_type == "device_ready":
|
||||
self.device_ready_seen = True
|
||||
self.firmware = str(message.get("fw", ""))
|
||||
return f"设备已识别 {self.firmware}"
|
||||
if msg_type == "command_ack":
|
||||
self.start_ack_seen = bool(message.get("accepted"))
|
||||
return "START 已确认" if self.start_ack_seen else "START 被拒绝"
|
||||
if msg_type == "status_snapshot":
|
||||
self.nodes.update(message["nodes"])
|
||||
active_node = message["active_node"]
|
||||
self.current_node = NODE_NAMES[active_node] if active_node >= 0 else ""
|
||||
if active_node >= 0:
|
||||
self.nodes[self.current_node] = "RUNNING"
|
||||
return "状态广播 " + ", ".join(f"{name}={self.nodes[name]}" for name in NODE_NAMES)
|
||||
if msg_type == "node_start":
|
||||
name = message["name"]
|
||||
self.current_node = name
|
||||
|
||||
@@ -93,6 +93,40 @@ class FactoryHostTests(unittest.TestCase):
|
||||
'"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"}'
|
||||
)
|
||||
ack = parse_ft1_line(
|
||||
'FT1 {"v":1,"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(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",'
|
||||
'"uptime_ms":100,"active_node":0,"nodes":{"BOOT_AUDIO":"WAIT",'
|
||||
'"POWER_ADC":"WAIT","AUDIO_LOOPBACK":"WAIT","ESIM":"WAIT",'
|
||||
'"DATA_CALL":"WAIT","NFC":"WAIT"}}'
|
||||
)
|
||||
second = parse_ft1_line(
|
||||
'FT1 {"v":1,"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"}}'
|
||||
)
|
||||
session = FT1Session()
|
||||
session.apply(first)
|
||||
session.apply(second)
|
||||
self.assertEqual(session.nodes["BOOT_AUDIO"], "PASS")
|
||||
self.assertEqual(session.current_node, "POWER_ADC")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "factory_protocol.h"
|
||||
|
||||
#define FT_PROTOCOL_LINE_MAX 1024
|
||||
#define FT_PROTOCOL_UART_PORT QL_DEBUG_UART_PORT
|
||||
|
||||
typedef struct {
|
||||
char session[32];
|
||||
@@ -20,9 +21,17 @@ typedef struct {
|
||||
u32 seq;
|
||||
ft_state_t nodes[FT_NODE_COUNT];
|
||||
int cdc_open;
|
||||
ql_mutex_t tx_mutex;
|
||||
ql_task_t status_task;
|
||||
volatile int status_broadcast;
|
||||
int active_node;
|
||||
} ft_protocol_context_t;
|
||||
|
||||
static ft_protocol_context_t g_protocol;
|
||||
static void ft_emit(const char *fmt, ...);
|
||||
u32 ft_now_ms(void);
|
||||
|
||||
static void ft_status_task(void *arg);
|
||||
|
||||
u32 ft_now_ms(void)
|
||||
{
|
||||
@@ -64,10 +73,16 @@ static void ft_emit(const char *fmt, ...)
|
||||
if (len < 0) return;
|
||||
if (len >= (int)sizeof(line)) len = sizeof(line) - 1;
|
||||
|
||||
printf("%s", line);
|
||||
if (g_protocol.cdc_open) {
|
||||
ql_uart_write(QL_USB_CDC_PORT, (unsigned char *)line, len);
|
||||
if (g_protocol.tx_mutex) ql_rtos_mutex_lock(g_protocol.tx_mutex, QL_WAIT_FOREVER);
|
||||
if (FT_PROTOCOL_UART_PORT == QL_DEBUG_UART_PORT) {
|
||||
/* DBG UART is the printf console on this board. Use the same
|
||||
transport as the visible boot/application logs so FT1 frames are
|
||||
actually received by the host terminal. */
|
||||
printf("%s", line);
|
||||
} else if (g_protocol.cdc_open) {
|
||||
ql_uart_write(FT_PROTOCOL_UART_PORT, (unsigned char *)line, len);
|
||||
}
|
||||
if (g_protocol.tx_mutex) ql_rtos_mutex_unlock(g_protocol.tx_mutex);
|
||||
}
|
||||
|
||||
static void ft_make_session(void)
|
||||
@@ -94,10 +109,50 @@ static void ft_make_session(void)
|
||||
|
||||
void ft_protocol_init(void)
|
||||
{
|
||||
int uart_ret;
|
||||
memset(&g_protocol, 0, sizeof(g_protocol));
|
||||
g_protocol.cdc_open =
|
||||
(ql_uart_open(QL_USB_CDC_PORT, QL_UART_BAUD_115200, QL_FC_NONE) == 0);
|
||||
g_protocol.active_node = -1;
|
||||
ql_rtos_mutex_create(&g_protocol.tx_mutex);
|
||||
uart_ret = ql_uart_open(FT_PROTOCOL_UART_PORT, QL_UART_BAUD_115200, QL_FC_NONE);
|
||||
g_protocol.cdc_open = (uart_ret == 0 || uart_ret == QL_UART_ALREAD_OPEN);
|
||||
ft_make_session();
|
||||
printf("[FT1] control UART port=%d open_ret=%d ready=%d\r\n",
|
||||
FT_PROTOCOL_UART_PORT, uart_ret, g_protocol.cdc_open);
|
||||
}
|
||||
|
||||
static void ft_status_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
while (g_protocol.status_broadcast) {
|
||||
int i;
|
||||
char payload[FT_PROTOCOL_LINE_MAX - 16];
|
||||
int offset = snprintf(payload, sizeof(payload),
|
||||
"{\"v\":%d,\"type\":\"status_snapshot\",\"seq\":%lu,\"session\":\"%s\",\"uptime_ms\":%lu,\"active_node\":%d,\"nodes\":{",
|
||||
FT_PROTOCOL_VERSION, (unsigned long)++g_protocol.seq,
|
||||
g_protocol.session, (unsigned long)ft_now_ms(),
|
||||
g_protocol.active_node);
|
||||
for (i = 0; i < FT_NODE_COUNT && offset > 0 && offset < (int)sizeof(payload); ++i) {
|
||||
offset += snprintf(payload + offset, sizeof(payload) - offset,
|
||||
"%s\"%s\":\"%s\"",
|
||||
i ? "," : "", ft_node_name((ft_node_t)i),
|
||||
ft_state_name(g_protocol.nodes[i]));
|
||||
}
|
||||
if (offset > 0 && offset < (int)sizeof(payload)) {
|
||||
snprintf(payload + offset, sizeof(payload) - offset, "}}");
|
||||
ft_emit("%s", payload);
|
||||
}
|
||||
ql_rtos_task_sleep_ms(500);
|
||||
}
|
||||
}
|
||||
|
||||
void ft_protocol_start_status_broadcast(void)
|
||||
{
|
||||
if (g_protocol.status_broadcast) return;
|
||||
g_protocol.status_broadcast = 1;
|
||||
/* The snapshot formatter keeps a near-1 KiB JSON buffer on the task
|
||||
stack; leave headroom for libc formatting and UART calls. */
|
||||
ql_rtos_task_create(&g_protocol.status_task, 8192, 15,
|
||||
"ft_status", ft_status_task, NULL);
|
||||
}
|
||||
|
||||
static void ft_copy(char *dst, unsigned int dst_size, const char *src)
|
||||
@@ -141,6 +196,7 @@ void ft_protocol_session_start(void)
|
||||
|
||||
void ft_protocol_node_start(ft_node_t node)
|
||||
{
|
||||
if (node >= 0 && node < FT_NODE_COUNT) g_protocol.active_node = node;
|
||||
ft_emit("{\"v\":%d,\"type\":\"node_start\",\"seq\":%lu,"
|
||||
"\"session\":\"%s\",\"uptime_ms\":%lu,\"imei\":\"%s\","
|
||||
"\"iccid\":\"%s\",\"nfc_uid\":\"%s\",\"node\":%d,"
|
||||
@@ -169,6 +225,7 @@ void ft_protocol_node_result(ft_node_t node, int passed, ft_error_t code,
|
||||
const char *result = passed ? "PASS" : "FAIL";
|
||||
if (node >= 0 && node < FT_NODE_COUNT) {
|
||||
g_protocol.nodes[node] = passed ? FT_STATE_PASS : FT_STATE_FAIL;
|
||||
g_protocol.active_node = -1;
|
||||
}
|
||||
if (!data_json || !data_json[0]) data_json = "{}";
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ typedef enum {
|
||||
} ft_state_t;
|
||||
|
||||
void ft_protocol_init(void);
|
||||
void ft_protocol_start_status_broadcast(void);
|
||||
void ft_protocol_set_imei(const char *imei);
|
||||
void ft_protocol_set_iccid(const char *iccid);
|
||||
void ft_protocol_set_nfc_uid(const char *uid);
|
||||
|
||||
@@ -31,6 +31,35 @@ void handle_nfc_bind(const char *uid) { (void)uid; }
|
||||
void handle_nfc_trigger(const char *uid) { (void)uid; }
|
||||
|
||||
static char g_ip_address[64];
|
||||
static volatile int g_led_mode;
|
||||
static ql_task_t g_led_task;
|
||||
|
||||
static void factory_led_task(void *arg)
|
||||
{
|
||||
int phase = 0;
|
||||
(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) {
|
||||
ql_gpio_set_level(FT_STATUS_LED_PIN, PIN_LEVEL_LOW);
|
||||
ql_rtos_task_sleep_ms(100);
|
||||
} else if (g_led_mode == 1) {
|
||||
ql_gpio_set_level(FT_STATUS_LED_PIN, PIN_LEVEL_HIGH);
|
||||
ql_rtos_task_sleep_ms(100);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void factory_led_mode(int mode)
|
||||
{
|
||||
g_led_mode = mode;
|
||||
}
|
||||
|
||||
static int iccid_valid(const char *iccid)
|
||||
{
|
||||
@@ -87,7 +116,16 @@ static ft_error_t run_power_adc(char *data, unsigned int data_size)
|
||||
static ft_error_t run_audio_loopback(char *data, unsigned int data_size)
|
||||
{
|
||||
u32 recorded_ms = 0;
|
||||
int ret = factory_audio_wait_and_loopback(FT_DIALOG_KEY_TIMEOUT_MS, &recorded_ms);
|
||||
int ret = factory_audio_play_prompt(FT_AUDIO_LOOPBACK_PATH);
|
||||
if (ret == -2) {
|
||||
return FT_ERR_AUDIO_FILE_MISSING;
|
||||
}
|
||||
if (ret != 0) {
|
||||
return FT_ERR_AUDIO_PLAY;
|
||||
}
|
||||
factory_led_mode(2);
|
||||
ret = factory_audio_wait_and_loopback(FT_DIALOG_KEY_TIMEOUT_MS, &recorded_ms);
|
||||
factory_led_mode(1);
|
||||
snprintf(data, data_size,
|
||||
"{\"recorded_ms\":%lu,\"max_ms\":%d,\"volume_percent\":100}",
|
||||
(unsigned long)recorded_ms, FT_AUDIO_MAX_MS);
|
||||
@@ -163,12 +201,22 @@ static ft_error_t run_nfc(char *data, unsigned int data_size)
|
||||
u8 ntag_data[180] = {0};
|
||||
u16 ntag_len = 0;
|
||||
u8 version;
|
||||
int audio_ret;
|
||||
u32 start = ft_now_ms();
|
||||
|
||||
audio_ret = factory_audio_play_prompt(FT_AUDIO_NFC_PATH);
|
||||
if (audio_ret == -2) {
|
||||
return FT_ERR_AUDIO_FILE_MISSING;
|
||||
}
|
||||
if (audio_ret != 0) {
|
||||
return FT_ERR_AUDIO_PLAY;
|
||||
}
|
||||
factory_led_mode(3);
|
||||
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);
|
||||
return FT_ERR_NFC_CHIP;
|
||||
}
|
||||
|
||||
@@ -179,11 +227,13 @@ 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);
|
||||
return FT_OK;
|
||||
}
|
||||
ql_rtos_task_sleep_ms(200);
|
||||
} while (ft_now_ms() - start < FT_NFC_TIMEOUT_MS);
|
||||
|
||||
factory_led_mode(1);
|
||||
return FT_ERR_NFC_TIMEOUT;
|
||||
}
|
||||
|
||||
@@ -213,10 +263,13 @@ static void factory_test_task(void *arg)
|
||||
|
||||
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);
|
||||
if (ql_dev_get_imei(imei, sizeof(imei)) == 0) {
|
||||
ft_protocol_set_imei(imei);
|
||||
}
|
||||
ft_protocol_session_start();
|
||||
ft_protocol_start_status_broadcast();
|
||||
|
||||
for (node = FT_NODE_BOOT_AUDIO; node < FT_NODE_COUNT; ++node) {
|
||||
ft_protocol_node_start(node);
|
||||
@@ -230,11 +283,7 @@ static void factory_test_task(void *arg)
|
||||
}
|
||||
}
|
||||
|
||||
audio_ret = factory_audio_play_prompt(FT_AUDIO_PASS_PATH);
|
||||
if (audio_ret == 0) {
|
||||
ql_rtos_task_sleep_ms(300);
|
||||
audio_ret = factory_audio_play_prompt(FT_AUDIO_SHUTDOWN_PATH);
|
||||
}
|
||||
audio_ret = factory_audio_play_prompt(FT_AUDIO_SHUTDOWN_PATH);
|
||||
if (audio_ret != 0) {
|
||||
ft_protocol_session_result(0, FT_ERR_FINAL_AUDIO);
|
||||
while (1) ql_rtos_task_sleep_s(60);
|
||||
|
||||
@@ -21,10 +21,13 @@
|
||||
#define FT_NFC_TIMEOUT_MS 60000
|
||||
|
||||
#define FT_LOOPBACK_KEY_PIN GPIO_PIN_NO_84
|
||||
#define FT_STATUS_LED_PIN GPIO_PIN_NO_70
|
||||
#define FT_PA_PIN GPIO_PIN_NO_68
|
||||
|
||||
#define FT_AUDIO_BOOT_PATH "U:/factory_boot_zh.mp3"
|
||||
#define FT_AUDIO_PASS_PATH "U:/factory_pass_zh.mp3"
|
||||
#define FT_AUDIO_LOOPBACK_PATH "U:/factory_audio_loopback_zh.mp3"
|
||||
#define FT_AUDIO_NFC_PATH "U:/factory_nfc_test_zh.mp3"
|
||||
#define FT_AUDIO_GNSS_PATH "U:/factory_gnss_test_zh.mp3"
|
||||
#define FT_AUDIO_SHUTDOWN_PATH "U:/factory_shutdown_zh.mp3"
|
||||
|
||||
typedef enum {
|
||||
|
||||
Reference in New Issue
Block a user