119 lines
3.7 KiB
Python
119 lines
3.7 KiB
Python
#!/usr/bin/env python3
|
||||
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""A/B:JSON motor/send vs ESF1 快发(HTTP send_raw / 持久 TCP)软件段延迟。
|
|||
|
|
|
|||
|
|
需要:tools/ecat ecat_native 已运行(总线可不必在 OP,未连接时会报未连接但仍计时)。
|
|||
|
|
|
|||
|
|
用法:
|
|||
|
|
python scripts/check_esf_latency.py
|
|||
|
|
python scripts/check_esf_latency.py --n 40
|
|||
|
|
"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import argparse
|
|||
|
|
import json
|
|||
|
|
import os
|
|||
|
|
import socket
|
|||
|
|
import statistics
|
|||
|
|
import struct
|
|||
|
|
import sys
|
|||
|
|
import time
|
|||
|
|
import urllib.request
|
|||
|
|
|
|||
|
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|||
|
|
from esp32_server import pack_esf, unpack_fsok # noqa: E402
|
|||
|
|
|
|||
|
|
PAGE = os.environ.get("DCU_PAGE_BASE", "http://127.0.0.1:2625")
|
|||
|
|
ECAT = PAGE + "/ecat/api"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def http(url, data: bytes, ctype="application/json", timeout=5.0):
|
|||
|
|
req = urllib.request.Request(
|
|||
|
|
url, data=data,
|
|||
|
|
headers={"Content-Type": ctype, "Accept": "application/json"},
|
|||
|
|
method="POST",
|
|||
|
|
)
|
|||
|
|
t0 = time.perf_counter()
|
|||
|
|
try:
|
|||
|
|
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|||
|
|
body = r.read()
|
|||
|
|
ms = (time.perf_counter() - t0) * 1000.0
|
|||
|
|
try:
|
|||
|
|
return ms, json.loads(body.decode("utf-8", "replace") or "{}")
|
|||
|
|
except Exception:
|
|||
|
|
return ms, {"_raw_len": len(body)}
|
|||
|
|
except Exception as e:
|
|||
|
|
return (time.perf_counter() - t0) * 1000.0, {"ok": False, "error": str(e)}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def pct(xs, p):
|
|||
|
|
if not xs:
|
|||
|
|
return None
|
|||
|
|
xs = sorted(xs)
|
|||
|
|
k = max(0, min(len(xs) - 1, int(round((p / 100.0) * (len(xs) - 1)))))
|
|||
|
|
return xs[k]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main() -> int:
|
|||
|
|
ap = argparse.ArgumentParser()
|
|||
|
|
ap.add_argument("--n", type=int, default=30)
|
|||
|
|
args = ap.parse_args()
|
|||
|
|
|
|||
|
|
frame = {"ch": 2, "id": 0x00030200, "dlc": 32,
|
|||
|
|
"data": "02" + "00" * 29 + "000000"} # 静止 CMD
|
|||
|
|
frames = [frame]
|
|||
|
|
esf = pack_esf(frames)
|
|||
|
|
js = json.dumps({"frames": frames}, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
|||
|
|
|
|||
|
|
ms_h, health = http(ECAT + "/native/health", b"{}")
|
|||
|
|
print(f"health {ms_h:.2f}ms {json.dumps(health, ensure_ascii=False)[:200]}")
|
|||
|
|
fsp = int(health.get("fast_send_port") or 0)
|
|||
|
|
print(f"fast_send_port={fsp}")
|
|||
|
|
|
|||
|
|
json_ms, raw_ms = [], []
|
|||
|
|
for _ in range(args.n):
|
|||
|
|
ms, _ = http(ECAT + "/motor/send", js)
|
|||
|
|
json_ms.append(ms)
|
|||
|
|
ms, _ = http(ECAT + "/motor/send_raw", esf, "application/octet-stream")
|
|||
|
|
raw_ms.append(ms)
|
|||
|
|
|
|||
|
|
tcp_ms = []
|
|||
|
|
if fsp > 0:
|
|||
|
|
sock = socket.create_connection(("127.0.0.1", fsp), timeout=None)
|
|||
|
|
sock.settimeout(2.0)
|
|||
|
|
try:
|
|||
|
|
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
for _ in range(args.n):
|
|||
|
|
t0 = time.perf_counter()
|
|||
|
|
sock.sendall(esf)
|
|||
|
|
resp = b""
|
|||
|
|
while len(resp) < 8:
|
|||
|
|
c = sock.recv(8 - len(resp))
|
|||
|
|
if not c:
|
|||
|
|
raise OSError("closed")
|
|||
|
|
resp += c
|
|||
|
|
tcp_ms.append((time.perf_counter() - t0) * 1000.0)
|
|||
|
|
r = unpack_fsok(resp)
|
|||
|
|
if not r.get("ok") and not tcp_ms:
|
|||
|
|
print("TCP 快发 ok=false", r)
|
|||
|
|
sock.close()
|
|||
|
|
|
|||
|
|
def show(name, xs):
|
|||
|
|
if not xs:
|
|||
|
|
print(f"{name}: (skip)")
|
|||
|
|
return
|
|||
|
|
print(f"{name}: n={len(xs)} mean={statistics.mean(xs):.3f}ms "
|
|||
|
|
f"p50={pct(xs,50):.3f} p90={pct(xs,90):.3f} min={min(xs):.3f} max={max(xs):.3f}")
|
|||
|
|
|
|||
|
|
show("JSON motor/send ", json_ms)
|
|||
|
|
show("HTTP send_raw ", raw_ms)
|
|||
|
|
show("TCP fast_send ", tcp_ms)
|
|||
|
|
print("口径:HTTP 客户端侧 POST 往返(含本机代理时);不含浏览器 JS/pywebview hop。")
|
|||
|
|
return 0
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
sys.exit(main())
|