Files
DCU_DesktopController/scripts/probe_ecat_slot_atomicity.py
T

137 lines
6.4 KiB
Python
Raw Normal View History

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