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()
|