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
+132 -14
View File
@@ -1,7 +1,7 @@
/**
* connection.js - 连接管理 + 系统状态显示
*
* 操作 DOM 元素 #pillConn / #pillConnTxt / #bConn 等(连接文案仅顶栏)。
* 操作 DOM 元素 #pillWifi/#pillEcat/#pillBle / #bConn 等(三链路胶囊在顶栏)。
*
* 共享状态(通过 window.* 引用):
* window.latestSystemStatus — 最近一次 /system/status 响应
@@ -14,7 +14,7 @@
*/
import {
formatBytes, formatTime, formatUptime, escapeHtml,
setTextIfChanged, setClassIfChanged, setQuietMode,
setTextIfChanged, setClassIfChanged, setQuietMode, setCellSource,
} from './utils.js';
import { addLog } from './log.js';
import { showToast } from './toast.js';
@@ -72,37 +72,155 @@ function transferOngoing() {
} catch (_) { return false; }
}
/**
* 顶栏三链路胶囊:WiFi / ECAT / BLE 独立亮灭。
* **唯一写入点**:WiFi 文案只在这里生成(mode + 连接态),别处禁止再拼字符串。
* ECAT 文案由 app.js 写入(0x1008 slave.name)。
* @param {boolean|null} wifiOn WiFi 在线;null=按 DeviceLink 快照
* @param {boolean|null} [bleOn] BLE 在线;null=读 window.__bleConnState
*/
/** @description EtherCAT AL 状态位 → 人话(1=INIT 2=PREOP 3=BOOT 4=SAFEOP 8=OP)。 */
function ecatAlText(state) {
if (state === null || state === undefined || !Number.isFinite(Number(state))) return '';
switch (Number(state) & 0x0F) {
case 1: return 'INIT';
case 2: return 'PREOP';
case 3: return 'BOOT';
case 4: return 'SAFEOP';
case 8: return 'OP';
default: return '';
}
}
export function updateLinkPills(wifiOn, bleOn) {
try {
var snap = deviceLink.getSnapshot();
var fresh = snap && snap.ts && (Date.now() - snap.ts) < 5000;
var w = wifiOn;
if (fresh && snap.links && snap.links.esp32) w = !!snap.links.esp32.online;
if (typeof wifiOn === 'boolean' && !fresh) w = wifiOn;
/* WiFi 胶囊:文案 = mode + 连接态 + IP + 信号(STA=Station 客户端模式) */
var pw = getEl('pillWifi'), pwT = getEl('pillWifiTxt');
if (pw) {
var mode = 'STA';
var ip = '';
var rssi = '';
try {
if (fresh && snap && snap.esp32Status) {
var stWifi = snap.esp32Status.wifi || {};
if (stWifi.mode) mode = stWifi.mode;
if (stWifi.ip) ip = stWifi.ip;
if (stWifi.rssi != null && stWifi.rssi !== '') rssi = String(stWifi.rssi) + ' dBm';
}
if (!ip) ip = deviceLink.getIp() || '';
} catch (_) {}
var wifiLabel = mode + (w ? ' 已连接' : ' 未连接');
if (w && ip) wifiLabel += ' · ' + ip;
if (w && rssi) wifiLabel += ' · ' + rssi;
/* 运行时覆盖(重启中/上传中/设备忙):优先显示,避免被 1Hz 轮询冲掉 */
if (window.__wifiPillOverride) wifiLabel = window.__wifiPillOverride;
setClassIfChanged(pw, 'pill link-pill ' + (window.__wifiPillOverride ? 'processing' : (w ? 'online' : 'offline')));
if (pwT) setTextIfChanged(pwT, wifiLabel);
}
/* ECAT 胶囊:灯色与文案**都由这里出**。此前 app.js 的 0x1008 probe 也写同一颗
pill 的 className,两边节奏/判据都不同,实测出现过"绿点 + 文字离线"的矛盾。
分级:后端/网卡没开=离线;开了且 PDO 循环在跑=在线;开了但循环没跑=琥珀并标 AL 档。 */
var pe = getEl('pillEcat'), peT = getEl('pillEcatTxt');
if (pe) {
var E = (fresh && snap && snap.links && snap.links.ecat) ? snap.links.ecat : null;
var eName = (E && E.deviceName) || window.__ecatProbeName || '';
var eCls, eTxt;
if (!E) { eCls = 'idle'; eTxt = '未探测'; }
else if (!E.busOpen) { eCls = 'offline'; eTxt = '离线(后端/网卡未开)'; }
else if (E.loop) { eCls = 'online'; eTxt = eName || (E.op ? 'OP 循环中' : '循环中'); }
else {
var al = ecatAlText(E.slaveState);
eCls = 'processing';
eTxt = (eName ? eName + ' · ' : '') + (al ? al + ' 无循环' : '未进 OP');
}
setClassIfChanged(pe, 'pill link-pill ' + eCls);
if (peT) setTextIfChanged(peT, eTxt);
}
/* BLE 胶囊 */
var pb = getEl('pillBle'), pbT = getEl('pillBleTxt');
if (pb) {
var b = bleOn;
if (b === null || b === undefined) {
try {
var st = window.__bleConnState;
if (st === true || st === false) b = st;
} catch (_) {}
}
if (b === null || b === undefined) {
setClassIfChanged(pb, 'pill link-pill idle');
if (pbT) setTextIfChanged(pbT, '未连接');
} else if (b) {
setClassIfChanged(pb, 'pill link-pill online');
var nm = window.__bleDeviceName || '已连接';
var did = window.__bleDeviceId;
var bl = did ? (nm + ' · ' + did) : nm;
if (pbT) setTextIfChanged(pbT, bl);
} else {
setClassIfChanged(pb, 'pill link-pill offline');
if (pbT) setTextIfChanged(pbT, '未连接');
}
}
} catch (_) {}
}
/** WiFi 胶囊临时覆盖文案(重启中/上传中/设备忙);传 null 清除。 */
export function setWifiPillOverride(text) {
window.__wifiPillOverride = text || null;
updateLinkPills(null, null);
}
export function updateConnectionUi(connected, text) {
/* DeviceLink 统一层:外壳 Owner 广播的新鲜快照优先于本页局部轮询结论,
避免 OTA「已连接」与 LAN9252「失败」同屏分裂。独立直开页无快照则用参数。 */
避免 OTA「已连接」与 LAN9252「失败」同屏分裂。独立直开页无快照则用参数。
双通路:WiFi 或 ECAT 任一在线即「在线」;角标标出实际在线链路。 */
var src = 'wifi';
try {
var snap = deviceLink.getSnapshot();
if (snap && snap.ts && (Date.now() - snap.ts) < 5000) {
var w = !!(snap.links && snap.links.esp32 && snap.links.esp32.online);
var e = !!(snap.links && snap.links.ecat && snap.links.ecat.online);
/* Hub 可把在线压成离线;**不得**把本页刚判的离线改回在线。
* Owner 要连失败 3 次才翻离线,期间若无脑覆盖,
* OTA 页超时判离线后灯又会被点绿 → 拔线后永远「已连接」。 */
if (!snap.links.esp32.online) connected = false;
* OTA 页超时判离线后灯又会被点绿 → 拔线后永远「已连接」。
* 双通路修正:WiFi 掉线但 ECAT 仍在线时,整体仍算在线(ECAT 撑着)。 */
if (!w && !e) connected = false;
else if (!connected && (w || e)) {
/* 仅当两路里至少一路 Hub 判在线,且本页因 WiFi 超时误判离线时,
* 才允许 Hub 把「在线」加回来(ECAT 撑场)。 */
if (e && !w) connected = true;
}
if (w && e) src = 'dual';
else if (e && !w) src = 'ecat';
else src = 'wifi';
if (!text) text = connected ? '已连接' : '离线';
if (connected && e && !w) text = text && text.indexOf('ECAT') < 0
? (text + ' · ECAT') : text;
} else {
/* 无快照:参数 connected 来自 WiFi 轮询 */
src = connected ? 'wifi' : 'off';
}
} catch (_) {}
} catch (_) {
src = connected ? 'wifi' : 'off';
}
var label = text || (connected ? '已连接' : '未连接');
/* 判离线立刻清实时指标:只改灯不等于清数据,否则 APP/缓存下会
* 出现「离线 + 温度/电压仍显示」的假活状态。 */
if (!connected && typeof window.clearLiveMetricsOnDisconnect === 'function') {
try { window.clearLiveMetricsOnDisconnect(); } catch (_) {}
}
// 仅顶栏连接胶囊(下方连接卡不再重复显示文案)
var pc = getEl('pillConn');
var pct = getEl('pillConnTxt');
if (pc) {
setClassIfChanged(pc, connected ? 'pill online' : 'pill offline');
if (pct) setTextIfChanged(pct, label);
}
// 系统状态卡徽章
// 顶栏三链路:只刷灯/来源角标;WiFi 文案由 updateLinkPills 统一生成
updateLinkPills(connected, null);
// 系统状态卡徽章(整体:任一数据通路在线即在线)
var bc = getEl('bConn');
if (bc) {
setTextIfChanged(bc, connected ? '在线' : '离线');
setClassIfChanged(bc, 'v ' + (connected ? 'ok' : 'err'));
setCellSource('bConn', connected ? src : 'off');
}
setQuietMode(!connected && !transferOngoing());
}