feat(motor): 新增 EtherCAT 周期速率扫描分析工具链,支持 sweep 脚本、指标校准、绘图与 HTML 报告生成

This commit is contained in:
cbb
2026-09-10 03:02:14 +08:00
parent 4e018af168
commit 81096d0425
20 changed files with 4544 additions and 219 deletions
+51 -4
View File
@@ -62,6 +62,14 @@ lock = threading.Lock()
stats = {"sends": 0, "send_err": 0, "t0": time.time()}
torn_events = [] # (t, node, dlc, data48hex, cfg_hex)
batch_events = []
fp_events = [] # 过滤后仍残余的指纹违规
fp_total = [0] # (弃用,保留兼容)
cfg_total = [0] # 监控实际消费的 CONFIG 帧总数(活性自证,非 0 才有资格谈违规=0)
fp_after = [0] # 过滤器放行掉的违规(应为 0)
fp_dropped = [0] # 过滤器正确丢弃的违规
fp_falsekill = [0] # 过滤器误杀的真帧(应为 0)
cfg_seen = {} # node -> 最近 4 个已接受 CONFIG hex(页面过滤器模拟)
cfg_susp = {} # node -> 连续疑似计数
torn_base = [0]
@@ -86,12 +94,14 @@ def sender():
except Exception:
time.sleep(0.05)
continue
if len(cfgs) >= 2:
# 判别实验:两电机反相正弦 + 不同速度限,给帧内容打节点指纹。
# node2: ang=1000+200sin, vel=200 node3: ang=1000-200sin, vel=150
if len(cfgs) >= 2 or time.time() - stats["t0"] > 3:
# 电机偶尔暂停 CONFIG 自报(如正弦轰炸后):拿不到回声就用页面默认
# 限压 24.0V(u16 LE 0x5DC0 → bytes c0 5d)兜底,保证指令压力不断。
fallback = bytes([0x03, 0xc0, 0x5d]) + bytes(21)
s = math.sin(2.0 * math.pi * (time.time() - stats["t0"]) / 10.0)
frames = []
for n, c in sorted(cfgs.items()):
for n in (2, 3):
c = cfgs.get(n, fallback)
ang = 1000.0 + (200.0 * s if n == 2 else -200.0 * s)
vel = 200.0 if n == 2 else 150.0
frames.append({"ch": 2, "id": hex((n << 16) | 0x0200), "dlc": 32,
@@ -162,11 +172,42 @@ def monitor():
node = (fid >> 16) & 0xFF
if (fid & 0xFFFF) == 0x0101:
cfgs[node] = d
cfg_total[0] += 1 # 活性自证:本拍监控实际看到的 CONFIG 帧数
if any(d[15:24]):
with lock:
if len(batch_events) < 40:
batch_events.append((round(time.time() - stats["t0"], 2),
node, d.hex()))
# CONFIG↔CONFIG 拼接:结构校验抓不到,用速度指纹暴露。
# 预热 3s:sender 尚未生效时电机回显的是页面/上一轮的 vel,
# 不是拼接,不能算违规。
if len(d) >= 15 and time.time() - stats["t0"] > 3.0:
vel = struct.unpack_from("<f", d, 7)[0]
exp_vel = 200.0 if node == 2 else 150.0
viol = abs(vel - exp_vel) > 1.0
with lock:
# 模拟页面 CONFIG 跨节点拼接过滤器(transport-ecat.js
# dispatchFrame 同逻辑),统计过滤后残余违规与误杀。
hx = d.hex()
seen = cfg_seen.setdefault(node, [])
own = hx in seen
foreign = (not own) and any(
hx in s2 for n2, s2 in cfg_seen.items() if n2 != node)
if own or not foreign or cfg_susp.get(node, 0) >= 5:
cfg_susp[node] = 0
seen.insert(0, hx)
del seen[4:]
if viol:
fp_after[0] += 1
if len(fp_events) < 40:
fp_events.append((round(time.time() - stats["t0"], 2),
node, vel, exp_vel, hx))
else:
cfg_susp[node] = cfg_susp.get(node, 0) + 1
if viol:
fp_dropped[0] += 1
else:
fp_falsekill[0] += 1
off += 8 + w
@@ -198,6 +239,12 @@ def main():
print(f"段4 污染事件: {len(batch_events)}(前 8)")
for e in batch_events[:8]:
print(f" t={e[0]}s node={e[1]} data={e[2]}")
print(f"CONFIG↔CONFIG 指纹(预热后): 违规={fp_after[0]+fp_dropped[0]} "
f"过滤器正确丢弃={fp_dropped[0]} 过滤后残余={fp_after[0]} "
f"误杀真帧={fp_falsekill[0]}(样本 {len(fp_events)})")
print(f"监控消费的 CONFIG 帧总数: {cfg_total[0]}(活性自证:>0 才说明违规=0 是真结果)")
for e in fp_events[:8]:
print(f" t={e[0]}s node={e[1]} vel={e[2]:.1f} 应为{e[3]:.0f} data={e[4]}")
if __name__ == "__main__":