diff --git a/audio_prompts/factory_test/customer_fs/factory_pass_zh.mp3 b/audio_prompts/factory_test/customer_fs/factory_pass_zh.mp3 deleted file mode 100644 index 63e45aa..0000000 Binary files a/audio_prompts/factory_test/customer_fs/factory_pass_zh.mp3 and /dev/null differ diff --git a/factory_host/README.md b/factory_host/README.md index 00e8cee..54e498e 100644 --- a/factory_host/README.md +++ b/factory_host/README.md @@ -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。 diff --git a/factory_host/USAGE.md b/factory_host/USAGE.md new file mode 100644 index 0000000..62bd13a --- /dev/null +++ b/factory_host/USAGE.md @@ -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 保存,以及正式固件换烧。 diff --git a/factory_host/build_factory_audio_package.py b/factory_host/build_factory_audio_package.py index b4838da..a8a49f5 100644 --- a/factory_host/build_factory_audio_package.py +++ b/factory_host/build_factory_audio_package.py @@ -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", ) diff --git a/factory_host/factory_host.py b/factory_host/factory_host.py index 4cd9eb0..87be243 100644 --- a/factory_host/factory_host.py +++ b/factory_host/factory_host.py @@ -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) diff --git a/factory_host/ft1_protocol.py b/factory_host/ft1_protocol.py index f351492..bec226b 100644 --- a/factory_host/ft1_protocol.py +++ b/factory_host/ft1_protocol.py @@ -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 diff --git a/factory_host/test_factory_host.py b/factory_host/test_factory_host.py index aa18415..5dcaaed 100644 --- a/factory_host/test_factory_host.py +++ b/factory_host/test_factory_host.py @@ -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() diff --git a/ql-application/threadx/interface/factory_test/factory_protocol.c b/ql-application/threadx/interface/factory_test/factory_protocol.c index d16d0e5..e92691c 100644 --- a/ql-application/threadx/interface/factory_test/factory_protocol.c +++ b/ql-application/threadx/interface/factory_test/factory_protocol.c @@ -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 = "{}"; diff --git a/ql-application/threadx/interface/factory_test/factory_protocol.h b/ql-application/threadx/interface/factory_test/factory_protocol.h index a53a8e4..d4890e9 100644 --- a/ql-application/threadx/interface/factory_test/factory_protocol.h +++ b/ql-application/threadx/interface/factory_test/factory_protocol.h @@ -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); diff --git a/ql-application/threadx/interface/factory_test/factory_test.c b/ql-application/threadx/interface/factory_test/factory_test.c index 66383e3..95c1a58 100644 --- a/ql-application/threadx/interface/factory_test/factory_test.c +++ b/ql-application/threadx/interface/factory_test/factory_test.c @@ -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); diff --git a/ql-application/threadx/interface/factory_test/factory_test.h b/ql-application/threadx/interface/factory_test/factory_test.h index 5929fa4..c80082d 100644 --- a/ql-application/threadx/interface/factory_test/factory_test.h +++ b/ql-application/threadx/interface/factory_test/factory_test.h @@ -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 {