1 Commits
Author SHA1 Message Date
cbb 514070b36d fix(ui): 「谁可以动总线」收到一处判定;healthCheck 与 OTA 页不再自己 loop_stop
同一症状(点一次发送→很久才动/不动)至少有五条路径能到达,上一轮只加固了
native 检测层那一条的一半。本 commit 关掉两条由我们自己的前端反复按 loop_stop
的成因——不需要硬件故障就能把健康总线掐停:motor/start 内部先 loop_stop,停手
>1000ms 从站过程数据看门狗就踢出 OP,反复这样会把 SSC 邮箱楔到只能断电。

1) healthCheck 违反自己的契约。文件注释写着「只读…绝不 motor/start、不动总线」,
   代码却是内联的 dc !== 1 || dcs !== 250 → motor/start。这把「回包没有 dc 字段」
   当成「DC 配错」,于是一篇错误回包/一份旧 exe 就打出一记 loop_stop,而这条路径
   只在 probe() 超时后走(那一刻已经有一记 loop_stop 在飞)。
   git 史:断言 383535b 引入即红 → 2304ed6 改走 ecat-bus.repairEcatDc(含「缺字段
   不动总线」守卫)转绿 → 7ea80f0 前端回滚删掉那次调用,又红。
2) OTA 负载页以「读数不新鲜」为理由无条件 motor/start。固件 <1.5.61(段5 恒 0)、
   GD32 没跑 APP、首个 1s 窗口没滚完都会读数不新鲜而循环完全健康——一个跟电机无关
   的页面于是周期性掐停总线。现在先证明再动手;native 完全没起仍算「该拉起」,
   刷写体验不变。

做法:判定与预算只放 ecat-bus.js——
  proveEcatLoop(h)  'up'|'down'|'unknown':三态是重点,证明不了就不许自动动手
  proveEcatDc(h)    同上,把「缺字段=证明不了」收编成唯一实现
  ensureEcatLoopRepair()  in-flight 锁 + 最小间隔 20s(时间戳先写后发,落
  localStorage 才跨标签页/跨 F5)+ 连续 3 次封顶;reason 随 body 走,供代理侧统一上限
repairEcatDc 新增「native 正在全量重连时不动总线」;isEcatBusHealthy 与 proveEcatLoop
共用同一处判定(两个「看起来在跑」迟早漂移)。transport-ecat 的 readHealth 附带原始
回包 raw,证明函数与界面读同一份,不再抄一遍字段名。

测试:test_ecat_autostart.mjs 26→57 项。stub 现在能喂任意 health 形状并按 reason
计数,新增 [2c] 反空断言(DC 确实配错时必须恰又一次且带 reason)、[2d] 三态矩阵、
[2e] OTA 决策、[2f] 预算(间隔/封顶/复位后放行)。另加 scripts/check_fe_guard_mutation.py:
把实现逐条退回旧形状看断言是否真变红(全绿=那条断言是自证的)——M1..M6 实测全部变红,
还原后基线仍 57/0。配套门槛见 tools/ecat/check_guard_regression.py 的 S6/S7。

未做(另计):loop=0 的三重闩锁(transport 的 started 位、transient 正则、device-link
的 e.loop=loadValid||op 假位——native /api/status 的 slave.state 是缓存,循环停了仍报 8)
与 UI 对 loop/guard 的可见性;以及 loop_stop 不清 g_cmd_q 造成的「重启那刻陈旧命令一起灌出去」。
2026-09-22 00:30:33 +08:00