feat: 添加局域网设备发现功能和链路胶囊优化
- 实现局域网设备发现面板,支持探测ESP32中继板并自动切换端点 - 添加POST /api/lan/probe接口用于设备探测,返回三态判决结果 - 实现快扫和全段扫描两种模式,支持种子地址优先探测 - 添加端点唯一写手机制,防止多处同时修改设备IP - 优化链路胶囊UI显示WiFi/EtherCAT/BLE连接状态和设备名称 - 修复EtherCAT链路OTA刷写期间PDO循环暂停和AL状态处理问题 - 调整PDO周期设置范围为0.125-4ms,支持亚毫秒显示为微秒 - 添加ESP32 WiFi使能开关控件样式和数据来源角标显示
This commit is contained in:
+177
-35
@@ -33,7 +33,7 @@
|
||||
* native 的两级邮箱自愈,最坏 7s+。**R1 后 _ecat_proxy 不对非幂等路径自动重发**
|
||||
* (假超时/服务刚拉起时返回 503,需用户/脚本显式重试);run 超时给 300s、其余 60s。
|
||||
*/
|
||||
import { ECAT_DEFAULT_ADAPTER } from "../common/ecat-bus.js";
|
||||
import { ECAT_DEFAULT_ADAPTER, runEcatBootstrap } from "../common/ecat-bus.js";
|
||||
|
||||
var BOOT_API = '/ecat/api/ota/ecat';
|
||||
var PUSH_BYTES = 24576; /* 转 HEX 后 49152 字符,留在 native 65535 的 body 上限内 */
|
||||
@@ -49,6 +49,25 @@ function el(id) { return document.getElementById(id); }
|
||||
function setTxt(id, t) { var n = el(id); if (n && n.textContent !== t) n.textContent = t; }
|
||||
function bytes(n) { return (n >>> 0).toLocaleString() + ' B'; }
|
||||
|
||||
/**
|
||||
* @description 写 bootEcatBusy / bootEcatIsBoot 的唯一出口,只为留一条带时刻的读数。
|
||||
* 为什么收在一处:这两个标志就是外壳 Owner 要不要重拉总线的门
|
||||
* (js/common/device-link.js 里 `if (window.bootEcatBusy || ...) return;` 与
|
||||
* `if (window.bootEcatIsBoot === true) return;`)。2026-10-12「交接后 EtherCAT 连不上、
|
||||
* 要重启才行」要看的就是「这扇门被谁在什么时候按住、按了多久」——散在八处各打一条
|
||||
* 日志必然漂,所以判据只留一个执行点。
|
||||
* 只在值真的翻转时打印:反复赋同一个值不是事件,刷屏反而盖掉真读数。
|
||||
*/
|
||||
function setEcatFlag(name, val) {
|
||||
var next = !!val;
|
||||
if (window[name] === next) return;
|
||||
window[name] = next;
|
||||
if (window.addLog) {
|
||||
window.addLog('I', 'ECAT刷写',
|
||||
'[门] ' + name + '=' + (next ? 'true' : 'false') + '(t=+' + Math.round(performance.now()) + 'ms)');
|
||||
}
|
||||
}
|
||||
|
||||
function hexOf(u8, start, end) {
|
||||
var s = '';
|
||||
for (var k = start; k < end; k++) s += HEX2[u8[k]];
|
||||
@@ -98,6 +117,12 @@ async function post(path, body, timeoutMs) {
|
||||
* /api/open 内部会先 loop_stop 再 ec_init + 重枚举,所以它一定掐掉正在跑 PDO 的电机循环;
|
||||
* 但刷写期间这不构成互相打架:BOOT 钉在 PREOP,电机页那套 2s 自愈探测器打 /motor/start
|
||||
* 也进不了 OP,循环起不来 —— 结构上就让位了,不需要额外互斥标志。
|
||||
* @note 必须带重试(最多 3 次,间隔 2s):commit 后新 APP 起板看到 warm 残值会自己补
|
||||
* 一发 LAN9252 RST# 对齐 POR(esc_lan9252.cpp:441-446),芯片+PHY 整颗复位 → I210 链路
|
||||
* 重协商 1~2s,第一拍 open 大概率落在抖动窗口里。而 /api/open 是非幂等路径,代理到点
|
||||
* 只回 503「请重试一次」、绝不自动重发(ecat_client.py R1/P0)—— 这句"请重试"只能在
|
||||
* 这里执行;不重试的话后面 6 轮 probe 全打在没重建的会话上,读数必然为空,却会被
|
||||
* 当成"从站 EtherCAT 没恢复"报给用户(2026-10-09 21:49 就是这么误判的)。
|
||||
*/
|
||||
async function ensureBusOpen(force) {
|
||||
if (!force) {
|
||||
@@ -112,9 +137,23 @@ async function ensureBusOpen(force) {
|
||||
var s = localStorage.getItem('dcu_ecat_adapter');
|
||||
if (s && s.trim()) substr = s.trim();
|
||||
} catch (_) {}
|
||||
var r = await post('open', { substr: substr }, 25000);
|
||||
showLog(r);
|
||||
return !!(r && r.ok === true);
|
||||
for (var i = 0; i < 3; i++) {
|
||||
if (i > 0) {
|
||||
window.addLog('I', 'ECAT刷写', '重开总线 第 ' + (i + 1) + '/3 次(等链路重协商)…');
|
||||
await new Promise(function (res) { setTimeout(res, 2000); });
|
||||
}
|
||||
var r = null;
|
||||
try {
|
||||
/* 35s:必须比代理的 30s 长,否则浏览器先断 —— 那样连后端那句结构化的
|
||||
* 「请重试一次」都拿不到,只剩一条没信息量的超时。 */
|
||||
r = await post('open', { substr: substr }, 35000);
|
||||
} catch (e) {
|
||||
r = { ok: false, log: ['重开请求异常: ' + e] };
|
||||
}
|
||||
showLog(r);
|
||||
if (r && r.ok === true) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @description 把 native 回的多行 log 逐条落到控制台日志(与 WiFi/BLE 那条同一个日志池)。 */
|
||||
@@ -200,14 +239,15 @@ export async function bootEcatProbe(verbose) {
|
||||
setTxt('bootEcatSw', ver);
|
||||
setTxt('bootEcatArm', r.armed ? '允许' : '不允许');
|
||||
setTxt('bootEcatRecv', (r.srv_recv != null ? bytes(r.srv_recv) : '—') + ' / ' + bytes(r.slave_recv || 0));
|
||||
window.bootEcatIsBoot = !!r.boot;
|
||||
setEcatFlag('bootEcatIsBoot', !!r.boot);
|
||||
refreshBootEcatUi();
|
||||
if (verbose) {
|
||||
window.addLog('I', 'ECAT刷写',
|
||||
'0x1008 = "' + (r.name || '—') + '" 0x100A = "' + (r.sw || '') + '" → ' +
|
||||
(r.boot ? '是引导程序,点主按钮可跳回 APP(0x2503)'
|
||||
: (r.name ? '是应用,点主按钮写 0x2502 进 BOOT'
|
||||
: '邮箱没回应,多半是老 BOOT 或栈没起来')));
|
||||
: '读不到 0x1008(reason=' + (r.reason || '旧 native 无此字段') +
|
||||
(r.detail ? ',' + r.detail : '') + ')')));
|
||||
}
|
||||
return r;
|
||||
}
|
||||
@@ -261,16 +301,53 @@ function setEcatMsg(msg) {
|
||||
|
||||
/**
|
||||
* @description 交接之后等 BOOT 真的回到总线,把身份读回来。
|
||||
* 这里刻意**不**再打一次 /api/open:probe 内部那两级自愈(单播活性判据 → 必要时全量
|
||||
* 重枚举)就是为"从站刚硬复位、ESC 里 STADR/SM/邮箱配置全被清成 0"准备的,实测只多付
|
||||
* ~180ms;而在这里多花的每一毫秒都是从 BOOT 那 10s 窗口里扣的。
|
||||
* 这里刻意**不**再无条件打一次 /api/open:probe 内部那两级自愈(单播活性判据 → 必要时
|
||||
* 全量重枚举)就是为"从站刚硬复位、ESC 里 STADR/SM/邮箱配置全被清成 0"准备的;
|
||||
* 而在这里多花的每一毫秒都是从 BOOT 那 10s 窗口里扣的。
|
||||
* @note 旧注释写的"probe 实测只多付 ~180ms"只在**从站健康**时成立(2026-10-13 正极
|
||||
* 实测 15-69ms)。从站推不进 PREOP 时它最坏要 28.5s,比前端 20s 超时还长 —— 那才是
|
||||
* 页面卡死、要重启主站的成因,native 侧已按墙钟预算封顶(最坏 ≈10.6s)。
|
||||
* @return {object|null} 最后一次 probe 读数;拿到 boot:true 就提前返回
|
||||
*/
|
||||
async function waitBootOnBus(maxTries, gapMs) {
|
||||
var last = null;
|
||||
var t0 = performance.now();
|
||||
for (var k = 0; k < maxTries; k++) {
|
||||
var rp = performance.now();
|
||||
last = await bootEcatProbe(false);
|
||||
/* 每轮都留一行,别只留最后一句总结:native 的 reason 分型(no_slave / no_preop /
|
||||
* reopen_fail / sdo_timeout / app)是"链路没恢复"和"probe 没重试"唯一的区分读数,
|
||||
* 以前 verbose=false 把它整轮丢弃,2026-10-12 那轮就是因此定不下断点。
|
||||
* 耗时也一起打:2026-10-13 实测「从站能枚举到、但 AL 推不进 PREOP」时单轮 probe
|
||||
* 要 28.5s(三级 dl_pull_preop × 4 次 2s statecheck),比前端 API_TMO.probe=20s
|
||||
* 还长 —— 浏览器先 abort、native 仍在锁内跑完它,于是等锁实测到 54s。native 现已
|
||||
* 按「必须短于 20s」配了墙钟预算(最坏 ≈10.6s)。没有这一列就看不出窗口被谁吃掉。 */
|
||||
if (window.addLog) {
|
||||
var s = (last && last.stage_ms) || {};
|
||||
window.addLog('I', 'ECAT刷写',
|
||||
'[盯BOOT] #' + (k + 1) + '/' + maxTries +
|
||||
' t=+' + Math.round(performance.now() - t0) + 'ms' +
|
||||
' 本轮=' + Math.round(performance.now() - rp) + 'ms' +
|
||||
' boot=' + (last && last.boot ? 'Y' : 'N') +
|
||||
' reason=' + ((last && last.reason) || '无响应') +
|
||||
' name="' + ((last && last.name) || '') + '"' +
|
||||
(s.total_ms != null
|
||||
? ' 分段[preop=' + s.preop_ms + '/第' + s.preop_try +
|
||||
' enum=' + s.enum_ms + '/第' + s.enum_try +
|
||||
' reopen=' + s.reopen_ms +
|
||||
' sdo=' + s.sdo_name_ms + ']'
|
||||
: ''));
|
||||
}
|
||||
if (last && last.boot) return last;
|
||||
/* 主站会话被它自己的全量重连兜底关掉了(ec_close 后 do_open 失败 → g_bus_open 留在
|
||||
* 0),之后每条请求都只会回 bus_closed,角色格永远停在「未探测」—— 2026-10-13 实测
|
||||
* t=+15s 就是这一格。这里立刻按上电那条路重开一次(复用 ensureBusOpen,不另写一套
|
||||
* 恢复逻辑),否则剩下几轮全打在死会话上,白烧 BOOT 的窗口。 */
|
||||
if (last && last.bus_open === 0) {
|
||||
window.addLog('W', 'ECAT刷写',
|
||||
'[盯BOOT] 主站会话已被兜底重连关掉(bus_open=0),按上电路径重开总线…');
|
||||
await ensureBusOpen(true);
|
||||
}
|
||||
await new Promise(function (res) { setTimeout(res, gapMs); });
|
||||
}
|
||||
return last;
|
||||
@@ -284,7 +361,7 @@ async function waitBootOnBus(maxTries, gapMs) {
|
||||
*/
|
||||
export async function bootEcatReboot() {
|
||||
if (window.bootEcatBusy) return;
|
||||
window.bootEcatBusy = true;
|
||||
setEcatFlag('bootEcatBusy', true);
|
||||
refreshBootEcatUi();
|
||||
var btn = el('bootEcatRebootBtn');
|
||||
var isBoot = window.bootEcatIsBoot === true;
|
||||
@@ -310,11 +387,11 @@ export async function bootEcatReboot() {
|
||||
}
|
||||
if (r.already) {
|
||||
if (op === 'jump_app') {
|
||||
window.bootEcatIsBoot = false;
|
||||
setEcatFlag('bootEcatIsBoot', false);
|
||||
setEcatStatus('ok', '对面本来就是应用');
|
||||
setEcatMsg('从站已在 APP,无需跳回');
|
||||
} else {
|
||||
window.bootEcatIsBoot = true;
|
||||
setEcatFlag('bootEcatIsBoot', true);
|
||||
setEcatStatus('ok', '对面本来就是引导程序');
|
||||
setEcatMsg('从站已在 BOOT,无需交接,可直接刷写');
|
||||
}
|
||||
@@ -324,11 +401,15 @@ export async function bootEcatReboot() {
|
||||
}
|
||||
if (op === 'jump_app') {
|
||||
setEcatMsg('跳回已受理,等 APP 上总线…');
|
||||
var a = await waitAppOnBus(10, 800);
|
||||
var a = await waitAppOnBus(12, 300);
|
||||
if (a && a.name && !a.boot) {
|
||||
window.bootEcatIsBoot = false;
|
||||
setEcatStatus('ok', '已回到应用');
|
||||
setEcatMsg('APP 已在总线应答(0x100A = ' + (a.sw || '—') + ')');
|
||||
setEcatFlag('bootEcatIsBoot', false);
|
||||
setEcatMsg('APP 已在总线应答(0x100A = ' + (a.sw || '—') + '),恢复连接…');
|
||||
var restored = await restoreEcatLikePowerOn();
|
||||
setEcatStatus(restored ? 'ok' : 'warn', restored ? '已回到应用' : 'APP 已起,连接未完全恢复');
|
||||
setEcatMsg(restored
|
||||
? 'APP 已回到总线,连接已按上电流程恢复(0x100A = ' + (a.sw || '—') + ')'
|
||||
: 'APP 已应答但 open/motor/start 未全部成功 —— 见日志,可再点『探测』或手动连接');
|
||||
} else {
|
||||
setEcatStatus('warn', '跳回后没等到 APP');
|
||||
setEcatMsg('写了 0x2503 但没等到应用 —— 看串口,或 A 区 CRC 不过');
|
||||
@@ -341,7 +422,7 @@ export async function bootEcatReboot() {
|
||||
setEcatMsg('交接已受理,等 BOOT 上总线…');
|
||||
var b = await waitBootOnBus(10, 800);
|
||||
if (b && b.boot) {
|
||||
window.bootEcatIsBoot = true;
|
||||
setEcatFlag('bootEcatIsBoot', true);
|
||||
setEcatStatus('ok', '已进引导程序');
|
||||
setEcatMsg('BOOT 已在总线应答(0x100A = ' + (b.sw || '—') + '),窗口 10s:可以直接刷写');
|
||||
} else {
|
||||
@@ -359,24 +440,53 @@ export async function bootEcatReboot() {
|
||||
setEcatMsg('交接/跳回请求异常');
|
||||
window.addLog('E', 'ECAT刷写', '交接/跳回请求异常:' + e);
|
||||
} finally {
|
||||
window.bootEcatBusy = false;
|
||||
setEcatFlag('bootEcatBusy', false);
|
||||
refreshBootEcatUi();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 跳回之后等 APP 真的回到总线(与 waitBootOnBus 对称)。
|
||||
* 首轮前先短等 300ms(覆盖 BOOT 80ms 静默 + APP init),gap 收紧到 300ms:
|
||||
* 旧 800ms 会让「其实已经好了」多愣一拍。
|
||||
*/
|
||||
async function waitAppOnBus(maxTries, gapMs) {
|
||||
await new Promise(function (res) { setTimeout(res, 300); });
|
||||
var last = null;
|
||||
for (var k = 0; k < maxTries; k++) {
|
||||
last = await bootEcatProbe(false);
|
||||
if (last && last.name && !last.boot) return last;
|
||||
await new Promise(function (res) { setTimeout(res, gapMs); });
|
||||
await new Promise(function (res) { setTimeout(res, gapMs || 300); });
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description APP 回总线后,按「刚上电」同一条链拉起连接:
|
||||
* health → open 网卡 → motor/start(PREOP→SAFEOP→OP + PDO 循环)。
|
||||
* 与 device-link Owner 的 bootstrapEcatBus / runEcatBootstrap 同一实现,
|
||||
* 不在本文件另写一套恢复逻辑。
|
||||
*/
|
||||
async function restoreEcatLikePowerOn() {
|
||||
setEcatMsg('按上电流程恢复连接(open + motor/start)…');
|
||||
try {
|
||||
var r = await runEcatBootstrap({
|
||||
ecatPost: post,
|
||||
fromLocalStorage: true,
|
||||
reason: 'user_probe',
|
||||
});
|
||||
if (r && r.ok) {
|
||||
window.addLog('I', 'ECAT刷写', r.reused
|
||||
? 'PDO 循环本已在跑,复用(与上电幂等语义一致)'
|
||||
: '已按上电流程恢复:open + motor/start(OP + PDO)');
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
window.addLog('W', 'ECAT刷写', '按上电流程恢复连接失败:' + (e && e.message ? e.message : e));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @description 放弃:丢弃服务端缓冲。总线此刻本来就没动过(推只到主站内存)。 */
|
||||
export async function bootEcatAbort() {
|
||||
if (window.bootEcatBusy) return;
|
||||
@@ -480,7 +590,7 @@ export async function bootEcatFlash() {
|
||||
}
|
||||
clearArm();
|
||||
|
||||
window.bootEcatBusy = true;
|
||||
setEcatFlag('bootEcatBusy', true);
|
||||
/* 置跨页"刷写中"标记(读侧见 common/utils.js flashBusyNow):交接与 BOOT↔APP
|
||||
* 期间 GD32 刚复位,ESP32 的 CAN 反馈查询(0xC1)必被链路层拒,问了也是白问、
|
||||
* 还会在日志里刷屏 —— CAN/电机页的 1Hz 轮询据此避让。
|
||||
@@ -667,9 +777,16 @@ export async function bootEcatFlash() {
|
||||
var up = !!(after && after.name && !after.boot);
|
||||
if (up) {
|
||||
setEcatStage(6, 'done', '已复位');
|
||||
setEcatStage(7, 'done', '完成');
|
||||
setEcatStatus('ok', '刷写完成');
|
||||
setEcatPct(100, '新 APP 已在总线上应答 · 0x100A = ' + (after.sw || '—'));
|
||||
setEcatStage(7, 'live', '恢复连接');
|
||||
setEcatStatus('active', '按上电流程恢复连接…');
|
||||
setEcatPct(100, '新 APP 已应答,恢复 OP/PDO…');
|
||||
var restoredAfterFlash = await restoreEcatLikePowerOn();
|
||||
setEcatStage(7, restoredAfterFlash ? 'done' : 'warn', restoredAfterFlash ? '完成' : '部分恢复');
|
||||
setEcatStatus(restoredAfterFlash ? 'ok' : 'warn',
|
||||
restoredAfterFlash ? '刷写完成' : '刷写完成,连接未完全恢复');
|
||||
setEcatPct(100, restoredAfterFlash
|
||||
? ('新 APP 已在总线 · 0x100A = ' + (after.sw || '—') + ' · 连接已按上电流程恢复')
|
||||
: ('新 APP 已应答 0x100A = ' + (after.sw || '—') + ',但 open/motor/start 未全部成功'));
|
||||
/* 刷前刷后版本一致 = 灌进去的多半还是原来那包(页面缓存旧文件/选错包),
|
||||
* 流程全绿说明不了"换了版本" —— 补一条可观察告警(只提示,不拦:同版本重刷合法)。 */
|
||||
if (pre && pre.sw && after.sw && after.sw === pre.sw) {
|
||||
@@ -679,17 +796,42 @@ export async function bootEcatFlash() {
|
||||
window.showToast('刷前刷后版本号相同,请确认刷的是最新固件', 'warning');
|
||||
}
|
||||
} else if (okRun) {
|
||||
/* commit 成功只证明 A 区写进去了;邮箱读不到 APP 名 = EtherCAT 没恢复,
|
||||
* 对用户就是「不能用」。这里禁止画成半成功:进度钉在提交完成那一档,
|
||||
* 阶段 6/7 全部警告态,文案直接说清副作用。 */
|
||||
setEcatStage(6, 'warn', '已复位');
|
||||
setEcatStage(7, 'err', 'EtherCAT 未恢复');
|
||||
setEcatStatus('err', '刷写已提交,但 EtherCAT 不可用');
|
||||
setEcatPct(100, '镜像已写入,但总线邮箱读不到 APP —— EtherCAT 当前不可用');
|
||||
window.addLog('E', 'ECAT刷写',
|
||||
'commit OK,但 0x1008 读不到 APP 名 = EtherCAT 未恢复(HBI/邮箱路径死)。' +
|
||||
'板上若仍是 1.6.39,warm jump 后 IDX 残值会让旧探针假阴性 —— 需刷 1.6.40;' +
|
||||
'若已是 1.6.40 仍这样,看串口 [ECAT][ESC] init 是 FAIL BYTE_TEST 还是 core not ready');
|
||||
/* commit 成功只证明 A 区写进去了;这一支里三种病必须分开说,且只有
|
||||
* reason=sdo_timeout(AL 已到位、0x1008 不答)才允许指向从站:
|
||||
* - 有 name 且 boot=true → 新 APP 没跑起来,还在引导程序(A 区/向量表的事)
|
||||
* - bus_closed/no_slave/no_preop/reopen_fail → 主站压根没重连上,读数不自证
|
||||
* - sdo_timeout → 邮箱路径真没应答,才轮到查固件
|
||||
* 以前这里不分 reason 一律写"HBI/邮箱路径死",2026-10-09 21:49 那次就是
|
||||
* 主站 6s 假超时后 6 轮 probe 全打在没重建的会话上,被误判成固件没恢复。 */
|
||||
var rsn = (after && after.reason) || 'bus_closed';
|
||||
var isBootNow = !!(after && after.name && after.boot);
|
||||
var slaveBlamed = (rsn === 'sdo_timeout');
|
||||
setEcatStage(6, isBootNow ? 'err' : 'warn', isBootNow ? '仍在引导程序' : '已复位');
|
||||
setEcatStage(7, 'err', slaveBlamed ? '邮箱无回应'
|
||||
: (isBootNow ? '没跳进 APP' : '主站未重连'));
|
||||
setEcatStatus('err', slaveBlamed ? '刷写已提交,但从站邮箱无回应'
|
||||
: (isBootNow ? '刷写已提交,但从站仍在引导程序'
|
||||
: '刷写已提交,但主站没能重连上总线'));
|
||||
setEcatPct(100, slaveBlamed
|
||||
? '镜像已写入,AL 到位但 0x1008 不答 —— 从站邮箱当前不可用'
|
||||
: (isBootNow ? '镜像已写入,对面仍是 BOOT —— 新 APP 没接棒'
|
||||
: '镜像已写入,主站这侧会话没重建(不是从站的读数)'));
|
||||
if (isBootNow) {
|
||||
window.addLog('E', 'ECAT刷写',
|
||||
'commit OK,但 0x1008 仍是 "' + after.name + '"(引导程序)—— ' +
|
||||
'新 APP 没跳起来:先点「探测」看 BOOT 侧 stay_in_ota/校验位,再看串口 [CRC]/[J5] 有没有走到跳转');
|
||||
} else if (slaveBlamed) {
|
||||
window.addLog('E', 'ECAT刷写',
|
||||
'commit OK,AL 已推进到 PREOP 但 0x1008 读不到(reason=sdo_timeout)= 从站邮箱无回应。' +
|
||||
'串口看 [ECAT][ESC] init 是 FAIL BYTE_TEST 还是 core not ready' +
|
||||
(after.detail ? ';native 侧原文:' + after.detail : ''));
|
||||
} else {
|
||||
window.addLog('E', 'ECAT刷写',
|
||||
'commit OK,但主站没重连上总线(reason=' + rsn + (after && after.detail ? ',' + after.detail : '') +
|
||||
')—— 这个读数只说明主站这侧没人,**不能**据此说从站 EtherCAT 死了。' +
|
||||
'等链路重协商(新 APP 起板会自己补一发 LAN9252 RST#)后点「探测」复核;' +
|
||||
'同时看 ESP32 侧 SPI 的 INIT_STATUS state/mod/fault_codes 定从站好坏');
|
||||
}
|
||||
} else {
|
||||
setEcatStage(6, 'err', '未确认');
|
||||
setEcatStage(7, 'err', '未完成');
|
||||
@@ -710,7 +852,7 @@ export async function bootEcatFlash() {
|
||||
setEcatPct(0, '刷写请求异常:' + (e && e.name === 'TimeoutError' ? '超时' : e));
|
||||
window.addLog('E', 'ECAT刷写', '刷写请求异常:' + e);
|
||||
} finally {
|
||||
window.bootEcatBusy = false;
|
||||
setEcatFlag('bootEcatBusy', false);
|
||||
/* 刷写收尾:撤掉跨页"刷写中"标记(正常与异常路径都走这里) */
|
||||
try { localStorage.removeItem('dcu_flash_busy_until'); } catch (_) {}
|
||||
if (runBtn) {
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
* 请求统一走 ConnectionHub(device-link.ecatPost),与 Owner 健康轮询同源。
|
||||
*/
|
||||
import { deviceLink } from '../common/device-link.js';
|
||||
import { setCellSource } from '../common/utils.js';
|
||||
import {
|
||||
proveEcatLoop, ensureEcatLoopRepair, noteEcatLoopUp,
|
||||
} from '../common/ecat-bus.js';
|
||||
@@ -79,6 +80,7 @@ function render(d) {
|
||||
setText(node, '--');
|
||||
setCls(node, 'v');
|
||||
setTitle(node, TIP_OFF);
|
||||
setCellSource('mGd32Load', 'off');
|
||||
return false;
|
||||
}
|
||||
var perm = Number(d.load_perm);
|
||||
@@ -86,6 +88,7 @@ function render(d) {
|
||||
setText(node, '--');
|
||||
setCls(node, 'v');
|
||||
setTitle(node, TIP_OFF);
|
||||
setCellSource('mGd32Load', 'off');
|
||||
return false;
|
||||
}
|
||||
var pct = perm / 10;
|
||||
@@ -93,6 +96,8 @@ function render(d) {
|
||||
/* ≥85% 变琥珀提示余量吃紧;其余用正常强调色 */
|
||||
setCls(node, pct >= 85 ? 'v warn' : 'v ok');
|
||||
setTitle(node, TIP_BASE + ';窗口序号 ' + d.seq + ',' + d.age_ms + 'ms 前更新');
|
||||
/* /api/app/load 走 EtherCAT → 标 ECAT 来源 */
|
||||
setCellSource('mGd32Load', 'ecat');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+18
-2
@@ -20,8 +20,8 @@
|
||||
* window.markGd32OtaUiComplete / window.cleanupAfterOtaFailure / window.otaSessionReset
|
||||
* window.stopRelayProgressWatch / window.pollLogs / window.emitRelayProgress
|
||||
*/
|
||||
import { formatUptime } from '../common/utils.js';
|
||||
import { updateConnectionUi, updateHealthUi, updateHeapAndTaskStatusUi } from '../common/connection.js';
|
||||
import { formatUptime, setCellSource } from '../common/utils.js';
|
||||
import { updateConnectionUi, updateHealthUi, updateHeapAndTaskStatusUi, setWifiPillOverride } from '../common/connection.js';
|
||||
import { setActivePhase, setProgress } from './progress.js';
|
||||
import { otaSessionReset, OtaSessionState } from './session.js';
|
||||
|
||||
@@ -154,9 +154,17 @@ export function clearLiveMetricsOnDisconnect() {
|
||||
function applyDashboardUi(data, ota, wifi) {
|
||||
updateHeapAndTaskStatusUi(data);
|
||||
var connected = !!wifi.connected;
|
||||
/* 正常状态轮询:清掉「重启中/上传中/设备忙」覆盖,WiFi 文案回到 updateLinkPills 唯一生成 */
|
||||
try {
|
||||
var busy = (typeof window.otaSessionActive === 'function' && window.otaSessionActive())
|
||||
|| window.isEsp32Rebooting;
|
||||
if (!busy) setWifiPillOverride(null);
|
||||
} catch (_) {}
|
||||
var connText = connected ? (wifi.mode || 'STA') + ' 已连接' : (wifi.mode || 'STA') + ' 未连接';
|
||||
updateConnectionUi(connected, connText);
|
||||
updateHealthUi(data.health, data.free_heap);
|
||||
/* 连接状态角标由 updateConnectionUi 按双通路在线情况标注,这里只标健康(WiFi /system/status) */
|
||||
setCellSource('mHealth', 'wifi');
|
||||
|
||||
if (data.version) {
|
||||
var pillVerEl = document.getElementById('pillVer');
|
||||
@@ -168,6 +176,7 @@ function applyDashboardUi(data, ota, wifi) {
|
||||
var mGd32App = document.getElementById('mGd32App');
|
||||
var mGd32Boot = document.getElementById('mGd32Boot');
|
||||
if (mGd32App) {
|
||||
setCellSource('mGd32App', 'wifi');
|
||||
if (ota.gd32_app && ota.gd32_app !== 'v0.0.0.0') {
|
||||
setNodeText(mGd32App, ota.gd32_app);
|
||||
setNodeClass(mGd32App, 'v ok');
|
||||
@@ -177,6 +186,7 @@ function applyDashboardUi(data, ota, wifi) {
|
||||
}
|
||||
}
|
||||
if (mGd32Boot) {
|
||||
setCellSource('mGd32Boot', 'wifi');
|
||||
if (ota.gd32_boot && ota.gd32_boot !== 'v0.0.0.0') {
|
||||
setNodeText(mGd32Boot, ota.gd32_boot);
|
||||
setNodeClass(mGd32Boot, 'v ok');
|
||||
@@ -193,6 +203,7 @@ function applyDashboardUi(data, ota, wifi) {
|
||||
* 1.5.36+ fault_codes[] 给出每模块具体原因,直接显示无需悬停。 */
|
||||
var mGd32InitEl = document.getElementById('mGd32Init');
|
||||
if (mGd32InitEl) {
|
||||
setCellSource('mGd32Init', 'wifi');
|
||||
var gi = data.gd32_init || null;
|
||||
/* 与 GD32 sys/initial.h AppInitializer::ModuleBit 位序一致 */
|
||||
var kModNames = ['nvic','systick','esp32_hold','usart0','adc','canfd',
|
||||
@@ -276,6 +287,7 @@ function applyDashboardUi(data, ota, wifi) {
|
||||
|
||||
var mTempEl = document.getElementById('mTemp');
|
||||
if (mTempEl) {
|
||||
setCellSource('mTemp', 'wifi');
|
||||
/* 与固件 app_config:TEMP_WARNING=80 / TEMP_CRITICAL=100 对齐 */
|
||||
if (Number.isFinite(data.temperature)) {
|
||||
var tC = data.temperature;
|
||||
@@ -300,6 +312,10 @@ function applyDashboardUi(data, ota, wifi) {
|
||||
var mGd32TempEl = document.getElementById('mGd32Temp');
|
||||
var mVccEl = document.getElementById('mVcc');
|
||||
var mVddaEl = document.getElementById('mVdda');
|
||||
/* gd32_adc 经 /system/status(ESP32 HTTP)→ WiFi */
|
||||
setCellSource('mGd32Temp', 'wifi');
|
||||
setCellSource('mVcc', 'wifi');
|
||||
setCellSource('mVdda', 'wifi');
|
||||
|
||||
function setMetricUi(el, text, cls, title) {
|
||||
if (!el) return;
|
||||
|
||||
Reference in New Issue
Block a user