From a1ec486c742d0c6d809be5c649b117fd35109917 Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Thu, 3 Sep 2026 02:35:24 +0800 Subject: [PATCH] =?UTF-8?q?fix(server):=20=E4=BF=AE=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E7=BA=A7=E5=8F=98=E9=87=8F=E7=B2=98=E8=BF=9E,=20op/start=20?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=E9=A2=84=E6=B8=85=E7=90=86=E9=87=8D=E6=9E=9A?= =?UTF-8?q?=E4=B8=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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(过程数据), 抓的现场会误导判断。 --- ecat_server.py | 56 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/ecat_server.py b/ecat_server.py index 09a0b9a..8733dab 100644 --- a/ecat_server.py +++ b/ecat_server.py @@ -96,7 +96,8 @@ EC_TIMEOUT_STATE_US = 2_000_000 g_master = None g_desc = "" -g_op_active = Falseg_httpd = None +g_op_active = False +g_httpd = None g_child = None # 窗口模式: HTTP 服务子进程(仅 GUI 父进程使用) g_port = None g_win = None @@ -319,7 +320,7 @@ def api_flash(req): def _require_op_ready(dc_cycle_us=0, dc_shift_us=0): - """进 OP: PREOP → config_map → (可选 DC) → SAFEOP → OP。返回 (log, ok)。 + """进 OP: [预清理重枚举] → PREOP → config_map → (可选 DC) → SAFEOP → OP。返回 (log, ok)。 注意 pysoem 语义:state_check(X) 只是轮询等待从站到达 X,不会发送 AL Control; 真正请求状态迁移必须 g_master.state = X_STATE 后 write_state()。旧版本进 SAFEOP @@ -337,6 +338,26 @@ def _require_op_ready(dc_cycle_us=0, dc_shift_us=0): log = [] if g_master is None: return ["未连接"], False + + # 0) 无条件预清理:完整回 INIT + config_init 重枚举(2026-09-03 01:49 复现实锤)。 + # 同一连接上反复 停止/启动 而不重枚举时,从站邮箱/PDI 通路逐轮退化:第 2 轮起 + # OP 态 SDO 读 0x2000 全部 WkcError(即 UI 的"SDO 读 0x2000 失败"),第 4 轮 + # config_map() 内 SOEM CoE 读(ethercatcoe.c ecx_SDOread 分段传输循环 memcpy + # 无边界检查,上游缺陷)吃到楔死邮箱的异常帧 → 原生访问违例(0xC0000005) + # → 服务进程闪退、GUI 连带退出。与 01:40 用户日志逐行吻合:最后一轮"进入 OP" + # 后无任何输出 = 进程死在 config_map,响应未及回传。config_init 重写 SM0/SM1 + # + 重建 slavelist,与实测恒有效的"断开重连"同机理;单从站成本 ~100ms。 + # 置于入口(而非失败后)使所有调用方(op_start/自动重试/op_cycle 兜底)全覆盖, + # 且在 s = slaves[0] 取引用之前执行(config_init 会重建 slaves 列表)。 + log.append("预清理:回 INIT + config_init 重枚举(等效断开重连,防楔死邮箱上 config_map 原生崩溃)…") + try: + g_master.state = pysoem.INIT_STATE | pysoem.STATE_ACK + g_master.write_state() + g_master.state_check(pysoem.INIT_STATE, EC_TIMEOUT_STATE_US) + g_master.config_init() + except Exception as e: + log.append(f"WARN: 预清理失败({type(e).__name__}: {e}),继续原序列") + s = g_master.slaves[0] # 1) INIT -> PREOP:SSC 在此启用邮箱 SM(APPL_StartMailboxHandler 直接 ack) @@ -389,7 +410,19 @@ def _require_op_ready(dc_cycle_us=0, dc_shift_us=0): try: g_master.config_map() except Exception as e: - return log + [f"config_map 失败: {type(e).__name__}: {e}"], False + # 顺带抓邮箱 SM 寄存器现场(8B: start/len/ctl/status/activate/pdi)—— + # 2026-09-03 02:12 实测楔死时 activate 位/StartAddr 异常(INIT 态读回全零, + # PREOP 态读回 SII 值),这是区分"邮箱 SM 被禁用/未配置"与"SSC 不处理邮箱" + # 的关键证据;失败信息里直接带上,抓 GD32 串口时可对照。 + # SM 寄存器 = 0x0800 + 8n, 邮箱是 SM0=0x0800 / SM1=0x0808; + # 0x0810/0x0818 是 SM2/SM3(过程数据), 勿混用。 + try: + sm0 = bytes(s._fprd(0x0800, 8, 20000)).hex().upper() + sm1 = bytes(s._fprd(0x0808, 8, 20000)).hex().upper() + sm_info = f"; 邮箱SM现场 SM0={sm0} SM1={sm1}(act位=byte[6],01=使能)" + except Exception: + sm_info = "" + return log + [f"config_map 失败: {type(e).__name__}: {e}{sm_info}"], False # 注意:这里**不要**写 0x1C32:01 / 0x1C33:01。 # @@ -597,18 +630,11 @@ def api_op_start(req): dc = _dc_args(req) log, ok = _require_op_ready(*dc) if not ok: - # 2026-09-03 自动重试一次:实测"断开重连后正常",把该恢复力内建—— - # 带 ACK 完整回 INIT(清从站残留错误位/SM 状态) + config_init 重枚举 - # (重写 SM/FMMU,等效重连的核心步骤) + 完整状态序列重来。 - # 固件≥1.5.48 的 0x204 守护也会在这轮里自愈 AL Event Mask。 - log.append("自动重试:完整回 INIT + 重枚举后重新执行序列…") - try: - g_master.state = pysoem.INIT_STATE | pysoem.STATE_ACK - g_master.write_state() - g_master.state_check(pysoem.INIT_STATE, EC_TIMEOUT_STATE_US) - g_master.config_init() - except Exception as e: - log.append(f"WARN: 重试前清理失败({type(e).__name__}: {e})") + # 自动重试一次:_require_op_ready 入口已自带"回 INIT + config_init 重枚举" + # 预清理(2026-09-03 01:49 复现实锤:楔死邮箱上跑 config_map 会触发 SOEM + # CoE 分段读的无边界 memcpy → 0xC0000005 原生崩溃闪退,预清理是唯一可靠 + # 规避路径),重试即完整等效"断开重连"。 + log.append("自动重试:重新执行完整序列(入口含重枚举预清理)…") log2, ok = _require_op_ready(*dc) log += log2 if not ok: