137 lines
6.4 KiB
Python
137 lines
6.4 KiB
Python
#!/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()
|