feat(link): 外壳启动强制 open+motor/start,不必等电机页切 EtherCAT

DeviceLink Owner 在控制台加载后自动 bootstrapEcatBus:status→open→
motor/start,沿用 hub 网卡与 period/spin 持久化。成功后不重复 start;
掉线 10s 退避重开。有线 ECAT 与 WiFi 不冲突。
This commit is contained in:
cbb
2026-09-16 12:24:45 +08:00
parent 4b4559e772
commit f9502579ef
4 changed files with 63 additions and 4 deletions
+3
View File
@@ -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 |
+57 -1
View File
@@ -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 {
+2 -2
View File
@@ -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) {
+1 -1
View File
@@ -181,6 +181,6 @@
<script src="js/common/app-lease.js"></script>
<!-- DeviceLink Owner:全链路健康轮询只在本外壳跑一次,各 iframe 订阅 -->
<script>window.__dcuDeviceLinkOwner = true;</script>
<script type="module" src="js/common/device-link.js?v=20260916a"></script>
<script type="module" src="js/common/device-link.js?v=20260916c"></script>
</body>
</html>