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

+4 -1
View File
@@ -1,5 +1,8 @@
.venv/
__pycache__/
*.pyc
analysis/apk/jadx-out/
.pytest_cache/
*.egg-info/
analysis/apk/jadx-out/
analysis/apk/native/
analysis/firmware/fw.asm
+12 -4
View File
@@ -29,11 +29,19 @@
- `analysis/`:web 抓取与协议原始报告、apk 反编译报告与中间产物、firmware 反汇编产物
- `tests/`:132 测试全绿
### 遗留(需实机)
### 遗留(需实机)→ 已备好执行器
1. 真机功能回归(设备不在 BLE 范围内;扫到 17 台其他 BLE 设备无 NRF- 前缀)
2. SUOTA 首刷验证(建议先用仅改版本字符串的镜像,firmware-analysis §5 风险提示)
3. `--mac` 路径 activate --auto 的 NVDS 字节序(EFEF 路径无歧义,优先用)
**执行入口:`.venv/bin/python tools/field_test.py`(计划 docs/field-test-plan.md,结果自动写 docs/field-test-result.md)**
1. 真机功能回归(设备不在 BLE 范围内;扫到 17 台其他 BLE 设备无 NRF- 前缀)→ 阶段 1–8
2. SUOTA 首刷验证(建议先用仅改版本字符串的镜像)→ 阶段 9(`--ota` 才执行,脚本自动制作等长测试镜像+回滚)
3. `--mac` 路径 activate --auto 的 NVDS 字节序(EFEF 路径无歧义,优先用)→ 阶段 1.2 自动反解对比
### 2026-07-25:workspace 整理清理
- 清理:删除可再生产物(apk/native/*.so 5.2MB、firmware/fw.asm 1.2MB、decomp 日志 5 个)与 __pycache__/.pytest_cache;再生方法写入 analysis/README.md
- 新增:docs/field-test-plan.md(10 阶段验收表)、tools/field_test.py(引导执行器)、examples/field_test.jsonl(batch 冒烟序列)
- 冒烟验证:field_test.py --help / jsonl 10 行可解析 / 测试镜像制作+CRC 均通过
### 会话时间线
+12 -1
View File
@@ -75,10 +75,21 @@ CLI 全部为单次执行语义,调度交给系统 cron:
- 模板渲染、二维码生成(本地 qrcode 库)、抖动算法全部本地实现,无任何 CDN/服务器依赖。
- 天气(CITY 代码)属原厂云功能,本地化范围外;`wifi` 命令仅透传配置。
## 真机全链路测试
设备到场后执行(计划:`docs/field-test-plan.md`):
```bash
.venv/bin/python tools/field_test.py # 引导式:自动判定+目视确认
.venv/bin/python tools/field_test.py --ota # 含高危固件烧录阶段(默认跳过)
```
结果自动写入 `docs/field-test-result.md`。
## 开发
```bash
.venv/bin/python -m pytest tests/ -q # 104 测试
.venv/bin/python -m pytest tests/ -q # 132 测试
```
治理规则见 `GOVERNANCE.md`;计划见 `PLAN.md`;协议规范见 `docs/protocol.md`。
+25
View File
@@ -0,0 +1,25 @@
# analysis/ — 逆向中间产物
> 治理规则:本目录放分析中间产物;报告类(*-analysis.md / protocol-raw.md)永久保留;
> 可从原始输入再生的体积大产物不入库(见下"再生方法")。
## 目录
| 目录 | 内容 | 关键文件 |
|---|---|---|
| `web/` | Web 上位机抓取(msppppclock.tech 全站 JS)+ 协议提取报告 | **protocol-raw.md**(协议唯一原始证据,带行号)、ble.js 等 8 个站点文件 |
| `apk/` | APK 反编译报告与证据 | **apk-analysis.md**(互证+SUOTA+激活)、decomp/*.java(报告行号引用的源码)、strings*.txt、xrefs-key-strings.txt |
| `firmware/` | 固件逆向脚本与函数表 | disasm.py、fw_parse.py、find_cmds.py、funcs.txt(报告在 ../../docs/firmware-analysis.md) |
## 已清理产物的再生方法
| 产物 | 再生命令 |
|---|---|
| `apk/native/*.so`(5.2MB) | `unzip -j ../../app/client-qbsg_v1.1.0.apk 'lib/arm64-v8a/*' -d apk/native/` |
| `firmware/fw.asm`(1.2MB) | `../../.venv/bin/python firmware/disasm.py` |
| jadx 全量反编译 | jadx 未被策略允许安装;decomp/ 已含全部报告引用文件 |
## 原始输入(只读,在 ../../app/)
- `firmware-PP_da14585_4.2_CH.img`(72288B,'pQ' 头 + CRC32 已验证)
- `client-qbsg_v1.1.0.apk`(9.3MB)
View File
Whitespace-only changes.
View File
Whitespace-only changes.
View File
Whitespace-only changes.
View File
Whitespace-only changes.
View File
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
View File
Whitespace-only changes.
File diff suppressed because it is too large. Load diff
+103
View File
@@ -0,0 +1,103 @@
# 软硬件全链路测试计划(设备到场后执行)
> 目的:验证三线逆向结论在真机上的正确性,完成 G3/G4 的实机门禁。
> 前置:设备充电开机;本机蓝牙可用;`tools/check_env.py` 通过。
> 执行方式:`.venv/bin/python tools/field_test.py [--mac AA:BB:..]`(引导式,自动+目视确认)。
> 危险操作仅 `--ota` 分支,默认跳过。
## 阶段 0 · 预检(自动)
| # | 检查 | 通过标准 |
|---|------|----------|
| 0.1 | check_env.py | 全 OK |
| 0.2 | 蓝牙适配器 | `ppclock scan` 返回 ≥0 设备不报错 |
| 0.3 | 固件样本 | `ppclock ota app/firmware-PP_da14585_4.2_CH.img` 预检 ok(**不烧录**) |
## 阶段 1 · 发现与识别(自动)
| # | 检查 | 通过标准 |
|---|------|----------|
| 1.1 | `scan` | 发现 `NRF-XXXXXX` 设备,记录 MAC |
| 1.2 | `activate --show-id` | 收到 14 hex 设备 ID;本地反解 MAC 与 1.1 的 MAC 对比(字节序验证 → 回填 DECISIONS.md) |
| 1.3 | 断线重连 | CLI 连续两次命令均成功 |
## 阶段 2 · 激活(自动+目视)
| # | 检查 | 通过标准 |
|---|------|----------|
| 2.1 | `activate --auto` | ok=true,code 与 keygen 一致;屏上激活提示(若有)消失 |
| 2.2 | 错误码激活 | 发 6 字节全 0 码:无惩罚、后续命令仍可用(验证固件不门禁结论 §4.3) |
## 阶段 3 · 时钟/模式族(目视)
| # | 命令 | 屏显预期 |
|---|------|----------|
| 3.1 | `time` | 屏显时间与手机/电脑一致 |
| 3.2 | `mode clock1/2/3` | 三种时钟版式切换 |
| 3.3 | `mode calendar1/2/3` | 三种日历版式切换 |
| 3.4 | `toggle hour_format` | 12/24h 切换 |
| 3.5 | `toggle clock_color` | 时钟颜色变化 |
| 3.6 | `toggle invert` | 反色 |
| 3.7 | `toggle font` | 字体变化 |
| 3.8 | `toggle rotate180` | 旋转 180°(恢复再按一次) |
| 3.9 | `mode tricolor` / `mode mono` | 三色/黑白切换 |
| 3.10 | `clear` | 全屏刷新 |
## 阶段 4 · 图像(目视)
| # | 命令 | 屏显预期 |
|---|------|----------|
| 4.1 | `image <棋盘+红块测试图> --slot 0` → `mode image0` | 棋盘清晰、红块为红 |
| 4.2 | 六算法各传一张 | 抖动风格差异可见(Floyd/Atkinson/Bayer/Stucki/Jarvis/none) |
| 4.3 | `image --mono` | 无红色通道 |
| 4.4 | `image --size 400x180`(小图 cod=04 路径) | 小图正常 |
| 4.5 | 亮度/对比度/饱和度/旋转参数 | 效果可见 |
| 4.6 | `image --slot 1..3` + `mode image1..3` | 多槽位(注意:固件 EA0x 为 no-op 风险 → 记录实际行为) |
## 阶段 5 · 模板(目视)
6 模板(schedule/businesscard/memo/course/qrcode/custom)各传一次:版式正确、中文无方块、`【…】` 字段为红。
## 阶段 6 · 倒计时(目视)
| # | 命令 | 预期 |
|---|------|------|
| 6.1 | `countdown 2026-12-31 --prefix 目标` | 时钟界面显示前缀+天数 |
| 6.2 | `countdown --mode calendar 2026-12-31 --prefix 中考` | 日历界面 |
| 6.3 | `countdown --show-calendar --prefix test` | upload_rili 序列 |
| 6.4 | `countdown --off` | 倒计时关闭 |
## 阶段 7 · 其他命令(含固件 no-op 验证)
| # | 命令 | 预期 |
|---|------|------|
| 7.1 | `sleep --on --start 23 --end 7` | 时段内不刷新(次晨验证);`sleep --off` 恢复 |
| 7.2 | `parking 13800138000` | 停车牌显示 |
| 7.3 | `lut 0x05` / `lut 0x15` | 红/黑深度变化 |
| 7.4 | `rotation --count 4`、`rotation --interval 5` | **验证固件 no-op 结论**:预期无效果(firmware-analysis §3.3) |
| 7.5 | `wifi SSID PASS` | **验证 no-op**:预期无效果 |
| 7.6 | `raw e2` / `raw 01 --channel epd` | 提交/刷新 |
## 阶段 8 · 异常与契约(自动)
| # | 检查 | 通过标准 |
|---|------|----------|
| 8.1 | 错误参数 | `--json` 输出 `{"ok":false,...}`,退出码 1 |
| 8.2 | 设备关机中途执行 | 退出码 2,error 含连接失败 |
| 8.3 | `batch examples/field_test.jsonl` | 单连接全序执行,results 逐行 ok |
## 阶段 9 · 固件可控(高危,默认跳过,`--ota` 才执行)
| # | 步骤 | 预案 |
|---|------|------|
| 9.1 | field_test.py 自动制作仅改版本串镜像(`V:PP_da14585_4.2`→`V:PP_da14585_TST`,等长),fw_pack 重打包,CRC 验证 | 本地完成,不触机 |
| 9.2 | `ppclock --mac X ota /tmp/fw_tst.img --yes` | 观察块进度 302 块全 ok |
| 9.3 | 设备重启后 `scan` + `time` | 设备复活且可用 |
| 9.4 | 回滚 | `ota 原镜像 --yes` 刷回 |
| 9.5 | 若 9.2 失败/变砖 | 记录 SERV_STATUS 错误码;设备有备 bank(0x38000 产品头机制,firmware-analysis §5),断电重连试刷原镜像 |
## 结果回填
- 每步实际结果记于本文件"实测"列(执行时生成 field-test-result.md)
- 与协议不符处 → 改 docs/protocol.md + 测试向量 + DECISIONS.md
- 全过 → PROGRESS.md 将 G3/G4 从"协议级/工具级"升级为"实机验证",Plan Task 9 勾掉
+10
View File
@@ -0,0 +1,10 @@
{"argv": ["time"]}
{"argv": ["mode", "clock1"]}
{"argv": ["mode", "calendar1"]}
{"argv": ["mode", "clock2"]}
{"argv": ["parking", "12345"]}
{"argv": ["countdown", "2026-12-31", "--prefix", "目标"]}
{"argv": ["sleep", "--on", "--start", "23", "--end", "7"]}
{"argv": ["sleep", "--off"]}
{"argv": ["mode", "clock1"]}
{"argv": ["clear"]}
+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()