Files
DCU_DesktopController/scripts/probe_ecat_slot_atomicity.py
T

137 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""ECAT 段2 撕裂 A/B 判据(修复前后的唯一对照指标)。
主判据(零假阳性,直接来自写侧契约):
固件 MotorSlot 的 seq 与 data 属同一次更新,seq 先自增、data 后 memcpy。
⇒ 同一 seq 在总线上只能对应唯一一份 48B payload。
若同一 seq 先后被读到两份不同 payload,只可能是消费者(PDI_Isr 的 buildInput)
在生产者 memcpy 中途取走了半成品 —— 即撕裂(含"seq 新、data 整体旧"的错拍型)。
1.6.5 的 seqlock 把整个更新段(seq 自增 + data 拷贝)都盖进奇数区间,
故修复后该不变量必须零例外。
功效条件:只有当该节点的 seq 在相邻两拍之间不前进时,同一 seq 才可能被读两次。
所以脚本同时报"同 seq 重复读"的覆盖率——重复率低则本次判定功效不足,需换工况重跑。
旁判据(与速率无关):字段值的"跳一下又回来"(median 偏离 >8× 局部抖动带且下一帧
回到带内)——真撕裂必然表现为这种形态。
"""
import json
import os
import socket
import statistics
import struct
import sys
import time
HOST = "127.0.0.1"
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8090
DUR_S = float(sys.argv[2]) if len(sys.argv) > 2 else 150.0
LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ecat_torn_hits.jsonl")
FLOATS = (("rtAngle", 13), ("rtVelocity", 17), ("rtCurrent", 21), ("testFloat", 25))
I16 = (("mcuTemp", 0), ("motTemp", 2), ("uCur", 4), ("vCur", 6), ("wCur", 8))
def 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"))
REQ = (f"POST /api/motor/state HTTP/1.0\r\nHost: {HOST}:{PORT}\r\n"
f"Content-Type: application/json\r\nContent-Length: 2\r\n"
"Connection: close\r\n\r\n{{}}").encode("ascii")
def main():
# per node: 当前 seq 的首见/末见 payload、上一 seq 的确定 payload、字段历史
st = {}
viol, spikes, polls, err = [], [], 0, 0
cov = 0
t0 = time.time()
lf = open(LOG, "w", encoding="utf-8")
while time.time() - t0 < DUR_S:
try:
snap = once()
except Exception:
err += 1
continue
polls += 1
for m in snap.get("motors") or []:
if not m.get("online") or not m.get("data"):
continue
nd, seq = m["node"], m["seq"]
d = bytes.fromhex(m["data"])
e = st.setdefault(nd, {"cur": None, "first": None, "last": None,
"done": None, "hist": {}, "seen": 0})
if e["cur"] == seq:
cov += 1 # 一次"同一 seq 被读第二遍"的判定机会
if e["first"] != d:
rec = {"t": round(time.time() - t0, 2), "node": nd, "seq": seq,
"first": e["first"].hex(), "later": d.hex(),
"prev_seq_data": (e["done"].hex() if e["done"] else None)}
# 分类:整体等于上一帧 = 错拍型;前缀/后缀混合 = 真混合
a, b, p = e["first"], d, e["done"]
rec["cls"] = ("STALE(错拍)" if (p is not None and a == p) else
"MATCH" if a == b else "VIOLATION")
if p is not None and a != p:
n = min(len(a), len(b), len(p))
rec["mix_j"] = next((j for j in range(n + 1)
if a[:j] == b[:j] and a[j:n] == p[j:n]), None)
viol.append(rec)
lf.write(json.dumps(rec, ensure_ascii=False) + "\n")
lf.flush()
e["last"] = d
else:
# seq 前进:上一 seq 的末见即其确定内容
if e["cur"] is not None and e["last"] is not None:
e["done"] = e["last"]
if len(e["last"]) >= 29: # 覆盖到 testFloat 末字节才做形态判据
for name, off in FLOATS:
e["hist"].setdefault(name, []).append(
struct.unpack_from("<f", e["last"], off)[0])
if len(e["hist"][name]) > 400:
e["hist"][name].pop(0)
# 形态判据:本帧各字段是否"跳出带又回来"需要下一帧,留到下次比较
for name, off in FLOATS:
h = e["hist"].get(name) or []
if len(h) >= 4:
a, b2 = h[-3], h[-2] # 上上帧 / 上一帧(刚确定)
# 检查上一帧相对其邻居是否孤立尖峰(其前其后由 hist 提供)
if len(h) >= 4:
pre, post = h[-4], h[-1]
band = max(abs(b2 - pre), abs(post - b2), 1e-6)
if abs(b2 - (pre + post) / 2) > 20 * band:
spikes.append({"t": round(time.time() - t0, 2),
"node": nd, "field": name,
"pre": pre, "spike": b2, "post": post})
e["cur"], e["first"], e["last"], e["seen"] = seq, d, d, 0
lf.close()
print(f"轮询 {polls} 次 / {DUR_S:.0f}s({polls/DUR_S:.0f} 次/s,连接错误 {err})")
print(f"判定功效:同一 seq 被读第二遍的机会 = {cov} 次")
print(f"主判据违规(同一 seq 出现两份不同 48B)= {len(viol)}")
for r in viol[:6]:
print(" ", r["cls"], f"t={r['t']}s node={r['node']} seq={r['seq']} mix_j={r.get('mix_j')}")
print(f" first={r['first'][:64]}")
print(f" later={r['later'][:64]}")
print(f"旁判据(孤立尖峰,形态学,可能有真突变混入)= {len(spikes)}")
for s in spikes[:4]:
print(f" t={s['t']}s node={s['node']} {s['field']}: {s['pre']:.6g} -> {s['spike']:.6g} -> {s['post']:.6g}")
print(f"明细: {LOG}")
if __name__ == "__main__":
main()