282 lines
11 KiB
Python
282 lines
11 KiB
Python
"""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),
|
|
}
|