102 lines
3.6 KiB
Python
102 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""ECAT 与 WiFi 两条链路对同一 seq 的 48B STATUS 做逐字节对照。
|
||
|
||
两条链路读的是 GD32 里同一张 MotorState 表,但读取上下文不同:
|
||
* ECAT 段2 由 buildInput 取表,跑在 PDI_Isr —— 会抢占主循环的写侧(本次修复点);
|
||
* WiFi 0xC5/buildPayload 与写侧同在主循环 —— 结构上不可能读到中间态。
|
||
所以 WiFi 侧可当参照:同一 seq 在两边必须逐字节相同(seq 由 global_seq 唯一确定,
|
||
一次更新只产生一个 seq)。不等 ⇒ ECAT 侧那一份是没写完的半成品。
|
||
|
||
用法: python scripts/probe_ecat_vs_wifi.py [秒数]
|
||
"""
|
||
import json
|
||
import socket
|
||
import struct
|
||
import sys
|
||
import time
|
||
import urllib.request
|
||
|
||
HOST, PORT = "127.0.0.1", 8090
|
||
DUR_S = float(sys.argv[1]) if len(sys.argv) > 1 else 60.0
|
||
ESP = "192.168.31.127"
|
||
REQ = (f"POST /api/motor/state HTTP/1.0\r\nHost: {HOST}:{PORT}\r\n"
|
||
"Content-Type: application/json\r\nContent-Length: 2\r\n"
|
||
"Connection: close\r\n\r\n{}").encode("ascii")
|
||
|
||
|
||
def ecat_once():
|
||
s = socket.create_connection((HOST, PORT))
|
||
try:
|
||
s.sendall(REQ)
|
||
buf = bytearray()
|
||
while True:
|
||
c = s.recv(65536)
|
||
if not c:
|
||
break
|
||
buf.extend(c)
|
||
finally:
|
||
s.close()
|
||
_, _, d = bytes(buf).partition(b"\r\n\r\n")
|
||
return json.loads(d.decode("utf-8", "replace"))
|
||
|
||
|
||
def wifi_once():
|
||
"""返回 {node: (seq, 48B)}。协议 ver=2:entry 92B,data 在 +12。"""
|
||
b = urllib.request.urlopen(f"http://{ESP}/motor/state?bin=1", timeout=3).read()
|
||
if len(b) < 12 or struct.unpack_from("<H", b, 0)[0] != 0x4D53 or b[2] != 2:
|
||
return {}
|
||
out, off = {}, 12
|
||
for _ in range(b[9]):
|
||
if off + 92 > len(b):
|
||
break
|
||
out[b[off]] = (struct.unpack_from("<I", b, off + 8)[0],
|
||
b[off + 12:off + 60])
|
||
off += 92
|
||
return out
|
||
|
||
|
||
def main():
|
||
cache = {} # node -> {seq: payload}(ECAT 侧见过的每一帧)
|
||
matched = mism = polls = wpolls = 0
|
||
bad = []
|
||
t0 = time.time()
|
||
while time.time() - t0 < DUR_S:
|
||
try:
|
||
snap = ecat_once()
|
||
polls += 1
|
||
for m in snap.get("motors") or []:
|
||
if m.get("online") and m.get("data"):
|
||
cache.setdefault(m["node"], {})[m["seq"]] = bytes.fromhex(m["data"])
|
||
if len(cache[m["node"]]) > 4000:
|
||
for k in sorted(cache[m["node"]])[:2000]:
|
||
del cache[m["node"]][k]
|
||
except Exception:
|
||
continue
|
||
try:
|
||
w = wifi_once()
|
||
wpolls += 1
|
||
except Exception:
|
||
continue
|
||
for node, (seq, payload) in w.items():
|
||
d = cache.get(node, {}).get(seq)
|
||
if d is None:
|
||
continue # 该 seq 未被 ECAT 侧采到,跳过
|
||
matched += 1
|
||
if d != payload:
|
||
mism += 1
|
||
bad.append((round(time.time() - t0, 2), node, seq, d.hex(), payload.hex()))
|
||
print(f"轮询 ECAT {polls} 次 / WiFi {wpolls} 次 / {DUR_S:.0f}s")
|
||
print(f"两侧同 seq 可比对 = {matched} 逐字节不一致 = {mism}")
|
||
for t, node, seq, e, w in bad[:6]:
|
||
j = next((i for i in range(48) if e[i] != w[i]), 48)
|
||
print(f" t={t}s node={node} seq={seq} 首字节差={j}")
|
||
print(f" ECAT={e}")
|
||
print(f" WiFi={w}")
|
||
if not matched:
|
||
print("功效不足:ECAT 帧率远高于 WiFi 采样,同 seq 交集为 0,本次判定不作数。")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|