246 lines
11 KiB
Python
246 lines
11 KiB
Python
#!/usr/bin/env python3
|
||
"""软硬件全链路引导测试(docs/field-test-plan.md 的执行器)。
|
||
用法:.venv/bin/python tools/field_test.py [--mac AA:BB:CC:DD:EE:FF] [--ota] [--no-pause]
|
||
自动步骤直接判定;目视步骤打印预期并等待回车;结果写 docs/field-test-result.md。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import datetime
|
||
import json
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
PPCLOCK = ROOT / ".venv" / "bin" / "ppclock"
|
||
RESULT = ROOT / "docs" / "field-test-result.md"
|
||
|
||
results = []
|
||
|
||
|
||
def run(args, timeout=120):
|
||
"""执行 ppclock --json,返回 (exit_code, dict|None, raw)。"""
|
||
cmd = [str(PPCLOCK), "--json"] + args
|
||
p = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
||
try:
|
||
out = json.loads(p.stdout.strip().splitlines()[-1])
|
||
except Exception:
|
||
out = None
|
||
return p.returncode, out, p.stdout + p.stderr
|
||
|
||
|
||
def record(step, name, expect, ok, note=""):
|
||
results.append({"step": step, "name": name, "expect": expect,
|
||
"ok": ok, "note": note})
|
||
mark = "✅" if ok else "❌"
|
||
print(f" {mark} {step} {name}" + (f" — {note}" if note else ""))
|
||
|
||
|
||
def step_auto(step, name, cli_args, expect, check=None, timeout=120):
|
||
code, out, raw = run(cli_args, timeout)
|
||
ok = code == 0 and out is not None and out.get("ok") is True
|
||
if ok and check:
|
||
ok = bool(check(out))
|
||
record(step, name, expect, ok, "" if ok else raw.strip()[:200])
|
||
return ok, out
|
||
|
||
|
||
def step_visual(step, name, cli_args, expect, pause, timeout=120):
|
||
code, out, raw = run(cli_args, timeout)
|
||
ok = code == 0 and out is not None and out.get("ok") is True
|
||
if not ok:
|
||
record(step, name, expect, False, raw.strip()[:200])
|
||
return
|
||
print(f" → 命令已发送成功。屏显预期:{expect}")
|
||
ans = "y" if not pause else input(" 屏显符合预期?[Y/n] ").strip().lower() or "y"
|
||
record(step, name, expect, ans != "n", "目视确认" if ans != "n" else "目视不符")
|
||
|
||
|
||
def make_test_images(tmp: Path):
|
||
from PIL import Image, ImageDraw
|
||
img = Image.new("RGB", (400, 300), (255, 255, 255))
|
||
d = ImageDraw.Draw(img)
|
||
for y in range(0, 300, 20):
|
||
for x in range(0, 400, 20):
|
||
if (x // 20 + y // 20) % 2:
|
||
d.rectangle([x, y, x + 19, y + 19], fill=(0, 0, 0))
|
||
d.rectangle([150, 100, 250, 200], fill=(255, 0, 0))
|
||
p1 = tmp / "checker_red.png"
|
||
img.save(p1)
|
||
img2 = Image.new("RGB", (400, 180), (255, 255, 255))
|
||
d2 = ImageDraw.Draw(img2)
|
||
d2.rectangle([50, 40, 350, 140], fill=(0, 0, 0))
|
||
p2 = tmp / "small.png"
|
||
img2.save(p2)
|
||
return p1, p2
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--mac", help="跳过扫描直连")
|
||
ap.add_argument("--ota", action="store_true", help="执行高危固件烧录阶段")
|
||
ap.add_argument("--no-pause", action="store_true", help="目视步骤全部记为通过(无人值守)")
|
||
args = ap.parse_args()
|
||
pause = not args.no_pause
|
||
mac_args = ["--mac", args.mac] if args.mac else []
|
||
tmp = Path(tempfile.mkdtemp(prefix="ppclock-field-"))
|
||
|
||
print("== 阶段 0 预检 ==")
|
||
code, out, raw = run(["ota", str(ROOT / "app" / "firmware-PP_da14585_4.2_CH.img")])
|
||
record("0.3", "固件样本预检", "precheck ok(不烧录)",
|
||
code == 0 and out and out.get("ok"), raw.strip()[:120])
|
||
|
||
print("== 阶段 1 发现与识别 ==")
|
||
mac = args.mac
|
||
if not mac:
|
||
ok, out = step_auto("1.1", "扫描发现 NRF- 设备", ["scan"],
|
||
"发现 NRF-XXXXXX",
|
||
lambda o: any(d["name"].startswith("NRF-")
|
||
for d in o["data"]["devices"]))
|
||
if ok:
|
||
dev = next(d for d in out["data"]["devices"] if d["name"].startswith("NRF-"))
|
||
mac = dev["address"]
|
||
print(f" → 选中 {dev['name']} {mac}")
|
||
mac_args = ["--mac", mac]
|
||
else:
|
||
print("未发现设备,终止。请确认设备已开机并在范围内。")
|
||
finish(1)
|
||
ok, out = step_auto("1.2", "读取设备 ID", mac_args + ["activate", "--show-id"],
|
||
"14hex 设备 ID",
|
||
lambda o: len(o["data"]["device_id"]) == 14)
|
||
if ok:
|
||
dev_id = out["data"]["device_id"]
|
||
sys.path.insert(0, str(ROOT / "src"))
|
||
from ppclock.protocol import recover_mac_from_device_id
|
||
rec = recover_mac_from_device_id(dev_id)
|
||
print(f" → ID={dev_id} 反解 MAC(NVDS序)={rec.hex(':')}")
|
||
|
||
print("== 阶段 2 激活 ==")
|
||
step_auto("2.1", "离线自动激活", mac_args + ["activate", "--auto"],
|
||
"keygen 下发成功")
|
||
ok, _ = step_auto("2.2", "错误码无惩罚", mac_args + ["activate", "000000000000"],
|
||
"发全0码后命令仍可用")
|
||
step_auto("2.2b", "错误码后命令可用", mac_args + ["mode", "clock1"],
|
||
"mode 仍成功(验证固件不门禁)")
|
||
|
||
print("== 阶段 3 时钟/模式(目视)==")
|
||
step_visual("3.1", "对时", mac_args + ["time"], "屏显时间正确", pause)
|
||
for m, desc in [("clock1", "时钟版式一"), ("clock2", "版式二"), ("clock3", "版式三"),
|
||
("calendar1", "日历版式一"), ("calendar3", "日历版式三")]:
|
||
step_visual(f"3.x", f"mode {m}", mac_args + ["mode", m], desc, pause)
|
||
for tg, desc in [("invert", "反色"), ("font", "字体变化"), ("rotate180", "旋转180°"),
|
||
("rotate180", "旋转恢复"), ("hour_format", "12/24h 切换"),
|
||
("clock_color", "颜色切换")]:
|
||
step_visual("3.x", f"toggle {tg}", mac_args + ["toggle", tg], desc, pause)
|
||
step_visual("3.10", "刷屏", mac_args + ["clear"], "全屏刷新", pause)
|
||
|
||
print("== 阶段 4 图像(目视)==")
|
||
p1, p2 = make_test_images(tmp)
|
||
step_visual("4.1", "棋盘+红块", mac_args + ["image", str(p1), "--slot", "0"],
|
||
"棋盘清晰、中央红块为红", pause)
|
||
run(mac_args + ["mode", "image0"])
|
||
step_visual("4.1b", "显示图一", mac_args + ["mode", "image0"], "上张图在屏", pause)
|
||
for algo in ["floydsteinberg", "bayer", "none"]:
|
||
step_visual("4.2", f"抖动 {algo}", mac_args + ["image", str(p1), "--algo", algo],
|
||
f"{algo} 风格可见", pause)
|
||
step_visual("4.4", "小图 400x180", mac_args + ["image", str(p2)],
|
||
"cod=04 路径小图正常", pause)
|
||
|
||
print("== 阶段 5 模板(目视)==")
|
||
for name, data in [
|
||
("custom", {"text": "全链路测试【通过】"}),
|
||
("schedule", {"items": [["09:00", "晨会"], ["18:30", "复盘【重要】"]]}),
|
||
("memo", {"title": "测试", "lines": ["第一条", "第二条"]}),
|
||
("businesscard", {"name": "测试", "title": "工程师", "phone": "138"}),
|
||
("course", {"grid": [["数学", "语文"], ["英语", "物理"]]}),
|
||
("qrcode", {"content": "https://example.com", "caption": "扫码测试"}),
|
||
]:
|
||
step_visual("5.x", f"模板 {name}",
|
||
mac_args + ["template", name, "--data", json.dumps(data, ensure_ascii=False)],
|
||
f"{name} 版式正确、红字为红", pause)
|
||
|
||
print("== 阶段 6 倒计时(目视)==")
|
||
step_visual("6.1", "时钟倒计时", mac_args + ["countdown", "2026-12-31", "--prefix", "目标"],
|
||
"前缀+天数", pause)
|
||
step_visual("6.2", "日历倒计时", mac_args + ["countdown", "--mode", "calendar",
|
||
"2026-12-31", "--prefix", "中考"], "日历界面倒计时", pause)
|
||
step_visual("6.4", "关闭倒计时", mac_args + ["countdown", "--off"], "倒计时消失", pause)
|
||
|
||
print("== 阶段 7 其他命令 ==")
|
||
step_visual("7.2", "停车牌", mac_args + ["parking", "13800138000"], "号码显示", pause)
|
||
step_visual("7.3", "LUT 红", mac_args + ["lut", "0x05"], "红色加深", pause)
|
||
ok, _ = run(mac_args + ["rotation", "--count", "4"])
|
||
record("7.4", "轮播(no-op 验证)", "命令成功但预期无屏效(固件 no-op §3.3)",
|
||
ok == 0, "已发送,目视应无变化")
|
||
ok, _ = run(mac_args + ["wifi", "TestAP", "pw123456"])
|
||
record("7.5", "WiFi(no-op 验证)", "命令成功但预期无屏效", ok == 0, "已发送")
|
||
step_visual("7.6", "raw 提交", mac_args + ["raw", "e2"], "强制刷新", pause)
|
||
|
||
print("== 阶段 8 异常与契约(自动)==")
|
||
code, out, raw = run(mac_args + ["countdown"])
|
||
record("8.1", "错误参数契约", "ok=false + 退出码1", code == 1 and out and not out["ok"])
|
||
ok, out = step_auto("8.3", "batch 批量", mac_args + ["batch", str(ROOT / "examples" / "field_test.jsonl")],
|
||
"单连接逐行 ok",
|
||
lambda o: all(r["ok"] for r in o["data"]["results"]))
|
||
|
||
if args.ota:
|
||
print("== 阶段 9 固件烧录(高危)==")
|
||
tst = make_test_firmware(tmp)
|
||
if tst is None:
|
||
record("9.1", "制作测试镜像", "版本串等长替换+CRC 有效", False, "版本串未找到")
|
||
else:
|
||
record("9.1", "制作测试镜像", "版本串等长替换+CRC 有效", True, str(tst))
|
||
ans = "y" if not pause else input(" 确认烧录测试镜像?[y/N] ").strip().lower()
|
||
if ans == "y":
|
||
ok, out = run(mac_args + ["ota", str(tst), "--yes"], timeout=600)
|
||
record("9.2", "OTA 烧录", "全部块 ok", ok == 0 and out and out.get("ok"),
|
||
str(out)[:200])
|
||
input(" 等待设备重启后回车…") if pause else None
|
||
ok, _ = run(["scan"])
|
||
record("9.3", "重启后设备可用", "scan 正常", ok == 0)
|
||
ans = "y" if not pause else input(" 回滚刷入原镜像?[Y/n] ").strip().lower() or "y"
|
||
if ans == "y":
|
||
ok, out = run(mac_args + ["ota", str(ROOT / "app" / "firmware-PP_da14585_4.2_CH.img"), "--yes"], timeout=600)
|
||
record("9.4", "回滚原镜像", "烧录成功", ok == 0 and out and out.get("ok"))
|
||
finish(0)
|
||
|
||
|
||
def make_test_firmware(tmp: Path):
|
||
"""仅改版本串的等长测试镜像(安全改动)。"""
|
||
sys.path.insert(0, str(ROOT))
|
||
from tools.fw import pack_image, unpack_body, parse_image
|
||
src = ROOT / "app" / "firmware-PP_da14585_4.2_CH.img"
|
||
body = bytearray(unpack_body(src.read_bytes()))
|
||
old = b"V:PP_da14585_4.2"
|
||
new = b"V:PP_da14585_TST"
|
||
assert len(old) == len(new)
|
||
idx = body.find(old)
|
||
if idx < 0:
|
||
return None
|
||
body[idx:idx + len(old)] = new
|
||
img = pack_image(bytes(body))
|
||
assert parse_image(img)["crc_valid"]
|
||
out = tmp / "fw_tst.img"
|
||
out.write_bytes(img)
|
||
return out
|
||
|
||
|
||
def finish(code):
|
||
passed = sum(1 for r in results if r["ok"])
|
||
total = len(results)
|
||
lines = [f"# 全链路测试结果({datetime.datetime.now():%Y-%m-%d %H:%M})",
|
||
f"\n通过 {passed}/{total}\n",
|
||
"| 步骤 | 名称 | 预期 | 结果 | 备注 |", "|---|---|---|---|---|"]
|
||
for r in results:
|
||
lines.append("| {step} | {name} | {expect} | {ok} | {note} |".format(
|
||
**r, ok="✅" if r["ok"] else "❌"))
|
||
RESULT.write_text("\n".join(lines), encoding="utf-8")
|
||
print(f"\n结果:{passed}/{total} 通过 → {RESULT}")
|
||
sys.exit(code)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|