Files
DCU_DesktopController/js/common/ecat-bus.js
T
cbb 261caf50b7 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。
2026-09-21 02:26:39 +08:00

185 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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();
}