Files
DCU_DesktopController/docs/compose/spec/host-code-quality.md
T

16 KiB
Raw Blame History

feature, status, updated, branch, commits
feature status updated branch commits
host-code-quality delivered 2026-09-23 main 2c6235d..b215fdf(实施 67dd170 / b70f5ed / 261caf5 / beeae8a / d2b7877 / 2304ed6 / 0660506 / c6510c1 / b215fdf)

上位机代码质量评估(冗余 / 复杂度 / 屎山风险)

Report

What was built — 全仓质量评估 + 按 R1–R16 分优先级整改。评估结论:综合 3.5/5(改出来的屎山,仍可救)。实施后:main.js 4647→1894 行;esp32_server.py 1536→986 行(逻辑进 ecat_client.py / ecat_proc.py);正式 scripts/ 128→16;根目录报告迁入 docs/reports/。

整改对照

ID 状态 落点
R1 非幂等不重发 done ecat_client.py _ecat_target_idempotent;副作用路径 autostart 后不自动重发
R2 网卡默认 I210 done README + ecat_proc.ECAT_DEFAULT_ADAPTER + js/common/ecat-bus.js
R3 跨进程互斥/复用 done _EcatProcXLock + 先复用健康监听实例
R4 拆 main.js done scope/ota/seq/conn-poll.js,入口只装配
R5 版本/参数下沉 done waitVersion / queryNodeIdParam 进 transport
R6 ECAT bootstrap 单点 done js/common/ecat-bus.js
R7 toast/quiet 去重 done toast-stack.js + utils
R8 拆 server done ecat_client.py + ecat_proc.py
R9 桥/代理 path 归一 done 客户端统一路径解析
R10 双路径 done(文档化) 桥与代理最终同 ecat_client,未强行合并
R11 小冗余 done BASE_DIR 单次等
R12 scripts done 出库 31 个,保留 16 个正式脚本
R13 lab done dump 出库,保留实验页
R14 报告归档 done docs/reports/
R15 README partial 结构/清单/CSS 所有权已改;长篇 ECAT 实测未整段迁出
R16 CSS partial 所有权注释 + ?v=;两套 btn/toast 刻意分域未强并

Verification —

  • py_compile esp32_server.py ecat_client.py ecat_proc.py PASS
  • test_esf_pack.py PASS;test_app_lease.py ALL PASS(7);test_motor_refresh.mjs 62/62 PASS
  • test_ecat_autostart.mjs 26/26 PASS(transport 探测/open/start/adapter;repairEcatDc 修复后 period 预期 0.25ms)
  • R1 非幂等:ecat_client._ecat_target_idempotent 源码+单测 PASS(默认 False,白名单只读);autostart 套件不覆盖 server 代理 503,回归需另补 stub
  • 全部新/改 JS node --check PASS
  • 真机 e2e_web_send_latency.py PASS 27/0 exit=0(2026-09-23 复测):DC=250µs loop=1 wkc=3;网页 HTTP send p50≈1.8ms;GD32 出口 p50≈1.5ms;点击→STATUS≈12ms;WiFi 对照可用。首次冷启动曾见 start 后 health loop=0 竞态,热总线复跑通过。
  • git 跟踪 scripts/=16;磁盘已清一批未跟踪 junk(仍可能有 ignore 内残留);「128→16」指 git 清单

Journey log —

  • worktree add 被沙箱拦 → 用户授权 main
  • P1/P2 子代理中断后主会话接手,验证后补提交
  • Measure-Object -Line 丢空行 → 用 ReadAllLines().Length
  • repairEcatDc 在 health 无 dc 字段时误调 motor/start,违反 healthCheck「只读」契约 → 已修
  • 顺序:先止血(R1–R3)再拆文件,避免在坏边界上大搬家

[S1] Problem

用户要求评估 G:\Robot_v2\DCU\tools\ESP32 这套 ESP32 调试上位机在持续迭代后,是否已经变成「屎山」:有没有冗余、过度复杂、功能堆叠和不可维护的结构。随后要求先提交基线,再按 R1–R16 逐项整改。

[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. 跨层架构观察

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 行数 < 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 总体积 < 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

  • T1: 全仓体积/文件/churn 盘点 — acceptance: 产出 scripts/js/server/lab 量化摘要 (covers: S2)
  • T2: 电机前端结构审计 — acceptance: main.js 热点、双链路残留、重复状态机带 file:line (covers: S2)
  • T3: esp32_server 职责与耦合审计 — acceptance: 九职责分段、越界项、critical 风险带 file:line (covers: S2)
  • T4: scripts/lab/_local 卫生审计 — acceptance: 分类数量、可删清单、必须保留清单 (covers: S2)
  • T5: 汇总评估文档 + 优先级重构清单 R1–R16 — acceptance: 本文档可独立评审 (covers: S2)
  • T6: 独立评审 — acceptance: 评审者对照证据复核 critical/major 发现与清单可行性;pass 无 critical (covers: S2; depends: T1–T5)