feat(web): 桌面原生桥 + 配置落盘 + 无设备静默态;排除调试脚本

变更点:
1. esp32_server.py
   - /api/motor/ui-state、/api/lab/ui-state:配置原子落盘 data/*.json(WebView2 清存储仍在)
   - _GuiApi.ecat_api/bridge_info:桌面壳经 GUI 进程裸 socket 直连 ecat_native,不经浏览器 HTTP
2. js/common/desktop-bridge.js(新)
   - 探测 pywebview.api(含 iframe top/parent),transport-ecat 优先走原生桥
3. connection.js + responsive.css + components.css
   - setQuietMode:离线且无传输时 html[data-dcu-calm=1] 掐常驻动画
   - 进度条斜纹仅 .live 挂载,空闲不再按刷新率空转重绘
4. device-link.js
   - 先查 native/health,已健康跳过 motor/start,避免 loop_stop 重进 OP 假超时
   - 周期强制 0.25ms + DC SYNC0 对齐;轮询自愈被配错的 DC/周期
5. motor/main.js + transport-ecat.js
   - 配置双写 localStorage + 磁盘 ui-state;pagehide/sendBeacon 刷盘;导入导出
   - 探测代际 probeGen;ECAT 失败阈值 12;跟踪 bootActive/slaveAl/slaveOp
6. lab/spi_test.html、ota/boot_ecat.js、can_monitor 等跟随桥/静默态
7. .gitignore
   - 排除 scripts/_* 与 anal/collect/measure/trace 等调试脚本、lab 产物、data/ 运行态
This commit is contained in:
cbb
2026-09-20 22:58:00 +08:00
parent e91d53e0fc
commit 383535bd1d
23 changed files with 2060 additions and 283 deletions
+50 -12
View File
@@ -24,6 +24,7 @@ var ESP32_FAIL_ONLINE = 3; /* 连续失败 ≥3 判离线 */
var ESP32_POLL_MS = 1000;
var ECAT_POLL_MS = 2000;
var ECAT_BUS_RETRY_MS = 10000; /* 开总线失败后的重试间隔 */
var ecatDcGuardBusy = false; /* DC/周期自愈 in-flight */
var LS_ECAT_PERIOD = "dcu_ecat_period_ms";
var LS_ECAT_SPIN = "dcu_ecat_spin";
var LS_ECAT_ADAPTER = "dcu_ecat_adapter";
@@ -164,9 +165,13 @@ function createOwner() {
}
/**
* 开总线:status →(未开则)open 网卡 → motor/start 进 OP + 起 PDO 循环。
* 开总线:health →(未就绪则)status →(未开则)open 网卡 → motor/start 进 OP + 起 PDO 循环。
* 与 motor/transport-ecat ensureOpen/ensureStarted 同源,由外壳 Owner 在
* 控制台启动时强制执行,用户不必先去电机页把链路切到 EtherCAT。
*
* 已健康(bus_open + loop)时**直接复用**,不打 motor/start:
* 那条路由会先 loop_stop 再重进 OP,和电机页探测撞车时会假超时
* (横幅 signal timed out)并平白增加 SSC 楔死风险。
*/
function bootstrapEcatBus() {
if (stopped || ecatBusBusy) return;
@@ -180,26 +185,44 @@ function createOwner() {
var body = {};
try {
var v = parseFloat(localStorage.getItem(LS_ECAT_PERIOD));
if (v > 0) body.period_ms = v;
/* 无显式值 / 值过慢 → 固定 0.25ms。1ms+DC 会让发送出口回到 ~1s。 */
if (v > 0 && v <= 0.5) body.period_ms = v;
else body.period_ms = 0.25;
var sp = localStorage.getItem(LS_ECAT_SPIN);
if (sp === "0") body.spin = false;
else if (sp === "1") body.spin = true;
} catch (_) {}
} catch (_) { body.period_ms = 0.25; }
/* EtherCAT 控制必须 DC SYNC0≈PDO 周期,且周期应保持 250µs 档 */
{
var per = Number(body.period_ms) || 0.25;
if (per > 0.5) per = 0.25;
body.period_ms = per;
var us = Math.round(per * 1000);
if (us < 250) us = 250;
body.dc_cycle_us = us;
body.dc_shift_us = 0;
}
ecatPost("status", {}, 6000).then(function (st) {
var open = !!(st && st.ok);
return open ? Promise.resolve(st)
: ecatPost("open", { substr: adapter }, 20000);
}).then(function (r) {
if (r && r.ok === false) {
throw new Error((r.log && r.log.join(" / ")) || "open 失败");
ecatPost("native/health", {}, 3000).then(function (h) {
if (h && h.ok && h.bus_open && h.loop) {
ecatBusReady = true;
return null; /* 已在跑:跳过 open + motor/start */
}
return ecatPost("motor/start", body, 30000);
return ecatPost("status", {}, 6000).then(function (st) {
var open = !!(st && st.ok);
return open ? Promise.resolve(st)
: ecatPost("open", { substr: adapter }, 20000);
}).then(function (r) {
if (r && r.ok === false) {
throw new Error((r.log && r.log.join(" / ")) || "open 失败");
}
return ecatPost("motor/start", body, 30000);
});
}).then(function (r) {
if (r && r.ok === false) {
throw new Error((r.log && r.log.join(" / ")) || "motor/start 失败");
}
ecatBusReady = true;
if (r) ecatBusReady = true;
}).catch(function () {
/* 网线没插/从站未上电:退避后由 pollEcat 再触发 */
}).then(function () {
@@ -227,6 +250,21 @@ function createOwner() {
if (e.busOpen && e.loop) ecatBusReady = true;
else if (ecatBusReady && !e.busOpen) ecatBusReady = false; /* 总线掉了 → 允许重开 */
publish();
/* 自愈:DC/周期被配错时发送出口秒级 —— 2s 轮询里拉回 250µs */
if (e.busOpen && e.loop && !ecatDcGuardBusy) {
ecatDcGuardBusy = true;
ecatPost("native/health", {}, 2000).then(function (h) {
if (!h || !h.ok) return null;
var dcs = Number(h.dc_cycle_us);
var per = Number(h.period_ms);
if (h.dc === 1 && dcs === 250 && (h.period_ms == null || Math.abs(per - 0.25) < 0.001)) return null;
return ecatPost("motor/start", {
period_ms: 0.25, dc_cycle_us: 250, dc_shift_us: 0
}, 30000);
}).catch(function () {}).then(function () {
ecatDcGuardBusy = false;
});
}
bootstrapEcatBus();
}).catch(function () {
/* 字段访问/异常不得让 ECAT 轮询永久停摆 */