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:
cbb
2026-10-13 00:48:34 +08:00
parent 514070b36d
commit eac3717f9a
41 changed files with 4229 additions and 934 deletions
+57 -28
View File
@@ -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)。 */