feat: 添加局域网设备发现功能和链路胶囊优化
- 实现局域网设备发现面板,支持探测ESP32中继板并自动切换端点 - 添加POST /api/lan/probe接口用于设备探测,返回三态判决结果 - 实现快扫和全段扫描两种模式,支持种子地址优先探测 - 添加端点唯一写手机制,防止多处同时修改设备IP - 优化链路胶囊UI显示WiFi/EtherCAT/BLE连接状态和设备名称 - 修复EtherCAT链路OTA刷写期间PDO循环暂停和AL状态处理问题 - 调整PDO周期设置范围为0.125-4ms,支持亚毫秒显示为微秒 - 添加ESP32 WiFi使能开关控件样式和数据来源角标显示
This commit is contained in:
+57
-28
@@ -6,7 +6,9 @@
|
||||
* health →(未就绪)status →(未开)open 网卡 → motor/start 进 OP + 起 PDO 循环
|
||||
*
|
||||
* 红线:
|
||||
* - DC 钉死 250µs(period_ms 默认/钳制 0.25,dc_cycle_us = max(250, round(ms*1000)))
|
||||
* - PDO 周期与 DC SYNC0 **始终同值**:dc_cycle_us = round(period_ms*1000),
|
||||
* 周期可在 UI 档位 0.125–4ms 内随时热改,不再钉死 0.25。
|
||||
* 默认仍是 0.25ms(历史稳妥档);≥1ms 时 GD32 发送出口可能变慢,属预期代价。
|
||||
* - 幂等:native/health 报 bus_open+loop 时**复用**,不再 motor/start
|
||||
* (start 会先 loop_stop,热路径假超时 / SSC 楔死风险)
|
||||
* - **自动化只能基于证明,人才能基于意图**:任何非用户直接触发的 motor/start
|
||||
@@ -22,12 +24,30 @@
|
||||
/** 网卡匹配子串默认值(代码/页面/测试的唯一来源;文档漂移以此为准)。 */
|
||||
export const ECAT_DEFAULT_ADAPTER = "I210";
|
||||
|
||||
/** 默认 PDO 周期(ms):1ms+DC 会让发送出口回到 ~1s,必须 250µs 档。 */
|
||||
/** 默认 PDO 周期(ms)。 */
|
||||
export const ECAT_DEFAULT_PERIOD_MS = 0.25;
|
||||
|
||||
/** DC SYNC0 目标周期(µs)。 */
|
||||
/** 允许的 PDO 周期上下限(ms)——与 spi_test 档位一致,可随时热改。 */
|
||||
export const ECAT_PERIOD_MIN_MS = 0.125;
|
||||
export const ECAT_PERIOD_MAX_MS = 4;
|
||||
|
||||
/** 默认 DC SYNC0 周期(µs):与默认 period 0.25ms 对齐;热改后 DC 跟随 period。 */
|
||||
export const ECAT_DC_CYCLE_US = 250;
|
||||
|
||||
/** 把任意 period 钳进合法区间;非法/缺省回落默认 0.25ms。 */
|
||||
export function clampEcatPeriodMs(v) {
|
||||
var per = Number(v);
|
||||
if (!(per > 0)) return ECAT_DEFAULT_PERIOD_MS;
|
||||
if (per < ECAT_PERIOD_MIN_MS) return ECAT_PERIOD_MIN_MS;
|
||||
if (per > ECAT_PERIOD_MAX_MS) return ECAT_PERIOD_MAX_MS;
|
||||
return per;
|
||||
}
|
||||
|
||||
/** period_ms → DC SYNC0 µs(与 period 锁死同值,不再是恒 250)。 */
|
||||
export function periodToDcUs(perMs) {
|
||||
return Math.round(clampEcatPeriodMs(perMs) * 1000);
|
||||
}
|
||||
|
||||
const LS_ECAT_PERIOD = "dcu_ecat_period_ms";
|
||||
const LS_ECAT_SPIN = "dcu_ecat_spin";
|
||||
const LS_ECAT_ADAPTER = "dcu_ecat_adapter";
|
||||
@@ -42,20 +62,15 @@ function readAdapterRaw() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 motor/start body:period 钳到 ≤0.5ms 且默认 0.25,DC 钉 250µs。
|
||||
* 构造 motor/start body:period 钳进 0.125–4ms,DC 与 period 同值。
|
||||
* @param {{period_ms?:number, spin?:boolean|null, fromLocalStorage?:boolean}} [opts]
|
||||
*/
|
||||
export function resolveEcatStartBody(opts) {
|
||||
opts = opts || {};
|
||||
var body = {};
|
||||
var per = Number(opts.period_ms);
|
||||
if (!opts.fromLocalStorage && opts.period_ms == null) {
|
||||
per = ECAT_DEFAULT_PERIOD_MS;
|
||||
} else if (!(per > 0) || per > 0.5) {
|
||||
per = ECAT_DEFAULT_PERIOD_MS;
|
||||
}
|
||||
var per = clampEcatPeriodMs(opts.period_ms);
|
||||
body.period_ms = per;
|
||||
body.dc_cycle_us = Math.max(ECAT_DC_CYCLE_US, Math.round(per * 1000));
|
||||
body.dc_cycle_us = periodToDcUs(per);
|
||||
body.dc_shift_us = 0;
|
||||
var spin = opts.spin;
|
||||
if (spin == null && opts.fromLocalStorage) {
|
||||
@@ -79,8 +94,8 @@ export function readStoredEcatPeriod() {
|
||||
localStorage.setItem(LS_PERIOD_MIG, "1");
|
||||
}
|
||||
var v = parseFloat(localStorage.getItem(LS_ECAT_PERIOD));
|
||||
/* 用户显式设过才用;否则 0.25ms */
|
||||
if (v > 0 && v !== 1) return v;
|
||||
/* 用户显式设过且落在合法档位内才用;否则回落默认 0.25ms */
|
||||
if (v > 0 && v >= ECAT_PERIOD_MIN_MS && v <= ECAT_PERIOD_MAX_MS) return v;
|
||||
return ECAT_DEFAULT_PERIOD_MS;
|
||||
} catch (_) {
|
||||
return ECAT_DEFAULT_PERIOD_MS;
|
||||
@@ -113,16 +128,26 @@ export function proveEcatLoop(h, opts) {
|
||||
|
||||
/**
|
||||
* 「DC/周期是否被*证明*配错」。缺字段 = 证明不了 = 不动总线(repairEcatDc 旧规则收编于此)。
|
||||
* 判据:motor 路径必须 DC on;period / dc_cycle_us 各自在合法区间;
|
||||
* 两者都有值时必须同值(period_ms*1000 ≈ dc_cycle_us)。
|
||||
* 周期本身可以是 0.125–4ms 内任意档 —— 与 period 锁死的 DC 不再判错。
|
||||
* @returns {"ok"|"misconfigured"|"unknown"}
|
||||
*/
|
||||
export function proveEcatDc(h) {
|
||||
if (!h || h.ok !== true) return "unknown";
|
||||
if (h.dc == null && h.period_ms == null && h.dc_cycle_us == null) return "unknown";
|
||||
if (h.dc === 1 && Number(h.dc_cycle_us) === ECAT_DC_CYCLE_US &&
|
||||
(h.period_ms == null || Math.abs(Number(h.period_ms) - ECAT_DEFAULT_PERIOD_MS) < 0.001)) {
|
||||
return "ok";
|
||||
if (h.dc !== 1) return "misconfigured";
|
||||
var per = h.period_ms == null ? null : Number(h.period_ms);
|
||||
var dc = h.dc_cycle_us == null ? null : Number(h.dc_cycle_us);
|
||||
var perOk = per == null || (per >= ECAT_PERIOD_MIN_MS && per <= ECAT_PERIOD_MAX_MS);
|
||||
var dcMin = Math.round(ECAT_PERIOD_MIN_MS * 1000);
|
||||
var dcMax = Math.round(ECAT_PERIOD_MAX_MS * 1000);
|
||||
var dcOk = dc == null || (dc >= dcMin && dc <= dcMax);
|
||||
if (!perOk || !dcOk) return "misconfigured";
|
||||
if (per != null && dc != null) {
|
||||
if (Math.abs(Math.round(per * 1000) - dc) > 1) return "misconfigured";
|
||||
}
|
||||
return "misconfigured";
|
||||
return "ok";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,10 +308,12 @@ export async function runEcatBootstrap(opts) {
|
||||
}
|
||||
|
||||
/**
|
||||
* DC/周期自愈:被**证明**配错(free-run 或 ≥1ms)时拉回 250µs。
|
||||
* 判据在 proveEcatDc 里(唯一实现):证明不了 = 不动总线。这条规则的存在理由就是
|
||||
* healthCheck 那份内联副本 —— 它把"没有 dc 字段"当成"DC 配错",于是一篇错误回包
|
||||
* 就打出一记 loop_stop 的 motor/start(记忆 ecat-bus-exclusive-route-restart-trips-watchdog)。
|
||||
* DC/周期自愈:被**证明**配错(free-run / period 越界 / period 与 DC 不同值)时
|
||||
* 按当前合法 period 把 DC 对齐回去。判据在 proveEcatDc 里(唯一实现):
|
||||
* 证明不了 = 不动总线。这条规则的存在理由就是 healthCheck 那份内联副本 ——
|
||||
* 它把"没有 dc 字段"当成"DC 配错",于是一篇错误回包就打出一记 loop_stop 的
|
||||
* motor/start(记忆 ecat-bus-exclusive-route-restart-trips-watchdog)。
|
||||
* 修复目标是「period 与 DC 同值」,不是「一律拉回 0.25」。
|
||||
* @param {(path:string,body?:object,timeoutMs?:number)=>Promise<any>} ecatPost
|
||||
* @param {object} [knownHealth] 调用方已经读到的**原始** native/health 回包(省一次往返,
|
||||
* 判据仍只走 proveEcatDc 这一处)。传错形状会被判成 unknown —— 也就是不动总线。
|
||||
@@ -304,13 +331,15 @@ export async function repairEcatDc(ecatPost, knownHealth) {
|
||||
if (proveEcatDc(h) !== "misconfigured") return null;
|
||||
/* native 正在全量重连(close→open→OP,秒级)时再插一记 motor/start 是自相残杀:
|
||||
* 两条恢复路各自 loop_stop,最后谁都不知道总线是谁停的。 */
|
||||
if (h.recovering) return null;
|
||||
return ecatPost("motor/start", {
|
||||
period_ms: ECAT_DEFAULT_PERIOD_MS,
|
||||
dc_cycle_us: ECAT_DC_CYCLE_US,
|
||||
dc_shift_us: 0,
|
||||
reason: "auto_dc_repair",
|
||||
}, 30000);
|
||||
if (h && h.recovering) return null;
|
||||
var per = readStoredEcatPeriod();
|
||||
if (h && h.period_ms != null) {
|
||||
var hp = Number(h.period_ms);
|
||||
if (hp >= ECAT_PERIOD_MIN_MS && hp <= ECAT_PERIOD_MAX_MS) per = hp;
|
||||
}
|
||||
var body = resolveEcatStartBody({ period_ms: per });
|
||||
body.reason = "auto_dc_repair";
|
||||
return ecatPost("motor/start", body, 30000);
|
||||
}
|
||||
|
||||
/** @description 读当前网卡子串(localStorage → 默认 I210)。 */
|
||||
|
||||
Reference in New Issue
Block a user