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:
+9
-13
@@ -12,7 +12,10 @@
|
||||
* 复用的函数(通过 window.* 引用):
|
||||
* window.getBaseUrl / window.updateSystemStatusUi / window.formatPercent
|
||||
*/
|
||||
import { formatBytes, formatTime, formatUptime, escapeHtml } from './utils.js';
|
||||
import {
|
||||
formatBytes, formatTime, formatUptime, escapeHtml,
|
||||
setTextIfChanged, setClassIfChanged, setQuietMode,
|
||||
} from './utils.js';
|
||||
import { addLog } from './log.js';
|
||||
import { showToast } from './toast.js';
|
||||
import {
|
||||
@@ -20,6 +23,10 @@ import {
|
||||
} from './heap-low-history.js';
|
||||
import { deviceLink } from './device-link.js';
|
||||
|
||||
/* R7:setQuietMode / setTextIfChanged / setClassIfChanged 全仓唯一实现在 utils.js。
|
||||
* 此处 re-export setQuietMode,保持既有 `import { setQuietMode } from './connection.js'` 可用。 */
|
||||
export { setQuietMode };
|
||||
|
||||
// ==================== 渲染缓存(性能) ====================
|
||||
// /system/status 每 1s(中继期 400ms)刷一次 UI。此前每轮全量 innerHTML 重建
|
||||
// CPU/任务栈列表 + 无条件写几十个节点,引发周期性样式重算/重排/重绘掉帧。
|
||||
@@ -40,12 +47,6 @@ function setTextById(id, value) {
|
||||
var v = String(value);
|
||||
if (el.textContent !== v) el.textContent = v;
|
||||
}
|
||||
function setClassIfChanged(el, cls) {
|
||||
if (el && el.className !== cls) el.className = cls;
|
||||
}
|
||||
function setTextIfChanged(el, txt) {
|
||||
if (el && el.textContent !== txt) el.textContent = txt;
|
||||
}
|
||||
/** 写前比较的宽度更新:用元素上的 _lastW 缓存避免读回 style 字符串。 */
|
||||
function setWidthIfChanged(el, widthStr) {
|
||||
if (el && el._lastW !== widthStr) {
|
||||
@@ -59,13 +60,8 @@ function setWidthIfChanged(el, widthStr) {
|
||||
* 无设备静默态:设备离线且没有传输在跑时,给 <html> 挂 data-dcu-calm="1",
|
||||
* 由 responsive.css 统一掐掉常驻动画。160Hz 屏上这些动效每帧触发重算+重绘,
|
||||
* 实测未接设备时 renderer+GPU 各占约一核的七成。
|
||||
* (实现在 js/common/utils.js,见文件头注释。)
|
||||
*/
|
||||
export function setQuietMode(quiet) {
|
||||
var v = quiet ? '1' : '0';
|
||||
if (document.documentElement.dataset.dcuCalm !== v) {
|
||||
document.documentElement.dataset.dcuCalm = v;
|
||||
}
|
||||
}
|
||||
|
||||
/** 本轮是否有传输/OTA 在跑(此时即使设备短暂不响应也要保留动效)。 */
|
||||
function transferOngoing() {
|
||||
|
||||
+24
-56
@@ -18,6 +18,10 @@
|
||||
import {
|
||||
getDeviceIp, saveDeviceIp, getBaseUrl as baseUrlFromIp
|
||||
} from "./base-url.js";
|
||||
import {
|
||||
runEcatBootstrap, repairEcatDc, getEcatAdapterOrDefault,
|
||||
ECAT_DEFAULT_ADAPTER,
|
||||
} from "./ecat-bus.js";
|
||||
|
||||
var CH_NAME = "dcu-device-link";
|
||||
var ESP32_FAIL_ONLINE = 3; /* 连续失败 ≥3 判离线 */
|
||||
@@ -28,7 +32,8 @@ var ecatDcGuardBusy = false; /* DC/周期自愈 in-flight */
|
||||
var LS_ECAT_PERIOD = "dcu_ecat_period_ms";
|
||||
var LS_ECAT_SPIN = "dcu_ecat_spin";
|
||||
var LS_ECAT_ADAPTER = "dcu_ecat_adapter";
|
||||
var DEFAULT_ECAT_ADAPTER = "I210";
|
||||
/* R6:网卡默认常量只在 ecat-bus.js 定义;此处 re-export 保持旧 import 路径可用 */
|
||||
var DEFAULT_ECAT_ADAPTER = ECAT_DEFAULT_ADAPTER;
|
||||
|
||||
function nowMs() { return Date.now(); }
|
||||
|
||||
@@ -45,7 +50,7 @@ function readAdapter() {
|
||||
var s = localStorage.getItem(LS_ECAT_ADAPTER);
|
||||
if (s && s.trim()) return s.trim();
|
||||
} catch (_) {}
|
||||
return DEFAULT_ECAT_ADAPTER;
|
||||
return getEcatAdapterOrDefault();
|
||||
}
|
||||
|
||||
function writeAdapter(substr) {
|
||||
@@ -165,13 +170,8 @@ function createOwner() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 开总线:health →(未就绪则)status →(未开则)open 网卡 → motor/start 进 OP + 起 PDO 循环。
|
||||
* 与 motor/transport-ecat ensureOpen/ensureStarted 同源,由外壳 Owner 在
|
||||
* 控制台启动时强制执行,用户不必先去电机页把链路切到 EtherCAT。
|
||||
*
|
||||
* 已健康(bus_open + loop)时**直接复用**,不打 motor/start:
|
||||
* 那条路由会先 loop_stop 再重进 OP,和电机页探测撞车时会假超时
|
||||
* (横幅 signal timed out)并平白增加 SSC 楔死风险。
|
||||
* 开总线:R6 —— 策略只在 js/common/ecat-bus.js(health→open→motor/start,
|
||||
* DC 250µs,已健康则复用)。外壳 Owner 启动时强制执行,用户不必先去电机页。
|
||||
*/
|
||||
function bootstrapEcatBus() {
|
||||
if (stopped || ecatBusBusy) return;
|
||||
@@ -182,47 +182,22 @@ function createOwner() {
|
||||
ecatBusNextTryAt = now + ECAT_BUS_RETRY_MS;
|
||||
|
||||
var adapter = readAdapter();
|
||||
var body = {};
|
||||
var period = null;
|
||||
var spin = null;
|
||||
try {
|
||||
var v = parseFloat(localStorage.getItem(LS_ECAT_PERIOD));
|
||||
/* 无显式值 / 值过慢 → 固定 0.25ms。1ms+DC 会让发送出口回到 ~1s。 */
|
||||
if (v > 0 && v <= 0.5) body.period_ms = v;
|
||||
else body.period_ms = 0.25;
|
||||
period = parseFloat(localStorage.getItem(LS_ECAT_PERIOD));
|
||||
var sp = localStorage.getItem(LS_ECAT_SPIN);
|
||||
if (sp === "0") body.spin = false;
|
||||
else if (sp === "1") body.spin = true;
|
||||
} catch (_) { body.period_ms = 0.25; }
|
||||
/* EtherCAT 控制必须 DC SYNC0≈PDO 周期,且周期应保持 250µs 档 */
|
||||
{
|
||||
var per = Number(body.period_ms) || 0.25;
|
||||
if (per > 0.5) per = 0.25;
|
||||
body.period_ms = per;
|
||||
var us = Math.round(per * 1000);
|
||||
if (us < 250) us = 250;
|
||||
body.dc_cycle_us = us;
|
||||
body.dc_shift_us = 0;
|
||||
}
|
||||
if (sp === "0") spin = false;
|
||||
else if (sp === "1") spin = true;
|
||||
} catch (_) {}
|
||||
|
||||
ecatPost("native/health", {}, 3000).then(function (h) {
|
||||
if (h && h.ok && h.bus_open && h.loop) {
|
||||
ecatBusReady = true;
|
||||
return null; /* 已在跑:跳过 open + motor/start */
|
||||
}
|
||||
return ecatPost("status", {}, 6000).then(function (st) {
|
||||
var open = !!(st && st.ok);
|
||||
return open ? Promise.resolve(st)
|
||||
: ecatPost("open", { substr: adapter }, 20000);
|
||||
}).then(function (r) {
|
||||
if (r && r.ok === false) {
|
||||
throw new Error((r.log && r.log.join(" / ")) || "open 失败");
|
||||
}
|
||||
return ecatPost("motor/start", body, 30000);
|
||||
});
|
||||
runEcatBootstrap({
|
||||
ecatPost: ecatPost,
|
||||
adapter: adapter,
|
||||
period_ms: period,
|
||||
spin: spin,
|
||||
}).then(function (r) {
|
||||
if (r && r.ok === false) {
|
||||
throw new Error((r.log && r.log.join(" / ")) || "motor/start 失败");
|
||||
}
|
||||
if (r) ecatBusReady = true;
|
||||
if (r && r.ok) ecatBusReady = true;
|
||||
}).catch(function () {
|
||||
/* 网线没插/从站未上电:退避后由 pollEcat 再触发 */
|
||||
}).then(function () {
|
||||
@@ -250,18 +225,10 @@ function createOwner() {
|
||||
if (e.busOpen && e.loop) ecatBusReady = true;
|
||||
else if (ecatBusReady && !e.busOpen) ecatBusReady = false; /* 总线掉了 → 允许重开 */
|
||||
publish();
|
||||
/* 自愈:DC/周期被配错时发送出口秒级 —— 2s 轮询里拉回 250µs */
|
||||
/* 自愈:DC/周期被配错时发送出口秒级 —— R6 单点 repairEcatDc */
|
||||
if (e.busOpen && e.loop && !ecatDcGuardBusy) {
|
||||
ecatDcGuardBusy = true;
|
||||
ecatPost("native/health", {}, 2000).then(function (h) {
|
||||
if (!h || !h.ok) return null;
|
||||
var dcs = Number(h.dc_cycle_us);
|
||||
var per = Number(h.period_ms);
|
||||
if (h.dc === 1 && dcs === 250 && (h.period_ms == null || Math.abs(per - 0.25) < 0.001)) return null;
|
||||
return ecatPost("motor/start", {
|
||||
period_ms: 0.25, dc_cycle_us: 250, dc_shift_us: 0
|
||||
}, 30000);
|
||||
}).catch(function () {}).then(function () {
|
||||
repairEcatDc(ecatPost).catch(function () {}).then(function () {
|
||||
ecatDcGuardBusy = false;
|
||||
});
|
||||
}
|
||||
@@ -410,4 +377,5 @@ try {
|
||||
|
||||
deviceLink = createClient();
|
||||
export { deviceLink, ecatPost, readAdapter as getEcatAdapter, writeAdapter as setEcatAdapter };
|
||||
export { ECAT_DEFAULT_ADAPTER };
|
||||
export default deviceLink;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* toast-stack.js — 电机控制台堆叠 toast(#toastContainer)
|
||||
*
|
||||
* motor 页不用 common/toast.js 的单 toast 条,而是左下角堆叠条。
|
||||
* setQuietMode 从 utils.js re-export,保证全仓只有一份实现。
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
import { setQuietMode } from "./utils.js";
|
||||
|
||||
export { setQuietMode };
|
||||
|
||||
/** @description 创建电机页 toast 工厂。默认容器 #toastContainer。 */
|
||||
export function createToastStack(containerSelector = "#toastContainer") {
|
||||
const $ = (s) => document.querySelector(s);
|
||||
return function toast(msg, type = "ok") {
|
||||
const host = $(containerSelector);
|
||||
if (!host) return;
|
||||
const el = document.createElement("div");
|
||||
el.className = `toast ${type}`;
|
||||
const ledCls = type === "ok" ? "led-on" : type === "warn" ? "led-warn" : "led-err";
|
||||
el.innerHTML = `<span class="led ${ledCls}" style="width:7px;height:7px"></span><span></span>`;
|
||||
el.lastElementChild.textContent = msg;
|
||||
host.appendChild(el);
|
||||
setTimeout(() => { el.classList.add("out"); setTimeout(() => el.remove(), 220); }, 3000);
|
||||
};
|
||||
}
|
||||
@@ -75,3 +75,33 @@ export function flashBusyNow() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @description 写前比较的 textContent 更新:值未变则不碰 DOM。 */
|
||||
export function setTextIfChanged(el, txt) {
|
||||
if (el && el.textContent !== txt) el.textContent = txt;
|
||||
}
|
||||
/** @description 写前比较的 className 更新。 */
|
||||
export function setClassIfChanged(el, cls) {
|
||||
if (el && el.className !== cls) el.className = cls;
|
||||
}
|
||||
/** @description 写前比较的 style[prop] 更新。 */
|
||||
export function setStyleIfChanged(el, prop, v) {
|
||||
if (el && el.style && el.style[prop] !== v) el.style[prop] = v;
|
||||
}
|
||||
/** @description classList.toggle 前比较,避免无谓样式失效。 */
|
||||
export function toggleClsIfChanged(el, cls, on) {
|
||||
if (!el) return;
|
||||
var has = el.classList.contains(cls);
|
||||
if (has !== !!on) el.classList.toggle(cls, !!on);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 无设备静默态:给 <html> 挂 data-dcu-calm,由 responsive.css 掐动画。
|
||||
* 全仓唯一实现(connection.js / motor main 均 import 此处)。
|
||||
*/
|
||||
export function setQuietMode(quiet) {
|
||||
var v = quiet ? '1' : '0';
|
||||
if (document.documentElement.dataset.dcuCalm !== v) {
|
||||
document.documentElement.dataset.dcuCalm = v;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user