chore: 解决 stash pop 与 device-link 合并冲突并收回本地 WIP

保留 ConnectionHub(deviceLink 导入/ECAT 自启/pmHold 幂等)与主线
WIP(断线清指标、AUTO_POLL=false、boot_ecat 等)。
This commit is contained in:
cbb
2026-09-16 12:31:39 +08:00
parent 0c772f5924
commit e98950cde2
12 changed files with 1026 additions and 248 deletions
+104 -6
View File
@@ -10,8 +10,19 @@
* 与 /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);
* - 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
@@ -23,9 +34,12 @@ import { deviceLink } from '../common/device-link.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'); }
@@ -41,8 +55,9 @@ function setTitle(node, title) {
}
var TIP_BASE = 'DCU_APP CPU 负载率:中断+非延迟处理(任务派发/OTA中继)占墙钟比,不含常开轮询与空转;EtherCAT 输入 PDO 段5,1s 窗口';
var TIP_OFF = TIP_BASE + ';当前不可用:控制台外壳启动时会自动 open+motor/start;'
+ '若仍为 --,检查网线/从站供电,或手动打开电机控制页选 EtherCAT。';
var TIP_OFF = TIP_BASE + ';当前不可用:外壳/本页会自动 open+motor/start;'
+ '若持续为 --,检查网卡/总线供电,或手动打开电机控制页选 EtherCAT。'
+ 'ECAT 刷写期间会暂停自动拉起。';
/** @description 把 /api/app/load 的 JSON 渲染到 #mGd32Load。返回值是否有效(驱动退避)。 */
function render(d) {
@@ -71,6 +86,81 @@ function render(d) {
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 'I210';
}
/** 刷写/上传进行中:绝不动总线(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; /* 页面上没有该指标就不再调度 */
@@ -78,10 +168,18 @@ async function tick() {
try {
ok = render(await deviceLink.ecatPost('app/load', {}, 3000));
} catch (_) { /* 代理/后端离线,走退避 */ }
if (!ok) {
failCount++;
} else {
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;