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(过程数据), 抓的现场会误导判断。
This commit is contained in:
+41
-15
@@ -96,7 +96,8 @@ EC_TIMEOUT_STATE_US = 2_000_000
|
|||||||
|
|
||||||
g_master = None
|
g_master = None
|
||||||
g_desc = ""
|
g_desc = ""
|
||||||
g_op_active = Falseg_httpd = None
|
g_op_active = False
|
||||||
|
g_httpd = None
|
||||||
g_child = None # 窗口模式: HTTP 服务子进程(仅 GUI 父进程使用)
|
g_child = None # 窗口模式: HTTP 服务子进程(仅 GUI 父进程使用)
|
||||||
g_port = None
|
g_port = None
|
||||||
g_win = None
|
g_win = None
|
||||||
@@ -319,7 +320,7 @@ def api_flash(req):
|
|||||||
|
|
||||||
|
|
||||||
def _require_op_ready(dc_cycle_us=0, dc_shift_us=0):
|
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;
|
注意 pysoem 语义:state_check(X) 只是轮询等待从站到达 X,不会发送 AL Control;
|
||||||
真正请求状态迁移必须 g_master.state = X_STATE 后 write_state()。旧版本进 SAFEOP
|
真正请求状态迁移必须 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 = []
|
log = []
|
||||||
if g_master is None:
|
if g_master is None:
|
||||||
return ["未连接"], False
|
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]
|
s = g_master.slaves[0]
|
||||||
|
|
||||||
# 1) INIT -> PREOP:SSC 在此启用邮箱 SM(APPL_StartMailboxHandler 直接 ack)
|
# 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:
|
try:
|
||||||
g_master.config_map()
|
g_master.config_map()
|
||||||
except Exception as e:
|
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。
|
# 注意:这里**不要**写 0x1C32:01 / 0x1C33:01。
|
||||||
#
|
#
|
||||||
@@ -597,18 +630,11 @@ def api_op_start(req):
|
|||||||
dc = _dc_args(req)
|
dc = _dc_args(req)
|
||||||
log, ok = _require_op_ready(*dc)
|
log, ok = _require_op_ready(*dc)
|
||||||
if not ok:
|
if not ok:
|
||||||
# 2026-09-03 自动重试一次:实测"断开重连后正常",把该恢复力内建——
|
# 自动重试一次:_require_op_ready 入口已自带"回 INIT + config_init 重枚举"
|
||||||
# 带 ACK 完整回 INIT(清从站残留错误位/SM 状态) + config_init 重枚举
|
# 预清理(2026-09-03 01:49 复现实锤:楔死邮箱上跑 config_map 会触发 SOEM
|
||||||
# (重写 SM/FMMU,等效重连的核心步骤) + 完整状态序列重来。
|
# CoE 分段读的无边界 memcpy → 0xC0000005 原生崩溃闪退,预清理是唯一可靠
|
||||||
# 固件≥1.5.48 的 0x204 守护也会在这轮里自愈 AL Event Mask。
|
# 规避路径),重试即完整等效"断开重连"。
|
||||||
log.append("自动重试:完整回 INIT + 重枚举后重新执行序列…")
|
log.append("自动重试:重新执行完整序列(入口含重枚举预清理)…")
|
||||||
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})")
|
|
||||||
log2, ok = _require_op_ready(*dc)
|
log2, ok = _require_op_ready(*dc)
|
||||||
log += log2
|
log += log2
|
||||||
if not ok:
|
if not ok:
|
||||||
|
|||||||
Reference in New Issue
Block a user