From 12879aa81a284005bb969d5d84b36a69ed524113 Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Sat, 5 Sep 2026 19:58:45 +0800 Subject: [PATCH] =?UTF-8?q?fix(motor):=20=E6=B8=85=E6=8E=89=E5=AD=98?= =?UTF-8?q?=E9=87=8F=204ms=20PDO=20=E5=91=A8=E6=9C=9F=E7=BC=93=E5=AD=98,?= =?UTF-8?q?=20=E6=B5=8B=E8=AF=95=E5=AF=B9=E9=BD=90=20pcore=5Fmask?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - transport-ecat: localStorage 的 dcu_ecat_period_ms 是 Python 时代默认 4ms 存进去的(browser_check_period.mjs 收尾也写 "4"), native 后端默认 已改小, 存量 "4" 会在每次 ensureStarted 把总线压回旧默认。加一次性 迁移标记 dcu_ecat_period_mig, 只删一次; 之后显式设 4 视为用户选择。 "恢复默认"从 setPdoPeriod(4) 改为删键跟随后端默认(固件修好前别低于 1ms)。 - test_motor_transport: 自旋钉核显示早已改用快照字段 pcore_mask(P 核), 测试还停在 ecore_mask 旧字段旧措辞, 本次对齐(含跨 32 位掩码、不连续 核号区间用例)。 验证: node scripts/test_motor_transport.mjs 66/66 通过。 --- js/motor/transport-ecat.js | 18 ++++++++++++++-- scripts/test_motor_transport.mjs | 36 ++++++++++++++++---------------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/js/motor/transport-ecat.js b/js/motor/transport-ecat.js index 2f69ba6..22059ae 100644 --- a/js/motor/transport-ecat.js +++ b/js/motor/transport-ecat.js @@ -119,8 +119,13 @@ export function createEcatTransport(ctx) { const LS_SPIN = "dcu_ecat_spin"; /* 保存的 PDO 周期(ms,允许亚毫秒 float,如 0.25=4kHz)。用户把周期调到 * 内核级档位后存这儿,后端重启、循环重开时原样带回 —— 用户要的 - * "后续随时可用"不该每次手敲一遍。恢复默认 = setPdoPeriod(4)。 */ + * "后续随时可用"不该每次手敲一遍。恢复默认 = 删掉这个键,跟随后端默认。 */ const LS_PERIOD = "dcu_ecat_period_ms"; + /* LS_PERIOD 的一次性迁移标记:Python 时代默认 4ms 曾被存进来(检查脚本 + * browser_check_period.mjs 收尾就写 "4"),native 后端默认已改小, + * 存量 "4" 会在每次 ensureStarted 把总线压回旧默认 4ms。只删一次,之后 + * 再设 4 视为用户显式选择,原样保留。 */ + const LS_PERIOD_MIG = "dcu_ecat_period_mig"; let started = false; // 后台循环是否已启动 let lastSnapshot = null; // 最近一次快照,供 fetchFeedback 复用(不重复打后端) @@ -212,6 +217,12 @@ export function createEcatTransport(ctx) { if (reqPeriodMs) body.period_ms = reqPeriodMs; else { try { + if (!localStorage.getItem(LS_PERIOD_MIG)) { + if (parseFloat(localStorage.getItem(LS_PERIOD)) === 4) { + localStorage.removeItem(LS_PERIOD); + } + localStorage.setItem(LS_PERIOD_MIG, "1"); + } const v = parseFloat(localStorage.getItem(LS_PERIOD)); if (v > 0) body.period_ms = v; } catch (_) { /* 隐私模式忽略 */ } @@ -483,6 +494,9 @@ export function createEcatTransport(ctx) { * 切走本传输:**故意不停后台 PDO 循环**。 * 从站 SM2 有 1000ms 过程数据看门狗,停超 1 秒会 OP→SAFEOP 掉线报 AL 0x001B; * 用户只是切到 WiFi 看看,不该把 EtherCAT 链路打断。 + * 真正该停的时机是"整个 app 关掉"—— 由服务端的 app 租约负责: + * 最后一个 app 窗口断开后 esp32_server 会调 /api/op/stop 停循环并退回 INIT, + * 这里(页面内)什么都不用做。 */ async teardown() { started = false; lastSnapshot = null; }, @@ -536,7 +550,7 @@ export function createEcatTransport(ctx) { * 亚毫秒周期自动就是自旋节拍(内核睡眠粒度 ~0.5ms 跑不出 250µs)。 * * 取值存 localStorage:后端重启后 ensureStarted 会原样带回。 - * 恢复默认:tp.setPdoPeriod(4)。 + * 恢复默认:删掉 dcu_ecat_period_ms,跟随后端默认(固件修好前别低于 1ms)。 */ async setPdoPeriod(ms) { const v = Number(ms); diff --git a/scripts/test_motor_transport.mjs b/scripts/test_motor_transport.mjs index 046445f..24169f5 100644 --- a/scripts/test_motor_transport.mjs +++ b/scripts/test_motor_transport.mjs @@ -354,40 +354,40 @@ section("[8] 快照→契约翻译(构造快照,跑生产代码)"); parts.some((p) => p.includes("自旋") && p.includes("1 核")), JSON.stringify(parts)); - /* (i3b) 钉到 E 核时要写出核号 —— 用户看到"占 1 核"会担心抢了性能核, - * 显示实际落在哪些逻辑处理器才能证明它是能效核。掩码是 64 位, + /* (i3b) 钉到 P 核时要写出核号 —— 自旋吃满一个核是用户能感知的代价, + * 列出实际落在哪些逻辑处理器,用户才知道占的是不是性能核。掩码是 64 位, * JS 移位只有 32 位,所以高低两段分别取,这里专门覆盖跨 32 位的情况。 */ snap = mkSnap([stale], { period_ms: 1, slot_period_ms: 1, spin: true, - ecore_mask: 0xfff0000 }); + pcore_mask: 0xfff0000 }); await tp.fetchMotorState(); parts = tp.describeStatus({ slave: { state: 8, name: "DCU Project" } }); - check("(i3b) 钉到 E 核时列出核号区间(16-27,不逐个罗列挤掉从站名)", - parts.some((p) => p.includes("E 核 16-27")), + check("(i3b) 钉到 P 核时列出核号区间(16-27,不逐个罗列挤掉从站名)", + parts.some((p) => p.includes("P 核 16-27")), JSON.stringify(parts)); - check("(i3b) 已列出 E 核号时不再写笼统的『占 1 核』", + check("(i3b) 已列出 P 核号时不再写笼统的『占 1 核』", !parts.some((p) => p.includes("占 1 核")), JSON.stringify(parts)); - check("(i3b) debug() 暴露 ecores 列表便于现场核对", - tp.debug().ecores.length === 12 && tp.debug().ecores[0] === 16 && - tp.debug().ecores[11] === 27, - JSON.stringify(tp.debug().ecores)); + check("(i3b) debug() 暴露 pcores 列表便于现场核对", + tp.debug().pcores.length === 12 && tp.debug().pcores[0] === 16 && + tp.debug().pcores[11] === 27, + JSON.stringify(tp.debug().pcores)); /* 高 32 位之外的核(>31)不能丢:用 2^40 验证跨段位 */ snap = mkSnap([stale], { period_ms: 1, slot_period_ms: 1, spin: true, - ecore_mask: 1 + 2 ** 40 }); + pcore_mask: 1 + 2 ** 40 }); await tp.fetchMotorState(); check("(i3b) 掩码跨 32 位时高段核号不丢(0 与 40)", - JSON.stringify(tp.debug().ecores) === JSON.stringify([0, 40]), - JSON.stringify(tp.debug().ecores)); + JSON.stringify(tp.debug().pcores) === JSON.stringify([0, 40]), + JSON.stringify(tp.debug().pcores)); /* 掩码为 0 = 探测失败/对称 CPU,要退回笼统措辞而不是显示空列表 */ snap = mkSnap([stale], { period_ms: 1, slot_period_ms: 1, spin: true, - ecore_mask: 0 }); + pcore_mask: 0 }); await tp.fetchMotorState(); parts = tp.describeStatus({ slave: { state: 8, name: "DCU Project" } }); - check("(i3b) 没钉核时退回『占 1 核』而不是列出空的 E 核", - parts.some((p) => p.includes("占 1 核") && !p.includes("E 核")), + check("(i3b) 没钉核时退回『占 1 核』而不是列出空的 P 核", + parts.some((p) => p.includes("占 1 核") && !p.includes("P 核")), JSON.stringify(parts)); - /* 不连续的核(混合架构常见:E 核未必连成一段)要压成多段而不是硬凑一个区间 */ + /* 不连续的核(混合架构常见:P 核未必连成一段)要压成多段而不是硬凑一个区间 */ snap = mkSnap([stale], { period_ms: 1, slot_period_ms: 1, spin: true, - ecore_mask: (1 << 16) | (1 << 17) | (1 << 20) | (1 << 24) }); + pcore_mask: (1 << 16) | (1 << 17) | (1 << 20) | (1 << 24) }); await tp.fetchMotorState(); parts = tp.describeStatus({ slave: { state: 8, name: "DCU Project" } }); check("(i3b) 不连续核压成 16-17,20,24(不硬凑区间)",