feat(server): app 窗口租约, 全部窗口关闭自动停 EtherCAT 循环

PDO 循环起来后 native 侧一直跑, 其 --parent-pid 自清理只覆盖"本服务拉起
的实例死掉"; 浏览器模式关窗后本服务还在、窗口模式复用现成 ecat_native
(parent 是别人)这两种常见离开方式都够不着 —— 从站卡在 OP、自旋节拍白占
一个核。现由每个 app 页面与页面服务器的 WebSocket 租约兜底:

- esp32_server.py: 新增 GET /ws/app-lease(手写 RFC6455 握手 + 帧泵)。
  连接计数归零后隔 3s GRACE 调后端 /api/op/stop 停循环退回 INIT; 断开
  感知靠 TCP FIN / Close 帧回显而非心跳(后台标签页节流不误停); 代理
  透传时 _note_ecat_loop 记账"循环是否本服务跑起来的", 只停自己确认过
  的; /api/shutdown 与 serve_forever 退出路径同步补停(定时器随进程死亡)。
- js/common/app-lease.js: 租约客户端, 3~30s 退避重连, file:// 静默跳过。
- 接入 tabs.html(外壳持有, 内部切标签不断连)与 pages/motor.html(单独
  打开时页面自持)。
- 测试: scripts/test_app_lease.py(裸 TCP 全路径, 假 _ecat_post) +
  scripts/test_app_lease_e2e.mjs(真 Edge headless + 假后端 8091, 不碰
  真总线)。README 补设计动机与要点。
This commit is contained in:
cbb
2026-09-05 19:58:25 +08:00
parent c189a8563e
commit 99fb9bcb4a
7 changed files with 636 additions and 0 deletions
+23
View File
@@ -302,6 +302,29 @@ esp32_app.vbs esp32_web.vbs serve.bat 三个启动入口
最坏约 24s,所以前端超时给 30s、代理也给 30s)。
4. **循环没跑** → 同一个 `/api/motor/start` 起后台 PDO 循环。
断线后每 2s 自动重来一遍,中途拔网线再插会自己恢复。
- **关掉 app 必停从站**(上面"自动接入"的反向操作,`esp32_server.py` 的
`_app_lease_*` 一节):每个 app 页面与页面服务器保持一条 WebSocket
(`js/common/app-lease.js` → `GET /ws/app-lease`),**最后一条**租约连接断开
(所有 app 窗口关闭)后,服务端调后端 `/api/op/stop` 停 PDO 循环并把从站退回
INIT。为什么必须有它:循环在喂从站 1000ms 过程数据看门狗,没人停它就永远
卡在 OP、自旋节拍白占一个核;而 native 的 `--parent-pid` 看门狗只覆盖
"本服务拉起的实例",浏览器模式关窗后服务还活着、复用现成实例时 parent
又是别人,两种常见离开方式都够不着。要点:
- 断开感知靠 TCP FIN / Close 帧回显,**不是心跳计时器** —— 后台标签页的
定时器节流不会把"挂着后台的 app"误判成已关闭(服务端必须回 Close 回显,
否则浏览器拆不掉 TCP,Edge 实测踩过);
- 断连后隔 3s GRACE 才停:F5 刷新只断几百毫秒,不该被当成关窗;
- 外壳内切标签不停(`tabs.html` 自己持有租约),与 `transport-ecat.teardown`
"切到 WiFi 看看不该打断 EtherCAT" 的既有设计一致;
- 只停"本服务经代理确认跑过"的循环(`g_ecat_motor_live` 代理记账),
直接从 ecat 控制台(:8090)起的循环不动;
- 服务进程自己的退出路径(`/api/shutdown`、Ctrl+C)同步补停一次 ——
租约定时器随进程死亡,等不到 GRACE 到点。
代价:每次重开 app 都要重走 PREOP→SAFEOP→OP,有概率撞 SSC 邮箱楔死
(见下一段)—— 这是"关闭必须停"这个要求本身的代价,不是机制缺陷。
自测:`python scripts/test_app_lease.py`(服务端逻辑,裸 TCP 全路径)+
`node scripts/test_app_lease_e2e.mjs`(真 Edge headless + 真服务器,
假后端钉在 8091,绝不碰真总线)。
- 接入失败时**必须能看到原因**:横幅与 `#devStatus` 显示归类后的可执行提示
(`summarizeLinkError`),完整诊断放在两者的 `title` 里。
典型:SSC 邮箱楔死会把 64 条重复 `MailboxError` + SM 现场(2000+ 字符)压成