feat(ecat): 启动后把 ecat_native 提到 RealTime;跳拍优化实验脚本与报告
- esp32_server: Popen 后外部 SetPriorityClass(RT/High),native 自设会被钳成 High - 入库 skip_* 标定/实验脚本与 REPORT_ecat_skip_opt_20260917 - 前端 WIP:layout/app/polling/boot_ecat/index - 不含 lab/skip_meter 原始采样与 preview 截图
This commit is contained in:
@@ -247,7 +247,7 @@ document.addEventListener('click', function (ev) {
|
||||
case 'ble-pick-file': var fi = document.getElementById('firmwareFile'); if (fi) fi.click(); break;
|
||||
case 'ble-clear-file': if (window.bleOta) window.bleOta.clearFile(); break;
|
||||
case 'start-upload': startUpload(arg); break;
|
||||
case 'boot-ecat-probe': bootEcatProbe(true); break;
|
||||
case 'boot-ecat-probe': if (!window.bootEcatBusy) bootEcatProbe(true); break;
|
||||
case 'boot-ecat-pick': bootEcatPick(); break;
|
||||
case 'boot-ecat-run': bootEcatFlash(); break;
|
||||
case 'boot-ecat-abort': bootEcatAbort(); break;
|
||||
|
||||
+14
-3
@@ -58,6 +58,7 @@ var statusPollInFlight = false; // 是否正在请求 /system/status,防止
|
||||
var logsInFlight = false; // 是否正在请求 /logs,防止重叠抢 socket
|
||||
var pageVisible = !document.hidden; // 页面可见性:切到后台/最小化/切标签会变 false
|
||||
var uploadPollChip = null; // 当前暂停轮询的上传目标,用于区分 WiFi/日志策略
|
||||
var networkDiagDone = false; // 本轮离线期间是否已做过网关诊断(防每秒刷屏)
|
||||
|
||||
/**
|
||||
* 调整 /system/status 目标间隔。GD32 中继期用 400ms 缩小「转发 N/M」相对设备日志的滞后;
|
||||
@@ -101,6 +102,7 @@ function scheduleLogsPoll(delayMs) {
|
||||
|
||||
export function startPolling() {
|
||||
window.consecutivePollFailures = 0;
|
||||
networkDiagDone = false;
|
||||
statusPollIntervalMs = 1000;
|
||||
if (statusTimerId) clearTimeout(statusTimerId);
|
||||
if (logsTimerId) clearTimeout(logsTimerId);
|
||||
@@ -112,6 +114,7 @@ export function pausePollingForUpload() {
|
||||
window.pollingPausedForUpload = true;
|
||||
uploadPollChip = window.activeUploadChip || window.flashTarget || null;
|
||||
window.consecutivePollFailures = 0;
|
||||
networkDiagDone = false;
|
||||
// ★ 上传大固件期间必须停掉 status/logs 轮询。
|
||||
// httpd max_open_sockets=4 + lru_purge:轮询会再占 socket,LRU 可能踢掉
|
||||
// 正在写 Flash 的 /ota/upload 连接 → 浏览器 upload 进度已 100%(发完),
|
||||
@@ -135,6 +138,7 @@ export function resumePollingAfterUpload() {
|
||||
window.pollingPausedForUpload = false;
|
||||
uploadPollChip = null;
|
||||
window.consecutivePollFailures = 0;
|
||||
networkDiagDone = false;
|
||||
statusPollIntervalMs = 1000;
|
||||
if (logsTimerId) clearTimeout(logsTimerId);
|
||||
// 上传结束立即刷新一次状态;若当前已在请求中,则让它完成后按新间隔继续
|
||||
@@ -201,6 +205,7 @@ export async function pollStatus() {
|
||||
// 恢复:之前有失败计数,说明设备曾离线,现在恢复在线
|
||||
if (window.consecutivePollFailures > 0) {
|
||||
window.consecutivePollFailures = 0;
|
||||
networkDiagDone = false;
|
||||
addLog('I', '系统', '设备已恢复在线,轮询恢复正常');
|
||||
showToast('设备已恢复在线', 'success');
|
||||
// 仅清理终态残留。禁止对活跃会话 otaSessionReset:
|
||||
@@ -278,13 +283,17 @@ export async function pollStatus() {
|
||||
var otaOngoing = window.pollingPausedForUpload ||
|
||||
sessionActive || sessionFailed ||
|
||||
window.isEsp32Rebooting || !!window.currentOtaTarget || lastDeviceBusy;
|
||||
if (pageVisible && window.consecutivePollFailures >= 5 && !otaOngoing) {
|
||||
/* EtherCAT 刷写/链路时不跑 WiFi 网关诊断:设备可能根本不在 WiFi 上,
|
||||
* 反复弹「网关可达;设备不响应」只会干扰。仍走下面的「未连接」UI。 */
|
||||
var ecatActive = window.connectionMode === 'ecat' || !!window.bootEcatBusy;
|
||||
if (pageVisible && window.consecutivePollFailures >= 5 && !otaOngoing &&
|
||||
!ecatActive && !networkDiagDone) {
|
||||
networkDiagDone = true;
|
||||
addLog('W', '诊断', '设备连续 5 次无响应,正在诊断网络...');
|
||||
var diag = await diagnoseNetwork();
|
||||
addLog('W', '诊断', diag);
|
||||
addLog('W', '诊断', '诊断结果: ' + diag);
|
||||
updateConnectionUi(false, '诊断中');
|
||||
window.applyDisconnectedRuntimeState();
|
||||
addLog('W', '诊断', '诊断结果: ' + diag);
|
||||
}
|
||||
// ★ 状态机优先:会话已 FAILED → 保持失败 UI,仅标连接胶囊为"设备忙/未连接"
|
||||
if (sessionFailed) {
|
||||
@@ -353,6 +362,7 @@ document.addEventListener('visibilitychange', function () {
|
||||
pageVisible = !document.hidden;
|
||||
if (!pageVisible) return;
|
||||
window.consecutivePollFailures = 0;
|
||||
networkDiagDone = false;
|
||||
// 切回前台立即刷新状态;若当前已在请求中,取消等待中的下一次让它尽快重排
|
||||
if (statusPollInFlight) {
|
||||
if (statusTimerId) clearTimeout(statusTimerId);
|
||||
@@ -419,6 +429,7 @@ setTimeout(function () {
|
||||
if (ipInput) {
|
||||
ipInput.addEventListener('change', function () {
|
||||
window.consecutivePollFailures = 0;
|
||||
networkDiagDone = false;
|
||||
addLog('I', '系统', 'IP 已修改,重新启动轮询...');
|
||||
if (!statusPollInFlight) {
|
||||
if (statusTimerId) clearTimeout(statusTimerId);
|
||||
|
||||
+87
-29
@@ -124,22 +124,42 @@ function showLog(res) {
|
||||
/**
|
||||
* @description 按"有没有已选 GD32 镜像 + 有没有任务在跑"重算按钮可用性。
|
||||
* 幂等,供 file-handle / console-mode / upload 各处调用;不缓存判据、不复制 arm 判断。
|
||||
* 交接按钮文案由最后一次 probe/reboot 结果驱动(window.bootEcatIsBoot)。
|
||||
*/
|
||||
export function refreshBootEcatUi() {
|
||||
var probeBtn = el('bootEcatProbeBtn');
|
||||
var runBtn = el('bootEcatRunBtn');
|
||||
var abortBtn = el('bootEcatAbortBtn');
|
||||
var rebootBtn = el('bootEcatRebootBtn');
|
||||
/* 别的通道在刷就互斥:三条传输共用同一块 flash,没有并发可言 */
|
||||
var otherBusy = !!window.isUploading ||
|
||||
!!(window.bleOta && window.bleOta.isUploading && window.bleOta.isUploading());
|
||||
var hasImg = !!window.gd32File;
|
||||
if (probeBtn) probeBtn.disabled = window.bootEcatBusy;
|
||||
if (rebootBtn) rebootBtn.disabled = window.bootEcatBusy || otherBusy;
|
||||
if (rebootBtn) {
|
||||
rebootBtn.disabled = window.bootEcatBusy || otherBusy;
|
||||
if (!window.bootEcatBusy) {
|
||||
var isBoot = window.bootEcatIsBoot === true;
|
||||
rebootBtn.textContent = isBoot ? '跳回应用' : '让应用回引导程序';
|
||||
rebootBtn.title = isBoot
|
||||
? '写 BOOT 的 CoE 0x2503:00=1,跳回 APP(BOOT ≥1.1.0.20)'
|
||||
: '写 APP 的 CoE 0x2502:00=1,让它复位进引导程序';
|
||||
}
|
||||
}
|
||||
if (runBtn) runBtn.disabled = window.bootEcatBusy || otherBusy || !hasImg;
|
||||
if (abortBtn) abortBtn.disabled = window.bootEcatBusy;
|
||||
}
|
||||
|
||||
function setRoleBadge(role, sw) {
|
||||
var badge = el('bootEcatRoleBadge');
|
||||
var txt = el('bootEcatRoleTxt');
|
||||
var swEl = el('bootEcatRoleSw');
|
||||
if (!badge) return;
|
||||
var r = role === 'boot' ? 'boot' : role === 'app' ? 'app' : 'unknown';
|
||||
badge.setAttribute('data-role', r);
|
||||
if (txt) {
|
||||
txt.textContent = r === 'boot' ? 'BOOT' : r === 'app' ? 'APP' : '未探测';
|
||||
}
|
||||
if (swEl) swEl.textContent = sw || (r === 'unknown' ? '—' : '版本未知');
|
||||
}
|
||||
|
||||
/** @description 复用 GD32 那条路的文件控件:同一条 processFile 做 .hex→bin 与镜像校验。 */
|
||||
export function bootEcatPick() {
|
||||
var input = el('fileInputGd32');
|
||||
@@ -156,26 +176,23 @@ export async function bootEcatProbe(verbose) {
|
||||
try {
|
||||
r = await api({ op: 'probe' });
|
||||
} catch (e) {
|
||||
setTxt('bootEcatWho', '后端无响应');
|
||||
setEcatConn('err', '后端无响应');
|
||||
setRoleBadge('unknown', null);
|
||||
if (verbose) window.addLog('E', 'ECAT刷写', '探测请求异常:' + e);
|
||||
return null;
|
||||
}
|
||||
var who = r.boot ? '引导程序' : (r.name ? '应用' : '读不到');
|
||||
var ver = r.sw || '—';
|
||||
setTxt('bootEcatWho', who);
|
||||
setRoleBadge(r.boot ? 'boot' : (r.name ? 'app' : 'unknown'), r.sw || null);
|
||||
setTxt('bootEcatName', r.name || '—');
|
||||
setTxt('bootEcatSw', ver);
|
||||
setTxt('bootEcatArm', r.armed ? '允许' : '不允许');
|
||||
setTxt('bootEcatRecv', bytes(r.slave_recv || 0));
|
||||
if (r.boot) setEcatConn('ok', '引导程序 ' + ver + ' · 可以刷写');
|
||||
else if (r.name) setEcatConn('warn', '应用 ' + ver + ' · 刷写会自动交接回 BOOT');
|
||||
else setEcatConn('err', '邮箱无回应');
|
||||
setTxt('bootEcatRecv', (r.srv_recv != null ? bytes(r.srv_recv) : '—') + ' / ' + bytes(r.slave_recv || 0));
|
||||
window.bootEcatIsBoot = !!r.boot;
|
||||
refreshBootEcatUi();
|
||||
if (verbose) {
|
||||
window.addLog('I', 'ECAT刷写',
|
||||
'0x1008 = "' + (r.name || '—') + '" 0x100A = "' + (r.sw || '') + '" → ' +
|
||||
(r.boot ? '是引导程序,可以刷写'
|
||||
: (r.name ? '是应用,刷写链里第 3 步会写它的 0x2502 让它回 BOOT(1.6.34 以前的 APP 没这个对象,只能走 SPI 0xB0 / CAN 0x320)'
|
||||
(r.boot ? '是引导程序,点主按钮可跳回 APP(0x2503)'
|
||||
: (r.name ? '是应用,点主按钮写 0x2502 进 BOOT'
|
||||
: '邮箱没回应,多半是老 BOOT 或栈没起来')));
|
||||
}
|
||||
return r;
|
||||
@@ -243,42 +260,71 @@ async function waitBootOnBus(maxTries, gapMs) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 单独一次「让应用回引导程序」:写 APP 的 CoE 0x2502:00 = 1。
|
||||
* 刷写链里本来就带这一步,这个按钮是给"只想把板子停在 BOOT 上等人查"的场合 ——
|
||||
* 交接一个字节 flash 都不写,代价只是从站重启一次、停在 BOOT 的 10s 窗口里等。
|
||||
* 跑着老 APP(<1.6.34)的机器字典里没有 0x2502,主站会把从站的 SDO abort 码原样带回来,
|
||||
* 日志里直接就能看出该退回 SPI 0xB0 / CAN 0x320。
|
||||
* @description 交接按钮总入口:按对端身份自动选方向。
|
||||
* - 已在 BOOT → op=jump_app(写 0x2503)
|
||||
* - 在 APP / 未知 → op=reboot(写 0x2502)
|
||||
* 刷写链里本来就带 reboot 这一步;本按钮给"只想手动来回切换"的场合。
|
||||
*/
|
||||
export async function bootEcatReboot() {
|
||||
if (window.bootEcatBusy) return;
|
||||
window.bootEcatBusy = true;
|
||||
refreshBootEcatUi();
|
||||
var btn = el('bootEcatRebootBtn');
|
||||
if (btn) btn.textContent = '交接中…';
|
||||
setEcatMsg('正在让应用回引导程序…');
|
||||
var isBoot = window.bootEcatIsBoot === true;
|
||||
if (btn) btn.textContent = isBoot ? '跳转中…' : '交接中…';
|
||||
setEcatMsg(isBoot ? '正在让引导程序跳回应用…' : '正在让应用回引导程序…');
|
||||
try {
|
||||
if (!await ensureBusOpen()) {
|
||||
setEcatStatus('err', '总线没打开');
|
||||
setEcatMsg('主站没能打开总线(见日志)');
|
||||
return;
|
||||
}
|
||||
var r = await api({ op: 'reboot' });
|
||||
/* 先用本次动作的方向;若 probe 结果与缓存不符,会话内纠正 */
|
||||
var op = window.bootEcatIsBoot === true ? 'jump_app' : 'reboot';
|
||||
var r = await api({ op: op });
|
||||
showLog(r);
|
||||
if (!r || r.ok !== true) {
|
||||
setEcatStatus('err', '交接被拒');
|
||||
setEcatMsg('交接没被受理(见日志里从站的 abort 码)');
|
||||
setEcatStatus('err', op === 'jump_app' ? '跳回被拒' : '交接被拒');
|
||||
setEcatMsg(op === 'jump_app'
|
||||
? '跳回没被受理(BOOT 需 ≥1.1.0.20;见日志 abort 码)'
|
||||
: '交接没被受理(见日志里从站的 abort 码)');
|
||||
await bootEcatProbe(false);
|
||||
return;
|
||||
}
|
||||
if (r.already) {
|
||||
setEcatStatus('ok', '对面本来就是引导程序');
|
||||
setEcatMsg('从站已在 BOOT,无需交接,可直接刷写');
|
||||
if (op === 'jump_app') {
|
||||
window.bootEcatIsBoot = false;
|
||||
setEcatStatus('ok', '对面本来就是应用');
|
||||
setEcatMsg('从站已在 APP,无需跳回');
|
||||
} else {
|
||||
window.bootEcatIsBoot = true;
|
||||
setEcatStatus('ok', '对面本来就是引导程序');
|
||||
setEcatMsg('从站已在 BOOT,无需交接,可直接刷写');
|
||||
}
|
||||
refreshBootEcatUi();
|
||||
await bootEcatProbe(true);
|
||||
return;
|
||||
}
|
||||
if (op === 'jump_app') {
|
||||
setEcatMsg('跳回已受理,等 APP 上总线…');
|
||||
var a = await waitAppOnBus(10, 800);
|
||||
if (a && a.name && !a.boot) {
|
||||
window.bootEcatIsBoot = false;
|
||||
setEcatStatus('ok', '已回到应用');
|
||||
setEcatMsg('APP 已在总线应答(0x100A = ' + (a.sw || '—') + ')');
|
||||
} else {
|
||||
setEcatStatus('warn', '跳回后没等到 APP');
|
||||
setEcatMsg('写了 0x2503 但没等到应用 —— 看串口,或 A 区 CRC 不过');
|
||||
window.addLog('E', 'ECAT刷写',
|
||||
'跳回请求已受理却等不到 APP 应答 —— 确认 BOOT ≥1.1.0.20 且 A 区镜像完整');
|
||||
}
|
||||
refreshBootEcatUi();
|
||||
return;
|
||||
}
|
||||
setEcatMsg('交接已受理,等 BOOT 上总线…');
|
||||
var b = await waitBootOnBus(10, 800);
|
||||
if (b && b.boot) {
|
||||
window.bootEcatIsBoot = true;
|
||||
setEcatStatus('ok', '已进引导程序');
|
||||
setEcatMsg('BOOT 已在总线应答(0x100A = ' + (b.sw || '—') + '),窗口 10s:可以直接刷写');
|
||||
} else {
|
||||
@@ -293,15 +339,27 @@ export async function bootEcatReboot() {
|
||||
}
|
||||
} catch (e) {
|
||||
setEcatStatus('err', '请求异常');
|
||||
setEcatMsg('交接请求异常');
|
||||
window.addLog('E', 'ECAT刷写', '交接请求异常:' + e);
|
||||
setEcatMsg('交接/跳回请求异常');
|
||||
window.addLog('E', 'ECAT刷写', '交接/跳回请求异常:' + e);
|
||||
} finally {
|
||||
window.bootEcatBusy = false;
|
||||
if (btn) btn.textContent = '让应用回引导程序';
|
||||
refreshBootEcatUi();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 跳回之后等 APP 真的回到总线(与 waitBootOnBus 对称)。
|
||||
*/
|
||||
async function waitAppOnBus(maxTries, gapMs) {
|
||||
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); });
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
/** @description 放弃:丢弃服务端缓冲。总线此刻本来就没动过(推只到主站内存)。 */
|
||||
export async function bootEcatAbort() {
|
||||
if (window.bootEcatBusy) return;
|
||||
|
||||
Reference in New Issue
Block a user