Files
DCU_DesktopController/docs/compose/spec/host-code-quality.md
T
cbb 27a8fc1797 docs(compose): 上位机代码质量评估 host-code-quality + 积分耦合报告产物
全仓冗余/复杂度审计(3.5/5),交付重构清单 R1–R16;
附带入库 ecat 积分耦合 HTML 报告与 N2/N3 成对测试脚本。
2026-09-21 02:12:04 +08:00

217 lines
15 KiB
Markdown
Raw 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: working-tree(主检出 main,用户授权;独立评审 pass,无 critical)
---
# 上位机代码质量评估(冗余 / 复杂度 / 屎山风险)
## Report
**What was built** — 本仓(`G:/Robot_v2/DCU/tools/ESP32`)全量代码质量评估文档。范围覆盖生产路径(`pages/` `js/` `css/` `esp32_server.py` `tabs.html`)以及 `scripts/` `lab/` `_local/` 与根目录报告;`tools/ecat` 仅评估本仓耦合面。方法:体积/行数盘点 + 三路只读审计(电机前端、Python 服务端、脚本杂物)+ 行号证据核验 + 独立评审。
**结论**:全仓综合 **3.5/5**——尚未不可救药,但已是明显「改出来的屎山」。不是乱写:transport 抽象、app-lease、大量实测注释说明有人在治理;真正的债是上帝文件、重复状态机、服务端越界替 ecat 补课、杂物堆积与文档漂移。交付重构清单 **R1–R16**(P0 风险 → P1 拆文件 → P2 服务端边界 → P3 卫生),本回合**未改任何业务代码**。
**Verification** —
- 行数量化:`main.js` total=4647 / nonempty=4398 / 顶层 `function|async function` 声明=178;`esp32_server.py` total=1536;`connection.js`=1207;`lab/spi_test.html`=2325
- 卫生量化:`scripts/`=128 文件,README 点名仅 12;`lab/`=114 文件 57.4MB;`_local/`=27 文件
- 证据抽查:`BASE_DIR` 双定义 L56/L134;README 默认 I226 vs 代码 I210(`device-link.js:31`、`motor.html:39`、`esp32_server.py:638`);`_ecat_proxy` L762–775 autostart 后同 body 重发,L1028–1030 注释自认非幂等风险
- 独立评审:**pass**,无 critical;major(表重复/Report 空/区间过宽)已在本版修订
- 本回合未跑业务测试(无业务代码变更)
**Journey log** —
- `git worktree add` 被沙箱拦截 → 用户授权主检出 main 写评估文档(与 pdo-fast-send 同路径)
- PowerShell `Measure-Object -Line` 会丢空行,应用 `ReadAllLines().Length` 做 total 口径
- 评估类 compose:Tasks 覆盖「产出评估」而非「实施重构」;R1–R16 是 backlog,不阻塞 delivered
- 屎山分不是「代码写得差」,是「边界跟不上迭代」:P0 先止血(非幂等重发),再拆文件
## [S1] Problem
用户要求评估 `G:\Robot_v2\DCU\tools\ESP32` 这套 ESP32 调试上位机在持续迭代后,是否已经变成「屎山」:有没有冗余、过度复杂、功能堆叠和不可维护的结构。本回合只交付评估结论与重构清单,**不改业务代码**。
## [S2] Design
### 评估范围与方法
- **范围**:本仓全量——生产路径(`pages/` `js/` `css/` `esp32_server.py` `tabs.html`)+ `scripts/` `lab/` `_local/` + 根目录报告。姊妹仓 `tools/ecat` 仅评估本仓对其的耦合面,不深挖 native 源码。
- **方法**:文件体积与行数盘点 → 模块职责/函数热点扫描 → 三路只读审计(电机前端、Python 服务端、脚本杂物)→ 对照 README/spec/gitignore 与近期 churn 核验证据。
- **判定标准**:
- **领域复杂度**:协议/双链路/实时性约束本身带来的必然复杂度,不可删。
- **结构债**:上帝文件、重复状态机、越界补丁、文档漂移——可治理。
- **卫生债**:一次性脚本、采样 dump、根目录报告——可归档删除。
### 总判定
| 维度 | 评分 (1=干净 … 5=屎山) | 一句话 |
|------|------------------------|--------|
| 电机前端 `js/motor/main.js` | **3.5** | 有意识治理中的上帝文件;再拖一轮需求会到 4+ |
| Python 服务端 `esp32_server.py` | **3.5** | 有纪律的补丁山:注释极好,职责爆炸 |
| 脚本 / lab / _local 杂物 | **4.0** | 策略(.gitignore)清楚,磁盘堆积严重 |
| **全仓综合** | **3.5 / 5** | **尚未不可救药,但已明显「改出来的屎山」;不是乱写,是架构边界跟不上迭代速度** |
**结论**:领域复杂度真实存在(CAN FD 协议、WiFi/EtherCAT 双链路、PDO 实时约束、pywebview 双进程),transport 抽象与大量设计注释说明有人在治理。真正的屎山信号是:**上帝文件未拆分、连接/总线状态机多处同源重复、服务端替 tools/ecat 扛了大量进程治理、杂物与文档漂移**。属于「有纪律的腐化」,还能救,但优先重构必须先做。
### 关键发现(证据)
#### A. 电机前端 —— 上帝文件 + 半截双链路
| 位置 | 问题 | 严重度 |
|------|------|--------|
| `js/motor/main.js` **4647 行(含空行)/ 219KB / 顶层 `function`+`async function` 声明 178 个** | 文件头自承「由 motor.html 内联脚本机械外提,逻辑未改」——只搬了位置,没拆职责 | major |
| 同文件六域同居 | UI 持久化 (~L203–526)、连接+轮询 (~L940–1768)、命令序列 (~L2503–2997)、版本双路径 (~L2998–3284)、OTA+IDB (~L3580–4211)、示波器 IIFE (~L4257–end) | major |
| 链路特判仍散落业务层 | `failThresholdFor` L157、probe 复核 L1291、在线语义 L1340、**版本等待双实现** L3156/L3244、**参数应答双通道** L3353/L3384 | major |
| 两套并行连接状态机 | main.js 自建 failCount/probe/pollGen(不 import `connection.js`);`connection.js` 另有 updateConnectionUi/testConnection | major |
| ECAT 总线 bootstrap 重复 | `device-link.js` `bootstrapEcatBus` L167–231 注释自承与 `transport-ecat.js` `ensureOpen/ensureStarted`「同源」 | major |
| 小重复 | `toast/showModal` main.js L566 自实现,其他页用 `common/toast.js`;`setClsIfChanged`/`setTextIfChanged` 与 connection.js 重复;`fetchDeviceJson` ≈ transport-wifi `getJson` | minor |
| transport 抽象本身 | `transport.js` 契约清晰(L11–26),wifi/ecat 实现分离得当;**这是正确决策,不是债** | (正向) |
#### B. Python 服务端 —— 单文件九职责 + 越界补丁
| 位置 | 问题 | 严重度 |
|------|------|--------|
| `esp32_server.py` **1536 行** | 静态服务、reload 注入、UI-state、ECAT 代理、autostart、ESF1 快发、GUI 桥、app-lease、窗口双进程/Job/单实例 —— 九类职责同居 | major |
| 模块级全局 14+ | `g_ecat_*` / `g_fast_*` / `g_app_*` / `g_win` / `g_job` 等(L136–156 起) | major |
| 同一后端三通道 | HTTP 代理 `_ecat_proxy` + GUI 桥 `_GuiApi.ecat_api` + ESF1 TCP `ecat_send_raw_frames`,路径归一化重复 | major |
| **代理失败后自动 autostart 并重发同一 POST** | L762–775;L1028–1030 注释自己承认「带副作用的请求假超时 = 再写一遍」;仅连接失败/超时异常路径触发,非任意错误码 | **critical**(设计风险) |
| 越界替 tools/ecat 补课 | 端口漂移探测、`--parent-pid`、taskkill 全杀、RealTime 提权、stdout 写到他仓、硬编码 `--substr I210 --mask 0x50`(L638) | major |
| 跨进程单实例不互斥 | GUI 进程与服务子进程**各自**可能 `_kill_all_ecat_native`;`g_mutex` 只在进程内(L592) | major |
| 文档漂移 | README L323 写默认网卡 **I226**,代码/页面/测试默认全是 **I210**(`device-link.js:31`、`motor.html:39`、`esp32_server.py:638`、`test_ecat_autostart.mjs`) | minor(但误导) |
| 冗余小点 | `BASE_DIR` 定义两次(L56 与 L134);motor/lab ui-state 路由近重复;前台窗口逻辑三处复制;端口遍历两份 | minor |
| 正向 | app-lease 接口面干净;锁与原子写有意识;双进程是实测踩坑后的合理架构;注释质量极高 | (正向) |
#### C. 脚本 / lab / _local —— 卫生债最重
| 路径 | 体量 | 判定 |
|------|------|------|
| `scripts/` | **128 个文件 / ~30MB** | 正式测试(README 点名)**仅 12 个**;`_tmp_`/`probe_`/`skip_`/`anal_`/`collect_` 约 **55+** 个属一次性排障残留 |
| `lab/` | **114 文件 / 57.4MB** | 大头是采样 dump(`ecat_passive_*.csv` 各 ~25MB 且疑似双份、`skip_meter/` 61 个 json);tabs 真正用到的只有 `spi_test.html`、`lan9252_pm.html` |
| `_local/` | 27 文件 / 1.6MB | 整目录已 gitignore;截图 + dbg 脚本,一次性 |
| 根目录报告 | `ECAT_*.html/md`、`GD32_OTA_FLOW_COMPARE.html`、`REPORT_*.md` ~130KB+ | 无 tabs/README 页面表引用,应归 `docs/` 或删 |
| `lab/spi_test.html` | **2325 行全内联**(非空 2258) | 单文件复杂度高于 motor.html;README 已标「临时功能」 |
| css/ | 结构合理(base/layout/components/can/motor/responsive) | 跨文件重复选择器约 40 个(`.btn`/`.toast`/`.header-bar`);can.css 内部 11 处重复 |
`.gitignore` 策略本身是清楚的(采样/探针/`_local` 不入库)——问题不在策略,而在**磁盘未清理 + 部分实验产物仍可能被误跟踪 + 根目录报告未归档**。
#### D. 跨层架构观察
```mermaid
flowchart TB
subgraph pages["页面层"]
tabs["tabs.html 外壳"]
motor["pages/motor.html"]
other["index / can / lab"]
end
subgraph motorJS["电机前端(上帝文件)"]
main["main.js 4647行<br/>状态+UI+OTA+序列+波形+轮询"]
tp["transport.js 契约"]
wifi["transport-wifi"]
ecat["transport-ecat"]
end
subgraph commonJS["common"]
dl["device-link ConnectionHub"]
conn["connection.js(OTA页用)"]
toast["toast.js"]
end
subgraph server["esp32_server.py 九职责"]
static["静态+reload"]
proxy["/ecat/api 代理"]
gui["_GuiApi + ESF1"]
lease["app-lease WS"]
win["窗口/双进程/Job"]
end
native["tools/ecat<br/>ecat_native.exe"]
tabs --> motor
motor --> main
main --> tp
tp --> wifi
tp --> ecat
main -. "不 import,自建状态机" .-> conn
main -. "自实现 toast" .-> toast
main --> dl
ecat -->|桥或HTTP| proxy
ecat -->|桌面桥| gui
dl -->|bootstrapEcatBus 与 transport 同源重复| proxy
proxy --> native
gui --> native
lease -->|停循环| proxy
```
**正确且该保留的决策**:
- 双链路 transport 抽象(业务共用、I/O 可换)
- tabs 多 iframe 保活
- app-lease 关窗停总线
- PDO 快发堵死慢发(pdo-fast-send)
- 丰富的实测注释与 compose 规格
**真正的屎山信号**:
1. `main.js` 只做了「机械外提」,六域仍同居
2. 连接状态机 ×2、ECAT bootstrap ×2、DOM diff 助手 ×3、toast ×2
3. `esp32_server.py` 替 ecat 扛进程治理,三通道同一后端,非幂等 POST 可重发
4. 版本/节点参数回执未下沉 transport,业务层链路特判 ~16 处
5. README(395 行 tribal knowledge)与代码默认值漂移(I226 vs I210)
6. scripts/lab 杂物 80%+ 与生产无关
### 重构清单(优先级,本回合不实施)
> 下列为建议 backlog,不构成本 feature 的未完成任务。实施前请单独开 compose feature 或直接授权改码。
#### P0 — 正确性 / 风险(先做)
| ID | 动作 | 验收 |
|----|------|------|
| R1 | `_ecat_proxy` 失败 autostart 后**禁止重发非幂等 POST**(`motor/start`/`flash`/`ota/ecat`/`eeprom/*` 等);仅幂等读路由可重试一次 | 源码无副作用路径二次 POST;回归 `test_ecat_autostart.mjs` + 人工确认 flash 路径 |
| R2 | 统一 ECAT 网卡默认值文档与代码(I210 vs I226 二选一) | README、`device-link.js`、`motor.html`、`esp32_server.py`、相关测试默认一致 |
| R3 | 跨进程对 `ecat_native` 的 kill/autostart 加文件锁或让 native 侧唯一 | GUI 进程与服务子进程不会互相 kill 对方刚拉起的实例 |
#### P1 — 拆上帝文件 / 收敛重复状态机
| ID | 动作 | 验收 |
|----|------|------|
| R4 | `main.js` 拆出 `motor/ota.js`、`motor/seq.js`、`motor/conn-poll.js`(或等价命名);入口只做装配 | main.js total 行数 &lt; 2000(目标);`test_motor_transport` / `browser_check_*` 全绿;引用处 `?v=` 已 bump |
| R5 | 版本等待与节点参数回执**下沉 transport 能力接口**,删除 main.js 内 wifi/ecat 双实现分支 | `rg 'tpId\\s*===\\s*\"ecat\"' js/motor/main.js` 在版本/参数相关函数中无命中 |
| R6 | ECAT `bootstrapEcatBus` 只保留一处(建议 device-link 为唯一 Owner,transport 只调用) | 全仓仅一处 health→open→motor/start 策略代码 |
| R7 | 电机页连接状态机与 common 收敛:要么复用 device-link 健康快照,要么抽 `conn-policy.js`;删除重复 DOM diff / toast 实现 | main.js 不再自实现 toast;failCount/probe 语义单一 |
#### P2 — 服务端边界
| ID | 动作 | 验收 |
|----|------|------|
| R8 | 从 `esp32_server.py` 拆出 `ecat_client.py`(端口探测 + 裸 socket + ESF1)与 `ecat_proc.py`(autostart/kill/parent-pid/RT) | server.py 只保留 HTTP/静态/lease/窗口;`test_esf_pack.py` 仍 PASS |
| R9 | 收敛 GUI 桥与 HTTP 代理的 path/body 归一化为同一函数 | 桥与代理共用路由表,无重复剥前缀逻辑 |
| R10 | 桌面 App 模式下设备状态/命令的 GUI 代理路径评估是否可统一(减少 desktop vs browser 双路径分支) | 文档明确两路径必要差异,或实现单一入口 |
| R11 | 删除/合并小冗余:`BASE_DIR` 双定义、ui-state 双路由参数化、前台焦点 helper 共用 | 源码 diff 可核对;无行为变化 |
#### P3 — 卫生与文档
| ID | 动作 | 验收 |
|----|------|------|
| R12 | 清理 `scripts/`:保留 README/spec/gitignore 白名单引用的测试;`_tmp_`/`skip_`/`anal_`/无引用 `probe_` 移出或删除 | scripts 目录正式脚本 ≤ 25 个(保留清单可一页列完);README 自测命令仍可执行 |
| R13 | 清理 `lab/` 采样 dump(csv/json/skip_meter);保留 tabs 引用的实验页 | lab 总体积 &lt; 5MB;`spi_test` / `lan9252_pm` 仍可从 tabs 打开 |
| R14 | `_local/` 与根目录报告归档:报告进 `docs/` 或删除;`_local` 保持 ignore | 仓库根无散落 REPORT/ECAT_*.html |
| R15 | README 瘦身或拆分:把长篇实测细节沉到 `docs/`,README 只留架构/入口/红线 | README 行数明显下降且默认值与代码一致 |
| R16 | CSS 去重:公共 chrome(`.btn`/`.toast`/`.header-bar`)收到 `components.css`;页面 CSS 只留本域 | 对 `.btn`/`.toast` 的重复定义处数可清点下降 |
### [S3] Out of Scope
- 本回合**不修改任何业务代码**(只产出本评估文档)
- 不深挖 `tools/ecat` 内部实现,只评估本仓耦合面
- 不做性能基准测试(延迟数字引用 README/既有报告)
- 不实施 R1–R16
- 不评估固件仓库(ESP32_COMM / GD32)
## Tasks
- [x] T1: 全仓体积/文件/churn 盘点 — acceptance: 产出 scripts/js/server/lab 量化摘要 (covers: S2)
- [x] T2: 电机前端结构审计 — acceptance: main.js 热点、双链路残留、重复状态机带 file:line (covers: S2)
- [x] T3: esp32_server 职责与耦合审计 — acceptance: 九职责分段、越界项、critical 风险带 file:line (covers: S2)
- [x] T4: scripts/lab/_local 卫生审计 — acceptance: 分类数量、可删清单、必须保留清单 (covers: S2)
- [x] T5: 汇总评估文档 + 优先级重构清单 R1–R16 — acceptance: 本文档可独立评审 (covers: S2)
- [x] T6: 独立评审 — acceptance: 评审者对照证据复核 critical/major 发现与清单可行性;pass 无 critical (covers: S2; depends: T1–T5)