桌面 APP 下 ECAT motor 发送只走 pywebview ESF1→native TCP; 删除桥内 HTTP/JSON 回落。附延迟测量与 pack 自测脚本与规格文档。
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())
|