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
+75 -1
View File
@@ -134,8 +134,12 @@ export function createWifiTransport(ctx) {
return {
id: "wifi",
label: "WiFi / HTTP",
/** OTA 走 ESP32 的 /motor/ota*(固件上传 + GD32 relay),EtherCAT 侧没有这条链路 */
/** OTA 走 ESP32 的 /motor/ota*(固件上传 + GD32 relay) */
supportsOta: true,
/** 进 BOOT 帧由上位机发(既有顺序:先帧后 relay),锁还没上,通道此刻是空的 */
otaHostSendsBootFrame: true,
/** 完整性口径:ESP32 从 flash 读回算 SHA-256,与浏览器算的比 */
otaIntegrity: "sha256-esp32-flash",
/** 0xC5 推送周期可与前端刷新率对齐 */
supportsPeriodSync: true,
@@ -229,6 +233,76 @@ export function createWifiTransport(ctx) {
return data.nodes.find((n) => n.node === node) || null;
},
/* ── 电机 OTA(ESP32 上传 + SPI 中继)────────────────────────────
* 还是原来那三条 HTTP:POST /motor/ota(写 ESP32 flash 并回读算 SHA-256)、
* POST /motor/ota/relay(SPI 0xD0 START + feedTask 喂数)、GET /motor/ota/status。
* 收进传输契约只有一个原因:ota.js 原先硬写 getBaseUrl(),等于假设「设备 IP
* 一定打得通」——纯 EtherCAT 场合那条前提不成立。行为与外提前完全一致。 */
/** 一次上传尝试:POST 整份镜像,拿设备回读的 flash SHA-256 与浏览器算的比。
* 不抛错——失败是这里的正常出口(ota.js 据此决定重传,最多 3 次)。 */
async otaUploadAttempt(bytes, shaHex, opts) {
const ch = (opts && opts.ch) | 0;
let json = {};
try {
const resp = await fetch(getBaseUrl() + `/motor/ota?ch=${ch}`, {
method: "POST",
headers: { "Content-Type": "application/octet-stream" },
body: bytes,
signal: AbortSignal.timeout(15000),
});
json = await resp.json().catch(() => ({}));
if (!resp.ok) throw new Error(json.error || `HTTP ${resp.status}`);
} catch (e) {
return { ok: false, note: (e && e.message) || String(e) };
}
const size = Number(json.size) || bytes.length;
if (!json.sha256 || String(json.sha256).length !== 64) {
/* 旧 ESP32 固件不回 SHA:没有对端读数可比,如实说明后放行(沿用既有行为),
* 后续 relay 若被拒会由那条报错兜住。 */
return { ok: true, size, note: "设备未返回 flash SHA(旧 ESP32 固件,建议先刷新 ESP32),跳过比对" };
}
const dev = String(json.sha256).toLowerCase();
return {
ok: dev === String(shaHex).toLowerCase(),
size,
note: dev === String(shaHex).toLowerCase()
? "flash 回读 SHA-256 一致"
: `flash SHA 不一致(设备 ${dev.slice(0, 16)}…)`,
};
},
/** 触发中继:GD32 开始握手,ESP32 起 feedTask 喂 SPI。失败抛错(上层按启动失败处理)。 */
async otaStartRelay(ch) {
const resp = await fetch(getBaseUrl() + `/motor/ota/relay?ch=${(ch | 0)}`, {
method: "POST",
signal: AbortSignal.timeout(8000),
});
const json = await resp.json().catch(() => ({}));
if (!resp.ok) throw new Error(json.error || `HTTP ${resp.status}`);
return { ok: true };
},
/** 进度:ESP32 已经给字符串状态,直接透传;session_error/note 是 ECAT 才有的字段,
* 这里补空值,让上层的显示逻辑不用分链路写两遍。 */
async otaStatus() {
const st = await getJson("/motor/ota/status", 2000);
return Object.assign({ session_error: 0, note: "" }, st);
},
/** 兜底清理:让 ESP32 退出 feedTask + GD32 resetToIdle。 */
async otaCancel() {
try {
const r = await fetch(getBaseUrl() + "/motor/ota/cancel", {
method: "POST",
signal: AbortSignal.timeout(5000),
});
return { ok: r.ok };
} catch (e) {
return { ok: false, note: (e && e.message) || String(e) };
}
},
/** 把前端刷新周期同步给 GD32(0xC5 主动推送周期),失败静默。 */
async setPeriod(ms) {
const v = Math.max(20, Math.min(65535, Math.round(ms)));