- 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 截图
137 lines
4.5 KiB
Python
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())
|