#!/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())