Files
banban/factory_host/ft1_protocol.py
T

247 lines
9.1 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",
}
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)
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
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 == "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),
}