/** * 浏览器实测:内核级 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);