refactor(motor): P1 拆分 main.js + 收敛连接/总线状态机

R4 外提 scope/ota/seq/conn-poll;R5 waitVersion/queryNodeIdParam 下沉
transport;R6 ecat-bus 单点 bootstrap;R7 toast-stack + utils 去重。
main.js 4647→~1894 行。?v= 已 bump 20260924a。
This commit is contained in:
cbb
2026-09-21 02:26:39 +08:00
parent b70f5ed0d6
commit 261caf50b7
14 changed files with 3470 additions and 3523 deletions
+184
View File
@@ -0,0 +1,184 @@
/**
* ecat-bus.js — EtherCAT 总线 bootstrap / open / motor-start 唯一实现
*
* 设备链路(device-link 外壳 Owner)与电机传输(transport-ecat)都调用本模块,
* 策略只保留一份:
* health →(未就绪)status →(未开)open 网卡 → motor/start 进 OP + 起 PDO 循环
*
* 红线:
* - DC 钉死 250µs(period_ms 默认/钳制 0.25,dc_cycle_us = max(250, round(ms*1000)))
* - 幂等:native/health 报 bus_open+loop 时**复用**,不再 motor/start
* (start 会先 loop_stop,热路径假超时 / SSC 楔死风险)
* - 网卡默认常量 ECAT_DEFAULT_ADAPTER 只在此定义
*/
"use strict";
/** 网卡匹配子串默认值(代码/页面/测试的唯一来源;文档漂移以此为准)。 */
export const ECAT_DEFAULT_ADAPTER = "I210";
/** 默认 PDO 周期(ms):1ms+DC 会让发送出口回到 ~1s,必须 250µs 档。 */
export const ECAT_DEFAULT_PERIOD_MS = 0.25;
/** DC SYNC0 目标周期(µs)。 */
export const ECAT_DC_CYCLE_US = 250;
const LS_ECAT_PERIOD = "dcu_ecat_period_ms";
const LS_ECAT_SPIN = "dcu_ecat_spin";
const LS_ECAT_ADAPTER = "dcu_ecat_adapter";
const LS_PERIOD_MIG = "dcu_ecat_period_mig";
function readAdapterRaw() {
try {
var s = localStorage.getItem(LS_ECAT_ADAPTER);
if (s && s.trim()) return s.trim();
} catch (_) {}
return ECAT_DEFAULT_ADAPTER;
}
/**
* 构造 motor/start body:period 钳到 ≤0.5ms 且默认 0.25,DC 钉 250µs。
* @param {{period_ms?:number, spin?:boolean|null, fromLocalStorage?:boolean}} [opts]
*/
export function resolveEcatStartBody(opts) {
opts = opts || {};
var body = {};
var per = Number(opts.period_ms);
if (!opts.fromLocalStorage && opts.period_ms == null) {
per = ECAT_DEFAULT_PERIOD_MS;
} else if (!(per > 0) || per > 0.5) {
per = ECAT_DEFAULT_PERIOD_MS;
}
body.period_ms = per;
body.dc_cycle_us = Math.max(ECAT_DC_CYCLE_US, Math.round(per * 1000));
body.dc_shift_us = 0;
var spin = opts.spin;
if (spin == null && opts.fromLocalStorage) {
try {
var sp = localStorage.getItem(LS_ECAT_SPIN);
if (sp === "0") spin = false;
else if (sp === "1") spin = true;
} catch (_) {}
}
if (spin === true || spin === false) body.spin = spin;
return body;
}
/** 从 localStorage 解析 period;一次性清理 Python 时代误存的 "4"。 */
export function readStoredEcatPeriod() {
try {
if (!localStorage.getItem(LS_PERIOD_MIG)) {
if (parseFloat(localStorage.getItem(LS_ECAT_PERIOD)) === 4) {
localStorage.removeItem(LS_ECAT_PERIOD);
}
localStorage.setItem(LS_PERIOD_MIG, "1");
}
var v = parseFloat(localStorage.getItem(LS_ECAT_PERIOD));
/* 用户显式设过才用;否则 0.25ms */
if (v > 0 && v !== 1) return v;
return ECAT_DEFAULT_PERIOD_MS;
} catch (_) {
return ECAT_DEFAULT_PERIOD_MS;
}
}
/** @description 只读健康:bus_open + loop = 后台 PDO 循环在跑。 */
export async function isEcatBusHealthy(ecatPost) {
try {
var h = await ecatPost("native/health", {}, 2500);
return !!(h && h.ok && h.bus_open && h.loop);
} catch (_) {
return false;
}
}
/**
* 确保网卡已打开。已连接则复用,不重复 open。
* @param {(path:string,body?:object,timeoutMs?:number)=>Promise<any>} ecatPost
* @param {string} [adapter]
*/
export async function openEcatAdapter(ecatPost, adapter) {
var substr = (adapter && String(adapter).trim()) || readAdapterRaw() || ECAT_DEFAULT_ADAPTER;
var st = await ecatPost("status", {}, 6000);
if (st && st.ok === true) return st;
var r = await ecatPost("open", { substr: substr }, 20000);
if (!r || r.ok !== true) {
var msg = (r && r.log && r.log.length) ? r.log.join(" / ") : "打开网卡失败";
var err = new Error(msg);
err.transient = false;
throw err;
}
return r;
}
/**
* 启动 OP + PDO 循环。force=false 且总线已健康则跳过 start。
* @returns {Promise<{reused:boolean, result?:object}>}
*/
export async function startEcatMotorLoop(ecatPost, opts) {
opts = opts || {};
if (!opts.force && await isEcatBusHealthy(ecatPost)) {
return { reused: true };
}
var body;
if (opts.body) body = opts.body;
else if (opts.fromLocalStorage) {
body = resolveEcatStartBody({ fromLocalStorage: true, period_ms: readStoredEcatPeriod() });
} else {
body = resolveEcatStartBody({ period_ms: opts.period_ms, spin: opts.spin });
}
var r = await ecatPost("motor/start", body, 30000);
if (r && r.ok === false) {
throw new Error((r.log && r.log.join(" / ")) || "motor/start 失败");
}
return { reused: false, result: r || {}, body: body };
}
/**
* 完整 bootstrap:health → open → motor/start。
* @param {{
* ecatPost: (path:string,body?:object,timeoutMs?:number)=>Promise<any>,
* adapter?: string, period_ms?: number, spin?: boolean|null, force?: boolean,
* fromLocalStorage?: boolean,
* }} opts
* @returns {Promise<{ok:true, reused:boolean, result?:object}>}
*/
export async function runEcatBootstrap(opts) {
var ecatPost = opts.ecatPost;
if (await isEcatBusHealthy(ecatPost) && !opts.force) {
return { ok: true, reused: true };
}
await openEcatAdapter(ecatPost, opts.adapter);
var st = await startEcatMotorLoop(ecatPost, {
force: true,
period_ms: opts.period_ms,
spin: opts.spin,
fromLocalStorage: opts.fromLocalStorage,
body: opts.body,
});
return { ok: true, reused: false, result: st.result };
}
/**
* DC/周期自愈:被配错(free-run 或 ≥1ms)时拉回 250µs。
* @returns {Promise<object|null>} null=无需修复
*/
export async function repairEcatDc(ecatPost) {
var h = await ecatPost("native/health", {}, 2000).catch(function () { return null; });
if (!h || !h.ok) return null;
var dcs = Number(h.dc_cycle_us);
var per = Number(h.period_ms);
if (h.dc === 1 && dcs === ECAT_DC_CYCLE_US &&
(h.period_ms == null || Math.abs(per - ECAT_DEFAULT_PERIOD_MS) < 0.001)) {
return null;
}
return ecatPost("motor/start", {
period_ms: ECAT_DEFAULT_PERIOD_MS,
dc_cycle_us: ECAT_DC_CYCLE_US,
dc_shift_us: 0,
}, 30000);
}
/** @description 读当前网卡子串(localStorage → 默认 I210)。 */
export function getEcatAdapterOrDefault() {
return readAdapterRaw();
}