From f9502579ef67f365261dccd402a1f98bccc9455b Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Wed, 16 Sep 2026 12:24:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(link):=20=E5=A4=96=E5=A3=B3=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E5=BC=BA=E5=88=B6=20open+motor/start=EF=BC=8C?= =?UTF-8?q?=E4=B8=8D=E5=BF=85=E7=AD=89=E7=94=B5=E6=9C=BA=E9=A1=B5=E5=88=87?= =?UTF-8?q?=20EtherCAT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DeviceLink Owner 在控制台加载后自动 bootstrapEcatBus:status→open→ motor/start,沿用 hub 网卡与 period/spin 持久化。成功后不重复 start; 掉线 10s 退避重开。有线 ECAT 与 WiFi 不冲突。 --- docs/compose/spec/device-link.md | 3 ++ js/common/device-link.js | 58 +++++++++++++++++++++++++++++++- js/ota/ecat_load.js | 4 +-- tabs.html | 2 +- 4 files changed, 63 insertions(+), 4 deletions(-) diff --git a/docs/compose/spec/device-link.md b/docs/compose/spec/device-link.md index 111a6f0..2ea96d1 100644 --- a/docs/compose/spec/device-link.md +++ b/docs/compose/spec/device-link.md @@ -48,6 +48,8 @@ can_monitor、motor、ecat_load、LAN9252 改为从 Hub 读端点/订在线布 3. **端点配置只在 ConnectionHub**:IP(`dcu_esp_ip`)、网卡(`dcu_ecat_adapter`)读写集中。 4. **命令仍页内发**,连接灯只认 DeviceLink 布尔(文案保留调用方语义)。 5. BLE 为一等通道但单独适配:只广播状态,GATT 细节留在 `ble/*`。 +6. **外壳启动强制开总线**:Owner 首轮即 `status→open→motor/start`;成功后不再重复 + `motor/start`,掉线后 10s 退避重试。有线 EtherCAT 与 WiFi 不争网卡。 ### ConnectionHub 客户端 API(`deviceLink`) @@ -66,6 +68,7 @@ Owner 消息:`hello` / `snapshot` / `set-ip` / `ble-state`。 | 位置 | 行为 | |------|------| | `tabs.html` | Owner | +| Owner `bootstrapEcatBus` | 外壳启动即 open+motor/start,不必等电机页切 EtherCAT | | `connection.js` | 快照新鲜时覆盖 connected 布尔 | | `app.js` | IP/baseURL 走 `deviceLink`(保留 `#espIp` 输入回退) | | `can/dbc.js` + `can.js` | setDeviceIp 广播;顶栏灯订阅 hub | diff --git a/js/common/device-link.js b/js/common/device-link.js index 983bb37..190f1ac 100644 --- a/js/common/device-link.js +++ b/js/common/device-link.js @@ -23,6 +23,9 @@ var CH_NAME = "dcu-device-link"; var ESP32_FAIL_ONLINE = 3; /* 连续失败 ≥3 判离线 */ var ESP32_POLL_MS = 1000; var ECAT_POLL_MS = 2000; +var ECAT_BUS_RETRY_MS = 10000; /* 开总线失败后的重试间隔 */ +var LS_ECAT_PERIOD = "dcu_ecat_period_ms"; +var LS_ECAT_SPIN = "dcu_ecat_spin"; var LS_ECAT_ADAPTER = "dcu_ecat_adapter"; var DEFAULT_ECAT_ADAPTER = "I210"; @@ -104,6 +107,11 @@ function createOwner() { var esp32Timer = null; var ecatTimer = null; var stopped = false; + /* 外壳启动即强制拉起总线(open + motor/start),不必等电机页切 EtherCAT。 + * 成功后不再重复 motor/start;掉线后按 ECAT_BUS_RETRY_MS 退避重试。 */ + var ecatBusReady = false; + var ecatBusBusy = false; + var ecatBusNextTryAt = 0; function publish() { snap.ts = nowMs(); @@ -155,6 +163,50 @@ function createOwner() { } } + /** + * 开总线:status →(未开则)open 网卡 → motor/start 进 OP + 起 PDO 循环。 + * 与 motor/transport-ecat ensureOpen/ensureStarted 同源,由外壳 Owner 在 + * 控制台启动时强制执行,用户不必先去电机页把链路切到 EtherCAT。 + */ + function bootstrapEcatBus() { + if (stopped || ecatBusBusy) return; + var now = nowMs(); + if (ecatBusReady) return; + if (now < ecatBusNextTryAt) return; + ecatBusBusy = true; + ecatBusNextTryAt = now + ECAT_BUS_RETRY_MS; + + var adapter = readAdapter(); + var body = {}; + try { + var v = parseFloat(localStorage.getItem(LS_ECAT_PERIOD)); + if (v > 0) body.period_ms = v; + var sp = localStorage.getItem(LS_ECAT_SPIN); + if (sp === "0") body.spin = false; + else if (sp === "1") body.spin = true; + } catch (_) {} + + ecatPost("status", {}, 6000).then(function (st) { + var open = !!(st && st.ok); + return open ? Promise.resolve(st) + : ecatPost("open", { substr: adapter }, 20000); + }).then(function (r) { + if (r && r.ok === false) { + throw new Error((r.log && r.log.join(" / ")) || "open 失败"); + } + return ecatPost("motor/start", body, 30000); + }).then(function (r) { + if (r && r.ok === false) { + throw new Error((r.log && r.log.join(" / ")) || "motor/start 失败"); + } + ecatBusReady = true; + }).catch(function () { + /* 网线没插/从站未上电:退避后由 pollEcat 再触发 */ + }).then(function () { + ecatBusBusy = false; + }); + } + function pollEcat() { if (stopped) return; var stP = ecatPost("status", {}, 4000).catch(function () { return null; }); @@ -164,7 +216,7 @@ function createOwner() { var st = arr[0], ld = arr[1]; var e = snap.links.ecat; e.online = !!(st && st.ok && st.slave); - e.busOpen = e.online; + e.busOpen = !!(st && st.ok); /* native status 不直接给 op/loop;用 load valid + slave state 粗判 */ e.op = !!(st && st.slave && st.slave.state != null && (st.slave.state & 0x0F) === 8); e.loadValid = !!(ld && ld.ok && ld.valid === true @@ -172,7 +224,10 @@ function createOwner() { e.loop = e.loadValid || e.op; e.loadPerm = (ld && Number.isFinite(ld.load_perm)) ? ld.load_perm : 0; e.ageMs = (ld && Number.isFinite(ld.age_ms)) ? ld.age_ms : -1; + if (e.busOpen && e.loop) ecatBusReady = true; + else if (ecatBusReady && !e.busOpen) ecatBusReady = false; /* 总线掉了 → 允许重开 */ publish(); + bootstrapEcatBus(); }).catch(function () { /* 字段访问/异常不得让 ECAT 轮询永久停摆 */ }).then(function () { @@ -208,6 +263,7 @@ function createOwner() { if (ch) ch.addEventListener("message", onMsg); pollEsp32(); pollEcat(); + bootstrapEcatBus(); publish(); return { diff --git a/js/ota/ecat_load.js b/js/ota/ecat_load.js index 77240d1..62ffdc1 100644 --- a/js/ota/ecat_load.js +++ b/js/ota/ecat_load.js @@ -41,8 +41,8 @@ function setTitle(node, title) { } var TIP_BASE = 'DCU_APP CPU 负载率:中断+非延迟处理(任务派发/OTA中继)占墙钟比,不含常开轮询与空转;EtherCAT 输入 PDO 段5,1s 窗口'; -var TIP_OFF = TIP_BASE + ';当前不可用:需要 ECAT 总线已打开且过程数据循环在跑' - + '(如打开电机控制页选 EtherCAT 传输),且 DCU_APP 固件 >= 1.5.61。'; +var TIP_OFF = TIP_BASE + ';当前不可用:控制台外壳启动时会自动 open+motor/start;' + + '若仍为 --,检查网线/从站供电,或手动打开电机控制页选 EtherCAT。'; /** @description 把 /api/app/load 的 JSON 渲染到 #mGd32Load。返回值是否有效(驱动退避)。 */ function render(d) { diff --git a/tabs.html b/tabs.html index f5dbee2..bfb840e 100644 --- a/tabs.html +++ b/tabs.html @@ -181,6 +181,6 @@ - +