--- feature: device-link status: delivered updated: 2026-09-16 branch: feat/device-link commits: 353d936..033ece2 --- # Device Link / ConnectionHub(全链路统一连接层) ## Report **What was built** — 在既有 DeviceLink 健康广播之上扩展为 ConnectionHub:`deviceLink` 统一持有 WiFi IP、EtherCAT 网卡子串与 `ecatPost`,并保留 Owner/Client 快照。OTA(app.js)、CAN、 can_monitor、motor、ecat_load、LAN9252 改为从 Hub 读端点/订在线布尔;BLE 仍经 `reportBle` 上报。连接灯只覆盖布尔,不替换调用方语义文案。 **Verification** — - `node --check` 全部改动 JS → PASS - `test_motor_transport.mjs` 58/58 PASS - `test_motor_refresh.mjs` 62/62 PASS - `test_ecat_autostart.mjs` 19/19 PASS - 独立评审后修复 3 个 critical:读路径 setIp 风暴、CAN 文案覆盖、嵌入页误标轮询离线 **Journey log** — - 「共用 IP」≠「共用连接」——健康态必须有单一 Owner - Hub subscribe 只覆盖在线布尔,绝不替换调用方语义文案 - 禁止在读路径调 setIp:Owner 会把每次 set-ip 当成立即重探 - 嵌入 can.html 时 hub 不得写 fbPollStatus(那是 feedback 轮询自己的状态) ## [S1] Problem 多 iframe 保活后,各页仍**各自**判定「设备连上了没有」,只是共用一个 IP 字符串: - OTA 页轮询 `/system/status` → 顶栏「WiFi 已连接」 - LAN9252 页轮询 `/spi/send` → 同一 IP 却显示「连接失败」 - EtherCAT 状态/负载又是一条独立轮询,网卡子串在 4 处重复读 `localStorage` - BLE 在页内 GATT,其它页完全不知 - CAN / can_monitor / motor 各有一套 `getBaseUrl` 包装 用户直觉是「同一条设备连接」,实现却是 N 条互不证明的 HTTP/蓝牙会话和重复端点配置。 ## [S2] Design ### 原则 1. **外壳持有链路健康轮询**(tabs.html Owner),功能页不再各自判定「设备在线」。 2. **BroadcastChannel**(`dcu-device-link`)广播快照;页内只订阅渲染。 3. **端点配置只在 ConnectionHub**:IP(`dcu_esp_ip`)、网卡(`dcu_ecat_adapter`)读写集中。 4. **命令仍页内发**,连接灯只认 DeviceLink 布尔(文案保留调用方语义)。 5. BLE 为一等通道但单独适配:只广播状态,GATT 细节留在 `ble/*`。 ### ConnectionHub 客户端 API(`deviceLink`) | API | 用途 | |-----|------| | `subscribe` / `getSnapshot` / `channelOnline` | 健康快照 | | `getIp` / `setIp` / `wifiBaseUrl` | WiFi 端点 | | `getEcatAdapter` / `setEcatAdapter` | EtherCAT 网卡子串 | | `ecatPost(path, body, timeoutMs)` | 统一 `/ecat/api/*` 代理请求 | | `reportBle` | BLE 状态上报 | Owner 消息:`hello` / `snapshot` / `set-ip` / `ble-state`。 ### 接入 | 位置 | 行为 | |------|------| | `tabs.html` | Owner | | `connection.js` | 快照新鲜时覆盖 connected 布尔 | | `app.js` | IP/baseURL 走 `deviceLink`(保留 `#espIp` 输入回退) | | `can/dbc.js` + `can.js` | setDeviceIp 广播;顶栏灯订阅 hub | | `can_monitor.js` | baseURL 走 base-url/hub;灯订阅 hub | | `motor/main.js` | IP 订阅 hub 同步 `state.ip` | | `motor/transport-ecat.js` | 网卡子串经 hub 读写 | | `ota/ecat_load.js` | `deviceLink.ecatPost` | | `lab/lan9252_pm.html` | 灯订阅 hub;open 网卡经 hub | | `lab/spi_test.html` | 共享 IP 键(自包含页不深耦合请求层) | | `js/ble/gatt.js` | 通知成功后 reportBle(true),失败/断 false | ## [S3] Out of Scope - 不串行化 SPI/CAN/OTA 命令队列 - 不迁 SharedWorker - 不改固件协议 - 不把 BLE GATT 强行对齐 HTTP/ECAT 请求接口 - spi_test 保持可整文件删除,不注入业务请求逻辑 ## Tasks - [x] T1: device-link.js Owner+Client + BC 协议 (covers: S2) - [x] T2: tabs.html 启动 Owner (covers: S2) - [x] T3: OTA connection.js 接入 DeviceLink (covers: S2) - [x] T4: LAN9252 灯与 SPI 文案 (covers: S2) - [x] T5: BLE 状态上报 (covers: S2) - [x] T6: 语法检查 + 评审修复 (covers: S2) - [x] T7: Hub 扩展 wifiBaseUrl/ecatPost/adapter API (covers: S2) - [x] T8: OTA/CAN/motor/ecat_load/lab 迁到 Hub 端点 (covers: S2)