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:
@@ -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)));
|
||||
|
||||
Reference in New Issue
Block a user