feat(motor): 新增 EtherCAT 周期速率扫描分析工具链,支持 sweep 脚本、指标校准、绘图与 HTML 报告生成
This commit is contained in:
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user