Files
DCU_DesktopController/js/ota/ecat_load.js
T
cbb 2304ed6999 refactor(motor+hygiene): P1 拆分 main.js 并收敛总线/toast + P3 卫生清理
R4: main.js 4647→1894 行,外提 scope/ota/seq/conn-poll
R5: waitVersion/queryNodeIdParam 下沉 transport
R6: ecat-bus.js 单点 bootstrap;repairEcatDc 缺 DC 字段不 motor/start
R7: toast-stack + utils 统一 DOM/quiet 助手
R12–R16: scripts 精简至正式清单、报告进 docs/reports、lab dump 清理、
README/CSS 所有权说明、网卡默认 I210 单点
2026-09-21 02:28:26 +08:00

198 lines
7.8 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_load.js - GD32(DCU_APP) CPU 负载率显示(EtherCAT 通道,仅 OTA 中继页使用)
*
* 数据来源:DCU_APP 1.5.61+ 把「中断 + 非延迟处理段」的占用率(1.5.63 收窄定义)写进
* EtherCAT 输入 PDO 段 5 诊断区(1s 滚动窗口,0.1% 分辨率),主站
* tools/ecat/ecat_native.exe 解码后由 /api/app/load 提供纯缓存读。
* 本模块经本服务器的同源代理 /ecat/api/* 调它(代理只收 POST,见 esp32_server
* _ecat_proxy),显示在「运行指标」卡的 #mGd32Load。
*
* 与 /system/status 轮询完全独立:ESP32 离线不影响本显示,反之亦然;
* 失败只静默退避,绝不计入 ESP32 的连接失败计数。
*
* **自动拉起**(2026-09):读到 invalid/过期时,代替用户走完
* 服务→网卡→OP→PDO 循环 四级自愈(与 motor/transport-ecat.js 同一套 API),
* 不必再单独打开电机页选 EtherCAT。避让:
* - ECAT 刷写中(window.bootEcatBusy)或其它 OTA 在跑时**绝不动总线**
* —— open 会 loop_stop、motor/start 会抢 OP,会把正在灌写的会话掐死;
* - 冷却 8s,避免无适配器/无从站时每秒刷 open;
* - 首次成功只打一条日志,之后静默。
* 首页需持有 app 租约(index.html 挂 app-lease.js),否则服务端在
* 租约归零宽限期后会把刚拉起的循环又停掉。
*
* 失效语义(都显示 '--'):
* - valid:false —— ECAT 后端没在跑 / 总线从未打开过 / 固件 < 1.5.61(段 5 恒 0)
* / 循环刚拉起、首个 1s 窗口还没滚完(seq 仍 0);
* - age_ms > 3000 —— PDO 循环已停(没有页面持有 app 租约时 ecat_native 会
* 自动停循环退回 INIT),负载值不再滚动,视为过期;
* - 代理 503 —— ecat_native 没在跑。退避轮询(1s→5s→30s),避免不用 EtherCAT
* 的用户按 1Hz 反复触发 esp32_server 的自动拉起逻辑。
*
* 请求统一走 ConnectionHub(device-link.ecatPost),与 Owner 健康轮询同源。
*/
import { deviceLink } from '../common/device-link.js';
import { ECAT_DEFAULT_ADAPTER } from '../common/ecat-bus.js';
var POLL_MS = 1000; /* 正常轮询节拍(负载窗口 1s 一滚,再快没意义) */
var STALE_MS = 3000; /* 超过 3 个窗口没刷新即视为过期 */
var START_COOLDOWN_MS = 8000; /* 自动 open/start 冷却,防无设备时刷屏 */
var failCount = 0; /* 连续失败/过期次数,驱动退避 */
var timerId = null;
var startAttemptAt = 0; /* 上次尝试自动拉起的时刻 */
var startLogged = false; /* 成功拉起只记一条日志 */
function el() { return document.getElementById('mGd32Load'); }
/* 写前比较:1Hz 轮询里大部分字段不变,无条件写会白白触发样式失效(同 stages.js) */
function setText(node, txt) {
if (node && node.textContent !== txt) node.textContent = txt;
}
function setCls(node, cls) {
if (node && node.className !== cls) node.className = cls;
}
function setTitle(node, title) {
if (node && node.title !== title) node.title = title;
}
var TIP_BASE = 'DCU_APP CPU 负载率:中断+非延迟处理(任务派发/OTA中继)占墙钟比,不含常开轮询与空转;EtherCAT 输入 PDO 段5,1s 窗口';
var TIP_OFF = TIP_BASE + ';当前不可用:外壳/本页会自动 open+motor/start;'
+ '若持续为 --,检查网卡/总线供电,或手动打开电机控制页选 EtherCAT。'
+ 'ECAT 刷写期间会暂停自动拉起。';
/** @description 把 /api/app/load 的 JSON 渲染到 #mGd32Load。返回值是否有效(驱动退避)。 */
function render(d) {
var node = el();
if (!node) return false;
var fresh = !!d && d.valid === true
&& Number.isFinite(d.age_ms) && d.age_ms >= 0 && d.age_ms <= STALE_MS;
if (!fresh) {
setText(node, '--');
setCls(node, 'v');
setTitle(node, TIP_OFF);
return false;
}
var perm = Number(d.load_perm);
if (!Number.isFinite(perm)) {
setText(node, '--');
setCls(node, 'v');
setTitle(node, TIP_OFF);
return false;
}
var pct = perm / 10;
setText(node, pct.toFixed(1) + '%');
/* ≥85% 变琥珀提示余量吃紧;其余用正常强调色 */
setCls(node, pct >= 85 ? 'v warn' : 'v ok');
setTitle(node, TIP_BASE + ';窗口序号 ' + d.seq + ',' + d.age_ms + 'ms 前更新');
return true;
}
function ecatPost(path, body, timeoutMs) {
return fetch('/ecat/api/' + path.replace(/^\/+/, ''), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body || {}),
signal: AbortSignal.timeout(timeoutMs || 8000),
cache: 'no-store',
}).then(function (r) { return r.json(); });
}
function adapterSubstr() {
try {
var s = localStorage.getItem('dcu_ecat_adapter');
if (s && s.trim()) return s.trim();
} catch (_) {}
return ECAT_DEFAULT_ADAPTER;
}
/** 刷写/上传进行中:绝不动总线(open=loop_stop,start=抢 OP)。 */
function busBusy() {
return !!window.bootEcatBusy || !!window.isUploading
|| !!(window.bleOta && window.bleOta.isUploading && window.bleOta.isUploading());
}
/**
* 确保网卡已开 + OP + 后台 PDO 循环在跑。
* 与 motor/transport-ecat.js 的 ensureOpen/ensureStarted 同一套 API,
* 只做布尔成败,不引入 transient/慢请求表(本页没有那套基础设施)。
*/
async function ensurePdoCycle() {
var st = null;
try {
st = await ecatPost('status', {}, 6000);
} catch (_) {
/* 后端未起:下面 open 会触发代理 _ecat_autostart */
}
if (!st || st.ok !== true) {
var o = await ecatPost('open', { substr: adapterSubstr() }, 25000);
if (!o || o.ok !== true) return false;
}
/* 周期/自旋与电机页共用 localStorage,用户调过的档位不被本页覆盖。 */
var body = {};
try {
var v = parseFloat(localStorage.getItem('dcu_ecat_period_ms'));
if (v > 0) body.period_ms = v;
var sp = localStorage.getItem('dcu_ecat_spin');
if (sp === '0' || sp === '1') body.spin = (sp === '1');
} catch (_) {}
/* motor/start 内部未进 OP 时自己走 PREOP→SAFEOP→OP,最坏约 24s */
var sr = await ecatPost('motor/start', body, 30000);
return !!(sr && sr.ok === true);
}
/** 读数无效时尝试自动拉起。冷却 + 刷写避让。返回是否本次调用成功启动。 */
async function tryAutoStart() {
if (busBusy()) return false;
var now = Date.now();
if (now - startAttemptAt < START_COOLDOWN_MS) return false;
startAttemptAt = now;
try {
var ok = await ensurePdoCycle();
if (ok) {
if (!startLogged) {
startLogged = true;
if (window.addLog) {
window.addLog('I', 'ECAT',
'已自动拉起 EtherCAT PDO 循环(GD32 负载将在首个 1s 窗口后显示)');
}
}
return true;
}
} catch (_) { /* 代理/后端离线、open/start 失败:走退避 */ }
return false;
}
async function tick() {
var node = el();
if (!node) return; /* 页面上没有该指标就不再调度 */
var ok = false;
try {
ok = render(await deviceLink.ecatPost('app/load', {}, 3000));
} catch (_) { /* 代理/后端离线,走退避 */ }
if (ok) {
failCount = 0;
} else {
var started = false;
try { started = await tryAutoStart(); } catch (_) {}
if (started) {
/* 刚 start 成功时 seq 仍可能是 0(首个 1s 窗口未滚完),
* 温和计数:保持 1s 节拍等窗口,不要立刻 5s/30s 退避。 */
failCount = Math.min(failCount, 1);
} else {
failCount++;
}
}
var wait = POLL_MS;
if (failCount > 6) wait = 30000;
else if (failCount > 2) wait = 5000;
timerId = setTimeout(tick, wait);
}
/** @description 启动轮询。幂等;页面上没有 #mGd32Load 时不启动。 */
export function startEcatLoadPoller() {
if (timerId !== null || !el()) return;
timerId = setTimeout(tick, POLL_MS);
}