feat(motor): ESP32 侧适配 EtherCAT native 后端, 清理 Python 时代残留
tools/ecat 后端已从 Python(ecat_server.py, 已删除)切到原生守护进程 build_native/ecat_native.exe。本仓(ESP32 电机控制台)的 EtherCAT 传输经 /ecat/api/* 代理打它, 这次把代理实现与全部相关措辞对齐到 native。 代理与自启(esp32_server.py): - ECAT_ENTRY/ECAT_PYW(src/ecat_server.py + venv/pythonw) → ECAT_NATIVE_EXE (build_native/ecat_native.exe); autostart 直接 Popen 单文件 exe, 不再经 venv/pythonw; 清场/--parent-pid/端口漂移三条契约 native 原样镜像, 注释同步 - 文件头与代理段里 ecat_server.py 措辞订正为 native 传输层注释(js/motor/*): - main/transport/transport-ecat/refresh-rate 里"后端是 ecat_server.py / 用 venv 拉起"等 Python 时代说法统一改为 ecat_native.exe - MOT_SLOTS_PER_PDO/MOTOR_SLOT_COUNT 引用改指 ecat_native.c(同名存在); MOTOR_SCAN_CYCLES native 无此常量, 改按真实来源写 ⌈27/7⌉=4 - main.js 环境错误分类正则 /venv|pysoem|Npcap/ → /Npcap|build_native| 守护进程|全被占/: 前两者 native 永不吐给前端(死分支), 后者对齐 autostart 真实文案, 让"exe 没编译"这类错误不被 140 字截断 README/motor.html 同步; 新增 scripts/browser_check_period.mjs(内核级 PDO 周期热调的浏览器实测, 与既有 browser_check_*.mjs 同族)。 验证: py_compile esp32_server.py 通过; 6 个改动 JS + 新脚本 node --check (ESM)全过; 正则对三类 native 错误命中、对废弃 venv/pysoem 不命中; js/ 与 scripts/ 重扫 ecat_server|venv|pysoem 已空。
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
* node scripts/test_motor_transport.mjs [--port 8080]
|
||||
*
|
||||
* 测什么:**真的 import 生产代码** js/motor/transport-*.js,用 Node 的
|
||||
* 全局 fetch 打真实的 /ecat/api/* 反向代理 → 真实 ecat_server → 真实从站。
|
||||
* 全局 fetch 打真实的 /ecat/api/* 反向代理 → 真实 ecat_native → 真实从站。
|
||||
* 不 mock 传输实现本身,否则测不到"契约对不对"这唯一值得测的东西。
|
||||
*
|
||||
* 覆盖:
|
||||
@@ -124,8 +124,8 @@ check("WiFi 支持 0xC5 周期同步", wifi.supportsPeriodSync === true);
|
||||
check("EtherCAT 不做周期同步(PDO 周期由后台循环维持)", ecat.supportsPeriodSync === false);
|
||||
check("EtherCAT 不回报设备 IP", ecat.reportedIp({}) === null);
|
||||
|
||||
/* ── 4~9. EtherCAT:打真实代理 + 真实 ecat_server ── */
|
||||
section("[4] EtherCAT 端到端(真实代理 → ecat_server → 从站)");
|
||||
/* ── 4~9. EtherCAT:打真实代理 + 真实 ecat_native ── */
|
||||
section("[4] EtherCAT 端到端(真实代理 → ecat_native → 从站)");
|
||||
let liveOk = false;
|
||||
try {
|
||||
const st = await ecat.fetchStatus();
|
||||
@@ -299,7 +299,7 @@ section("[8] 快照→契约翻译(构造快照,跑生产代码)");
|
||||
st.motors.find((x) => x.node === 4).config_data === CFG2);
|
||||
|
||||
/* (h) 关键:seq 停止不动超过阈值 → online 转 false。
|
||||
* ecat_server 的快照**永不删除**见过的槽(电机拔了也还在),
|
||||
* 后端的快照**永不删除**见过的槽(电机拔了也还在),
|
||||
* 只认从站的 online 标志会让界面上永远挂着一台不存在的电机。 */
|
||||
const stale = { slot: 0, node: 3, ch: 1, online: true, dlc: 32, seq: 506, data: STATUS_HEX };
|
||||
snap = mkSnap([stale]);
|
||||
@@ -430,6 +430,68 @@ section("[8] 快照→契约翻译(构造快照,跑生产代码)");
|
||||
check("(i3) setSpin(null) 发空请求体,交回后端自动决定",
|
||||
spinBody && !("spin" in spinBody), JSON.stringify(spinBody));
|
||||
|
||||
/* (i4) 内核级周期热调(250µs = 4kHz):必须随时可改、走 /motor/period。
|
||||
* 走 motor/start 是当年把板子弄楔死的路 —— 它先停循环,
|
||||
* 停手 >1000ms 从站 SM2 看门狗直接踢出 OP。 */
|
||||
globalThis.localStorage = (() => {
|
||||
const m = new Map();
|
||||
return {
|
||||
getItem: (k) => (m.has(k) ? m.get(k) : null),
|
||||
setItem: (k, v) => m.set(k, String(v)),
|
||||
removeItem: (k) => m.delete(k),
|
||||
};
|
||||
})();
|
||||
let periodBody = null, periodHits = 0, startBody2 = null, startHits2 = 0;
|
||||
globalThis.fetch = async (url, init) => {
|
||||
const u = String(url);
|
||||
if (u.includes("/motor/rx")) return jsonResp({ ok: true, log: [], frames: [], remain: 0 });
|
||||
if (u.includes("/motor/state")) return jsonResp(Object.assign({ ok: true, log: [] }, snap));
|
||||
if (u.includes("/motor/period")) {
|
||||
periodHits++; periodBody = JSON.parse(init.body);
|
||||
return jsonResp({ ok: true, log: [], period_ms: periodBody.period_ms, spin: true });
|
||||
}
|
||||
if (u.includes("/motor/start")) {
|
||||
startHits2++; startBody2 = JSON.parse(init.body);
|
||||
return jsonResp({ ok: true, log: [], period_ms: startBody2.period_ms || 4, spin: true });
|
||||
}
|
||||
return jsonResp({ ok: true, log: [] });
|
||||
};
|
||||
check("(i4) setPdoPeriod(0.25) 带 period_ms:0.25 走 /motor/period 并返回新值",
|
||||
(await tp.setPdoPeriod(0.25)) === 0.25 && periodBody.period_ms === 0.25,
|
||||
JSON.stringify(periodBody));
|
||||
check("(i4) 热调周期不碰 motor/start(停循环 = 看门狗掉 OP 的楔死路径)",
|
||||
periodHits === 1 && startHits2 === 0,
|
||||
JSON.stringify({ periodHits, startHits2 }));
|
||||
check("(i4) 周期值持久化到 localStorage(服务重启后不用重敲)",
|
||||
globalThis.localStorage.getItem("dcu_ecat_period_ms") === "0.25");
|
||||
await tp.probe();
|
||||
check("(i4) 循环重启(ensureStarted)自动带上持久化的周期",
|
||||
startHits2 === 1 && startBody2.period_ms === 0.25, JSON.stringify(startBody2));
|
||||
let threwPeriod = false;
|
||||
try { await tp.setPdoPeriod(0); } catch (_) { threwPeriod = true; }
|
||||
try { await tp.setPdoPeriod("abc"); } catch (_) { threwPeriod = true; }
|
||||
check("(i4) ≤0 / 非法周期在本地就拒绝,不发给后端", threwPeriod);
|
||||
|
||||
/* 状态栏显示口径:<1ms 必须写成 µs。"PDO 0.25ms" 这种写法没人读,
|
||||
* 而且和后端 _fmt_period 的口径要一致。 */
|
||||
snap = mkSnap([stale], { period_ms: 0.25, slot_period_ms: 0.25, spin: true });
|
||||
await tp.fetchMotorState();
|
||||
parts = tp.describeStatus({ slave: { state: 8, name: "DCU Project" } });
|
||||
check("(i4) 亚毫秒周期显示 µs(PDO 250µs,不是 PDO 0.25ms)",
|
||||
parts.some((p) => p.includes("PDO 250µs")), JSON.stringify(parts));
|
||||
snap = mkSnap([stale], { period_ms: 0.5, slot_period_ms: 1, spin: true });
|
||||
await tp.fetchMotorState();
|
||||
parts = tp.describeStatus({ slave: { state: 8, name: "DCU Project" } });
|
||||
check("(i4) 单槽≠周期时 µs/ms 各自按自己的档位显示",
|
||||
parts.some((p) => p.includes("PDO 500µs") && p.includes("单槽 1ms")),
|
||||
JSON.stringify(parts));
|
||||
snap = mkSnap([stale], { period_ms: 4, slot_period_ms: 4, spin: false });
|
||||
await tp.fetchMotorState();
|
||||
parts = tp.describeStatus({ slave: { state: 8, name: "DCU Project" } });
|
||||
check("(i4) 别处(rate_probe/另一标签页)改的周期每拍从快照同步",
|
||||
parts.some((p) => p.includes("PDO 4ms")) && tp.debug().periodMs === 4,
|
||||
JSON.stringify(parts));
|
||||
|
||||
/* (j) 发送:多帧一次提交(同一 PDO 周期 / 同一 seq 沿)→ 逐帧 results */
|
||||
let sentBody = null;
|
||||
globalThis.fetch = async (url, init) => {
|
||||
|
||||
Reference in New Issue
Block a user