Files
DCU_DesktopController/docs/compose/spec/host-code-quality.md
T
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

6.7 KiB
Raw Blame History

feature, status, updated, branch, commits
feature status updated branch commits
host-code-quality delivered 2026-09-23 main 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

  • T1: 全仓评估与 R1–R16 清单 — acceptance: 规格含证据与优先级 (covers: S2)
  • T2: P0 服务端风险修复 — acceptance: 幂等策略 + I210 + 跨进程锁 (covers: S2)
  • T3: P2/P3 服务端拆分与卫生 — acceptance: ecat_client/proc + scripts/reports (covers: S2)
  • T4: 界面故障根因与回滚 — acceptance: overlay 契约恢复,UI 门禁通过 (covers: S2)
  • T5: 安全重做 scope/seq/ota — acceptance: 每步 UI 门禁 + node --check (covers: S2)
  • T6: 收尾 e2e + 规格与现状对齐 — acceptance: e2e 结果记入 Report;对照表反映 partial/not-in-tree (covers: S2)