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
+24 -16
View File
@@ -3,7 +3,7 @@
*
* CAN 页面的编排逻辑:
* - cacheElements:缓存 DOM 元素到 el(dbc.js 导出的共享 el 对象)
* - setConnectionState:顶栏 pills(连接/IP/RSSI/运行/版本)
* - setConnectionState:顶栏 pills(网络状态合并 IP/RSSI、运行/版本)
* - testConnection:GET /system/status,静默刷新顶栏(5s)
* - clearDbc / handleDbcFile / handleDbcError:DBC 文件读取与解析流程编排
* - bindEvents:绑定所有事件监听器(调用子模块的 bindNodeFilterEvents 等)
@@ -19,7 +19,7 @@
/* JS 间 import 一律不带 ?v=,版本戳只出现在 HTML 入口标签上,
* 避免同一模块因 query 不同被浏览器实例化两次(state/toast 单例分裂) */
import {
state, el, getBaseUrl, getDeviceIp, setDeviceIp, parseDbc, readDbcText, buildSummary,
state, el, getBaseUrl, getDeviceIp, parseDbc, readDbcText, buildSummary,
rebuildMergedDbc, makeDbcSourceId
} from './can/dbc.js';
import {
@@ -44,7 +44,7 @@ import {
sendManualFrame,
resetManualFields,
loadPreviewToManual
} from './can/send.js?v=20260801f';
} from './can/send.js?v=20261010a';
import {
scheduleCanPersist,
loadCanSession,
@@ -71,7 +71,7 @@ function cacheElements() {
'messageList', 'sendSelectedBtn', 'generatedHex', 'generatedBytes', 'encodeHint',
'dbcResp', 'dbcRespText', 'loadPreviewToRawBtn', 'manualHint', 'sendManualBtn',
'resetManualBtn', 'manualResp', 'manualRespText', 'historyList', 'toast', 'toastIco',
'toastMsg', 'canConnPill', 'canConnTxt', 'canIpPill', 'canRssiPill', 'canUptimePill',
'toastMsg', 'canNetPill', 'canNetTxt', 'canUptimePill',
'canVersionPill', 'canChannelPill',
'dbcSummaryPill', 'messageSummaryPill',
'selectedCountPill', 'selectAllBox', 'expandAllBtn', 'collapseAllBtn', 'sendCheckedBtn',
@@ -84,19 +84,19 @@ function cacheElements() {
// ==================== 连接状态(仅顶栏 pills,与工作台去重) ====================
function setConnectionState(connected, text, buildVersion, buildTime, wifi, uptimeSec) {
if (el.canConnPill) {
el.canConnPill.className = 'pill ' + (connected ? 'online' : 'offline');
}
if (el.canConnTxt) el.canConnTxt.textContent = text;
/* STA/IP/RSSI 合并成一个网络胶囊,减少顶栏碎片(与 OTA 页 link-pill 文案一致) */
wifi = wifi || {};
if (el.canIpPill) {
var label = text || (connected ? '已连接' : '未连接');
if (connected) {
var ip = wifi.ip || getDeviceIp();
el.canIpPill.textContent = 'IP ' + (ip || '--');
}
if (el.canRssiPill) {
if (ip) label += ' · ' + ip;
var rssiOk = wifi.rssi !== undefined && wifi.rssi !== null && wifi.rssi !== -127;
el.canRssiPill.textContent = rssiOk ? ('RSSI ' + wifi.rssi + ' dBm') : 'RSSI --';
if (rssiOk) label += ' · ' + wifi.rssi + ' dBm';
}
if (el.canNetPill) {
el.canNetPill.className = 'pill link-pill ' + (connected ? 'online' : 'offline');
}
if (el.canNetTxt) el.canNetTxt.textContent = label;
if (el.canUptimePill) {
el.canUptimePill.textContent = '运行 ' + (uptimeSec !== undefined && uptimeSec !== null
? formatUptime(uptimeSec) : '--');
@@ -122,7 +122,15 @@ async function testConnection() {
var data = await resp.json();
state.latestStatus = data;
var wifi = data.wifi || {};
if (wifi.ip) setDeviceIp(wifi.ip);
/* 不再用设备自报 IP 回写共享端点:AP 回退或 STA 掉租约时固件上报的是
* 初值 "0.0.0.0"(ESP32_COMM/main/server/http_handlers.cpp:1776),
* 回写会让 http://0.0.0.0(=回环)成为六个标签一起用的端点。
* 这里只提示不符,换不换由人在发现面板/输入框点。 */
var curIp = deviceLink.getIp();
if (wifi.ip && curIp && String(wifi.ip) !== String(curIp)) {
showToast('设备自报 IP=' + wifi.ip + ',与当前端点 ' + curIp + ' 不符(AP 回退或已换址)',
'warning', { force: true });
}
var connected = !!wifi.connected;
var mode = wifi.mode || 'STA';
var text = connected ? (mode + ' 已连接') : (mode + ' 未连接');
@@ -412,13 +420,13 @@ function init() {
}
setTimeout(testConnection, 150);
/* 周期性刷新顶栏 STA/IP/RSSI/运行时间(与监控 1Hz 解耦,避免重复 pill) */
/* 周期性刷新顶栏网络状态/运行时间(与监控 1Hz 解耦,避免重复 pill) */
setInterval(testConnection, 5000);
/* ConnectionHub 快照只覆盖在线布尔,不改 testConnection 写的语义文案(STA/HTTP…) */
deviceLink.subscribe(function (s) {
if (!s || !s.ts || (Date.now() - s.ts) >= 5000) return;
var on = !!(s.links && s.links.esp32 && s.links.esp32.online);
if (el.canConnPill) el.canConnPill.className = 'pill ' + (on ? 'online' : 'offline');
if (el.canNetPill) el.canNetPill.className = 'pill link-pill ' + (on ? 'online' : 'offline');
});
/* 右侧抽屉:CAN 接收监控 */
initCanMonitor();