Files

131 lines
8.5 KiB
JavaScript
Raw Permalink Normal View History

/**
* 浏览器实测:ECAT 刷新速度下限档 6ms(不限速档)。
* 单测只验了纯逻辑;这里验真机行为:
* - 输入框 min/step 随链路变(否则浏览器把合法的 6ms 标成 :invalid)
* - 6ms 真的比 50ms 快,且拿到实测频率而不是理论 166Hz
* - 存量缓存里的旧 1ms 会被抬到 6ms(绘制上限之上没有消费者)
* - 不限速档**不**亮卡顿黄标(每拍撞上在途请求是预期行为)
* - 切回 WiFi 时 6ms 不会被带过去(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](默认 2625) */
const PORT = Number(process.argv[2] || 2625);
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 应放开到 6/1");
let dom = JSON.parse(await ev(`JSON.stringify({min: document.querySelector("#refreshRate").min, step: document.querySelector("#refreshRate").step})`));
check("min=6(否则浏览器校验会把 6ms 判成非法)", dom.min === "6", JSON.stringify(dom));
check("step=1(下限档要能逐 ms 微调)", 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] 设成 6ms —— 应被接受且明显更快");
await ev(`(() => { const i=document.querySelector("#refreshRate"); i.value="6"; 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("6ms 被接受(没被钳回 50)", st.eff === 6, JSON.stringify(st));
check("输入框显示 6", st.val === "6", JSON.stringify(st));
check("通过浏览器原生校验(min 已同步)", st.valid === true, JSON.stringify(st));
check("只写进 ecat 那一份,wifi 仍 250", st.map.ecat === 6 && st.map.wifi === 250, JSON.stringify(st));
const hz1 = await measure(4);
out.push(` 实测 ${hz1.toFixed(1)} Hz(每拍一次 HTTP 往返,6ms 档约 160Hz 量级)`);
check("6ms 明显快于 50ms 档", hz1 > hz50 * 1.5, `6ms=${hz1.toFixed(1)} vs 50ms=${hz50.toFixed(1)}`);
check("但达不到理论 166Hz 以上(受每拍 HTTP 往返限制)", hz1 < 200, `实测 ${hz1.toFixed(1)}`);
out.push("\n[C2] 硬填旧的 1ms —— 应被抬到下限 6ms");
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("旧 1ms 被抬到 6ms", st.eff === 6, JSON.stringify(st));
check("输入框回填 6", st.val === "6", JSON.stringify(st));
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 应是实测值,不是理论 166Hz");
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("显示的不是谎报的理论值", shown < 200, `显示 ${shown}Hz`);
check("显示值与实测频率同量级(±60%)",
Math.abs(shown - hz1) / Math.max(hz1, 1) < 0.6, `显示 ${shown} vs 实测 ${hz1.toFixed(1)}`);
}
out.push("\n[F] 切到 WiFi:6ms 不能带过去(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 的 6)", 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:仍记着自己的 6ms");
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 生效值回到 6ms", st.eff === 6, JSON.stringify(st));
check("min 重新放开到 6", st.min === "6", JSON.stringify(st));
/* 刷新页面后 6ms 应持久化 */
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 仍是 6ms(持久化 + 加载时不被钳高)", st.eff === 6, 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);