From 353d936abb9ce3861d96ba00c6923e592f521f50 Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Wed, 16 Sep 2026 02:54:08 +0800 Subject: [PATCH] =?UTF-8?q?docs(compose):=20device-link=20=E8=A7=84?= =?UTF-8?q?=E6=A0=BC=20delivered?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/compose/spec/device-link.md | 109 ++++++++++++------------------- 1 file changed, 42 insertions(+), 67 deletions(-) diff --git a/docs/compose/spec/device-link.md b/docs/compose/spec/device-link.md index 627cf32..4d608fd 100644 --- a/docs/compose/spec/device-link.md +++ b/docs/compose/spec/device-link.md @@ -1,18 +1,37 @@ --- feature: device-link -status: designed +status: delivered updated: 2026-09-16 branch: feat/device-link -commits: +commits: be26f4daa27cba70b4baeaeb3865f38ed51b2e71..6b5a9f9 --- # Device Link(全链路统一连接层) ## Report +**What was built** — `js/common/device-link.js` 在 `tabs.html` 以 Owner 身份常驻轮询 +ESP32 `/system/status` 与 EtherCAT `/ecat/api/status`+`app/load`,经 +`BroadcastChannel('dcu-device-link')` 广播快照;功能页 Client 订阅。 +`connection.js` 的连接灯在快照 <5s 时只覆盖**在线布尔**,保留「设备忙」等语义文案。 +LAN9252 页 SPI 失败显示「设备在线 · SPI 失败」,不再误标设备离线。 +BLE GATT 在通知开启成功后 `reportBle(true)`,失败/断开上报 false。 + +**Verification** — +- `node --check`:device-link.js / connection.js / gatt.js PASS +- tabs.html、lan9252_pm 内联 script 抽出 `node --check` PASS +- 独立评审后修复:ECAT `Promise.all` 补 `.catch`+必重挂;去掉跨 iframe 无效的 + `window.__dcuDeviceLinkSnap`;BLE 半开连接不再 sticky online + +**Journey log** — +- 「共用 IP」≠「共用连接」——健康态必须有单一 Owner +- 外壳 Owner + 页内 Client 双 BC 实例符合 BroadcastChannel 语义(同上下文不自发自收) +- SPI/业务失败与设备离线必须分文案,否则子系统故障会伪装成掉线 +- ECAT 轮询的 reschedule 必须在 `.catch` 之后,否则一次异常永久停摆 + ## [S1] Problem -多 iframe 保活后,各页仍**各自** `fetch` 设备,只是共用 `localStorage.dcu_sp_ip`: +多 iframe 保活后,各页仍**各自** `fetch` 设备,只是共用 `localStorage.dcu_esp_ip`: - OTA 页轮询 `/system/status` → 顶栏「WiFi 已连接」 - LAN9252 页轮询 `/spi/send` → 同一 IP 却显示「连接失败 · signal timed out」 @@ -20,7 +39,6 @@ commits: - BLE 在页内 GATT,其它页完全不知 用户直觉是「同一条设备连接」,实现却是 N 条互不证明的 HTTP/蓝牙会话。 -结果:一页绿灯一页红灯,像系统坏了,其实是**健康态没有单一真相源**。 ## [S2] Design @@ -28,78 +46,35 @@ commits: 1. **外壳持有链路健康轮询**(tabs.html 常驻),功能页不再各自判定「设备在线」。 2. **BroadcastChannel**(`dcu-device-link`)广播快照;页内只订阅渲染。 -3. **命令仍页内发**(SPI 0xA2、CAN send、OTA upload 等),但**连接灯只认 DeviceLink**。 -4. SPI/业务命令失败显示为 **子系统故障**(如「SPI 超时」),不得覆盖成「设备离线」——除非 DeviceLink 已判 ESP32 离线。 -5. 页面可**独立打开**(无外壳):检测不到 owner 时回退本地轮询,行为与改前一致。 +3. **命令仍页内发**,但**连接灯只认 DeviceLink**(布尔优先,文案保留调用方语义)。 +4. SPI 失败 →「设备在线 · SPI 失败」;DeviceLink offline 才显示离线。 +5. 独立直开页:无快照时回退本地轮询。 ### 模块 -`js/common/device-link.js` 同一文件双角色: +`js/common/device-link.js` Owner+Client;消息:`hello` / `snapshot` / `set-ip` / `ble-state`。 -| 角色 | 谁加载 | 职责 | -|------|--------|------| -| 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 是否可见无关;隐藏页仍能显示统一灯(打开即亮)。 +| 位置 | 行为 | +|------|------| +| `tabs.html` | Owner | +| `connection.js` | 快照新鲜时覆盖 connected 布尔 | +| `lan9252_pm.html` | 订阅灯 + `setSpiFail` | +| `js/ble/gatt.js` | 通知成功后 reportBle(true),失败/断 false | ## [S3] Out of Scope -- 不把 SPI/CAN/OTA 命令全部塞进共享队列(串行化另开) +- 不串行化 SPI/CAN/OTA 命令队列 - 不迁 SharedWorker -- 不改 ESP32/ECAT 固件协议 -- 不合并 BLE GATT 到外壳(本轮只广播状态) +- 不改固件协议 +- 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) +- [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)