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 造成的「重启那刻陈旧命令一起灌出去」。
This commit is contained in:
+16
-10
@@ -42,6 +42,9 @@
|
||||
* 「ECAT 槽判活阈值」一节,纯函数以便脱离 DOM 自测。 */
|
||||
import { staleLimitMs } from "./refresh-rate.js";
|
||||
import { deviceLink } from "../common/device-link.js";
|
||||
/* 总线"能不能动"的判据只有一份,放在 ecat-bus.js(与 device-link 同源、同样不带
|
||||
* ?v= ⇒ 浏览器按同一 URL 去重,Owner 与本模块共用同一个模块实例与同一份预算)。 */
|
||||
import { proveEcatLoop, repairEcatDc, noteEcatLoopUp } from "../common/ecat-bus.js";
|
||||
import {
|
||||
desktopEcatPost, desktopBridgeLabel, getDesktopEcatApi,
|
||||
desktopEcatSendRaw, hasDesktopFastSend,
|
||||
@@ -334,14 +337,19 @@ export function createEcatTransport(ctx) {
|
||||
op: !!h.op,
|
||||
loop: !!h.loop,
|
||||
cycles: Number(h.cycles) || 0,
|
||||
/* 原始回包:证明函数与界面要读 dc/period_ms/guard/recovering/pm_hold 这些
|
||||
* 只存在于 native 侧的字段。再抄一遍字段名 = 迟早与 native 漂移,
|
||||
* 而漂移的方向通常是"页面以为一切正常"。 */
|
||||
raw: h,
|
||||
};
|
||||
}
|
||||
|
||||
/** 总线已开 + PDO 循环在跑 → 视为链路就绪,跳过 motor/start。 */
|
||||
/** 总线已开 + PDO 循环在跑 → 视为链路就绪,跳过 motor/start。
|
||||
* 判据是 ecat-bus.proveEcatLoop 的 'up'(三态里只有 up 算健康)。 */
|
||||
async function isBusHealthy() {
|
||||
try {
|
||||
const h = await readHealth();
|
||||
return !!(h && h.busOpen && h.loop);
|
||||
return proveEcatLoop(h && h.raw) === "up";
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
@@ -614,20 +622,18 @@ export function createEcatTransport(ctx) {
|
||||
const h = await readHealth();
|
||||
if (!h || !h.busOpen) throw new Error("EtherCAT 服务未打开网卡");
|
||||
if (!h.loop) throw new Error("PDO 循环未运行");
|
||||
/* 自愈:DC/周期被配错(free-run 或 ≥1ms)时发送出口会到秒级 —— 拉回 250µs */
|
||||
/* DC 自愈只能走 ecat-bus.repairEcatDc:它"证明不了配错就不动总线"。
|
||||
* 以前这里是内联的 dc !== 1 || dcs !== 250 —— 把"回包没有 dc 字段"当成
|
||||
* "DC 配错",于是一篇错误回包/一份旧 exe 就打出一记 loop_stop 的
|
||||
* motor/start,而这条路径恰好只在 probe() 超时(已有一记 loop_stop 在飞)后走。 */
|
||||
try {
|
||||
const raw = await api("native/health", {}, 2000);
|
||||
const dc = raw && raw.dc;
|
||||
const dcs = raw && raw.dc_cycle_us;
|
||||
const per = raw && raw.period_ms;
|
||||
if (dc !== 1 || dcs !== 250 || (per != null && Math.abs(per - 0.25) > 0.001)) {
|
||||
await api("motor/start", { period_ms: 0.25, dc_cycle_us: 250, dc_shift_us: 0 }, 30000);
|
||||
}
|
||||
await repairEcatDc(api, h.raw);
|
||||
} catch (_) { /* 自愈失败仍按 health 结果返回 */ }
|
||||
const st = await api("status", {}, 2500);
|
||||
if (!st || st.ok !== true) throw new Error("从站状态不可读");
|
||||
if (st && typeof st === "object") st.__op = !!h.op;
|
||||
started = true;
|
||||
noteEcatLoopUp(); /* 走到这里 health 已证明 loop=1:把自动重启预算交还给下一次故障 */
|
||||
try {
|
||||
const snap = await api("motor/state", {}, 2500);
|
||||
if (snap && snap.ok === true) syncFromSnapshot(snap);
|
||||
|
||||
Reference in New Issue
Block a user