13 Commits
Author SHA1 Message Date
cbb a56b702d83 feat!: Python 后端整体切换为原生 C/SOEM 守护进程(ecat_native)
big-bang 迁移(硬约束: Npcap 网卡独占, 无法双主站并存), 四阶段真机验收的
完整契约与对拍记录见 MIGRATION.md —— Python 已删, 该文件是唯一行为基准。

- 新增 ecat_native.c: 与 Python 同构的全路由 native 守护 —— 总线生命周期
  open/close/status/op.start/op.stop, 电机 motor.start/stop/spin/state/
  send/rx/period/dbg, can/send|state, 配置维护 sdo/eeprom.read|write|
  verify/flash/sync_diag, 静态托管 src/web, shutdown/focus/--parent-pid
  看门狗/独立启动清场/端口漂移; 独占 8090 即生产链路。PDO 热循环天花板
  ~5kHz → ~7kHz。
- 删除 src/ecat_server.py(2826 行)、src/test_motor_backend.py(684 行)、
  ecat_app.vbs(桌面窗口随 pywebview 依赖一并移除): Python 后端不留副本。
- 编码回归改金标准: native_enc_diff.py 对照 native_enc_golden.json
  (阶段 2 与 Python _fill_can_out 逐字节对拍通过后固化, 不碰网卡),
  当场抓掉 json_tok 不跳闭合引号会静默丢 motor/send 参数的真 bug。
- build_native.bat: 编译前跑 gen_eeprom.py 生成 eeprom_image.h 供 native
  #include —— 同一生成器同一套断言, 断言失败构建中止; 改 SII 重跑即可。
- 修 flash 探针字历史隐患(本文件与 ecat_native.c 同步改): SCRATCH_WORD
  0x0200 实际落在 TxPDO 类别区内部(类别区已长到 0x0281), 单独跑 eeprom
  探针不烧录会把真数据擦成空洞; 移到 SII END 之后的 0x0400。
- 启动器 ecat_web.vbs/bat 改拉 build_native\ecat_native.exe
  (--substr I226 --web src\web)。
- 收录 soem-src wpcap 链接库(vendored, 构建必需)。

两轮回归修复(细节见 MIGRATION.md):
1) op/start 卡 AL 0x001B(SM 看门狗): 固件"自己写 1s 看门狗"的假设破产,
   do_op_start 第 5 步改为 ~5ms 节奏喂帧 ≥1s 再请求 OP, 24s 两次失败
   变 1.2s 进 OP。
2) OP 态数据冻结(从站 PDI_Isr SM2 分支被旗标挡住, 固件侧根因): 主站加
   楔死看门狗(输出窗口轮转 + 回显冻结 128 周期判死)与 /api/motor/state
   自动恢复(10s 冷却, 快照新增 wr 字段)。

已知差异: SDO 失败文本无 pysoem abort code 粒度; 250µs 亚毫秒周期从站
吃不下, 修复前勿用。build_native/ 构建产物入 .gitignore, 由脚本重建。
2026-09-05 16:01:15 +08:00
cbb 79633a2b0f feat(motor): 自适应 PDO 窗口与通道活动指示,新增原生 SOEM 时序诊断
后端 PDO 循环改为按在线槽收缩状态窗口,稀疏总线下单槽每周期都能刷新,
并补上尾部槽的发现扫描游标;新增自旋节拍源(阈值/显式覆盖)、P 核识别
与线程级钉核,自旋期间释放 GIL 以免 HTTP 线程被饿死;启动时先关停旧实例,
避免两个进程争用 Npcap 适配器。

记录每拍完整时间线、GC stop-the-world 事件与网格跳拍数,经 /api/motor/dbg
导出用于归因固定频率的超时;配
2026-09-05 01:20:14 +08:00
cbb 979da72181 refactor: 入口提到根目录, 代码收进 src/
上一版把入口塞进 launch/、代码摊在根目录, 正好反了: 双击入口该第一眼看到,
实现细节该收起来。现在:
  根目录  5 个 .bat/.vbs 入口 + README + requirements
  src/    ecat_server / ecat_tool / ecat_flash_eeprom / test_motor_backend
  src/web    前端资源      src/diag   独立诊断脚本

启动器改为 cd 进 %~dp0src 再跑(vbs 用 CurrentDirectory = base & "\src"),
工作目录落在 src 才能让 tool→flash_eeprom、test→server 的 import 解析到。
ecat_server.py 的 BASE_DIR 取自 __file__, 与 src/web 仍是同层父子关系,
_STATIC_MAP 和窗口模式的子进程 spawn 都无需改动。

.gitignore 补 sii.json / *.sii / eeprom_*.bin —— eeprom read --out 的导出
产物, 属运行结果不该入库。

验证: 7 个 py 全编译通过; test_motor_backend 8 项全通过; ecat.bat 与
run_scan.bat 实跑列出适配器; 服务起后 4 条静态路由 200 且 MIME 正确;
Edge headless 渲染主页与 motor 页, CSS 294 规则、9 节点样式生效, 零加载
失败零 JS 异常; bat 的 %~dp0 与 vbs 的 GetParentFolderName 实测均落在
项目根, src/ 下五个目标文件全部命中。
2026-09-03 22:27:23 +08:00
cbb beb33ffe50 refactor: 按角色分 web/ diag/ launch/ 三层, 根目录只留主程序
18 个文件平铺根目录太乱, 按角色归拢:
  web/     ecat.html/css + motor.html/js —— 前端资源
  diag/    ecat_scan / ecat_sniff / ecat_esc_regs —— 独立诊断脚本, 互不依赖
  launch/  5 个 .bat/.vbs 双击入口
根目录只留 ecat_server / ecat_tool / ecat_flash_eeprom / test_motor_backend:
这几个互相 import(tool→flash_eeprom, test→server), 挪进子目录就得改 import
或加包, 收益不抵成本。

配套改动:
  _STATIC_MAP 的值加 web/ 前缀 —— URL 路径刻意保持不变(/、/ecat.css、
  /motor.html), 页面内相对引用、书签、外部链接一律不用改。
  launch/ 里 %~dp0 现在指向 launch\, 五个启动器统一改为先取上一层作 ROOT
  (bat 用 %~dp0.., vbs 用两次 GetParentFolderName), run_scan 顺带指向
  diag\ecat_scan.py。

验证: 7 个 py 全编译通过; test_motor_backend 8 项全通过; 服务实起后 6 条
静态路由全 200 且 MIME 正确(js 是 application/javascript)、404 兜底正常;
Edge headless 渲染主页与 motor 页, CSS 294 规则加载、9 个节点样式生效、
零加载失败零 JS 异常; bat/vbs 的 ROOT 解析实测指到项目根, 五个目标文件
全部命中; ecat_tool adapters 与 diag/ecat_scan.py 实跑正常。
2026-09-03 22:08:38 +08:00
cbb 47ea85fb23 docs: README 同步电机控制台, 记下邮箱楔死判据
脚本表: 四个已删探针换成 motor.html/motor.js 与 test_motor_backend.py。
启动说明: 补电机控制台入口(/motor.html, 需先连接并进 OP)。
注意事项: 记下"SM 寄存器正常但 SDO 持续失败 = SSC u16EscAddrSendMbx 错位"
这个判据与固件 1.5.52 分界; 提醒 ecat.css 被两个页面共用, ?v= 要一起 bump。
2026-09-03 21:43:47 +08:00
cbb 3bfa240dfa chore: 移除四个一次性邮箱楔死探针
_probe_wedge/_probe_wedge2/_probe_stuck/_probe_scan 已完成使命: 根因查明为
SSC 的 u16EscAddrSendMbx 错位 —— 响应帧写到 SM1 start+4/+8 且逐会话累积
(断电才清零), 主站从固定 0x1200 读永远对不上帧头; SM 寄存器本身恒正确,
所以"SM0/SM1 配置正常却持续失败"就是该错位的判据。

结论已沉淀进 ecat_server.py config_map 失败分支的 SM 现场转储(扫全 4 个
SM, 0x0800+8n)与注释, 固件 1.5.52+ 已加守护自动写回恢复。探针不再需要。
2026-09-03 21:42:45 +08:00
cbb 85d326d445 feat(motor): 电机控制台前端 + 后端自测脚本
motor.html/motor.js: N1-N9 九节点控制台, 走 EtherCAT 过程数据(不经
WiFi/HTTP)。节点总览、CMD 控制(8 种 Control_Mode)、STATUS 实时解码、
示波器曲线、故障标签。样式复用 ecat.css(它与 ESP32 motor.css md5 一致),
不再拷第三份避免三处漂移; 本页专有的链路状态条/槽扫描指示走内联 style。

test_motor_backend.py: 不接硬件的后端自测, 假从站模拟 GD32
buildInput/processOutput, 覆盖 8 项(见 f09decc)。跑法:
  venv\Scripts\python.exe test_motor_backend.py
2026-09-03 21:42:19 +08:00
cbb f09decc127 feat(server): CANFD 批量收发区 + 电机控制台后端 API
PDO 布局扩展(与 GD32 1.5.56 对齐):
  段2 [48..383]  CANFD 批量发送区 —— 多帧共用一个 seq 沿, 从站在同一次
                 processOutput 里先发单帧再发批量, 9 帧 DLC32 由原来 9 个
                 周期压缩到 1 个周期。
  段4 [360..639] CANFD 批量接收区 —— 一周期回收多帧, 支持 DLC64 满长 CANFD,
                 带 lost/pending 计数。
  单帧区 [0..47] 布局不动, 批量区 count=0 时逐字节兼容 1.5.55, 老主站与
  CAN 调试页行为不变。

新增 API: /api/can/{send,state} 与 /api/motor/{start,stop,state,send,rx};
电机 PDO 后台循环靠滚动窗口扫 27 个槽, 命令走队列按 seq 沿下发。
ecat.html 加「电机控制台」入口与「CAN 转发」标签页。

test_motor_backend.py 8 项全通过(不接硬件, 假从站模拟 GD32 buildInput/
processOutput): DLC32 不截断、滚动窗口扫全槽、STATUS 32B 完整回传、
队列不互相覆盖、循环可干净停止、批量同步下发、DLC64 回收、1.5.55 兼容。
2026-09-03 21:42:07 +08:00
cbb 71c4ee6a14 fix(css): 补 motor.html 节点总览的 .node-item/.ni-* 样式
motor.js renderNodeList() 渲染的是 .node-item + .ni-led/.ni-name/
.ni-fault/.ni-sub, 但 ecat.css 里只有另一套 .node-card(供 ecat.html 的
单从站卡片)。类名对不上, N1-N9 九行渲染成无样式裸 div = "样式丢了"。

补齐单行紧凑布局(LED | Nx | 故障标签 | 右对齐读数)及状态态:
  .online   opacity 复原 + LED 转绿发光
  .active   边框转主色 + 底色加深
  故障      .ni-fault.has 显示为红色 pill, 边框同步转红
  离线      opacity .5 + 灰 LED 无光晕
沿用既有 CSS 变量与 .node-card 的视觉语言, 未改动 .node-card 本身。

Edge headless + CDP 实测计算样式确认: 9 项竖排不重叠、margin-left:auto
右对齐生效、四种状态切换正确。CSS 改动故 bump ?v=20260903b。
2026-09-03 21:40:26 +08:00
cbb 8d414ae3fb docs: 定名 dcu-ecat-master, 登记 _probe_scan.py
目录名 ecat 过泛(tools/ 下另有 ESP32), 按同级项目 DCU_APP / ESP32_COMM /
spi_link 的风格定名 dcu-ecat-master: dcu 限定归属, ecat-master 表明是
EtherCAT 主站侧工具(区别于从站固件)。远端托管用小写连字符形式。
名字写入 README 标题(对外可见)与 .git/description(本地元数据)。

同时把 _probe_scan.py(扫描响应帧头在 SM1 RAM 的确切偏移)登记进脚本表,
补齐 _probe_wedge2 / _probe_stuck 两行说明。
2026-09-03 02:41:19 +08:00
cbb fc9e8f34e5 diag: 加邮箱楔死探针三件套, 移除 _repro_hang.py
_probe_wedge.py   逐环节定位死点: A 写 SM0 的 WKC → B 0x220 bit0 是否置位
                  → C SSC 是否清事件并置 SM1 就绪 → D SM1 是否滞留旧响应,
                  用于区分 ESC 硬件楔死与 SSC 软件死循环。
_probe_wedge2.py  按真实邮箱协议走: 查写邮箱空 → 写满 256B → 验事件/响应。
_probe_stuck.py   验"SM1 响应滞留 → 邮箱死锁"假说: 复现失败 → 手动读走 SM1
                  清 bit3 → 再读是否恢复。若恢复则主站侧可自愈。

_repro_hang.py 的闪退成因已定位(见 a1ec486, config_map 撞楔死邮箱), 且规避
已内建到 op/start 入口, 该复现脚本失去用途, 删除。

README 同步脚本清单, 并记下 SM = 0x0800 + 8n 这个易错点。
.gitignore 排除 _analyze_log.py(硬编码已失效的临时日志路径)。
2026-09-03 02:36:07 +08:00
cbb a1ec486c74 fix(server): 修模块级变量粘连, op/start 入口预清理重枚举
1) g_op_active = Falseg_httpd = None 这行把 None 赋给了 g_op_active 和
   笔误变量 Falseg_httpd, 导致 g_httpd 在模块级从未定义, 第 744 行
   if g_httpd is not None 在服务启动前命中即 NameError。拆成两行。
   语法合法故初次提交未拦住。

2) _require_op_ready 入口无条件预清理(回 INIT + config_init 重枚举):
   同一连接反复停止/启动而不重枚举时从站邮箱通路逐轮退化, 第 4 轮
   config_map() 内 SOEM CoE 分段读的无边界 memcpy 吃到楔死邮箱异常帧
   触发 0xC0000005 闪退。置于入口使所有调用方全覆盖, 单从站成本 ~100ms。
   op_start 的重试分支因此简化为直接重跑序列。

3) config_map 失败时附带邮箱 SM 寄存器现场, 便于对照 GD32 串口日志。
   地址按 SM = 0x0800 + 8n 修正为 SM0=0x0800 / SM1=0x0808;
   原写的 0x0810/0x0818 实为 SM2/SM3(过程数据), 抓的现场会误导判断。
2026-09-03 02:35:24 +08:00
cbb d594b8fd94 init: DCU LAN9252 EtherCAT 主站工具集
现有工具纳入版本管理:
- ecat_tool.py: 一体化命令行主站(status/flash/eeprom/sdo/send)
- ecat_server.py + ecat.html/css: 网页/桌面控制台(端口 8090)
- ecat_scan.py / ecat_esc_regs.py / ecat_sniff.py: bring-up 扫描、
  网络侧 ESC 寄存器诊断、裸帧抓包对照
- ecat_flash_eeprom.py: 经网线烧写 SII 镜像 + 逐字校验
- *.bat / *.vbs 启动入口

新增 .gitignore(排除 venv/ 与 __pycache__)、.gitattributes(不改写换行符)、
requirements.txt(锁定 pysoem 1.1.13 等依赖)、README.md。
2026-09-03 01:56:29 +08:00