Files
DCU_DesktopController/docs/compose/spec/device-link.md
T
cbb 670b833dc6 feat(link): DeviceLink 外壳单例统一 ESP32/ECAT/BLE 健康态
各页只共用 IP、各自 fetch 导致同屏「已连接/连接失败」分裂。
外壳 Owner 轮询 /system/status 与 EtherCAT 状态,BroadcastChannel
广播快照;连接灯只认共享层,SPI 失败单独文案不覆盖设备离线。
2026-09-16 02:50:12 +08:00

106 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
feature: device-link
status: designed
updated: 2026-09-16
branch: feat/device-link
commits:
---
# Device Link(全链路统一连接层)
## Report
## [S1] Problem
多 iframe 保活后,各页仍**各自** `fetch` 设备,只是共用 `localStorage.dcu_sp_ip`:
- OTA 页轮询 `/system/status` → 顶栏「WiFi 已连接」
- LAN9252 页轮询 `/spi/send` → 同一 IP 却显示「连接失败 · signal timed out」
- EtherCAT 负载/状态又是一条独立轮询
- BLE 在页内 GATT,其它页完全不知
用户直觉是「同一条设备连接」,实现却是 N 条互不证明的 HTTP/蓝牙会话。
结果:一页绿灯一页红灯,像系统坏了,其实是**健康态没有单一真相源**。
## [S2] Design
### 原则
1. **外壳持有链路健康轮询**(tabs.html 常驻),功能页不再各自判定「设备在线」。
2. **BroadcastChannel**(`dcu-device-link`)广播快照;页内只订阅渲染。
3. **命令仍页内发**(SPI 0xA2、CAN send、OTA upload 等),但**连接灯只认 DeviceLink**。
4. SPI/业务命令失败显示为 **子系统故障**(如「SPI 超时」),不得覆盖成「设备离线」——除非 DeviceLink 已判 ESP32 离线。
5. 页面可**独立打开**(无外壳):检测不到 owner 时回退本地轮询,行为与改前一致。
### 模块
`js/common/device-link.js` 同一文件双角色:
| 角色 | 谁加载 | 职责 |
|------|--------|------|
| Owner | `tabs.html`(`window.__dcuDeviceLinkOwner = true`) | 轮询 ESP32 `/system/status`、ECAT `/ecat/api/status`+`/api/app/load`;维护快照并广播 |
| Client | 各功能页 | `subscribe()` 收快照;`setIp()`;`reportBle()` 上报 GATT 状态 |
### 快照契约
```js
{
ts: 1710000000000,
ip: "192.168.31.127",
links: {
esp32: { online: boolean, ageMs: number, failStreak: number },
ecat: { online: boolean, busOpen: boolean, op: boolean, loop: boolean,
loadValid: boolean, loadPerm: number, ageMs: number },
ble: { online: boolean, deviceName: string, owner: string|null }
},
esp32Status: object|null // 最近一次 /system/status 全文(可 null)
}
```
### 消息
| type | 方向 | 语义 |
|------|------|------|
| `hello` | Client→Owner | 请求立刻回一份 snapshot |
| `snapshot` | Owner→All | 全量快照(ESP32 状态变化或 1s 心跳时发) |
| `set-ip` | Client→Owner | 改设备 IP;Owner 写 localStorage 并立刻重拉 |
| `ble-state` | Client→Owner | `{online, deviceName}` GATT 连/断 |
Owner 用 `BroadcastChannel`;无 BC 的环境 client 降级为「仅 localStorage 读 IP + 本地轮询」。
### 接入点(本轮)
| 页面/模块 | 改动 |
|-----------|------|
| `tabs.html` | 置 owner 标志并 `import` device-link 启动 |
| `pages/index.html` + `js/common/polling.js`/`connection.js` | 有 DeviceLink 时:连接胶囊/健康读快照;`pollStatus` 仍可保留作数据源或改为消费 `esp32Status`(见任务) |
| `lab/lan9252_pm.html` | 连接灯改读 DeviceLink;`/spi/send` 失败文案改为「SPI 失败」 |
| `js/ble/gatt.js` 或 session | connect/disconnect 时 `reportBle` |
| `js/ota/ecat_load.js` | 可选:owner 已有 load 时页内 poller 可只读快照(本轮先并存,避免双写 UI) |
### 超时与判定
- ESP32:成功 → `online=true, failStreak=0`;连续失败 ≥3 → `online=false`
- ECAT:`/api/status` ok 且 slave 存在 → 轨道 `busOpen`;`/app/load` `valid && age_ms≤3000` → load 新鲜
- BLE:仅 GATT 主人上报;无上报时 `online=false`
### 与 keep-alive 的关系
DeviceLink 跑在**外壳**,与 iframe 是否可见无关;隐藏页仍能显示统一灯(打开即亮)。
## [S3] Out of Scope
- 不把 SPI/CAN/OTA 命令全部塞进共享队列(串行化另开)
- 不迁 SharedWorker
- 不改 ESP32/ECAT 固件协议
- 不合并 BLE GATT 到外壳(本轮只广播状态)
## Tasks
- [ ] T1: `device-link.js` Owner+Client + BroadcastChannel 协议 — acceptance: 单测式手工:外壳起后 postMessage/snapshot 可被 mock client 收到 (covers: S2)
- [ ] T2: `tabs.html` 启动 Owner — acceptance: 打开外壳后 BC 上出现 esp32/ecat 快照,且关页后不再刷 (covers: S2)
- [ ] T3: OTA 页连接态改读 DeviceLink(独立打开回退) — acceptance: 外壳内 OTA 与 LAN9252 的「在线/离线」一致;LAN9252 SPI 失败不再显示设备离线 (covers: S2)
- [ ] T4: LAN9252 页灯与文案接入 — acceptance: 同 (covers: S2)
- [ ] T5: BLE GATT 状态上报 — acceptance: BLE 连上后快照 ble.online=true,其它页可观察 (covers: S2)
- [ ] T6: 语法检查 + 评审 — acceptance: node --check / 人工路径说明写入 Report (covers: S2)