桌面 APP 下 ECAT motor 发送只走 pywebview ESF1→native TCP; 删除桥内 HTTP/JSON 回落。附延迟测量与 pack 自测脚本与规格文档。
110 lines
4.7 KiB
Markdown
110 lines
4.7 KiB
Markdown
---
|
||
feature: pdo-fast-send
|
||
status: delivered
|
||
updated: 2026-09-23
|
||
branch: main
|
||
commits: working-tree (uncommitted; dual-repo main authorized)
|
||
---
|
||
|
||
# PDO 快发(网页按钮 → 原生入队二进制直通)
|
||
|
||
## Report
|
||
|
||
**What was built** — 桌面 APP(esp32_app)下,电机页 EtherCAT 链路的**全部** motor
|
||
发送**强制**走 ESF1 二进制快发:JS `desktopEcatSendRaw` → GUI 进程
|
||
`ecat_send_raw` 打包 → native 持久 TCP 口(health.`fast_send_port`)入 `g_cmd_q`。
|
||
**已堵死慢发**:Python 桥删除 HTTP `send_raw` / JSON `motor/send` 回落;前端在
|
||
`hasDesktopFastSend()` 为真时失败直接 `ok:false`,不再 `api("motor/send")`。
|
||
浏览器无壳标签仍走 HTTP `/ecat/api/motor/send`(脚本/诊断同理)。布局:请求
|
||
magic `ESF1` + nframes + 72B/帧;响应 `FSOK` 8 字节。
|
||
|
||
**Verification** —
|
||
- `build_native.bat` PASS;health `fast_send_port` 存在,loop=1 wkc=3
|
||
- `scripts/test_esf_pack.py` PASS;`py_compile esp32_server.py` PASS
|
||
- `node --check` desktop-bridge / transport-ecat / transport / main PASS
|
||
- `scripts/check_esf_latency.py`:TCP FSOK p50≈0.046–0.08ms(HTTP 对照仍在 native 路由上)
|
||
- 源码检查:`ecat_send_raw_frames` 无 `motor/send`/`http_send_raw` 回落;
|
||
`sendViaFastOrJson` 有桥时不调 `api("motor/send")`
|
||
- `test_motor_transport.mjs` 68/68 PASS(Node 无 pywebview → 走 HTTP 契约路径)
|
||
- `test_motor_refresh.mjs` 62/62 PASS
|
||
- 独立评审 CRITICAL/MAJOR(锁/静态缓冲/端口缓存)此前已修复
|
||
|
||
**Journey log** —
|
||
- 用户期望「纳秒进 PDO」不可达;可压的是控制面,入队后仍是 250µs 一拍 PDO
|
||
- pywebview JS→Python hop 仍在,TCP 快发已把 native 侧压到 ~50–80µs
|
||
- 沙箱拦 `git worktree add` → 用户明确授权双仓 main
|
||
- 持久 TCP 必须 send+recv 整段持锁,否则 FSOK 错配
|
||
- **2026-09-23:用户要求堵死慢发**——桌面强制 ESF1、桥内删除 HTTP/JSON 回落;
|
||
浏览器/脚本仍保留 native HTTP 路由
|
||
|
||
## [S1] Problem
|
||
|
||
电机控制台在 EtherCAT 链路上点发送时,信号要走很长控制面才进 `ecat_native`
|
||
的 `g_cmd_q`:浏览器 fetch→代理→JSON,桌面 APP 再叠 pywebview 桥。用户要
|
||
「一点按钮极短 CPU 周期内进 PDO」。纳秒不可达;native 入队后到总线由 PDO
|
||
周期决定(250µs 档),可压缩的是控制面。HTTP 路径还存在 **8–12ms 调度尾部**,
|
||
必须防止桌面壳误走慢路径。
|
||
|
||
## [S2] Design
|
||
|
||
### 目标行为(2026-09-23 修订:堵死慢发)
|
||
|
||
1. 桌面 APP + 桥可用:ECAT 全部 motor 发送 **只走** ESF1 TCP;失败 `ok:false`,
|
||
**无** HTTP/JSON 回落。
|
||
2. Python `ecat_send_raw_frames`:**仅** TCP ESF1(失败可重探端口后再试 TCP 一次)。
|
||
3. 浏览器标签(无 pywebview):仍走 `/ecat/api/motor/send`。
|
||
4. native `/api/motor/send` 与 `/api/motor/send_raw` **保留**(浏览器/诊断脚本)。
|
||
5. UI 不新增专用开关;`sendHint` / `desktopBridgeLabel` 反映通道形态。
|
||
6. 不承诺纳秒。
|
||
|
||
### 工作区(用户授权覆盖)
|
||
|
||
- ESP32 与 tools/ecat 均在 **main** 上改(2026-09-23 授权;沙箱拦 worktree add)。
|
||
- native:`tools/ecat/ecat_native.c` + `build_native.bat`。
|
||
|
||
### 二进制帧布局(小端)
|
||
|
||
**请求**:
|
||
|
||
```
|
||
u32 magic = 0x31465345 /* ESF1 */
|
||
u8 nframes /* 1..64 */
|
||
u8 flags
|
||
u16 reserved
|
||
repeat nframes: /* 72B each */
|
||
u8 ch, u8 dlc, u16 pad, u32 id, u8 data[64]
|
||
```
|
||
|
||
**响应**:`u32 FSOK(0x4B4F5346) | u8 ok | u8 queued | u16 backlog`
|
||
|
||
### Native
|
||
|
||
- `127.0.0.1` `bind(0)` 快发口;health 增加 `fast_send_port`。
|
||
- 持久连接读 ESF1 → `parse_esf` → 共用 `enqueue_frames` → FSOK。
|
||
- HTTP `/api/motor/send_raw` 与 JSON `/api/motor/send` 仍服务浏览器/脚本。
|
||
- `send_raw` 解析缓冲为栈上(HTTP 多线程)。
|
||
|
||
### GUI 桥(esp32_server.py)
|
||
|
||
- `pack_esf` / `unpack_fsok`;`_GuiApi.ecat_send_raw`。
|
||
- **仅**持久 TCP;connect+sendall+recv 整段持锁。
|
||
- 失败:关 socket + `g_fast_send_port=0` → 重探后再试 TCP 一次 → 仍败则 ok:false。
|
||
- **无** HTTP/JSON 回落代码。
|
||
|
||
### 前端
|
||
|
||
- `desktop-bridge.js`:`desktopEcatSendRaw` / `hasDesktopFastSend`。
|
||
- `transport-ecat.js`:`sendViaFastOrJson` 有桥时只调快发。
|
||
- 缓存版本 `?v=20260923i`。
|
||
|
||
### Out of Scope
|
||
|
||
- 纳秒;共享内存;预装载+位触发;浏览器强制禁 HTTP;删除 native JSON 路由;
|
||
改 PDO 周期;WiFi 链路。
|
||
|
||
## Tasks
|
||
|
||
- [x] T1: native 快发口 + `/api/motor/send_raw` + health 字段 (covers: S2)
|
||
- [x] T2: Python 打包 + `ecat_send_raw` TCP-only(无回落) (covers: S2; depends: T1)
|
||
- [x] T3: desktop-bridge + transport-ecat 有桥强制快发 (covers: S2; depends: T2)
|
||
- [x] T4: 延迟脚本 + 文档 + 堵死慢发修订 (covers: S2; depends: T1) |