Files
DCU_DesktopController/lab/lan9252_pm.html
T
cbb 209a869f96 feat(motor): 反馈刷新率 Hz 适配(0x0400/0x0401 扩展格式)+ ECAT 密排槽与判活自适应
反馈刷新率(配套电机固件 v0.4.1:0x0400 DLC4/0x0401 DLC2 扩展格式,
选择器 0x0100=STATUS · 0x0101=CONFIG 帧,协议值为周期 ms):
- 「节点 ID」页签新增 STATUS/CONFIG 刷新率读取/写入;界面单位一律 Hz
  (写入按 周期ms=round(1000/Hz) 换算下发,读取反算 Hz 并附原始周期,
  取整有损如实展示),0=停发,写入掉电保存;配置值以「读取」回读电机为准
- WiFi 应答回执走 0xC1 per-ID 快照(/can/feedback 全量 entries):
  GD32 0xC0 ACK 仅 1 字节状态、不携带电机回帧;同 ID 回帧快照只存最新
  一条,故一次快照只配一次请求(STATUS/CONFIG 分开读写)
- ECAT 回执走批量接收区 takeNodeIdReport,按长度分派 2B 旧格式/4B GET/5B SET ACK
- GET_NODE_ID 迁移到扩展选择器 0x0000,兼容旧固件 2B 节点 ID 应答
  (SET DLC2 旧格式行为不变)
- transport-wifi 新增 fetchFeedbackFull(全量 CAN 反馈快照)

EtherCAT/基础设施(密排槽固件 1.6.21 配套):
- 快照槽位 5→9 一拍整轮(扫描周期数 6→1);判活阈值随实测 seq 间隔
  自适应 clamp(3×EWMA, 2s, 30s),秒级回报档不再被固定 2s 误判离线;
  STATUS 面板 Hz 明确为网页到达口径(按槽 seq 实测电机回报率的方案已否掉)
- esp32_server.py:ecat_native 单实例收紧(后起独占 + taskkill 残留实例,
  双实例并存会抢 Npcap 致后起者抓不到从站)+ native stdout 落盘
- flash_gd32.py:刷写后校验「GD32 真的在跑新代码」(版本变化或 uptime 归零),
  防"写入≠在跑"假台账
- lab:新增 LAN9252 电源诊断台、PDO 布局页;SPI 测速并入 EtherCAT 接收看板
  并更名「SPI/ECAT 通信测速」;移除 continuity_test.html;新增 ECAT 单拍耗时
  优化报告;/_local/ 排除入库;测试同步更新(62+58 项全过)
2026-09-12 20:10:47 +08:00

580 lines
26 KiB
HTML
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.
<!DOCTYPE html>
<!--
lan9252_pm.html - LAN9252 芯片级电源管理诊断台
─────────────────────────────────────────────
风格复用全站设计体系(css/base.css + layout.css + components.css,与 OTA 中继/电机页同源),
布局沿用 .main-layout 双列栅格:左 400px 操作列(休眠/恢复),右侧观测列(状态仪表+日志)。
设备连接不设独立区块:直接读全局 localStorage['dcu_esp_ip'](js/common/base-url.js 同 key
同默认值 192.168.31.127,与 OTA/CAN/电机页共享,改 IP 请到 OTA 页)。
设备侧(DCU_APP 1.6.23):SPI 0xA2 action 30~34 → EscLan9252::pmDrive()
30(0x1E)=读状态 31(0x1F)=进 D1 32(0x20)=进 D2 33(0x21)=进 D3 34(0x22)=手动唤醒
NRST 兜底复用 action 20/21 → EscLan9252::nrstDrive()(拉低/推挽高 = 复位脉冲)
HTTP 链路:POST {ip}/spi/send body={type:"A2", data:"<action hex>", timeout_ms}
响应 recv hex 首字节 = status:bit0=READY bit1=PM_SLEEP_EN bit3:2=PM_MODE;
0xEE=无效 action 0xE1=HBI 未就绪;唤醒另有 1=成功 0=超时。
恢复可靠性(为什么睡不死):ESP32↔GD32 的 SPI 链路与 LAN9252 完全解耦(独立外设/中断),
芯片睡得再深 0xA2 命令照样送达 GD32;NRST 硬复位覆盖 LAN9252 的一切软件可达状态
(D3 睡死/HBI 假死都在内);GD32 自身挂了还有断电重启兜底(固件 hardwareReset 自动重走
受控复位序列)。页面常驻 2s 自动状态轮询,睡没睡一目了然。
-->
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LAN9252 电源管理台</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="../css/base.css?v=857f83a5">
<link rel="stylesheet" href="../css/layout.css?v=acd17001">
<link rel="stylesheet" href="../css/components.css?v=a2c3d4e5">
<link rel="stylesheet" href="../css/responsive.css?v=46449483">
<style>
/* 本页专属少量补充(令牌/组件全走 base/layout/components) */
.pm-grid { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:10px; margin-bottom:14px; }
.pm-cell { background:var(--bg-input); border:1px solid var(--border); border-radius:var(--radius-sm); padding:10px 12px; }
.pm-cell .k { color:var(--text-label); font-size:.7rem; letter-spacing:.08em; display:block; margin-bottom:2px; }
.pm-cell .v { font-family:var(--font-display); font-size:1.5rem; font-weight:700; }
.v-ok { color:var(--green); } .v-warn { color:var(--gd32-hi); } .v-bad { color:var(--red); } .v-dim { color:var(--text-label); }
.pm-sleep-banner { display:none; margin-bottom:14px; padding:11px 15px; border-radius:var(--radius-sm);
background:var(--gd32-dim); border:1px solid rgba(252,191,63,.4); color:var(--gd32-hi);
font-weight:600; font-size:.88rem; align-items:center; gap:10px; }
.pm-sleep-banner.show { display:flex; }
.pm-vbtn { display:flex; flex-direction:column; gap:9px; }
.pm-vbtn .btn { width:100%; }
.pm-table { width:100%; border-collapse:collapse; font-size:.78rem; }
.pm-table th,.pm-table td { border:1px solid var(--border); padding:5px 10px; text-align:left; }
.pm-table th { color:var(--text-label); font-weight:600; background:var(--bg-card); }
.pm-table code { font-family:var(--mono); color:var(--mem-hi); }
.pm-log { background:var(--bg-input); border:1px solid var(--border); border-radius:var(--radius-sm);
font-family:var(--mono); font-size:.73rem; padding:10px 12px; height:300px; overflow-y:auto; }
.pm-log div { padding:1px 0; color:var(--text-dim); word-break:break-all; }
.pm-log .t { color:var(--text-label); }
.pm-log .ok { color:var(--green); } .pm-log .warn { color:var(--gd32-hi); } .pm-log .err { color:var(--red); }
.btn-armed { background:var(--red) !important; color:#fff !important; animation:blink 1s infinite; }
.pm-note { color:var(--text-dim); font-size:.78rem; margin-top:10px; line-height:1.7; }
.pm-note b { color:var(--gd32-hi); font-weight:600; }
.pm-note code { font-family:var(--mono); color:var(--mem-hi); background:var(--bg-card); border-radius:4px; padding:0 5px; }
.pm-chain { list-style:none; margin:0 0 4px; padding:0; }
.pm-chain li { position:relative; padding:6px 0 6px 34px; font-size:.82rem; color:var(--text-dim); }
.pm-chain li b { color:var(--text); }
.pm-chain li::before { content:attr(data-n); position:absolute; left:0; top:5px; width:22px; height:22px;
border-radius:50%; background:var(--gd32-dim); color:var(--gd32-hi); font-size:.72rem; font-weight:700;
display:flex; align-items:center; justify-content:center; border:1px solid rgba(252,191,63,.4); }
.pm-chain li + li::after { content:""; position:absolute; left:10px; top:-14px; height:14px;
border-left:1px dashed var(--border-strong); }
</style>
</head>
<body>
<div class="container">
<!-- ====== HEADER ====== -->
<header class="header-bar">
<div class="header-brand">
<span class="brand-logo" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round">
<path d="M12 3v8"/><path d="M6.3 6.5a8 8 0 1 0 11.4 0"/><path d="M8 2h8" opacity=".5"/>
</svg>
</span>
<div class="brand-text">
<strong>LAN9252 电源管理台</strong>
<span class="brand-sub">EtherCAT ESC · D1/D2/D3 休眠 · DCU_APP ≥1.6.23</span>
</div>
</div>
<div class="header-status">
<span class="pill offline" id="pillConn"><span class="indicator"></span><span id="pillConnTxt">连接中…</span></span>
<span class="pill" id="pillIp">IP --</span>
</div>
</header>
<!-- ====== MAIN ====== -->
<div class="main-layout">
<!-- 左列:操作 -->
<div>
<div class="card card--mem">
<div class="card-head"><span class="ico">🌙</span> 进入低功耗档</div>
<div class="card-body">
<div class="pm-vbtn">
<button class="btn btn-primary" id="btnD1">进 D1 (31)</button>
<button class="btn btn-primary" id="btnD2">进 D2 (32)</button>
<button class="btn btn-danger" id="btnD3">进 D3 (33)</button>
</div>
<div class="pm-note">
<b>D1</b> 系统时钟关、晶振+PLL 保持(有自动唤醒路径)。<br>
<b>D2</b> PLL 请求关(两 PHY 已掉电才真关)。<br>
<b>D3</b> 晶振/PLL 全关,最省电、<b>只能手动唤醒</b>(二次确认)。<br>
<b>休眠期间 EtherCAT 中断、主站 wkc 报错属预期</b>;ESC 寄存器配置全保留,唤醒后自清睡眠位。
D3 进入前固件自动掉电两 PHY,唤醒后自动恢复(自协商需数秒)。
</div>
</div>
</div>
<div class="card card--gd32">
<div class="card-head"><span class="ico">🛟</span> 恢复链 —— 为什么睡不死</div>
<div class="card-body">
<ul class="pm-chain">
<li data-n="1"><b>action 34 软唤醒</b>(正常路径,≤2ms)</li>
<li data-n="2"><b>NRST 硬复位</b>:PB10 拉低 60ms 释放,等效上电复位,覆盖 D3 睡死/HBI 假死等一切软件状态</li>
<li data-n="3"><b>GD32 断电重启</b>:复位期间 PB10 浮空由 R44 上拉维持,重启后固件 hardwareReset() 自动重走受控复位</li>
</ul>
<div class="btn-row" style="margin-top:8px">
<button class="btn btn-danger" id="btnNrst" style="width:100%">🛟 ECAT_NRST 硬复位 LAN9252</button>
</div>
<div class="pm-note">
<b>可靠性</b>:ESP32↔GD32 的 SPI 链路与 LAN9252 完全解耦(独立外设/中断),芯片睡得再深
<code>0xA2</code> 照样送达 GD32——恢复失败的唯一前提是 ESP32 和 GD32 同时失能,那已不是休眠问题。<br>
NRST 复位会清掉 SSC 已配置的芯片状态,EtherCAT 需主站重新走状态机。
</div>
</div>
</div>
<div class="card card--status">
<div class="card-head"><span class="ico">📋</span> status 字节判读(recv 首字节)</div>
<div class="card-body">
<table class="pm-table">
<tr><th>位/值</th><th>含义</th></tr>
<tr><td><code>bit0</code></td><td>READY:1=D0 且 PLL 稳定</td></tr>
<tr><td><code>bit1</code></td><td>PM_SLEEP_EN:1=低功耗档</td></tr>
<tr><td><code>bit3:2</code></td><td>PM_MODE 寄存器位(仅 SLEEP_EN=1 时有效;未睡眠可能残留旧档)</td></tr>
<tr><td><code>0xEE</code></td><td>无效 action</td></tr>
<tr><td><code>0xE1</code></td><td>HBI 未就绪(勿操作电源)</td></tr>
<tr><td><code>唤醒 1/0</code></td><td>成功 / 10ms 超时</td></tr>
</table>
</div>
</div>
</div>
<!-- 右列:观测 -->
<div>
<div class="card card--mem">
<div class="card-head"><span class="ico">🔋</span> 芯片电源状态 <span style="margin-left:auto;font-size:.68rem;color:var(--text-label);font-weight:400">2s 自动刷新</span></div>
<div class="card-body">
<div class="pm-sleep-banner" id="sleepBanner">🌙 芯片处于低功耗档 —— EtherCAT 通信已中断,点「手动唤醒」恢复</div>
<div class="pm-grid">
<div class="pm-cell"><span class="k">READY</span><span class="v v-dim" id="stReady">--</span></div>
<div class="pm-cell"><span class="k">SLEEP_EN</span><span class="v v-dim" id="stSleep">--</span></div>
<div class="pm-cell"><span class="k">电源档</span><span class="v v-dim" id="stMode">--</span></div>
<div class="pm-cell"><span class="k">原始字节</span><span class="v v-dim" id="stRaw">--</span></div>
</div>
<div class="btn-row" style="margin-top:0">
<button class="btn btn-outline" id="btnStatus">读状态 (30)</button>
<button class="btn btn-success" id="btnWake">⏻ 手动唤醒 (34)</button>
</div>
<div class="pm-note">唤醒轮询上限 10ms(手册标称 ≤2ms)。超时多半是芯片不在睡眠或 HBI 异常——以「读状态」为准。</div>
</div>
</div>
<div class="card card--mem">
<div class="card-head"><span class="ico">📜</span> 操作日志</div>
<div class="card-body" style="padding-top:10px">
<div class="pm-log" id="log"></div>
</div>
</div>
</div>
</div>
</div>
<script>
(function () {
"use strict";
var $ = function (id) { return document.getElementById(id); };
var logBox = $("log");
/* 与 js/common/base-url.js 同 key 同默认值:全局唯一设备 IP,不设独立连接区 */
var LS_IP = "dcu_esp_ip", DEFAULT_IP = "192.168.31.127";
function deviceIp() {
try { var s = localStorage.getItem(LS_IP); if (s && s.trim()) return s.trim(); } catch (e) {}
return DEFAULT_IP;
}
$("pillIp").textContent = "IP " + deviceIp();
function log(msg, cls) {
var d = document.createElement("div");
if (cls) d.className = cls;
var t = document.createElement("span");
t.className = "t";
t.textContent = "[" + new Date().toLocaleTimeString("zh-CN", { hour12: false }) + "] ";
d.appendChild(t);
d.appendChild(document.createTextNode(msg));
logBox.appendChild(d);
while (logBox.childNodes.length > 300) logBox.removeChild(logBox.firstChild);
logBox.scrollTop = logBox.scrollHeight;
}
/* 通用 0xA2 透传:actionHex 单字节 hex。recv hex 首字节 = status */
function sendA2(actionHex, timeoutMs) {
return fetch("http://" + deviceIp() + "/spi/send", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ type: "A2", data: actionHex, timeout_ms: timeoutMs || 1500 }),
signal: AbortSignal.timeout((timeoutMs || 1500) + 2000),
}).then(function (r) {
return r.json().then(function (j) {
if (!r.ok || j.status !== "ok") throw new Error(j.message || ("HTTP " + r.status));
var hex = String(j.recv || "");
if (hex.length < 2) throw new Error("响应 payload 异常: " + JSON.stringify(hex));
return parseInt(hex.slice(0, 2), 16);
});
});
}
var MODES = ["D0", "D1", "D2", "D3"];
/* 有效电源档:SLEEP_EN=0 时芯片在 D0 工作(EtherCAT/READY 正常),
* PM_MODE 位可能残留上次进档写入的值(板级实测常停在 D2=0x09)。
* 只展示寄存器 MODE 会误导;有效档以 SLEEP_EN 为准。 */
function effectiveMode(b) {
if (b === 0xEE || b === 0xE1) return null;
if ((b & 2) === 0) return "D0";
return MODES[(b >> 2) & 3];
}
function rawMode(b) {
return MODES[(b >> 2) & 3];
}
function describe(b) {
if (b === 0xEE) return "无效 action (0xEE)";
if (b === 0xE1) return "HBI 未就绪 (0xE1),请确认 GD32 已启动且 ECAT init 通过";
var eff = effectiveMode(b), reg = rawMode(b);
var extra = ((b & 2) === 0 && reg !== "D0")
? "(寄存器 PM_MODE 残留 " + reg + ",未睡眠 → 有效 D0)" : "";
return "READY=" + (b & 1) + " SLEEP_EN=" + ((b >> 1) & 1) +
" 有效档=" + eff + " 寄存器MODE=" + reg + extra +
" (0x" + b.toString(16).toUpperCase().padStart(2, "0") + ")";
}
function setCell(id, text, cls) {
var e = $(id); e.textContent = text; e.className = "v " + cls;
}
function renderStatus(b) {
var ready = (b & 1) !== 0, sleep = (b & 2) !== 0;
var mode = effectiveMode(b) || MODES[(b >> 2) & 3];
setCell("stReady", ready ? "1" : "0", ready ? "v-ok" : "v-warn");
setCell("stSleep", sleep ? "1" : "0", sleep ? "v-warn" : "v-ok");
setCell("stMode", mode, mode === "D0" ? "v-ok" : "v-warn");
setCell("stRaw", "0x" + b.toString(16).toUpperCase().padStart(2, "0"), "v-dim");
$("sleepBanner").className = "pm-sleep-banner" + (sleep ? " show" : "");
}
/* 跨标签/重载保持睡眠:HBI 读会唤醒芯片,页面销毁后现场丢失会导致
* 切回时自动 poll(1E) 把睡眠叫醒。用 sessionStorage 记档位,重载时跳过自动读。 */
var SS_SLEEP = "lan9252_pm_asleep";
function saveSleepFlag(n) {
try { sessionStorage.setItem(SS_SLEEP, String(n)); } catch (e) {}
}
function clearSleepFlag() {
try { sessionStorage.removeItem(SS_SLEEP); } catch (e) {}
}
function loadSleepFlag() {
try {
var v = sessionStorage.getItem(SS_SLEEP);
if (v === "1" || v === "2" || v === "3") return parseInt(v, 10);
} catch (e) {}
return 0;
}
function renderAssumedSleep(n) {
/* 不读设备,按上次会话档位恢复 UI(芯片可能仍睡着) */
setCell("stReady", "--", "v-dim");
setCell("stSleep", "1", "v-warn");
setCell("stMode", "D" + n, "v-warn");
setCell("stRaw", "未读", "v-dim");
$("sleepBanner").textContent =
"🌙 上次会话处于 D" + n + " —— 已跳过自动状态读(避免 HBI 读唤醒)。点「手动唤醒」或「读状态」继续(读会唤醒)";
$("sleepBanner").className = "pm-sleep-banner show";
}
/* ---- 自动状态轮询:连接后 2s 一次;读到睡眠态即暂停(1.6.24 实测 HBI 读
* 事务本身会唤醒睡眠中的芯片,轮询等于不停叫醒它);连败 3 次标红停轮。
* pollGen:丢弃 enterMode/唤醒 之后仍在途的过期 1E 响应,避免竞态覆盖 UI。 ---- */
var pollFail = 0, pollTimer = null, pollPaused = false, pollGen = 0;
function setConn(ok, text) {
var p = $("pillConn");
p.className = "pill " + (ok ? "online" : "offline");
$("pillConnTxt").textContent = text;
}
function bumpPollGen() { return ++pollGen; }
/* 仅在确认芯片已离开睡眠(唤醒成功 / 读到 SLEEP_EN=0 / NRST)时清 flag */
function markAwake() {
clearSleepFlag();
$("sleepBanner").textContent =
"🌙 芯片处于低功耗档 —— EtherCAT 通信已中断,点「手动唤醒」恢复";
}
function pollOnce() {
if (pollPaused) return;
var gen = bumpPollGen();
sendA2("1E").then(function (b) {
if (gen !== pollGen || pollPaused) return; /* 过期响应 */
pollFail = 0;
setConn(true, "已连接");
renderStatus(b);
if ((b & 2) !== 0 && b !== 0xEE && b !== 0xE1) {
/* 睡眠态:本读已把它唤醒;暂停轮询等用户主动操作 */
pollPaused = true;
setConn(true, "睡眠中 · 自动刷新已暂停");
log("检测到睡眠态(本次读取已将其唤醒)。轮询暂停;点「读状态」或「唤醒」后恢复。", "warn");
} else {
if (b !== 0xEE && b !== 0xE1) markAwake();
pollTimer = setTimeout(pollOnce, 2000);
}
}).catch(function () {
if (gen !== pollGen || pollPaused) return;
pollFail++;
if (pollFail >= 3) {
setConn(false, "连接失败 · 5s 后重试");
pollTimer = setTimeout(probe, 5000);
} else {
pollTimer = setTimeout(pollOnce, 2000);
}
});
}
function resumePolling() {
if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; }
pollPaused = false;
bumpPollGen();
pollTimer = setTimeout(pollOnce, 2000);
}
/* ESP32 可达性探测:只打 /system/status,不碰 LAN9252 HBI(睡眠恢复路径用) */
function probeEspOnly() {
setConn(false, "连接中…");
fetch("http://" + deviceIp() + "/system/status", { signal: AbortSignal.timeout(2500) })
.then(function (r) {
if (!r.ok) throw new Error("HTTP " + r.status);
setConn(true, "上次睡眠中 · 轮询已暂停");
log("ESP32 可达: " + deviceIp() + "(未读 LAN9252,保持睡眠)", "ok");
})
.catch(function (e) {
setConn(false, "连接失败 · 5s 后重试");
log("连接失败: " + e.message + "(IP 取自 OTA 页的设备地址 " + deviceIp() + ")", "err");
pollTimer = setTimeout(probeEspOnly, 5000);
});
}
function probe() {
setConn(false, "连接中…");
fetch("http://" + deviceIp() + "/system/status", { signal: AbortSignal.timeout(2500) })
.then(function (r) {
if (!r.ok) throw new Error("HTTP " + r.status);
setConn(true, "已连接");
log("设备可达: " + deviceIp(), "ok");
pollOnce();
})
.catch(function (e) {
setConn(false, "连接失败 · 5s 后重试");
log("连接失败: " + e.message + "(IP 取自 OTA 页的设备地址 " + deviceIp() + ")", "err");
pollTimer = setTimeout(probe, 5000);
});
}
function busy(btn, fn) {
btn.addEventListener("click", function () {
btn.disabled = true;
Promise.resolve().then(fn).catch(function (e) { log("失败: " + e.message, "err"); })
.then(function () { btn.disabled = false; });
});
}
/* 唤醒/NRST 后重拉 EtherCAT 主站 OP。
* 实测(2026-09-12):软唤醒后芯片 READY≠主站可进 OP——ESC SM/FMMU 在睡眠后失效,
* motor/start 卡在 SAFEOP AL=0x001E Invalid input configuration(偶发能过、多数失败),
* 电机页还会每 2s 探测重试把同一错误刷屏。
* 可靠路径只有:NRST 清 SSC → 等 EEPROM/ESC 启动 → motor/start 走 INIT→PREOP→SAFEOP→OP。
* 故唤醒后一律先 NRST,不再「先试 start 失败再 NRST」。 */
function ecatPost(path, body, timeoutMs) {
return fetch("/ecat/api/" + path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body || {}),
signal: AbortSignal.timeout(timeoutMs || 30000),
}).then(function (r) {
return r.json().then(function (j) {
return { ok: !!(j && j.ok), log: (j && j.log) || [], json: j };
});
});
}
function ecatMotorStart(timeoutMs) {
return ecatPost("motor/start", {}, timeoutMs || 30000);
}
function ecatEnsureOpen() {
/* bus_open=0 时 motor/start 直接回「未连接」。与电机页 ensureOpen 同源。 */
return ecatPost("native/health", {}, 6000).then(function (h) {
var open = h.json && h.json.bus_open;
if (open) return { ok: true, log: ["总线已 open"], json: h.json };
var adapter = "I210";
try { adapter = localStorage.getItem("dcu_ecat_adapter") || "I210"; } catch (e) {}
log("总线未 open,调用 /api/open (" + adapter + ") …");
return ecatPost("open", { substr: adapter }, 20000);
});
}
function nrstPulse() {
log("NRST 拉低 100ms …", "warn");
return sendA2("15").then(function () {
return new Promise(function (r) { setTimeout(r, 100); });
}).then(function () {
return sendA2("14");
}).then(function () {
/* EEPROM 加载 + HBI 探针:2s 不够时 start 会「重枚举没找到从站」 */
log("NRST 已释放,等待 ESC/EEPROM 启动 4s …");
return new Promise(function (r) { setTimeout(r, 4000); });
});
}
function recoverEcatMaster() {
log("重连 EtherCAT 主站:stop → open → NRST → motor/start …", "warn");
/* 先停在跑的 PDO 循环,避免与 NRST/init 抢总线 */
return ecatPost("motor/stop", {}, 8000).catch(function () { return { ok: true, log: ["stop 跳过"] }; })
.then(function (s) {
if (s && s.log && s.log.length) log("stop: " + s.log.join(";"));
return ecatEnsureOpen();
})
.then(function (op) {
if (!op.ok) log("open 失败: " + op.log.join(";"), "warn");
else log("open: " + op.log.join(";"), "ok");
return nrstPulse();
})
.then(function () {
return sendA2("1E");
})
.then(function (b) {
log("复位后状态: " + describe(b));
renderStatus(b);
markAwake();
return ecatMotorStart(30000);
})
.then(function (r) {
if (r.ok) {
log("主站已进 OP:" + r.log.join(";"), "ok");
setConn(true, "已连接");
resumePolling();
return r;
}
log("motor/start 失败:" + r.log.join(";") + " → 再 stop+open+NRST 重试", "err");
return ecatPost("motor/stop", {}, 8000)
.then(function () { return ecatEnsureOpen(); })
.then(function () { return nrstPulse(); })
.then(function () { return ecatMotorStart(30000); })
.then(function (r2) {
if (r2.ok) {
log("第二次重试主站已进 OP:" + r2.log.join(";"), "ok");
setConn(true, "已连接");
} else {
log("两次后仍失败:" + r2.log.join(";") + "。请查网线/供电,或到电机页手动连接。", "err");
}
resumePolling();
return r2;
});
})
.catch(function (e) {
log("重连主站异常: " + e.message + "(ecat_native 未起?)", "err");
resumePolling();
return { ok: false, log: [e.message] };
});
}
busy($("btnStatus"), function () {
return sendA2("1E").then(function (b) {
renderStatus(b);
log("状态: " + describe(b));
if ((b & 2) === 0 && b !== 0xEE && b !== 0xE1) markAwake();
resumePolling();
});
});
busy($("btnWake"), function () {
log("手动唤醒(HBI 写 BYTE_TEST)…");
bumpPollGen();
clearSleepFlag();
return sendA2("22").then(function (b) {
if (b === 1) {
log("唤醒成功: " + describe(b), "ok");
renderStatus(b);
markAwake();
setConn(true, "已连接");
/* 芯片 READY ≠ 主站 OP;必须 NRST 再 start,否则 SAFEOP AL=0x001E */
return recoverEcatMaster();
} else if (b === 0xEE || b === 0xE1) {
log("设备拒绝: " + describe(b), "err");
renderStatus(b);
resumePolling();
} else {
log("唤醒超时(status=0x" + b.toString(16) + "),改走 NRST 硬复位恢复", "err");
renderStatus(b);
return recoverEcatMaster();
}
});
});
function enterMode(n, hex) {
log("进入 D" + n + " …" + (n === 3 ? "(先掉电两 PHY)" : ""));
/* 作废在途 1E,防止旧 poll 响应把睡眠 UI 打回 D0 */
bumpPollGen();
return sendA2(hex).then(function (b) {
if (b === 0xEE || b === 0xE1) {
log("设备拒绝: " + describe(b), "err");
renderStatus(b);
return;
}
log("已进入 D" + n + "(固件置信返回,未读回)。", "warn");
if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; }
pollPaused = true;
setConn(true, "睡眠中 · 轮询已暂停");
saveSleepFlag(n);
renderStatus(b);
/* 固件返回可能是新 status 或旧字节;强制按 D{n} 展示并记住 */
setCell("stSleep", "1", "v-warn");
setCell("stMode", "D" + n, "v-warn");
$("sleepBanner").textContent =
"🌙 芯片处于低功耗档 —— EtherCAT 通信已中断,点「手动唤醒」恢复(切页回来会保持此睡眠态)";
$("sleepBanner").className = "pm-sleep-banner show";
});
}
busy($("btnD1"), function () { return enterMode(1, "1F"); });
busy($("btnD2"), function () { return enterMode(2, "20"); });
/* D3 二次确认:3 秒内再点一次才执行 */
(function () {
var armed = false, timer = null;
$("btnD3").addEventListener("click", function () {
if (!armed) {
armed = true;
$("btnD3").classList.add("btn-armed");
$("btnD3").textContent = "⚠ 再点一次确认进 D3";
timer = setTimeout(function () { disarm(); }, 3000);
return;
}
clearTimeout(timer); disarm();
$("btnD3").disabled = true;
enterMode(3, "21").catch(function (e) { log("失败: " + e.message, "err"); })
.then(function () { $("btnD3").disabled = false; });
});
function disarm() {
armed = false;
$("btnD3").classList.remove("btn-armed");
$("btnD3").textContent = "进 D3 (33)";
}
})();
/* NRST 硬复位:0x15 拉低 → 100ms → 0x14 释放 → 等 2s → 重拉主站 */
busy($("btnNrst"), function () {
bumpPollGen();
clearSleepFlag();
return recoverEcatMaster();
});
/* 启动:若上次会话记着睡眠档,跳过自动 1E(会唤醒),只探 ESP32 可达性 */
var restoredSleep = loadSleepFlag();
if (restoredSleep) {
pollPaused = true;
renderAssumedSleep(restoredSleep);
setConn(true, "上次睡眠中 · 轮询已暂停");
log("恢复上次会话:D" + restoredSleep + " 睡眠中。为避免 HBI 读唤醒芯片,已跳过自动状态刷新;点「读状态」或「手动唤醒」继续。", "warn");
probeEspOnly();
} else {
log("就绪。状态每 2s 自动刷新;改设备 IP 请到 OTA 中继页(全局共享)。", "warn");
probe();
}
})();
</script>
</body>
</html>