135 lines
5.4 KiB
Python
135 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
||||
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""GD32 断电/复位后一键恢复 EtherCAT,并**验收**恢复是否真生效。
|
|||
|
|
|
|||
|
|
用法(GD32 上电稳定后):
|
|||
|
|
python scripts/check_recover_after_reset.py
|
|||
|
|
python scripts/check_recover_after_reset.py --skip-e2e
|
|||
|
|
|
|||
|
|
与旧版的差别:旧版全程只 print、没有任何失败路径(--skip-e2e 时无条件退 0),
|
|||
|
|
所以"跑过了"和"恢复了"是两件事。现在每一步都有断言,退出码 0/1/2 是真门槛:
|
|||
|
|
0=全部通过 1=有断言失败(别去点页面,先修) 2=前置不满足(native/页面没起)
|
|||
|
|
"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import json
|
|||
|
|
import os
|
|||
|
|
import subprocess
|
|||
|
|
import sys
|
|||
|
|
import time
|
|||
|
|
import urllib.request
|
|||
|
|
|
|||
|
|
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|||
|
|
sys.path.insert(0, os.path.join(ROOT, "scripts"))
|
|||
|
|
from guard_probe import Blocked, bridge_of, open_link, guard_of # noqa: E402
|
|||
|
|
|
|||
|
|
ESP = os.environ.get("DCU_ESP_IP", "192.168.31.127")
|
|||
|
|
|
|||
|
|
FAILS: list[str] = []
|
|||
|
|
|
|||
|
|
|
|||
|
|
def ck(cond, msg):
|
|||
|
|
print((" OK " if cond else " FAIL ") + msg)
|
|||
|
|
if not cond:
|
|||
|
|
FAILS.append(msg)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main() -> int:
|
|||
|
|
skip_e2e = "--skip-e2e" in sys.argv
|
|||
|
|
print("=== 1) WiFi motReset(可选,拉一下 GD32 应用层) ===")
|
|||
|
|
try:
|
|||
|
|
cfg = bytearray(24)
|
|||
|
|
cfg[0] = 1 # motReset bit0
|
|||
|
|
body = json.dumps({"ch": 2, "frames": [{"id": 0x00030301, "dlc": 24,
|
|||
|
|
"data": cfg.hex()}]}).encode()
|
|||
|
|
req = urllib.request.Request("http://%s/can/send_batch" % ESP, data=body,
|
|||
|
|
headers={"Content-Type": "application/json"},
|
|||
|
|
method="POST")
|
|||
|
|
t0 = time.perf_counter()
|
|||
|
|
with urllib.request.urlopen(req, timeout=6) as r:
|
|||
|
|
j = json.loads(r.read().decode("utf-8", "replace") or "{}")
|
|||
|
|
print(" wifi motReset %.0fms %s" % ((time.perf_counter() - t0) * 1000, j.get("status")))
|
|||
|
|
except Exception as e:
|
|||
|
|
print(" wifi motReset skip: %s" % e) # 可选步骤,不作为门槛
|
|||
|
|
time.sleep(1.5)
|
|||
|
|
|
|||
|
|
lk = open_link()
|
|||
|
|
print("=== 2) force_reopen + DC250 motor/start ===")
|
|||
|
|
try:
|
|||
|
|
h = lk.health()
|
|||
|
|
print(" before: dc=%s us=%s loop=%s op=%s wkc=%s"
|
|||
|
|
% (h.get("dc"), h.get("dc_cycle_us"), h.get("loop"), h.get("op"), h.get("wkc")))
|
|||
|
|
except Exception as e:
|
|||
|
|
print(" health fail: %s" % e)
|
|||
|
|
|
|||
|
|
r = lk.api("/api/motor/start", {"period_ms": 0.25, "dc_cycle_us": 250,
|
|||
|
|
"dc_shift_us": 0, "force_reopen": True})
|
|||
|
|
print(" motor/start force_reopen ok=%s period=%s" % (r.get("ok"), r.get("period_ms")))
|
|||
|
|
print(" log: %s" % (r.get("log"),))
|
|||
|
|
ck(bool(r.get("ok")), "motor/start 回执 ok=true")
|
|||
|
|
|
|||
|
|
# force_reopen 之后 loop 需要一点时间起来(冷启动见过 loop=0 的采样竞态),
|
|||
|
|
# 所以要"轮询到恢复"而不是只读一次;上限 15s。
|
|||
|
|
good = None
|
|||
|
|
t_end = time.time() + 15.0
|
|||
|
|
while time.time() < t_end:
|
|||
|
|
h = lk.health()
|
|||
|
|
if (h.get("dc") == 1 and h.get("dc_cycle_us") == 250 and h.get("loop") == 1
|
|||
|
|
and h.get("op") == 1 and (h.get("wkc") or 0) >= 3):
|
|||
|
|
good = h
|
|||
|
|
break
|
|||
|
|
time.sleep(0.5)
|
|||
|
|
ck(good is not None,
|
|||
|
|
"15s 内 health 达到 dc=1/250us/loop=1/op=1/wkc>=3(现在 %s)"
|
|||
|
|
% ({k: (good or h).get(k) for k in ("dc", "dc_cycle_us", "loop", "op", "wkc")}))
|
|||
|
|
|
|||
|
|
g = guard_of(lk.health())
|
|||
|
|
t0 = g["alive_ticks"]
|
|||
|
|
time.sleep(1.0)
|
|||
|
|
g1 = guard_of(lk.health())
|
|||
|
|
ck(g1["alive_ticks"] - t0 >= 3,
|
|||
|
|
"守卫心跳 +%d/1s,armed=%s fires=%s(fires 若在本步之后才涨,说明是守卫救回来的)"
|
|||
|
|
% (g1["alive_ticks"] - t0, g1["armed"], g1["fires"]))
|
|||
|
|
|
|||
|
|
print("=== 3) 发一帧看 GD32 出口 ===")
|
|||
|
|
pre = bridge_of(lk.state())
|
|||
|
|
br_pre = (pre.get("tx_count"), pre.get("ack"))
|
|||
|
|
r, ms = lk.send()
|
|||
|
|
print(" send %.1fms ok=%s queued=%s" % (ms, r.get("ok"), r.get("queued")))
|
|||
|
|
t_sent = time.perf_counter()
|
|||
|
|
outlet = None
|
|||
|
|
while time.perf_counter() - t_sent < 3.0:
|
|||
|
|
br = bridge_of(lk.state())
|
|||
|
|
if (br.get("tx_count"), br.get("ack")) != br_pre and br.get("tx_count") is not None:
|
|||
|
|
outlet = (time.perf_counter() - t_sent) * 1000.0
|
|||
|
|
print(" 出口 %.0fms tx/ack %s → %s"
|
|||
|
|
% (outlet, br_pre, (br.get("tx_count"), br.get("ack"))))
|
|||
|
|
break
|
|||
|
|
time.sleep(0.002)
|
|||
|
|
ck(outlet is not None,
|
|||
|
|
"3s 内 GD32 出口跟上(否则就是输出 PDO 没被消费,不是网页慢)")
|
|||
|
|
if outlet is None:
|
|||
|
|
print(" → 仍失败:先确认 GD32 已完整上电跑 APP;若 health 全绿而出口恒冻,"
|
|||
|
|
"等 8s 看 guard.fires 是否 +1(守卫会自动 close→open),"
|
|||
|
|
"或跑 tools/ecat/check_guard_regression.py 定位。")
|
|||
|
|
|
|||
|
|
if not skip_e2e:
|
|||
|
|
print("\n=== 4) 网页全链路自测 ===")
|
|||
|
|
py = os.environ.get("MIMO_PYTHON") or sys.executable
|
|||
|
|
script = os.path.join(ROOT, "scripts", "check_web_send_latency.py")
|
|||
|
|
rc = subprocess.call([py, script, "--n", "6"], cwd=ROOT)
|
|||
|
|
ck(rc == 0, "check_web_send_latency.py 退出码 %d" % rc)
|
|||
|
|
|
|||
|
|
print("\nVERDICT %s%s" % ("PASS" if not FAILS else "FAIL",
|
|||
|
|
"" if not FAILS else " " + "; ".join(FAILS[:3])))
|
|||
|
|
return 0 if not FAILS else 1
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
try:
|
|||
|
|
sys.exit(main())
|
|||
|
|
except Blocked as e:
|
|||
|
|
print("BLOCKED 前置不满足:%s" % e)
|
|||
|
|
print("VERDICT BLOCKED")
|
|||
|
|
sys.exit(2)
|