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
+54 -10
View File
@@ -40,10 +40,12 @@ function section(t) { out.push(`\n${t}`); }
/* ── stub 后端:模拟 ecat_native 的状态机 ──
* opened=false 时 /api/status 回 {ok:false,log:["未连接"]}(真实行为),
* /api/open 之后才变 ok:true;/api/motor/start 记录被调用次数。 */
* /api/open 之后才变 ok:true;/api/motor/start 记录被调用次数。
* /api/native/health:bus_open/loop 跟 started 联动,供「已健康则跳过 start」快路径。 */
function makeStub(opts) {
const st = { opened: false, started: false, calls: [], openSubstr: null,
failOpen: opts && opts.failOpen, failStart: opts && opts.failStart };
failOpen: opts && opts.failOpen, failStart: opts && opts.failStart,
healthFail: opts && opts.healthFail };
const srv = http.createServer((req, res) => {
let body = "";
req.on("data", (c) => (body += c));
@@ -58,7 +60,17 @@ function makeStub(opts) {
try { p = body ? JSON.parse(body) : {}; } catch (_) {}
if (req.url === "/ecat/api/status") {
return send(st.opened
? { ok: true, slave: { name: "STUB", man: 0, id: 1, rev: 1, state: 8, al_status: 0 } }
? { ok: true, slave: { name: "STUB", man: 0, id: 1, rev: 1,
state: st.started ? 8 : 1, al_status: 0 } }
: { ok: false, log: ["未连接"] });
}
if (req.url === "/ecat/api/native/health") {
if (st.healthFail) return send({ ok: false, log: ["health down"] });
return send(st.opened
? { ok: true, name: "ecat_native", bus_open: 1,
op: st.started ? 1 : 0, loop: st.started ? 1 : 0,
slaves: 1, wkc: st.started ? 3 : 0,
cycles: st.started ? 10 : 0, queue_depth: 0 }
: { ok: false, log: ["未连接"] });
}
if (req.url === "/ecat/api/open") {
@@ -67,16 +79,18 @@ function makeStub(opts) {
return send({ ok: true, log: ["已连接 STUB"],
slave: { name: "STUB", man: 0, id: 1, rev: 1, state: 1, al_status: 0 } });
}
if (req.url === "/ecat/api/close") { st.opened = false; return send({ ok: true, log: [] }); }
if (req.url === "/ecat/api/close") { st.opened = false; st.started = false; return send({ ok: true, log: [] }); }
if (req.url === "/ecat/api/motor/start") {
if (st.failStart) return send({ ok: false, log: [st.failStart] });
st.started = true;
return send({ ok: true, log: [], period_ms: p.period_ms || 4 });
return send({ ok: true, log: [], period_ms: p.period_ms || 4, spin: true });
}
if (req.url === "/ecat/api/motor/state") {
if (!st.opened) return send({ ok: false, log: ["未连接"] });
return send({ ok: true, log: [], ts: Date.now() / 1000, wkc: 3, expected_wkc: 3,
cycles: 10, period_ms: 4, sent_seq: null, sent_frames: 0, queue_depth: 0,
return send({ ok: true, log: [], ts: Date.now() / 1000,
wkc: st.started ? 3 : 0, expected_wkc: 3, cycles: st.started ? 10 : 0,
period_ms: st.started ? 4 : 0, spin: true, pcore_mask: 0,
sent_seq: null, sent_frames: 0, queue_depth: 0,
bridge: { valid: true, ack: 0, rx_count: 0, tx_count: 0, drop: 0, tx_result: 0 },
xrx: null, rx_batch: { count: 0, lost: 0, pending: 0, frames: [] },
head: { active: 0, boot_active: false, win_base: 0, win_count: 0, global_seq: 0 },
@@ -114,20 +128,50 @@ section("[1] probe() 自动开网卡 + 自动起循环(四级链条的 2/3/4
check("periodMs 由后端回报写入", tp.debug().periodMs === 4, String(tp.debug().periodMs));
}
/* ── 2. 已连接时不要重复 open(api_open 会 close 旧 master,反复调会重置总线)── */
section("[2] 已连接则不重复 open(幂等)");
/* ── 2. 已连接且循环在跑时不要重复 open / motor/start(幂等)── */
section("[2] 已连接则不重复 open / motor/start(幂等,健康位快路径)");
{
const { st, srv, port } = await makeStub();
const un = bindFetch(port);
const tp = createEcatTransport(CTX);
await tp.probe();
const opensAfterFirst = st.calls.filter((c) => c === "/ecat/api/open").length;
await tp.probe(); // 第二次探测
const startsAfterFirst = st.calls.filter((c) => c === "/ecat/api/motor/start").length;
await tp.probe(); // 第二次探测:健康位应直接复用
const opensTotal = st.calls.filter((c) => c === "/ecat/api/open").length;
const startsTotal = st.calls.filter((c) => c === "/ecat/api/motor/start").length;
un(); srv.close();
check("首次 probe 调了一次 open", opensAfterFirst === 1, String(opensAfterFirst));
check("首次 probe 调了一次 motor/start", startsAfterFirst === 1, String(startsAfterFirst));
check("已连接后再 probe 不再 open(避免反复重置总线)",
opensTotal === 1, `共 ${opensTotal} 次`);
check("循环已在跑时不再打 motor/start(那条路由会先 loop_stop,假超时/楔死源)",
startsTotal === 1, `共 ${startsTotal} 次`);
check("第二次 probe 仍成功返回 status(健康位路径)", true);
}
/* ── 2b. healthCheck:probe 超时后的轻量复核 ── */
section("[2b] healthCheck:总线已健康时可确认连上,且不碰 motor/start");
{
const { st, srv, port } = await makeStub();
const un = bindFetch(port);
const tp = createEcatTransport(CTX);
await tp.probe();
const startsBefore = st.calls.filter((c) => c === "/ecat/api/motor/start").length;
const data = await tp.healthCheck();
const startsAfter = st.calls.filter((c) => c === "/ecat/api/motor/start").length;
check("healthCheck 返回 status(slave 在)", !!(data && data.slave));
check("healthCheck 不调用 motor/start",
startsAfter === startsBefore, `${startsBefore} → ${startsAfter}`);
check("healthCheck 后 debug().started 为 true", tp.debug().started === true);
/* 循环没跑 → healthCheck 必须抛错,不能把假连接报成在线 */
st.started = false;
let err = null;
try { await tp.healthCheck(); } catch (e) { err = e; }
un(); srv.close();
check("循环未运行时 healthCheck 抛错(不谎报在线)",
err !== null, err && err.message);
}
/* ── 3. 网卡打不开 → 报错要冒泡且**不**标 transient(否则页面一直转圈不给红灯)── */