"""Parser and state model for the Banban FT1 factory-test protocol.""" from __future__ import annotations import json from dataclasses import dataclass, field from typing import Any PREFIX = "FT1 " VERSION = 1 MESSAGE_TYPES = { "session_start", "node_start", "node_progress", "node_result", "session_result", "power_off", "device_ready", "command_ack", "status_snapshot", } NODE_NAMES = ( "BOOT_AUDIO", "POWER_ADC", "AUDIO_LOOPBACK", "ESIM", "DATA_CALL", "NFC", ) ERROR_MESSAGES = { 0: "成功", 1001: "ES8311 初始化或设备 ID 检查失败", 1002: "开机提示音文件不存在", 1003: "开机提示音播放失败或超时", 1101: "ADC 初始化或读取失败", 1102: "电压低于 3500mV", 1103: "电压高于 4200mV", 1201: "录音句柄或缓冲区创建失败", 1202: "没有录到有效 PCM", 1203: "PCM 回放失败", 1204: "60 秒内未按下 Loopback 按键", 1401: "eSIM 未进入 READY", 1402: "ICCID 读取失败", 1403: "ICCID 格式错误", 1501: "4G 网络注册超时", 1502: "IPv4 PDP 激活失败", 1503: "未获取有效 IPv4 地址", 1701: "前置节点未全部通过,NFC 锁定", 1702: "DP1312EA 版本不是 0x82", 1703: "60 秒内未成功刷卡", 1704: "NFC UID 无效", 1801: "最终提示音播放失败", 1802: "正常关机接口异常返回", } class ProtocolError(ValueError): pass def error_text(code: Any) -> str: try: number = int(code) except (TypeError, ValueError): return f"未知失败码 {code}" return ERROR_MESSAGES.get(number, f"未知失败码 {number}") def _require_int(message: dict, key: str, minimum: int = 0) -> int: value = message.get(key) if isinstance(value, bool) or not isinstance(value, int) or value < minimum: raise ProtocolError(f"字段 {key} 必须是大于等于 {minimum} 的整数") return value def parse_ft1_line(line: str) -> dict | None: """Parse one serial line. Non-FT1 lines return None and are not records.""" line = line.strip("\r\n") if not line.startswith(PREFIX): return None try: message = json.loads(line[len(PREFIX) :]) except json.JSONDecodeError as exc: raise ProtocolError(f"JSON 无效: {exc.msg}") from exc if not isinstance(message, dict): raise ProtocolError("协议载荷必须是 JSON 对象") if message.get("v") != VERSION: raise ProtocolError(f"不支持的协议版本: {message.get('v')}") msg_type = message.get("type") if msg_type not in MESSAGE_TYPES: raise ProtocolError(f"不支持的消息类型: {msg_type}") _require_int(message, "seq", 1) _require_int(message, "uptime_ms", 0) if not isinstance(message.get("session"), str) or not message["session"]: raise ProtocolError("缺少 session") if msg_type in {"node_start", "node_progress", "node_result"}: node = _require_int(message, "node", 0) if node >= len(NODE_NAMES): raise ProtocolError(f"节点编号越界: {node}") if message.get("name") != NODE_NAMES[node]: raise ProtocolError(f"节点名称不匹配: {node}/{message.get('name')}") if msg_type == "node_progress": data = message.get("data") if not isinstance(data, dict): raise ProtocolError("node_progress 缺少 data") current = _require_int(data, "current", 0) required = _require_int(data, "required", 1) if current > required: raise ProtocolError("节点进度超过 required") if msg_type == "node_result": if message.get("result") not in {"PASS", "FAIL"}: raise ProtocolError("node_result 的 result 必须为 PASS 或 FAIL") _require_int(message, "code", 0) _require_int(message, "elapsed_ms", 0) if not isinstance(message.get("data"), dict): raise ProtocolError("node_result 的 data 必须是对象") if msg_type == "session_result": if message.get("result") not in {"PASS", "FAIL"}: raise ProtocolError("session_result 的 result 必须为 PASS 或 FAIL") _require_int(message, "fail_code", 0) _require_int(message, "duration_ms", 0) nodes = message.get("nodes") if not isinstance(nodes, dict): raise ProtocolError("session_result 缺少 nodes") missing = [name for name in NODE_NAMES if nodes.get(name) not in {"WAIT", "PASS", "FAIL"}] if missing: 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 @dataclass class FT1Session: session: str = "" last_seq: int = 0 firmware: str = "" start_time: str | None = None imei: str = "" iccid: str = "" nfc_uid: str = "" result: str = "" fail_code: int = 0 duration_ms: int = 0 current_node: str = "" 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) warnings: list[str] = field(default_factory=list) def reset(self, session: str = "") -> None: fresh = type(self)(session=session) self.__dict__.update(fresh.__dict__) def apply(self, message: dict) -> str: msg_type = message["type"] incoming_session = message["session"] if msg_type == "session_start" and incoming_session != self.session: self.reset(incoming_session) elif not self.session: self.reset(incoming_session) self.warnings.append("未收到 session_start,记录可能不完整") elif incoming_session != self.session: raise ProtocolError(f"会话突变: {self.session} -> {incoming_session}") seq = message["seq"] if seq <= self.last_seq: raise ProtocolError(f"seq 未递增: {seq} <= {self.last_seq}") if self.last_seq and seq != self.last_seq + 1: self.warnings.append(f"消息序号缺口: {self.last_seq} -> {seq}") self.last_seq = seq self.messages.append(message) for key in ("imei", "iccid", "nfc_uid"): value = message.get(key) if isinstance(value, str) and value: setattr(self, key, value) if msg_type == "session_start": self.session_start_seen = True 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 self.nodes[name] = "RUNNING" return f"开始 {name}" if msg_type == "node_progress": name = message["name"] data = message["data"] self.current_node = name self.nodes[name] = f"{data['current']}/{data['required']}" self.node_details[name] = {"data": data} return f"{name} {data['current']}/{data['required']}" if msg_type == "node_result": name = message["name"] self.nodes[name] = message["result"] self.node_details[name] = { "code": message["code"], "elapsed_ms": message["elapsed_ms"], "data": message["data"], } return f"{name} {message['result']} ({message['code']})" if msg_type == "session_result": self.session_result_seen = True self.result = message["result"] self.fail_code = message["fail_code"] self.duration_ms = message["duration_ms"] self.start_time = message.get("start_time") self.nodes.update(message["nodes"]) return f"整机 {self.result}: {error_text(self.fail_code)}" self.power_off_seen = True return "设备准备关机" @property def complete(self) -> bool: return self.session_start_seen and self.session_result_seen @property def passed_count(self) -> int: return sum(state == "PASS" for state in self.nodes.values()) def summary(self) -> dict: return { "protocol_version": VERSION, "session": self.session, "firmware": self.firmware, "start_time": self.start_time, "imei": self.imei, "iccid": self.iccid, "nfc_uid": self.nfc_uid, "result": self.result or "INCOMPLETE", "fail_code": self.fail_code, "fail_reason": error_text(self.fail_code) if self.result else "会话未完成", "duration_ms": self.duration_ms, "nodes": dict(self.nodes), "node_details": dict(self.node_details), "session_start_seen": self.session_start_seen, "session_result_seen": self.session_result_seen, "power_off_seen": self.power_off_seen, "protocol_complete": self.complete, "last_seq": self.last_seq, "warnings": list(self.warnings), }