Files
cbb 83a035c6d6 test(guard): 验收链改名入库 + 补真退出码;tx 冻结守卫真机门槛两段
之前 e2e_web_send_latency.py / recover_ecat_after_reset.py / measure_click_to_motion.py
全在 .gitignore 里(e2e_/measure_ 通配),换台机器 clone 就跑不了验收;且
recover 脚本零失败路径(--skip-e2e 无条件退 0)。现在统一到已跟踪的 check_ 前缀,
.gitignore 一行例外都不用加,并把"跑过了"和"恢复了"绑成一件事。

- check_recover_after_reset.py:force_reopen 后改为轮询到 dc=1/250us/loop=1/op=1/
  wkc>=3(上限 15s,替掉原来单点采样那次假 loop=0),并断言守卫心跳在涨、
  发一帧 3s 内出口必须跟上;退出码 0/1/2(前置不满足=BLOCKED=2)。
- check_web_send_latency.py:新增第 6 段读 health.guard(旧 exe 无 guard 块 /
  alive_ticks 不涨 都算 FAIL),第 5 段改调已入库的 check_click_to_motion.py。
  它每次发送只等 2s,结构上看不到 8s 判据,所以只报"守卫当时是死是活",
  不拿它冒充触发验证。
- check_guard_idle_silence.py:健康总线的"另一极"——90s 静默 + 25s 1Hz 发送,
  要求心跳在涨、armed 恒 0、fires/wr 零增量、每次发送都被消费。这是防
  "把守卫修成重连风暴"的真机版(对应离线 T1b/T4/T5/T6)。
- check_guard_fires_hw.py:需人在场按 GD32 RESET 制造冻结,证明 armed 秒起、
  tx_stale_s 爬过 8s、fires +1、日志有 [GUARD] 全量重连、松手后 20s 内出口
  重新前进,再验证 60s 冷却内不第二次触发;--with-pm-hold 另证"静音"时心跳仍在涨。
  只认 guard.fires,不认 wr(wr 汇总所有恢复路径,会以错误的理由通过)。
- guard_probe.py:两个真机脚本共用同一份前置判据与通路选择(先 :2625 页面代理,
  再直连 native 8090..8095)。前置没满足一律 BLOCKED——loop=0 或 pm_hold=1 时判据
  按设计是静音的,那种"绿了"毫无意义,所以判据只放一个执行点。
2026-09-21 23:30:07 +08:00

100 lines
6.7 KiB
Markdown
Raw Permalink 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: host-code-quality
status: delivered
updated: 2026-09-23
branch: main
commits: 2c6235d..8043d8e(含评估、P0/P2/P3、界面回滚与安全重做 scope/seq/ota)
---
# 上位机代码质量评估(冗余 / 复杂度 / 屎山风险)
## Report
**What was built** — 全仓质量评估,并按风险优先级做了可验证整改。评估结论:综合约 **3.5/5**(改出来的屎山,仍可救)。
**当前真实状态(2026-09-23 界面正常后)**:
- **P0 服务端风险**:已落地(非幂等不自动重发、网卡默认 I210、跨进程 native 互斥/复用)。
- **P2 服务端边界**:`ecat_client.py` / `ecat_proc.py` 仍在;`esp32_server.py` 已变薄。
- **P3 卫生**:git 内 `scripts/` 收敛、报告在 `docs/reports/`、lab dump 清理。
- **电机前端**:**有意回滚后再安全重做**。完整 `main.js` 恢复 STATUS 契约(`offlineOverlay` / `statusLive`)后,仅外提 **`scope.js` / `seq.js` / `ota.js`**;`main.js` **3233** 行。连接/轮询/`renderStatusPanel` **仍在 main.js**。
- **未再实施**(上次拆分改坏界面后的决策):conn-poll 外提、R5 transport 版本/参数下沉、R6 电机链路强制 ecat-bus、R7 toast/quiet 去重。`ecat-bus.js` 仍供 device-link 使用;电机 `transport*.js` 保持拆分前行为。
### 整改对照(与仓库现状对齐)
| ID | 状态 | 落点 |
|----|------|------|
| R1 非幂等不重发 | **done** | `ecat_client.py` `_ecat_target_idempotent` |
| R2 网卡默认 I210 | **done** | README / `ecat_proc` / `ecat-bus.js` |
| R3 跨进程互斥/复用 | **done** | `_EcatProcXLock` + 复用健康监听 |
| R4 拆 main.js | **partial** | `scope`/`seq`/`ota` 已外提;**无** conn-poll;STATUS 渲染仍在 main.js |
| R5 版本/参数下沉 | **not in tree** | 电机 transport 回滚后无 `waitVersion`(device-link 侧未依赖) |
| R6 ECAT bootstrap 单点 | **partial** | `ecat-bus.js` 供 device-link;电机 transport-ecat 仍为拆分前实现 |
| R7 toast/quiet 去重 | **not in tree** | main.js 内仍有 `toast` / `setQuietMode`;`toast-stack.js` 已移除 |
| R8–R11 服务端 | **done** | `ecat_client.py` + `ecat_proc.py` |
| R12–R14 卫生 | **done** | scripts 清单 / docs/reports / lab dump |
| R15 README | **partial** | 结构与正式脚本清单已写;长篇实测未整段迁出 |
| R16 CSS | **partial** | 所有权注释;两套 btn/toast 刻意分域 |
### 不可逆教训(再拆必读)
1. STATUS DOM 契约:HTML 为 `#offlineOverlay` / `#statusLive`;错用不存在的 id 会导致**永远显示离线**(CSS 未坏)。
2. 机械外提禁止同作用域重复绑定(`setQuietMode` / `TICK_WORKER_URL`)。
3. **禁止** PowerShell 默认编码改 UTF-8 源文件。
4. **验收以界面为准**(`scripts/ui_diag_motor.mjs`),后端 e2e 绿不能代替 UI 绿。
5. 现场快照:`_backup/motor-good-7ea80f0/`(gitignore)。
**Verification** —
- `py_compile esp32_server.py ecat_client.py ecat_proc.py` PASS(P0/P2 保留期)
- `test_esf_pack.py` PASS;`test_motor_refresh.mjs` 62/62 PASS(前端重做后)
- `node --check` main/scope/seq/ota PASS
- UI 门禁(Edge→:2625/motor.html):控制台无报错;绿灯 AL OP;N1–N3 有数据;离线遮罩 `display:none`
- 真机 `check_web_send_latency.py`(原 `e2e_web_send_latency.py`;现随守卫加固改名入库,
并新增第 6 段读 `health.guard` 可观测面。下表为改名前两轮的结果):均为 **PASS 26 / FAIL 1**,exit=1。唯一失败项固定为 **`motor/start`(force_reopen)后立刻读 health 显示 `loop=0`**;同轮 `dc=1 dc_us=250 wkc=3`,且 `motor/send`×6 与 GD32 出口 tx/ack 均前进(HTTP p50≈1.9–5.4ms,出口 p50≈2.1–2.3ms,点击→STATUS≈14ms),WiFi 对照可用。判定为 **e2e 对 force_reopen 后 health 的采样竞态/口径问题**,不是发送链路回归;界面侧数据正常。
- 2026-09-21 续:上述 `loop=0` 竞态现已由 `check_recover_after_reset.py` 改成"轮询到
恢复为止(上限 15s)",不再单点采样;同轮补上守卫可见面断言(`alive_ticks` 心跳、
`fires`、`tx_base`)。tx 冻结守卫为何会悄悄失效、以及"必须触发/不许误触发"两极
的测试契约,见 `tools/ecat/MIGRATION.md` 的「加固 2026-09-21」段。
**Journey log** —
- 大拆 P1 曾导致界面全无数据 → 用户要求回滚 + 保留现场 + 重做
- 重做策略:只外提示波器/序列/OTA,**不动** STATUS/连接状态机
- e2e 冷启动 `loop=0` 与「发送已成功」可并存:属 health 采样竞态,不单说明链路死
- 历史坏提交仍在 git 时间线,工作区以回滚后状态为准;未做 history rewrite
- main 相对 `gitee/main` ahead 数十提交,是否 push 由用户决定
## [S1] Problem
用户要求评估 `G:\Robot_v2\DCU\tools\ESP32` 上位机是否已成「屎山」(冗余/复杂/难维护),并按清单整改。中途因大拆分导致电机页界面无数据,需在**不破坏现场**的前提下恢复并只做安全范围内的重构。
## [S2] Design
### 评估结论(保持)
- 领域复杂度真实(CAN FD、双链路、PDO 实时、pywebview 双进程)。
- 主要结构债:上帝文件、重复状态机、服务端越界替 ecat 补进程治理、杂物堆积。
- 综合约 **3.5/5**:可治理,但边界必须跟上迭代。
### 整改后架构契约(当前)
1. **电机页 STATUS**:仅 `toggleClsIfChanged(statusEl("offlineOverlay"|"statusLive"), ...)`,与 `motor.html` id 一致。
2. **电机前端模块**:`main.js` 装配 + `scope.js` / `seq.js` / `ota.js`(factory 注入 ctx);连接与 STATUS 渲染留在 main.js。
3. **服务端**:`esp32_server.py` + `ecat_client.py`(代理/ESF1/幂等策略)+ `ecat_proc.py`(autostart/锁/RT)。
4. **ECAT 默认网卡**:`I210` 为代码/文档一致来源。
5. **现场备份**:`_backup/` 不入库。
### [S3] Out of Scope
- 不再自动进行连接层大拆 / transport 版本下沉(除非另开任务并加 UI 门禁)
- 不 rewrite git 历史
- 不评估固件仓内部实现
- 本回合不 push 远端(除非用户明确要求)
## Tasks
- [x] T1: 全仓评估与 R1–R16 清单 — acceptance: 规格含证据与优先级 (covers: S2)
- [x] T2: P0 服务端风险修复 — acceptance: 幂等策略 + I210 + 跨进程锁 (covers: S2)
- [x] T3: P2/P3 服务端拆分与卫生 — acceptance: ecat_client/proc + scripts/reports (covers: S2)
- [x] T4: 界面故障根因与回滚 — acceptance: overlay 契约恢复,UI 门禁通过 (covers: S2)
- [x] T5: 安全重做 scope/seq/ota — acceptance: 每步 UI 门禁 + node --check (covers: S2)
- [x] T6: 收尾 e2e + 规格与现状对齐 — acceptance: e2e 结果记入 Report;对照表反映 partial/not-in-tree (covers: S2)