/** * 浏览器实测:ECAT 刷新速度 1ms(不限速档)。 * 单测只验了纯逻辑;这里验真机行为: * - 输入框 min/step 随链路变(否则浏览器把合法的 1ms 标成 :invalid) * - 1ms 真的比 50ms 快,且拿到实测频率而不是理论 1000Hz * - 不限速档**不**亮卡顿黄标(每拍撞上在途请求是预期行为) * - 切回 WiFi 时 1ms 不会被带过去(wifi 下限 50ms) */ 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"); /* 页面服务端口可传参:node scripts/xxx.mjs [port](默认 8080) */ 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"); "ok"`); await send("Page.navigate", { url: PAGE }); await sleep(3500); /* 请求计数探针 */ await ev(`(() => { window.__cnt = 0; if (!window.__origFetch) window.__origFetch = window.fetch; window.fetch = function (u) { if (String(u).includes("motor/state")) window.__cnt++; return window.__origFetch.apply(this, arguments); }; return "ok"; })()`); async function measure(sec) { await ev(`window.__cnt = 0; "ok"`); await sleep(sec * 1000); return await ev(`window.__cnt`) / sec; } const pre = JSON.parse(await ev(`JSON.stringify({tp: motorDbg.state.tpId, conn: motorDbg.state.connected, eff: motorDbg.state.refreshIntervalMs})`)); out.push("\n[前置] " + JSON.stringify(pre)); check("已在 ecat 链路且已连接", pre.tp === "ecat" && pre.conn === true, JSON.stringify(pre)); out.push("\n[A] ECAT 下输入框的 min/step 应放开到 1"); let dom = JSON.parse(await ev(`JSON.stringify({min: document.querySelector("#refreshRate").min, step: document.querySelector("#refreshRate").step})`)); check("min=1(否则浏览器校验会把 1ms 判成非法)", dom.min === "1", JSON.stringify(dom)); check("step=1(微调箭头能落到 1ms 档)", dom.step === "1", JSON.stringify(dom)); out.push("\n[B] 基线:ECAT 默认 50ms"); const hz50 = await measure(3); out.push(` 实测 ${hz50.toFixed(1)} Hz`); check("50ms 档约 20Hz", hz50 > 12 && hz50 < 30, `实测 ${hz50.toFixed(1)}`); out.push("\n[C] 设成 1ms —— 应被接受且明显更快"); await ev(`(() => { const i=document.querySelector("#refreshRate"); i.value="1"; i.dispatchEvent(new Event("change")); return "ok"; })()`); await sleep(800); let st = JSON.parse(await ev(`JSON.stringify({eff: motorDbg.state.refreshIntervalMs, val: document.querySelector("#refreshRate").value, valid: document.querySelector("#refreshRate").checkValidity(), map: motorDbg.state.refreshByTp})`)); check("1ms 被接受(没被钳回 50)", st.eff === 1, JSON.stringify(st)); check("输入框显示 1", st.val === "1", JSON.stringify(st)); check("通过浏览器原生校验(min 已同步)", st.valid === true, JSON.stringify(st)); check("只写进 ecat 那一份,wifi 仍 250", st.map.ecat === 1 && st.map.wifi === 250, JSON.stringify(st)); const hz1 = await measure(4); out.push(` 实测 ${hz1.toFixed(1)} Hz(每拍一次 HTTP 往返,实测约 125Hz 量级)`); check("1ms 明显快于 50ms 档", hz1 > hz50 * 1.5, `1ms=${hz1.toFixed(1)} vs 50ms=${hz50.toFixed(1)}`); check("但达不到 1000Hz(受每拍 HTTP 往返限制,符合预期)", hz1 < 400, `实测 ${hz1.toFixed(1)}`); out.push("\n[D] 不限速档不应亮卡顿黄标"); st = JSON.parse(await ev(`JSON.stringify({stuck: motorDbg.state.tickStuck, cls: document.querySelector("#chSelect").classList.contains("stuck"), title: document.querySelector("#chSelect").title})`)); check("tickStuck 保持 0(每拍撞上在途请求是预期行为,不算卡顿)", st.stuck === 0, JSON.stringify(st)); check("通道下拉框没有 .stuck 黄标", st.cls === false, JSON.stringify(st)); check("没有误导性的卡顿提示文案", !st.title, JSON.stringify(st)); out.push("\n[E] STATUS 面板的 Hz 应是实测值,不是理论 1000Hz"); const hzText = await ev(`document.querySelector("#statusIdLabel").textContent`); out.push(` 面板显示: ${hzText}`); const m = String(hzText).match(/([\d.]+)\s*Hz/); check("面板有 Hz 读数", !!m || /\u2026/.test(String(hzText)), hzText); if (m) { const shown = parseFloat(m[1]); check("显示的不是谎报的 1000Hz", shown < 400, `显示 ${shown}Hz`); check("显示值与实测频率同量级(±60%)", Math.abs(shown - hz1) / Math.max(hz1, 1) < 0.6, `显示 ${shown} vs 实测 ${hz1.toFixed(1)}`); } out.push("\n[F] 切到 WiFi:1ms 不能带过去(wifi 下限 50ms)"); await ev(`(() => { const s=document.querySelector("#transportSelect"); s.value="wifi"; s.dispatchEvent(new Event("change")); return "ok"; })()`); await sleep(1500); st = JSON.parse(await ev(`JSON.stringify({eff: motorDbg.state.refreshIntervalMs, val: document.querySelector("#refreshRate").value, min: document.querySelector("#refreshRate").min, step: document.querySelector("#refreshRate").step})`)); check("WiFi 生效值 250(不是 ECAT 的 1)", st.eff === 250, JSON.stringify(st)); check("WiFi 下 min 收回 50", st.min === "50", JSON.stringify(st)); check("WiFi 下 step 收回 50", st.step === "50", JSON.stringify(st)); /* 在 WiFi 下硬填 1ms,应被钳到 50 */ await ev(`(() => { const i=document.querySelector("#refreshRate"); i.value="1"; i.dispatchEvent(new Event("change")); return "ok"; })()`); await sleep(500); st = JSON.parse(await ev(`JSON.stringify({eff: motorDbg.state.refreshIntervalMs, val: document.querySelector("#refreshRate").value})`)); check("WiFi 下填 1ms 被钳到 50(无线往返撑不住)", st.eff === 50, JSON.stringify(st)); out.push("\n[G] 切回 ECAT:仍记着自己的 1ms"); await ev(`(() => { const s=document.querySelector("#transportSelect"); s.value="ecat"; s.dispatchEvent(new Event("change")); return "ok"; })()`); await sleep(2500); st = JSON.parse(await ev(`JSON.stringify({eff: motorDbg.state.refreshIntervalMs, val: document.querySelector("#refreshRate").value, min: document.querySelector("#refreshRate").min})`)); check("ECAT 生效值回到 1ms", st.eff === 1, JSON.stringify(st)); check("min 重新放开到 1", st.min === "1", JSON.stringify(st)); /* 刷新页面后 1ms 应持久化 */ await send("Page.navigate", { url: PAGE }); await sleep(3000); st = JSON.parse(await ev(`JSON.stringify({eff: motorDbg.state.refreshIntervalMs, val: document.querySelector("#refreshRate").value, map: motorDbg.state.refreshByTp})`)); check("刷新页面后 ECAT 仍是 1ms(持久化 + 加载时不被钳高)", st.eff === 1, JSON.stringify(st)); check("刷新后 wifi 那一份仍是 50(上面手动改过)", st.map.wifi === 50, JSON.stringify(st)); console.log(out.join("\n")); console.log("\n" + "=".repeat(56)); console.log(`通过 ${pass} 项,失败 ${fail} 项`); sock.close(); process.exit(fail === 0 ? 0 : 1);