feat(link): ConnectionHub 统一 WiFi/ECAT/BLE 端点与健康态
各页不再各自读写 dcu_esp_ip / dcu_ecat_adapter:deviceLink 提供 wifiBaseUrl/ecatPost/adapter API,OTA/CAN/motor/ecat_load/lab 走同一入口。 连接灯只覆盖在线布尔,保留调用方语义文案。
This commit is contained in:
+103
-45
@@ -1,39 +1,82 @@
|
||||
/**
|
||||
* device-link.js — 全链路设备健康层(外壳 Owner + 功能页 Client)
|
||||
* device-link.js — 全链路连接中心(ConnectionHub)
|
||||
*
|
||||
* 问题:各页各自 fetch,只共用 IP 字符串 → 一页「已连接」一页「失败」。
|
||||
* 方案:tabs.html 常驻 Owner 轮询 ESP32 /system/status 与 EtherCAT /api/status
|
||||
* + /api/app/load,经 BroadcastChannel('dcu-device-link') 广播快照;
|
||||
* 功能页只订阅渲染。业务命令(SPI/CAN/OTA)仍页内发,失败不得把
|
||||
* 「设备离线」灯打红——灯只认本层。
|
||||
* 统一管理页面上的 WiFi / EtherCAT / BLE 三条链路:
|
||||
* - 健康轮询:外壳 Owner 常驻拉 ESP32 /system/status 与 EtherCAT status+load
|
||||
* - 端点配置:设备 IP(dcu_esp_ip)、网卡子串(dcu_ecat_adapter)只在此读写
|
||||
* - 通道适配:wifiBaseUrl / ecatPost 供各页复用,禁止再散落 localStorage
|
||||
*
|
||||
* 角色:
|
||||
* - Owner:tabs.html 在加载本文件前设 window.__dcuDeviceLinkOwner = true
|
||||
* - Owner:tabs.html 加载前设 window.__dcuDeviceLinkOwner = true
|
||||
* - Client:任意页 import { deviceLink }
|
||||
* 独立直开页面(无外壳)时 client 收不到 snapshot,可退回本地轮询。
|
||||
* 独立直开页无 Owner 时,client 仍可读 IP / 发 ecatPost,只是无跨页快照。
|
||||
*
|
||||
* 命令仍由业务页自己发;连接灯只认本层健康态(布尔优先,文案保留调用方语义)。
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
import {
|
||||
getDeviceIp, saveDeviceIp, getBaseUrl as baseUrlFromIp
|
||||
} from "./base-url.js";
|
||||
|
||||
var CH_NAME = "dcu-device-link";
|
||||
var ESP32_FAIL_ONLINE = 3; /* 连续失败 ≥3 判离线 */
|
||||
var ESP32_POLL_MS = 1000;
|
||||
var ECAT_POLL_MS = 2000;
|
||||
var LS_ESP_IP = "dcu_esp_ip";
|
||||
var DEFAULT_IP = "192.168.31.127";
|
||||
var LS_ECAT_ADAPTER = "dcu_ecat_adapter";
|
||||
var DEFAULT_ECAT_ADAPTER = "I210";
|
||||
|
||||
function nowMs() { return Date.now(); }
|
||||
|
||||
function readIp() {
|
||||
try {
|
||||
var s = localStorage.getItem(LS_ESP_IP);
|
||||
if (s && s.trim()) return s.trim();
|
||||
} catch (_) {}
|
||||
return DEFAULT_IP;
|
||||
return getDeviceIp();
|
||||
}
|
||||
|
||||
function writeIp(ip) {
|
||||
if (!ip || !String(ip).trim()) return;
|
||||
try { localStorage.setItem(LS_ESP_IP, String(ip).trim()); } catch (_) {}
|
||||
saveDeviceIp(ip);
|
||||
}
|
||||
|
||||
function readAdapter() {
|
||||
try {
|
||||
var s = localStorage.getItem(LS_ECAT_ADAPTER);
|
||||
if (s && s.trim()) return s.trim();
|
||||
} catch (_) {}
|
||||
return DEFAULT_ECAT_ADAPTER;
|
||||
}
|
||||
|
||||
function writeAdapter(substr) {
|
||||
if (!substr || !String(substr).trim()) return;
|
||||
try { localStorage.setItem(LS_ECAT_ADAPTER, String(substr).trim()); } catch (_) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一 /ecat/api/* 代理请求(同源,esp32_server 转发到 ecat_native)。
|
||||
* @param {string} path 相对路径,如 "status" / "app/load" / "open"
|
||||
* @param {object} body JSON body,缺省 {}
|
||||
* @param {number} timeoutMs 默认 5000
|
||||
* @returns {Promise<object>} 解析后的 JSON
|
||||
*/
|
||||
async function ecatPost(path, body, timeoutMs) {
|
||||
var p = String(path || "").replace(/^\/+/, "");
|
||||
var resp = await fetch("/ecat/api/" + p, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body || {}),
|
||||
cache: "no-store",
|
||||
signal: AbortSignal.timeout(timeoutMs || 5000)
|
||||
});
|
||||
var text = await resp.text();
|
||||
var json = {};
|
||||
try { json = text ? JSON.parse(text) : {}; } catch (_) {
|
||||
throw new Error("响应非合法 JSON");
|
||||
}
|
||||
if (!resp.ok) {
|
||||
var err = new Error("HTTP " + resp.status);
|
||||
err.status = resp.status;
|
||||
err.json = json;
|
||||
throw err;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
function emptySnapshot() {
|
||||
@@ -89,11 +132,10 @@ function createOwner() {
|
||||
|
||||
function pollEsp32() {
|
||||
if (stopped) return;
|
||||
var ip = readIp();
|
||||
var ac = null;
|
||||
try { ac = new AbortController(); } catch (_) {}
|
||||
var tmo = null;
|
||||
fetch("http://" + ip + "/system/status" + (window._memLayout ? "" : "?layout=1"), {
|
||||
fetch(baseUrlFromIp() + "/system/status" + (window._memLayout ? "" : "?layout=1"), {
|
||||
signal: ac ? ac.signal : undefined,
|
||||
cache: "no-store"
|
||||
}).then(function (r) {
|
||||
@@ -115,15 +157,8 @@ function createOwner() {
|
||||
|
||||
function pollEcat() {
|
||||
if (stopped) return;
|
||||
var base = "/ecat/api/";
|
||||
var stP = fetch(base + "status", {
|
||||
method: "POST", body: "{}", cache: "no-store",
|
||||
signal: AbortSignal.timeout(4000)
|
||||
}).then(function (r) { return r.json(); }).catch(function () { return null; });
|
||||
var ldP = fetch(base + "app/load", {
|
||||
method: "POST", body: "{}", cache: "no-store",
|
||||
signal: AbortSignal.timeout(4000)
|
||||
}).then(function (r) { return r.json(); }).catch(function () { return null; });
|
||||
var stP = ecatPost("status", {}, 4000).catch(function () { return null; });
|
||||
var ldP = ecatPost("app/load", {}, 4000).catch(function () { return null; });
|
||||
|
||||
Promise.all([stP, ldP]).then(function (arr) {
|
||||
var st = arr[0], ld = arr[1];
|
||||
@@ -151,11 +186,13 @@ function createOwner() {
|
||||
if (m.type === "hello") { publish(); return; }
|
||||
if (m.type === "set-ip") {
|
||||
writeIp(m.ip);
|
||||
/* 立刻重拉 ESP32(旧 timer 下一拍也会跟上) */
|
||||
try { fetch("http://" + readIp() + "/system/status", { cache: "no-store",
|
||||
signal: AbortSignal.timeout(2000) }).then(function (r) { return r.json(); })
|
||||
.then(function (j) { setEsp32(j, true); })
|
||||
.catch(function () { setEsp32(null, false); });
|
||||
try {
|
||||
fetch(baseUrlFromIp() + "/system/status", {
|
||||
cache: "no-store",
|
||||
signal: AbortSignal.timeout(2000)
|
||||
}).then(function (r) { return r.json(); })
|
||||
.then(function (j) { setEsp32(j, true); })
|
||||
.catch(function () { setEsp32(null, false); });
|
||||
} catch (_) {}
|
||||
return;
|
||||
}
|
||||
@@ -207,10 +244,19 @@ function createClient() {
|
||||
try { ch.postMessage({ type: "hello" }); } catch (_) {}
|
||||
}
|
||||
|
||||
function applyLocalBle(online, deviceName) {
|
||||
if (last) {
|
||||
last.links.ble.online = !!online;
|
||||
last.links.ble.deviceName = deviceName || "";
|
||||
last.links.ble.owner = online ? "iframe" : null;
|
||||
last.ts = nowMs();
|
||||
emit();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
/** 当前快照(可能为 null,Owner 尚未广播时) */
|
||||
/* ── 快照 ── */
|
||||
getSnapshot: function () { return last; },
|
||||
/** 订阅快照;返回取消函数 */
|
||||
subscribe: function (fn) {
|
||||
if (typeof fn !== "function") return function () {};
|
||||
listeners.push(fn);
|
||||
@@ -220,11 +266,30 @@ function createClient() {
|
||||
if (i >= 0) listeners.splice(i, 1);
|
||||
};
|
||||
},
|
||||
/** 指定通道当前是否在线;无快照时回退 false(调用方自行降级) */
|
||||
channelOnline: function (name) {
|
||||
if (!last || !last.links) return false;
|
||||
var L = last.links[name];
|
||||
return !!(L && L.online);
|
||||
},
|
||||
|
||||
/* ── WiFi 端点 ── */
|
||||
setIp: function (ip) {
|
||||
writeIp(ip);
|
||||
if (ch) { try { ch.postMessage({ type: "set-ip", ip: readIp() }); } catch (_) {} }
|
||||
},
|
||||
getIp: function () { return readIp(); },
|
||||
wifiBaseUrl: function () { return baseUrlFromIp(); },
|
||||
|
||||
/* ── EtherCAT 端点与请求 ── */
|
||||
getEcatAdapter: function () { return readAdapter(); },
|
||||
setEcatAdapter: function (substr) {
|
||||
writeAdapter(substr);
|
||||
return readAdapter();
|
||||
},
|
||||
ecatPost: ecatPost,
|
||||
|
||||
/* ── BLE 状态上报 ── */
|
||||
reportBle: function (online, deviceName) {
|
||||
var payload = {
|
||||
type: "ble-state",
|
||||
@@ -233,16 +298,9 @@ function createClient() {
|
||||
owner: "iframe"
|
||||
};
|
||||
if (ch) { try { ch.postMessage(payload); } catch (_) {} }
|
||||
/* 本地立刻改一份,单页直开也能看到 */
|
||||
if (last) {
|
||||
last.links.ble.online = !!online;
|
||||
last.links.ble.deviceName = deviceName || "";
|
||||
last.links.ble.owner = online ? "iframe" : null;
|
||||
last.ts = nowMs();
|
||||
emit();
|
||||
}
|
||||
applyLocalBle(online, deviceName);
|
||||
},
|
||||
/** 是否有外壳 Owner 在管(收到过 snapshot 或自身是 owner) */
|
||||
|
||||
hasOwner: function () { return !!last; }
|
||||
};
|
||||
}
|
||||
@@ -257,5 +315,5 @@ try {
|
||||
} catch (_) {}
|
||||
|
||||
deviceLink = createClient();
|
||||
export { deviceLink };
|
||||
export { deviceLink, ecatPost, readAdapter as getEcatAdapter, writeAdapter as setEcatAdapter };
|
||||
export default deviceLink;
|
||||
|
||||
Reference in New Issue
Block a user