2026-09-04 12:18:06 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* motor/refresh-rate.js — 刷新速度的「每链路一份」纯逻辑
|
|
|
|
|
|
*
|
|
|
|
|
|
* 为什么单独一个文件:这里是唯一带分支的部分(默认值、钳制、旧数据迁移),
|
|
|
|
|
|
* 而 main.js 里剩下的都是 DOM 副作用。抽出来后自测能直接跑生产代码
|
|
|
|
|
|
* (见 scripts/test_motor_refresh.mjs),不用为了测三个 if 去搭 DOM 桩。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 为什么两条链路不能共用一个值:往返代价差一个数量级。
|
|
|
|
|
|
* wifi 250ms(下限 50ms)—— 走无线 + ESP32 HTTP,还要与 GD32 0xC5 主动推送周期对齐;
|
2026-09-07 12:19:26 +08:00
|
|
|
|
* ecat 50ms(下限 6ms)—— 只是从本机后台 PDO 快照里读一次(经代理实测均值 ≈ 1.4ms),
|
2026-09-04 12:18:06 +08:00
|
|
|
|
* PDO 总线周期由后台循环独立维持(默认 4ms,喂 1000ms 看门狗),
|
|
|
|
|
|
* 界面读得快只是显示更跟手,不给总线加负担。
|
2026-09-07 12:19:26 +08:00
|
|
|
|
* 6ms 下限的来由:实测 1ms 档能跑到 ~550Hz,而画面绘制上限由显示器
|
|
|
|
|
|
* 刷新率封顶(本机 160Hz),160Hz 以上那部分轮询没有消费者,
|
|
|
|
|
|
* 纯粹是 HTTP 过路费(每拍 ≈1.6KB 快照,renderer / 网络服务 /
|
|
|
|
|
|
* browser / Python 代理各被点一次)。填 6 的语义仍是「不限速」,
|
|
|
|
|
|
* 见 UNTHROTTLED_MAX_MS。
|
2026-09-04 12:18:06 +08:00
|
|
|
|
* 共用一个值的实际后果:ECAT 的快值带到 WiFi 上会让 HTTP 请求排队
|
|
|
|
|
|
* (tickStuck 常亮黄),WiFi 的 250ms 带到 ECAT 上又白白浪费了本地快照的低延迟。
|
2026-09-12 20:10:47 +08:00
|
|
|
|
*
|
|
|
|
|
|
* 后半部分还住着一块与「实测数据率」相关的纯逻辑(同样为了脱离 DOM 自测):
|
|
|
|
|
|
* staleLimitMs —— EtherCAT 槽判活的离线阈值,随实测 seq 间隔自适应,
|
|
|
|
|
|
* transport-ecat.js 用它,慢速回报的电机不再被固定 2s 误判离线。
|
2026-09-04 12:18:06 +08:00
|
|
|
|
*/
|
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
|
|
/** 每条链路的默认刷新间隔(ms)。新链路在这里加一行即可。 */
|
|
|
|
|
|
export const REFRESH_DEFAULTS = { wifi: 250, ecat: 50 };
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 每条链路允许的**最小**间隔(ms)。这一项按链路分开,是因为下限的成因不同:
|
|
|
|
|
|
* wifi 50ms —— 下限来自链路本身:每拍是一次无线 HTTP 往返,再快只会让请求
|
|
|
|
|
|
* 排队积压(tickStuck 亮黄),毫无收益。
|
2026-09-07 12:19:26 +08:00
|
|
|
|
* ecat 6ms —— 读的是本机后台 PDO 快照,没有总线代价,允许"全速跑"。
|
|
|
|
|
|
* 注意 6ms **不等于** 166Hz:单飞语义下上一拍完成才发下一拍,
|
2026-09-05 03:56:52 +08:00
|
|
|
|
* 而每拍要过一次 HTTP(代理 → ecat_native → 回来),实测均值
|
2026-09-07 12:19:26 +08:00
|
|
|
|
* ≈ 1.8ms,所以 6ms 档实测约 160Hz —— 正好贴着本机 160Hz 屏的
|
|
|
|
|
|
* 绘制上限,再快只是多收一份 HTTP 过路费。填 6 的语义仍是
|
2026-09-04 12:18:06 +08:00
|
|
|
|
* "不要人为限速",相应地要关掉卡顿黄标与理论 Hz 显示,
|
|
|
|
|
|
* 见 UNTHROTTLED_MAX_MS。
|
|
|
|
|
|
*/
|
2026-09-07 12:19:26 +08:00
|
|
|
|
export const REFRESH_MIN_BY_TP = { wifi: 50, ecat: 6 };
|
2026-09-04 12:18:06 +08:00
|
|
|
|
/** 兜底最小值:未知链路 id 时用(防御,正常不该出现)。 */
|
|
|
|
|
|
export const REFRESH_MIN_MS = 50;
|
|
|
|
|
|
export const REFRESH_MAX_MS = 5000;
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 低于此间隔视为「不限速」模式:设定值已经小于单次往返耗时,
|
|
|
|
|
|
* 节拍源会比请求完成得更快,于是每拍都撞上"上一拍还在飞"。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 这在 1ms 档是**设计行为而不是故障**,所以调用方要据此关掉两处显示:
|
|
|
|
|
|
* 1. tickStuck 卡顿黄标 —— 否则通道下拉框会常亮黄并报"连续 N 拍未按时完成",
|
|
|
|
|
|
* 而用户要的就是"能多快就多快";
|
|
|
|
|
|
* 2. 「≈N Hz」的理论值 —— 1ms 硬算是 1000Hz,浏览器里实测约 125Hz,那是谎报。
|
|
|
|
|
|
* 这时只显示按数据到达时间实测出来的频率。
|
2026-09-05 03:56:52 +08:00
|
|
|
|
* 取 20ms(50Hz):浏览器里每拍实际约 8ms(fetch + 代理 + ecat_native 全链),
|
2026-09-04 12:18:06 +08:00
|
|
|
|
* 20ms 留了足够余量不误判;又远小于常用档位(50/250ms),不会误伤正常设置。
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const UNTHROTTLED_MAX_MS = 20;
|
|
|
|
|
|
|
|
|
|
|
|
/** 该间隔是否处于「不限速」区间(见 UNTHROTTLED_MAX_MS)。 */
|
|
|
|
|
|
export function isUnthrottled(ms) {
|
|
|
|
|
|
return Number.isFinite(ms) && ms < UNTHROTTLED_MAX_MS;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 某条链路的最小间隔。 */
|
|
|
|
|
|
export function minRefreshFor(tpId) {
|
|
|
|
|
|
const v = REFRESH_MIN_BY_TP[tpId];
|
|
|
|
|
|
return v === undefined ? REFRESH_MIN_MS : v;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 按**该链路自己的**下限钳制(这是本文件里唯一需要区分链路的地方)。 */
|
|
|
|
|
|
function clampFor(tpId, v) {
|
|
|
|
|
|
return Math.max(minRefreshFor(tpId), Math.min(REFRESH_MAX_MS, v));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 全新的一份默认表(调用方可随意改,不会污染 REFRESH_DEFAULTS)。 */
|
|
|
|
|
|
export function defaultRefreshMap() {
|
|
|
|
|
|
return Object.assign({}, REFRESH_DEFAULTS);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从 localStorage 读出的内容归一成完整的「链路 → 间隔」表。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param raw 持久化的 refreshByTp(可能是 undefined / 脏数据)
|
|
|
|
|
|
* @param legacySingle 旧版单个 refreshRate 字段(两条链路曾共用一个值)
|
|
|
|
|
|
*
|
|
|
|
|
|
* 迁移取向:旧的单值是**为 WiFi 调的**(那时只有 WiFi 链路),所以只迁到
|
|
|
|
|
|
* wifi,ecat 留给默认 50ms —— 把一个为无线调出来的 250ms 套到本地快照上,
|
|
|
|
|
|
* 等于让用户平白无故继承一个不合适的值。
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function normalizeRefreshMap(raw, legacySingle) {
|
|
|
|
|
|
const out = defaultRefreshMap();
|
|
|
|
|
|
if (raw && typeof raw === "object") {
|
|
|
|
|
|
for (const [id, v] of Object.entries(raw)) {
|
|
|
|
|
|
const n = parseInt(v, 10);
|
|
|
|
|
|
/* 只认已知链路:脏 key 直接丢,避免 localStorage 里攒出永不使用的条目 */
|
|
|
|
|
|
if (Number.isFinite(n) && out[id] !== undefined) out[id] = clampFor(id, n);
|
|
|
|
|
|
}
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
|
|
|
|
|
const legacy = parseInt(legacySingle, 10);
|
|
|
|
|
|
if (Number.isFinite(legacy)) out.wifi = clampFor("wifi", legacy);
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 取某条链路的生效值:脏值/缺失回落到该链路默认,再按该链路下限钳制。 */
|
|
|
|
|
|
export function pickRefresh(map, tpId) {
|
|
|
|
|
|
const raw = map ? map[tpId] : undefined;
|
|
|
|
|
|
const n = parseInt(raw, 10);
|
|
|
|
|
|
if (Number.isFinite(n)) return clampFor(tpId, n);
|
|
|
|
|
|
return REFRESH_DEFAULTS[tpId] || REFRESH_DEFAULTS.wifi;
|
|
|
|
|
|
}
|
2026-09-12 20:10:47 +08:00
|
|
|
|
|
|
|
|
|
|
/* ═══════════ ECAT 槽判活阈值:随实测 seq 间隔自适应 ═══════════
|
|
|
|
|
|
*
|
|
|
|
|
|
* 背景:电机 CAN 回报率可变(可在电机侧调档),回报周期调到秒级后 seq 几秒
|
|
|
|
|
|
* 才动一次,固定 2s 判活阈值会在两次**正常**回报之间把电机判离线。
|
|
|
|
|
|
* 注:STATUS 面板的 Hz 刻意**不**按 seq 实测电机回报率 —— 面板要显示的是
|
|
|
|
|
|
* 网页实际刷新率(数据到达口径),WiFi 慢于电机时按电机档位显示就是谎报;
|
|
|
|
|
|
* seq 只用于这里的判活。 */
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 槽 seq 停涨多久算离线的**下限**(ms),生效阈值 = staleLimitMs(实测间隔)。
|
|
|
|
|
|
* 下限 2s 的来由(历史):要大于「扫完全表 + 一点抖动」—— 1.6.15 滑窗时代
|
|
|
|
|
|
* period 10ms × 6 周期 = 60ms,2s 有充足余量;1.6.21 密排后一拍整轮,更不是问题。
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const STALE_OFFLINE_MS = 2000;
|
|
|
|
|
|
/** 生效阈值 = 实测 seq 变化间隔(EWMA) × 该系数。3 倍 ≈ 连丢 3 拍才判离线。 */
|
|
|
|
|
|
export const STALE_IV_FACTOR = 3;
|
|
|
|
|
|
/** 阈值上限:防一次长停顿(后台挂起/总线冻结恢复)把 EWMA 拉飞后迟迟不判离线。 */
|
|
|
|
|
|
export const STALE_MAX_MS = 30000;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 该槽的离线阈值:clamp(3 × EWMA(实测 seq 变化间隔), 2s, 30s)。
|
|
|
|
|
|
* 电机 CAN 回报率可变:回报周期调到秒级后,固定 2s 会在两次正常回报之间
|
|
|
|
|
|
* 把电机判离线(LED 闪烁/波形虚线/面板切离线层),阈值必须跟着间隔放大。
|
|
|
|
|
|
* ivMs = 0(还没观察到第二次变化)时返回下限 2s,与旧固定阈值行为一致。
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function staleLimitMs(ivMs) {
|
|
|
|
|
|
return Math.max(STALE_OFFLINE_MS, Math.min(STALE_MAX_MS, (ivMs || 0) * STALE_IV_FACTOR));
|
|
|
|
|
|
}
|