Files
DCU_DesktopController/scripts/browser_check_period.mjs
T
cbb c189a8563e feat(motor): ESP32 侧适配 EtherCAT native 后端, 清理 Python 时代残留
tools/ecat 后端已从 Python(ecat_server.py, 已删除)切到原生守护进程
build_native/ecat_native.exe。本仓(ESP32 电机控制台)的 EtherCAT 传输经
/ecat/api/* 代理打它, 这次把代理实现与全部相关措辞对齐到 native。

代理与自启(esp32_server.py):
- ECAT_ENTRY/ECAT_PYW(src/ecat_server.py + venv/pythonw) → ECAT_NATIVE_EXE
  (build_native/ecat_native.exe); autostart 直接 Popen 单文件 exe, 不再经
  venv/pythonw; 清场/--parent-pid/端口漂移三条契约 native 原样镜像, 注释同步
- 文件头与代理段里 ecat_server.py 措辞订正为 native

传输层注释(js/motor/*):
- main/transport/transport-ecat/refresh-rate 里"后端是 ecat_server.py /
  用 venv 拉起"等 Python 时代说法统一改为 ecat_native.exe
- MOT_SLOTS_PER_PDO/MOTOR_SLOT_COUNT 引用改指 ecat_native.c(同名存在);
  MOTOR_SCAN_CYCLES native 无此常量, 改按真实来源写 ⌈27/7⌉=4
- main.js 环境错误分类正则 /venv|pysoem|Npcap/ → /Npcap|build_native|
  守护进程|全被占/: 前两者 native 永不吐给前端(死分支), 后者对齐 autostart
  真实文案, 让"exe 没编译"这类错误不被 140 字截断

README/motor.html 同步; 新增 scripts/browser_check_period.mjs(内核级 PDO
周期热调的浏览器实测, 与既有 browser_check_*.mjs 同族)。

验证: py_compile esp32_server.py 通过; 6 个改动 JS + 新脚本 node --check
(ESM)全过; 正则对三类 native 错误命中、对废弃 venv/pysoem 不命中;
js/ 与 scripts/ 重扫 ecat_server|venv|pysoem 已空。
2026-09-05 03:56:52 +08:00

88 lines
5.1 KiB
JavaScript
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.
/**
* 浏览器实测:内核级 PDO 周期热调(250µs=4kHz)+ 持久化 + µs 显示。
* 单测(test_motor_transport.mjs 的 i4 段)只验传输层契约;这里验真页面真后端:
* - setPdoPeriod 走 /motor/period,绝不碰 motor/start
* (后者会停 PDO 循环,停手 >1s 从站掉 OP —— 把板子弄楔死的那条路)
* - 状态栏亚毫秒显示 µs,自旋/E 核标注正确
* - 页面重载后 ensureStarted 自动带回持久化周期(内核级档位跨服务重启存活)
* - 换回 4ms 后自旋标注消失(自动退回内核睡眠,不白烧核)
*/
const CDP = "http://127.0.0.1:9333";
const list = await (await fetch(CDP + "/json/list")).json();
const page = list.find((t) => t.type === "page");
const sock = new WebSocket(page.webSocketDebuggerUrl);
let id = 0; const pending = new Map();
await new Promise((r, j) => { sock.onopen = r; sock.onerror = j; });
sock.onmessage = (e) => { const m = JSON.parse(e.data); if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id); } };
const send = (method, params = {}) => { const myId = ++id; return new Promise((res) => { pending.set(myId, res); sock.send(JSON.stringify({ id: myId, method, params })); }); };
async function ev(expr) {
const r = await send("Runtime.evaluate", { expression: expr, awaitPromise: true, returnByValue: true });
return r.result && r.result.result && r.result.result.value;
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let pass = 0, fail = 0; const out = [];
const check = (n, c, d) => { if (c) { pass++; out.push(" \u2713 " + n); } else { fail++; out.push(" \u2717 " + n + (d ? " \u2014 " + d : "")); } };
await send("Runtime.enable"); await send("Page.enable");
const PORT = Number(process.argv[2] || 8080);
const PAGE = `http://127.0.0.1:${PORT}/pages/motor.html`;
/* 干净起点:ECAT 链路,清掉周期/节拍残留偏好(后端保持当前值不动) */
await send("Page.navigate", { url: PAGE }); await sleep(2000);
await ev(`localStorage.setItem("dcu_motor_transport","ecat");
localStorage.removeItem("dcu_motor_ui_v2");
localStorage.removeItem("dcu_ecat_period_ms");
localStorage.removeItem("dcu_ecat_spin"); "ok"`);
await send("Page.navigate", { url: PAGE }); await sleep(4000);
/* 请求计数探针:区分 /motor/period 与 /motor/start */
await ev(`(() => {
window.__per = 0; window.__start = 0;
if (!window.__origFetch) window.__origFetch = window.fetch;
window.fetch = function (u) { const s = String(u);
if (s.includes("motor/period")) window.__per++;
if (s.includes("motor/start")) window.__start++;
return window.__origFetch.apply(this, arguments); };
return "ok"; })()`);
const pre = JSON.parse(await ev(`JSON.stringify({tp: motorDbg.state.tpId, conn: motorDbg.state.connected})`));
check("已在 ecat 链路且已连接", pre.tp === "ecat" && pre.conn === true, JSON.stringify(pre));
out.push("\n[A] setPdoPeriod(0.25):走 /motor/period,不碰 motor/start");
const startBefore = await ev(`window.__start`);
const got = await ev(`motorDbg.state.tp.setPdoPeriod(0.25)`);
await sleep(1200);
check("返回新周期 0.25", got === 0.25, String(got));
const cnt = JSON.parse(await ev(`JSON.stringify({per: window.__per, start: window.__start})`));
check("命中 /motor/period 一次", cnt.per === 1, JSON.stringify(cnt));
check("没碰 /motor/start(停循环=看门狗掉OP的楔死路径)",
cnt.start === startBefore, JSON.stringify({ cnt, startBefore }));
let st = JSON.parse(await ev(`JSON.stringify({txt: document.querySelector("#devStatus").textContent, dbg: motorDbg.state.tp.debug()})`));
out.push(" 状态栏: " + st.txt);
check("状态栏显示 PDO 250µs(亚毫秒用 µs 不用 0.25ms)", /PDO 250µs/.test(st.txt), st.txt);
check("亚毫秒周期自动自旋并标注(⚡自旋节拍)", /自旋节拍/.test(st.txt), st.txt);
check("debug().periodMs === 0.25", st.dbg.periodMs === 0.25, JSON.stringify(st.dbg));
check("周期持久化到 localStorage",
(await ev(`localStorage.getItem("dcu_ecat_period_ms")`)) === "0.25");
out.push("\n[B] 页面重载:ensureStarted 自动带回 0.25ms(跨重启档位不丢)");
await send("Page.navigate", { url: PAGE }); await sleep(4000);
st = JSON.parse(await ev(`JSON.stringify({txt: document.querySelector("#devStatus").textContent, per: motorDbg.state.tp.debug().periodMs})`));
check("重载后仍是 PDO 250µs", /PDO 250µs/.test(st.txt) && st.per === 0.25, JSON.stringify(st));
out.push("\n[C] 换回 4ms:µs 与自旋标注都消失(自动退回内核睡眠)");
await ev(`motorDbg.state.tp.setPdoPeriod(4)`);
await sleep(1200);
st = JSON.parse(await ev(`JSON.stringify({txt: document.querySelector("#devStatus").textContent})`));
out.push(" 状态栏: " + st.txt);
check("显示 PDO 4ms", /PDO 4ms/.test(st.txt), st.txt);
check("4ms 自动退回内核睡眠,无自旋标注", !/自旋/.test(st.txt), st.txt);
check("localStorage 更新为 4", (await ev(`localStorage.getItem("dcu_ecat_period_ms")`)) === "4");
console.log(out.join("\n"));
console.log("\n" + "=".repeat(56));
console.log(`通过 ${pass} 项,失败 ${fail} 项`);
sock.close();
process.exit(fail === 0 ? 0 : 1);