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 截图
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user