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:
+35
-18
@@ -32,24 +32,31 @@
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/** 单个 PDO 周期回传的状态槽数(与 ecat_native.c 的 MOT_SLOTS_PER_PDO 一致;
|
||||
* 固件 1.6.4 起槽长 56B = 8B 头 + 48B STATUS,段 2 的 280B 正好放 5 个) */
|
||||
const MOT_SLOTS_PER_PDO = 5;
|
||||
/** 状态槽总数 = 9 节点 × 3 通道(ecat_native.c 的 MOTOR_SLOT_COUNT) */
|
||||
/* 判活阈值纯逻辑(随实测 seq 间隔自适应)住在 refresh-rate.js 的
|
||||
* 「ECAT 槽判活阈值」一节,纯函数以便脱离 DOM 自测。 */
|
||||
import { staleLimitMs } from "./refresh-rate.js";
|
||||
|
||||
/** 单个 PDO 周期回传的状态槽数。
|
||||
* 固件 1.6.21 起段 2 密排整轮:一拍带齐总线电机(最多 9),空槽不占位。
|
||||
* 旧 1.6.15 滑窗为 5 槽;后端 decode_input 按 magic@35 自适应,本常量仅
|
||||
* 影响"扫完全表"的展示口径,不再决定能否拿齐电机。 */
|
||||
const MOT_SLOTS_PER_PDO = 9;
|
||||
/** 状态槽总数 = 9 节点 × 3 通道(ecat_native.c 的 MOTOR_SLOT_COUNT)。
|
||||
* 1.6.21 密排后每节点只报最新一条,经 (node-1)*3+ch 落回本表。 */
|
||||
const MOTOR_SLOT_COUNT = 27;
|
||||
/** 扫完全表所需周期数(= ⌈MOTOR_SLOT_COUNT / MOT_SLOTS_PER_PDO⌉ = ⌈27/5⌉ = 6)。
|
||||
* 注意:这**只**是开机首圈全表扫描的窗口数,不是稳态下单槽的刷新分频 ——
|
||||
* 后端会把窗口收缩到只盖有主的槽(窗口自适应),稳态下在线电机
|
||||
* 每个 PDO 周期都刷新。要显示"数据多新"请用快照里的 slot_period_ms。 */
|
||||
const MOTOR_SCAN_CYCLES = 6;
|
||||
/** 扫完全表所需周期数。1.6.21 密排下恒 1(一拍整轮);旧滑窗为 ⌈27/5⌉=6。
|
||||
* 显示"数据多新"请用快照里的 slot_period_ms。 */
|
||||
const MOTOR_SCAN_CYCLES = 1;
|
||||
|
||||
/**
|
||||
* 槽 seq 停止增长多久算离线(ms)。
|
||||
* 为什么需要它:后端的快照把见过的槽**永久累积**,电机拔了也不会消失,
|
||||
* 只能靠 seq 是否还在涨来判活。阈值要大于"扫完全表 + 一点抖动",否则
|
||||
* 慢周期下会把正常电机误判离线:period 10ms × 6 周期 = 60ms,取 2s 有充足余量。
|
||||
* 槽 seq 判活阈值:**不写死** —— 随该槽实测 seq 变化间隔自适应
|
||||
* clamp(3×EWMA, 2s, 30s),见 refresh-rate.js 的 staleLimitMs()。
|
||||
* 为什么需要判活:后端的快照把见过的槽**永久累积**,电机拔了也不会消失,
|
||||
* 只能靠 seq 是否还在涨来判活。为什么必须自适应:电机 CAN 回报率可变,
|
||||
* 回报周期调到秒级后,固定 2s 会在两次正常回报之间把正常电机判离线
|
||||
* (LED 闪烁、波形画虚线、面板切离线覆盖层);下限 2s 仍兜底快周期
|
||||
* (要大于"扫完全表 + 一点抖动",1.6.15 滑窗时代 period 10ms × 6 周期 = 60ms)。
|
||||
*/
|
||||
const STALE_OFFLINE_MS = 2000;
|
||||
|
||||
/** 位掩码 → 逻辑处理器号列表。JS 移位只有 32 位,所以用除法逐位取。 */
|
||||
function pcoreList(mask) {
|
||||
@@ -101,7 +108,7 @@ export function createEcatTransport(ctx) {
|
||||
const configs = new Map(); // node -> 24B CONFIG hex(0x000N0101)
|
||||
const versions = new Map(); // node -> {node, seq, version} 供 fetchVersionNode
|
||||
const nodeIdReports = new Map();// node -> 应答 hex(0x000N0400/0401)
|
||||
const seqSeen = new Map(); // "ch:node" -> {seq, at, rx} 判活 + 计更新次数
|
||||
const seqSeen = new Map(); // "ch:node" -> {seq, at, rx, iv} 判活 + 计更新次数;iv=实测变化间隔 EWMA(ms)
|
||||
let periodMs = 0; // 后台 PDO 循环周期(由 /api/motor/start 回报)
|
||||
/* 单个电机状态的真实刷新周期(ms,来自快照的 slot_period_ms)。
|
||||
* 与 periodMs 的区别是要点:PDO 周期是"总线跑多快",而某个电机的状态要等
|
||||
@@ -278,7 +285,8 @@ export function createEcatTransport(ctx) {
|
||||
|
||||
/**
|
||||
* 快照 → WiFi 的 /motor/state 契约。
|
||||
* 关键点:online 不能直接用从站给的标志位,还要叠一层 seq 判活(见 STALE_OFFLINE_MS)。
|
||||
* 关键点:online 不能直接用从站给的标志位,还要叠一层 seq 判活
|
||||
* (阈值随实测回报周期自适应,见 refresh-rate.js 的 staleLimitMs)。
|
||||
*/
|
||||
function snapshotToMotorState(snap) {
|
||||
const now = Date.now();
|
||||
@@ -288,12 +296,21 @@ export function createEcatTransport(ctx) {
|
||||
const prev = seqSeen.get(key);
|
||||
let rx = prev ? prev.rx : 0;
|
||||
let at = prev ? prev.at : now;
|
||||
let iv = prev ? prev.iv : 0;
|
||||
if (!prev || prev.seq !== m.seq) {
|
||||
if (prev) {
|
||||
/* 实测变化间隔 EWMA(α=0.3):判活阈值跟着电机回报率走。电机改回报
|
||||
* 档后 ~3 次新间隔收敛;突发降速无法预知,收敛前最多闪一次离线。 */
|
||||
const d = now - prev.at;
|
||||
iv = iv ? iv + (d - iv) * 0.3 : d;
|
||||
}
|
||||
rx = rx + 1; // 本地累计"看到过多少次更新",补 WiFi 的 rx_count 语义
|
||||
at = now;
|
||||
}
|
||||
seqSeen.set(key, { seq: m.seq, at, rx });
|
||||
const fresh = (now - at) <= STALE_OFFLINE_MS;
|
||||
seqSeen.set(key, { seq: m.seq, at, rx, iv });
|
||||
/* 从站说在线 **且** seq 还在动才算在线:快照永不删除历史槽。
|
||||
* 阈值随实测回报周期放大(慢速电机不误判),下限 2s 兜底快周期。 */
|
||||
const fresh = (now - at) <= staleLimitMs(iv);
|
||||
motors.push({
|
||||
node: m.node,
|
||||
ch: m.ch,
|
||||
|
||||
Reference in New Issue
Block a user