Files

246 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()