Files
DCU_DesktopController/scripts/test_ecat_autostart.mjs
T

185 lines
8.6 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.
/**
* test_ecat_autostart.mjs — EtherCAT 自动接入链条自测(Node,无浏览器)
*
* 跑法:node scripts/test_ecat_autostart.mjs [--port 2625]
*
* 测什么:**真的 import 生产代码** transport-ecat.js,验证"自动接入"这条
* 四级链条的每一级都被正确驱动,且失败原因可读。
*
* 为什么要 stub 后端:板子当前 SSC 邮箱楔死(config_map 失败,ecat_native.c 里
* 同样注明必须断电复位 MCU),真实链路走不到 OP。但"自动开网卡→自动进 OP→自动起循环"
* 这三级的**驱动逻辑**必须能独立验证,否则一块坏板子就让这段代码永远测不到。
* 服务自动拉起那一级已在真机实测过(16s 起、带 --parent-pid),见 README。
*/
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import http from "node:http";
const __dir = dirname(fileURLToPath(import.meta.url));
if (typeof globalThis.performance === "undefined") {
globalThis.performance = { now: () => Number(process.hrtime.bigint() / 1000n) / 1000 };
}
/* transport-ecat.js 会读 localStorage(网卡子串),Node 里没有,给个内存版 */
const _ls = new Map();
globalThis.localStorage = {
getItem: (k) => (_ls.has(k) ? _ls.get(k) : null),
setItem: (k, v) => _ls.set(k, String(v)),
removeItem: (k) => _ls.delete(k),
};
const { createEcatTransport } = await import("../js/motor/transport-ecat.js");
let pass = 0, fail = 0;
const out = [];
function check(name, cond, detail) {
if (cond) { pass++; out.push(` \u2713 ${name}`); }
else { fail++; out.push(` \u2717 ${name}${detail ? " \u2014 " + detail : ""}`); }
}
function section(t) { out.push(`\n${t}`); }
/* ── stub 后端:模拟 ecat_native 的状态机 ──
* opened=false 时 /api/status 回 {ok:false,log:["未连接"]}(真实行为),
* /api/open 之后才变 ok:true;/api/motor/start 记录被调用次数。 */
function makeStub(opts) {
const st = { opened: false, started: false, calls: [], openSubstr: null,
failOpen: opts && opts.failOpen, failStart: opts && opts.failStart };
const srv = http.createServer((req, res) => {
let body = "";
req.on("data", (c) => (body += c));
req.on("end", () => {
st.calls.push(req.url);
const send = (o) => {
const b = Buffer.from(JSON.stringify(o), "utf8");
res.writeHead(200, { "Content-Type": "application/json", "Content-Length": b.length });
res.end(b);
};
let p = {};
try { p = body ? JSON.parse(body) : {}; } catch (_) {}
if (req.url === "/ecat/api/status") {
return send(st.opened
? { ok: true, slave: { name: "STUB", man: 0, id: 1, rev: 1, state: 8, al_status: 0 } }
: { ok: false, log: ["未连接"] });
}
if (req.url === "/ecat/api/open") {
if (st.failOpen) return send({ ok: false, log: [st.failOpen] });
st.opened = true; st.openSubstr = p.substr;
return send({ ok: true, log: ["已连接 STUB"],
slave: { name: "STUB", man: 0, id: 1, rev: 1, state: 1, al_status: 0 } });
}
if (req.url === "/ecat/api/close") { st.opened = false; return send({ ok: true, log: [] }); }
if (req.url === "/ecat/api/motor/start") {
if (st.failStart) return send({ ok: false, log: [st.failStart] });
st.started = true;
return send({ ok: true, log: [], period_ms: p.period_ms || 4 });
}
if (req.url === "/ecat/api/motor/state") {
if (!st.opened) return send({ ok: false, log: ["未连接"] });
return send({ ok: true, log: [], ts: Date.now() / 1000, wkc: 3, expected_wkc: 3,
cycles: 10, period_ms: 4, sent_seq: null, sent_frames: 0, queue_depth: 0,
bridge: { valid: true, ack: 0, rx_count: 0, tx_count: 0, drop: 0, tx_result: 0 },
xrx: null, rx_batch: { count: 0, lost: 0, pending: 0, frames: [] },
head: { active: 0, boot_active: false, win_base: 0, win_count: 0, global_seq: 0 },
motors: [] });
}
if (req.url === "/ecat/api/motor/rx") return send({ ok: true, log: [], frames: [], remain: 0 });
return send({ ok: false, log: ["接口不存在: " + req.url] });
});
});
return new Promise((r) => srv.listen(0, "127.0.0.1", () => r({ st, srv, port: srv.address().port })));
}
const CTX = { getBaseUrl: () => "", recordSlow: () => {}, isTransient: () => false };
/** 把 transport 的相对路径 fetch 指到 stub 上 */
function bindFetch(port) {
const real = globalThis.fetch;
globalThis.fetch = (u, i) =>
real(typeof u === "string" && u.startsWith("/") ? `http://127.0.0.1:${port}${u}` : u, i);
return () => { globalThis.fetch = real; };
}
/* ── 1. 未连接时 probe() 应自动 open 再 start ── */
section("[1] probe() 自动开网卡 + 自动起循环(四级链条的 2/3/4 级)");
{
const { st, srv, port } = await makeStub();
const un = bindFetch(port);
const tp = createEcatTransport(CTX);
await tp.probe();
un(); srv.close();
check("先探 status 发现未连接", st.calls[0] === "/ecat/api/status", st.calls[0]);
check("自动调用 /api/open(代替用户去点连接按钮)", st.calls.includes("/ecat/api/open"));
check("open 用默认网卡子串 I210", st.openSubstr === "I210", String(st.openSubstr));
check("随后自动调用 /api/motor/start(进 OP + 起后台循环)", st.started === true);
check("open 在 start 之前", st.calls.indexOf("/ecat/api/open") < st.calls.indexOf("/ecat/api/motor/start"));
check("periodMs 由后端回报写入", tp.debug().periodMs === 4, String(tp.debug().periodMs));
}
/* ── 2. 已连接时不要重复 open(api_open 会 close 旧 master,反复调会重置总线)── */
section("[2] 已连接则不重复 open(幂等)");
{
const { st, srv, port } = await makeStub();
const un = bindFetch(port);
const tp = createEcatTransport(CTX);
await tp.probe();
const opensAfterFirst = st.calls.filter((c) => c === "/ecat/api/open").length;
await tp.probe(); // 第二次探测
const opensTotal = st.calls.filter((c) => c === "/ecat/api/open").length;
un(); srv.close();
check("首次 probe 调了一次 open", opensAfterFirst === 1, String(opensAfterFirst));
check("已连接后再 probe 不再 open(避免反复重置总线)",
opensTotal === 1, `共 ${opensTotal} 次`);
}
/* ── 3. 网卡打不开 → 报错要冒泡且**不**标 transient(否则页面一直转圈不给红灯)── */
section("[3] 打不开网卡时的错误语义");
{
const { srv, port } = await makeStub({ failOpen: "没有找到描述包含 'I210' 的适配器" });
const un = bindFetch(port);
const tp = createEcatTransport(CTX);
let err = null;
try { await tp.probe(); } catch (e) { err = e; }
un(); srv.close();
check("probe 抛错(不静默成功)", err !== null);
check("错误消息带候选网卡线索", err && /适配器/.test(err.message), err && err.message);
check("不标 transient(外部条件不满足,不是瞬时抖动)", err && err.transient === false,
err && String(err.transient));
}
/* ── 4. 进 OP 失败(板子邮箱楔死那类)→ 原因要原样带上来 ── */
section("[4] 进 OP 失败时原因可读");
{
const { srv, port } = await makeStub({
failStart: "config_map 失败: ConfigMapError —— SM0/SM1 配置正常却持续失败=SSC 邮箱地址错位楔死" });
const un = bindFetch(port);
const tp = createEcatTransport(CTX);
let err = null;
try { await tp.probe(); } catch (e) { err = e; }
un(); srv.close();
check("probe 抛错", err !== null);
check("错误里保留楔死关键词(供上层归类成'需复位 MCU')",
err && /楔死/.test(err.message), err && err.message.slice(0, 80));
}
/* ── 5. 换网卡:立即 close 并用新子串重开 ── */
section("[5] 换网卡后用新子串重连");
{
const { st, srv, port } = await makeStub();
const un = bindFetch(port);
const tp = createEcatTransport(CTX);
await tp.probe();
const changed = await tp.setAdapter("X710");
check("setAdapter 返回 true(确实变了)", changed === true);
check("换网卡会 close 旧连接", st.calls.includes("/ecat/api/close"));
await tp.probe();
un(); srv.close();
check("重新 open 用的是新子串", st.openSubstr === "X710", String(st.openSubstr));
check("同值 setAdapter 不重连", (await tp.setAdapter("X710")) === false);
check("网卡子串已持久化", _ls.get("dcu_ecat_adapter") === "X710", _ls.get("dcu_ecat_adapter"));
check("debug() 暴露当前网卡", tp.debug().adapter === "X710");
}
console.log(out.join("\n"));
console.log(`\n${"=".repeat(56)}`);
console.log(`通过 ${pass} 项,失败 ${fail} 项`);
process.exit(fail === 0 ? 0 : 1);