feat(motor): 反馈刷新率 Hz 适配(0x0400/0x0401 扩展格式)+ ECAT 密排槽与判活自适应
反馈刷新率(配套电机固件 v0.4.1:0x0400 DLC4/0x0401 DLC2 扩展格式, 选择器 0x0100=STATUS · 0x0101=CONFIG 帧,协议值为周期 ms): - 「节点 ID」页签新增 STATUS/CONFIG 刷新率读取/写入;界面单位一律 Hz (写入按 周期ms=round(1000/Hz) 换算下发,读取反算 Hz 并附原始周期, 取整有损如实展示),0=停发,写入掉电保存;配置值以「读取」回读电机为准 - WiFi 应答回执走 0xC1 per-ID 快照(/can/feedback 全量 entries): GD32 0xC0 ACK 仅 1 字节状态、不携带电机回帧;同 ID 回帧快照只存最新 一条,故一次快照只配一次请求(STATUS/CONFIG 分开读写) - ECAT 回执走批量接收区 takeNodeIdReport,按长度分派 2B 旧格式/4B GET/5B SET ACK - GET_NODE_ID 迁移到扩展选择器 0x0000,兼容旧固件 2B 节点 ID 应答 (SET DLC2 旧格式行为不变) - transport-wifi 新增 fetchFeedbackFull(全量 CAN 反馈快照) EtherCAT/基础设施(密排槽固件 1.6.21 配套): - 快照槽位 5→9 一拍整轮(扫描周期数 6→1);判活阈值随实测 seq 间隔 自适应 clamp(3×EWMA, 2s, 30s),秒级回报档不再被固定 2s 误判离线; STATUS 面板 Hz 明确为网页到达口径(按槽 seq 实测电机回报率的方案已否掉) - esp32_server.py:ecat_native 单实例收紧(后起独占 + taskkill 残留实例, 双实例并存会抢 Npcap 致后起者抓不到从站)+ native stdout 落盘 - flash_gd32.py:刷写后校验「GD32 真的在跑新代码」(版本变化或 uptime 归零), 防"写入≠在跑"假台账 - lab:新增 LAN9252 电源诊断台、PDO 布局页;SPI 测速并入 EtherCAT 接收看板 并更名「SPI/ECAT 通信测速」;移除 continuity_test.html;新增 ECAT 单拍耗时 优化报告;/_local/ 排除入库;测试同步更新(62+58 项全过)
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
* 8 节点 ID 应答取走即清
|
||||
* 9 fetchFeedback:无数据的字段必须是 null 而不是 0(监控 UI 约定)
|
||||
* 10 代理不可用时报错信封可读(不是裸 500/CORS 报错)
|
||||
* 11 判活阈值随实测回报周期自适应(电机可变 CAN 刷新率,慢速不误判离线)
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -312,10 +313,36 @@ section("[8] 快照→契约翻译(构造快照,跑生产代码)");
|
||||
await new Promise((r) => setTimeout(r, 2100));
|
||||
snap = mkSnap([stale]);
|
||||
st = await tp.fetchMotorState();
|
||||
check("(h) seq 停超 2s → online 转 false(快照不删历史槽,必须靠 seq 判活)",
|
||||
check("(h) seq 停超 2s → online 转 false(快周期兜底下限,快照不删历史槽)",
|
||||
st.motors.find((x) => x.node === 3).online === false,
|
||||
`online=${st.motors.find((x) => x.node === 3).online}`);
|
||||
|
||||
/* (h2) 判活阈值随实测回报周期自适应(电机可变 CAN 刷新率)。
|
||||
* 用另一台"慢速电机"(N8@CH0,回报周期 ~1.2s)验证:固定 2s 阈值会在
|
||||
* 两次正常回报之间把它判离线(LED 闪/波形虚线/面板切离线层);
|
||||
* 新阈值 = clamp(3×EWMA, 2s, 30s) ≈ 3.6s —— 2.5s 静默必须仍算在线,
|
||||
* 静默超 ~3.6s 才判离线。间隔用真睡(与 (h) 同理,判活用本地时间)。 */
|
||||
const slow = (seq) => [{ slot: 21, node: 8, ch: 0, online: true, dlc: 48, seq, data: STATUS_HEX }];
|
||||
snap = mkSnap(slow(900));
|
||||
await tp.fetchMotorState(); /* 首见(不计间隔) */
|
||||
await new Promise((r) => setTimeout(r, 1200));
|
||||
snap = mkSnap(slow(901));
|
||||
await tp.fetchMotorState(); /* 间隔#1 ≈1.2s */
|
||||
await new Promise((r) => setTimeout(r, 1200));
|
||||
snap = mkSnap(slow(902));
|
||||
await tp.fetchMotorState(); /* 间隔#2 ≈1.2s → EWMA≈1.2s,阈值≈3.6s */
|
||||
snap = mkSnap(slow(902)); /* seq 停住:两次回报之间的静默 */
|
||||
await new Promise((r) => setTimeout(r, 2500));
|
||||
st = await tp.fetchMotorState();
|
||||
check("(h2) 慢速电机(1.2s/帧)静默 2.5s 仍在线(固定 2s 阈值会误判)",
|
||||
st.motors.find((x) => x.node === 8).online === true,
|
||||
`online=${st.motors.find((x) => x.node === 8).online}`);
|
||||
await new Promise((r) => setTimeout(r, 1400)); /* 累计静默 ≥3.9s > 阈值 ≈3.6s */
|
||||
st = await tp.fetchMotorState();
|
||||
check("(h2) 静默超自适应阈值(≈3.6s)后才判离线(拔线仍能查出)",
|
||||
st.motors.find((x) => x.node === 8).online === false,
|
||||
`online=${st.motors.find((x) => x.node === 8).online}`);
|
||||
|
||||
/* (i) 通信异常(wkc != expected)→ feedback 全部 null,而不是 0 */
|
||||
snap = mkSnap([stale], { wkc: 0, expected_wkc: 3 });
|
||||
await tp.fetchMotorState();
|
||||
|
||||
Reference in New Issue
Block a user