feat: automate FT1 factory test status reporting
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user