Files
DCU_DesktopController/scripts/browser_check_1ms.mjs
T
cbb 2d1743ca4f feat(motor): 支持 WiFi 与 EtherCAT 双链路传输
- 抽象电机传输层,WiFi 与 EtherCAT 实现分离,解码、状态面板、波形和命令序列共用
- 新增 EtherCAT 后端反向代理与自动拉起,解决跨端口 CORS 与预检失败问题
- 刷新速度按链路独立存储,支持 EtherCAT 低延迟读取与 WiFi 稳定轮询
- 更新电机页链路选择、不支持页签置灰样式、README 说明及相关测试脚本
2026-09-04 12:18:06 +08:00

123 lines
7.9 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.
/**
* 浏览器实测: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);