Files
DCU_DesktopController/scripts/skip_rate_meter.py
T
cbb e91d53e0fc feat(ecat): 启动后把 ecat_native 提到 RealTime;跳拍优化实验脚本与报告
- esp32_server: Popen 后外部 SetPriorityClass(RT/High),native 自设会被钳成 High
- 入库 skip_* 标定/实验脚本与 REPORT_ecat_skip_opt_20260917
- 前端 WIP:layout/app/polling/boot_ecat/index
- 不含 lab/skip_meter 原始采样与 preview 截图
2026-09-17 02:32:20 +08:00

137 lines
4.5 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""跳拍率标定:固定时长采 /api/motor/dbg,输出可对比 JSON。
主判据: late_per_s, skip_per_s, send_stall_frac, rx_stall_frac
用法:
python scripts/skip_rate_meter.py --tag base --secs 20
"""
from __future__ import annotations
import argparse
import json
import os
import time
import urllib.request
BASE = os.environ.get("ECAT_BASE", "http://127.0.0.1:8090")
OUTDIR = os.path.join(os.path.dirname(__file__), "..", "lab", "skip_meter")
def post(path: str, body=None, timeout=5.0):
data = json.dumps(body or {}).encode()
req = urllib.request.Request(
BASE + path, data=data, headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.loads(r.read().decode())
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--tag", default="sample")
ap.add_argument("--secs", type=float, default=20.0)
ap.add_argument("--hz", type=float, default=5.0)
ap.add_argument("--out", default=None)
args = ap.parse_args()
st0 = post("/api/motor/state")
period = float(st0.get("period_ms") or 1.0)
t0 = time.time()
pulses = []
last_t = None
n_poll = 0
# dbg t0 uses now_s() (QPC-based), NOT unix time. Track by last-seen t0 only.
while time.time() - t0 < args.secs:
dbg = post("/api/motor/dbg")
n_poll += 1
cyc = dbg.get("cyc") or []
if last_t is not None:
new = [r for r in cyc if r[0] > last_t]
else:
# first poll: adopt the whole ring's last timestamp as cursor;
# count only k>0 from now on via subsequent polls. Also count
# k>0 already in ring as "pre-existing" (not in window rate).
new = []
if cyc:
last_t = cyc[-1][0]
elif last_t is None:
last_t = None
for r in new:
if len(r) < 8:
continue
k = int(r[5])
if k <= 0:
continue
send_us = (r[2] - r[1]) * 1e6
rx_us = (r[3] - r[2]) * 1e6
pulses.append(
{
"t0": r[0],
"k": k,
"send_us": send_us,
"rx_us": rx_us,
"work_us": (r[4] - r[0]) * 1e6,
"sd": float(r[6]) if len(r) > 6 else None,
"rxc": int(r[7]) if len(r) > 7 else None,
"kind": (
"send"
if send_us >= 500
else ("rx" if rx_us >= 500 else "other")
),
}
)
time.sleep(max(0.0, 1.0 / args.hz))
st1 = post("/api/motor/state")
wall = time.time() - t0
n_late = len(pulses)
n_skip = sum(p["k"] for p in pulses)
by = {"send": 0, "rx": 0, "other": 0}
for p in pulses:
by[p["kind"]] += 1
# approximate expected beats in window
# use cycles delta when available
cyc0 = st0.get("cycles") or 0
cyc1 = st1.get("cycles") or 0
dcyc = max(0, cyc1 - cyc0)
cu = st1.get("cyc_us") or {}
result = {
"tag": args.tag,
"ts": time.time(),
"wall_s": round(wall, 3),
"period_ms": period,
"pcore_mask": st1.get("pcore_mask"),
"wkc": st1.get("wkc"),
"spin": st1.get("spin"),
"cycles_delta": dcyc,
"achieved_hz": round(dcyc / wall, 2) if wall > 0 else None,
"late_per_s": round(n_late / wall, 3) if wall else None,
"skip_per_s": round(n_skip / wall, 3) if wall else None,
"late_frac_pct": round(100.0 * n_late / dcyc, 4) if dcyc else None,
"skip_frac_pct": round(100.0 * n_skip / dcyc, 4) if dcyc else None,
"send_stall_frac": round(by["send"] / n_late, 4) if n_late else None,
"rx_stall_frac": round(by["rx"] / n_late, 4) if n_late else None,
"n_late": n_late,
"n_skip": n_skip,
"by_kind": by,
"window_cyc_us": cu,
"n_poll": n_poll,
"pulses_sample": pulses[:40],
}
os.makedirs(OUTDIR, exist_ok=True)
out = args.out or os.path.join(OUTDIR, f"{args.tag}_{int(time.time())}.json")
with open(out, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(json.dumps({k: result[k] for k in result if k != "pulses_sample"},
ensure_ascii=False, indent=2))
print(f"WROTE {out}")
return 0
if __name__ == "__main__":
raise SystemExit(main())