chore: workspace 清理(-6.5MB 可再生产物)+ 全链路测试资产(field-test-plan/field_test.py/examples)

This commit is contained in:
agent committed 2026-07-25 09:09:41 +00:00
1 parent 2bdbf1898c
commit fe7f53d35c
16 files changed
+411 -32106

No files matched your search

+245
View File
@@ -0,0 +1,245 @@
#!/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()