跳到主要内容

事件

主站和从站事件通过 add_* 方法注册回调函数,通过 remove_* 方法移除。

主站级回调

主站级事件直接通过 master.on_* 方法注册底层回调。

高级事件系统

使用 MasterEvents 和 SlaveEvents 类进行更精细的事件管理,支持多监听器和自动路由。

自动日志

所有事件(PDO 周期回调除外)触发时系统均自动记录日志,无论是否订阅。确保关键运行状态不会被静默丢失。

PDO 周期回调(on_pdo_cycle())为高频回调,不记录日志。

架构变更(2026-05-21)

已移除两项失效回调:

  • 异步 PDO 周期回调(旧 on_pdo_cycle(sync=False))—— on_pdo_cycle() 仅保留同步回调。
  • 输入数据变化回调(旧 on_input_changed() / slave.events.add_input_changed())—— 该机制原靠 DLL 层每周期 memcmp 检测,已不再驱动。

金路径是 inputs_mapping / from_address 映射一次(见 PDO 输入输出)。需感知输入变化,读钉页字段或把快照与上一周期比对。

功能概览​

类别方法读写说明自动日志
PDO 周期回调master.on_pdo_cycle()只写PDO 周期回调(同步)—
状态事件master.on_state_change()只写从站状态变化✓
热插拔事件master.on_slave_discovery()只写从站发现/丢失✓
master.events.add_slave_identity_mismatch()只写从站身份不符(换成错误型号/低版本)✓
异常事件master.on_emergency()只写CoE Emergency 紧急消息✓
master.on_pdo_frame_loss()只写PDO 连续丢帧(按组独立跟踪)✓
master.set_dc_sync_lost_callback()只写DC 同步丢失✓
冗余事件master.on_redundancy_mode_changed()只写冗余模式变化✓
master.events.add_slave_port_link_changed()只写从站端口 link 变化(P0-P3 断开/恢复)✓
日志master.set_log_callback()只写日志消息回调 (category, message)—

PDO 周期回调​

on_pdo_cycle()​

def on_pdo_cycle(self, callback: Callable[[int], None], sync: bool = True)

注册 PDO 周期回调(同步模式)。native 每周期只发布一个 latest stamp,Python 回调在一条 NORMAL daemon worker 线程执行(不是 PDO 周期线程):慢回调只丢周期、不会拖住总线。

sync 参数(2026-05-21 起仅向后兼容)

异步周期回调机制已移除。sync 参数仅为不破坏旧代码而保留,任何取值均注册同步回调;用户代码始终在普通 worker 线程执行。

参数:

  • callback — 回调函数 (master_index)
  • sync (bool) — 已弃用,保留仅为向后兼容;取值不影响行为(始终同步)

示例(钉页 0 拷贝,映射在回调外只做一次):

inputs = slave.pdo.inputs_mapping(InPdo)
outputs = slave.pdo.outputs_mapping(OutPdo)

def on_pdo_sync(master_index):
outputs.control_word = 0x000F
outputs.target_position = inputs.actual_position

master.on_pdo_cycle(on_pdo_sync)

快照备选(每拍 Copy,拷出来的实例不会自己变;禁止 from_buffer_copy 冒充钉页):

import struct

def on_pdo_sync(master_index):
raw = slave.pdo.inputs
frame = bytearray(slave.pdo.outputs or b'')
if len(frame) >= 2:
struct.pack_into('<H', frame, 0, 0x000F)
slave.pdo.write_outputs(bytes(frame))

master.on_pdo_cycle(on_pdo_sync)
注意

回调中不要执行耗时操作(如 SDO 读写、文件 I/O、锁等待):回调跑在普通 worker 线程,慢回调不会阻塞总线,但会丢掉中间周期。

状态事件​

on_state_change()​

def on_state_change(self, callback: Callable[[int, int, int, int], None],
sync: bool = True) -> None

注册从站状态变化回调。由后台线程监控并回调。始终自动记录日志。

回调参数:

  • master_index (int) — 主站索引
  • slave_index (int) — 从站索引(1-based)
  • old_state (int) — 变化前的状态(EcState 枚举值)
  • new_state (int) — 变化后的状态(EcState 枚举值)

示例:

from darra_ethercat import EcALState

def on_state_change(mi, si, old, new):
print(f"从站 {si}: 0x{old:02X} -> 0x{new:02X}")

slave = master[si]
if slave.error_code != EcALState.NO_ERROR:
print(f" 错误码: 0x{slave.error_code.value:04X}")

master.on_state_change(on_state_change)
error_code

slave.error_code 返回 EcALState 枚举(AL Status Code),即状态切换失败的具体原因。

热插拔事件​

on_slave_discovery()​

def on_slave_discovery(self, callback: Callable[[int, int, bool], None],
sync: bool = True) -> None

注册从站发现/丢失回调。热插拔断开时触发,PDO 线程内置的恢复状态机会自动恢复从站(零 PDO 影响)。始终自动记录日志。

回调参数:

  • master_index (int) — 主站索引
  • slave_index (int) — 从站索引(1-based)
  • is_found (bool) — True=从站上线,False=从站离线

示例:

def on_discovery(mi, si, found):
if found:
print(f"从站 {si} 上线")
else:
print(f"从站 {si} 离线")

master.on_slave_discovery(on_discovery)

is_slave_offline() (MasterEvents)​

def is_slave_offline(self, slave_index: int) -> bool

查询从站是否已被事件系统确认为离线状态。需通过 MasterEvents 实例访问。

参数:

  • slave_index (int) — 从站索引(1-based)

返回值:

  • bool — 从站离线返回 True

示例:

events = master.events
# ... 注册回调 ...

if events.is_slave_offline(2):
print("从站 2 处于离线状态")

# 获取所有离线从站集合
offline = events.offline_slaves # Set[int]
print(f"离线从站: {offline}")

slave_identity_mismatch (MasterEvents)​

def add_slave_identity_mismatch(self,
callback: Callable[[SlaveIdentityMismatchEventArgs], None]) -> None

从站断电重插后身份不符时触发:EtherCAT 识别状态机读取到的 Vendor/Product 与配置不匹配,或 Revision 低于配置(向后兼容:实际 Revision ≥ 配置值视为匹配)。始终自动记录日志。

触发后从站进入 IDENT_REJECTED 状态,不会自动重探测(防止错设备反复刷屏)。操作员检查/更换设备后需调用 master.acknowledge_slave_replacement() 让 SDK 重新探测。

相关结构:

@dataclass
class SlaveIdentityMismatchEventArgs:
master_index: int # 主站索引
slave_index: int # 从站索引(1-based)
expected_vendor: int # 配置期望的厂商 ID
expected_product: int # 配置期望的产品代码
expected_revision: int # 配置期望的最低修订号
actual_vendor: int # 当前实际厂商 ID
actual_product: int # 当前实际产品代码
actual_revision: int # 当前实际修订号

示例:

from darra_ethercat import SlaveIdentityMismatchEventArgs

events = master.events

def on_identity_mismatch(args: SlaveIdentityMismatchEventArgs):
print(f"从站 {args.slave_index} 身份不符:")
print(f" 期望: Vendor=0x{args.expected_vendor:08X}, "
f"Product=0x{args.expected_product:08X}, Rev>=0x{args.expected_revision:08X}")
print(f" 实际: Vendor=0x{args.actual_vendor:08X}, "
f"Product=0x{args.actual_product:08X}, Rev=0x{args.actual_revision:08X}")
# UI 弹窗提示用户换回正确设备, 确认后调用:
# master.acknowledge_slave_replacement(args.slave_index)

events.add_slave_identity_mismatch(on_identity_mismatch)
去重机制

同一从站进入 IDENT_REJECTED 状态仅触发一次事件。调用 acknowledge_slave_replacement() 后重置探测,身份仍不匹配会再次触发。

异常事件​

周期异常必须报警

周期按 DENI/LoopCycle 设定,最短 125µs,不是写死 1ms。rt_cnt 应对齐 1e9/LoopCycleNs。连续偏离设定 ±20%,或 max jitter 超过周期的 25%,必须写日志并在 UI 报警。同时核对 pdo_frame_loss、dc_sync_lost。隔离未生效只允许抖动变大,不允许周期漂走还不报。不要在 PDO 热路径里做报警 I/O。

on_emergency()​

def on_emergency(self, callback: Callable) -> None

注册 CoE Emergency 紧急消息回调。从站固件检测到错误时发送,数据格式遵循 CANopen Emergency 协议(CiA 301)。始终自动记录日志。

回调参数: (master_index, slave_index, error_code, error_reg, b1, w1, w2)

  • master_index (int) — 主站索引
  • slave_index (int) — 从站索引(1-based)
  • error_code (int) — 错误代码(CANopen Emergency Error Code,对象 0x603F)
  • error_reg (int) — 错误寄存器(对象 0x1001)
  • b1 (int) — 制造商特定数据(字节 3)
  • w1 (int) — 制造商特定数据(字节 4-5)
  • w2 (int) — 制造商特定数据(字节 6-7)

常见 Emergency Error Code:

  • 0x0000 — 错误已复位
  • 0x1000 — 通用错误
  • 0x2000 — 电流错误
  • 0x3000 — 电压错误
  • 0x4000 — 温度错误
  • 0x5000 — 设备硬件错误
  • 0x6000 — 设备软件错误
  • 0x7000 — 附加模块错误
  • 0x8000 — 监控错误(通信)
  • 0xFF00 — 制造商特定错误

示例:

def on_emcy(mi, si, err_code, err_reg, b1, w1, w2):
print(f"从站 {si} 紧急消息: 错误码=0x{err_code:04X}, 寄存器=0x{err_reg:02X}")

master.on_emergency(on_emcy)
Python 特有语法糖

回调风格之外,Python 还可用 async for ev in master.emergencies(): 异步生成器以流式 API 消费 EMCY 事件(master.state_changes() / master.slave_state_changes() 同理)。详见 异步生成器。

on_pdo_frame_loss()​

def on_pdo_frame_loss(self, callback: Callable[[int, int, int, int], None]) -> None

注册 PDO 连续丢帧回调。当连续丢帧达到阈值时触发,单次丢帧仅计数不触发。每组独立跟踪。始终自动记录日志。

回调参数: (master_index, group, consecutive_lost, total_lost)

  • master_index (int) — 主站索引
  • group (int) — 发生丢帧的组号(0-7),对应从站分组
  • consecutive_lost (int) — 该组连续丢帧数
  • total_lost (int) — 该组累计丢帧数

示例:

def on_loss(mi, group, consecutive, total):
print(f"组 {group} 丢帧: 连续={consecutive}, 累计={total}")

master.on_pdo_frame_loss(on_loss)
丢帧统计

PDO 丢帧的详细统计(按组查询)请参考 主站诊断 - PDO 丢帧。

set_dc_sync_lost_callback()​

def set_dc_sync_lost_callback(self, callback: Optional[Callable[[int, int, int], None]]) -> None

设置 DC 同步丢失回调。当从站从同步→失同步(超出阈值)时触发一次,持续超出不重复触发。传入 None 清除回调。始终自动记录日志。

回调参数: (master_index, slave_index, diff_ns)

  • master_index (int) — 主站索引
  • slave_index (int) — 失同步的从站索引(1-based)
  • diff_ns (int) — 当前与参考时钟的时间差(纳秒)

示例:

master.diagnostics_info.sync_window_threshold = 500  # 500ns

def on_sync_lost(mi, si, diff_ns):
print(f"从站 {si} DC 同步丢失: 偏差 {diff_ns}ns")

master.set_dc_sync_lost_callback(on_sync_lost)
单个从站 DC 诊断

slave.diagnostics.dc.is_in_sync 和 slave.diagnostics.dc.sync_time_difference 可查询从站当前同步状态。详见 从站诊断 - DC 同步。

输入数据变化(已移除,改用轮询)​

已移除(2026-05-21)

on_input_changed()(主站级)和 slave.events.add_input_changed()(从站级)已删除。

旧机制靠底层每周期 memcmp 检测输入 PDO 变化并回调。应用层 PDO 自持过程映像(金路径映射一次钉页,或整帧 Copy 快照)后,该机制不再驱动。

需感知输入变化,金路径是映射一次(inputs_mapping)后每周期读钉页字段比对;快照场景则在 on_pdo_cycle() 同步回调或自建轮询线程里读 slave.pdo.inputs(owned 快照),并与上一周期比对:

_last_inputs = {}

def on_pdo_cycle(mi):
for si in range(1, len(master) + 1):
cur = master[si].pdo.inputs
if cur is not None and _last_inputs.get(si) != cur:
_last_inputs[si] = cur
print(f"从站 {si} 输入变化: {cur[:4].hex()}")

master.on_pdo_cycle(on_pdo_cycle)
结构体视图

bind_pdo_struct 内部转调 inputs_mapping / outputs_mapping,是 from_address 钉页(不是 from_buffer_copy 快照):输入方向每次读即最新值,输出方向改字段即写总线。只需快照时用 inputs 整帧拷贝后 struct.unpack_from。详见 PDO 输入输出。

冗余事件​

on_redundancy_mode_changed()​

def on_redundancy_mode_changed(self, callback: Callable[[int, int, int], None]) -> None

冗余运行模式发生变化时触发。old_mode / new_mode 对应 RingMode 枚举值(RingMode.INACTIVE=0, RingMode.DUAL=1, RingMode.DEGRADED=2)。

回调参数: (master_index, old_mode, new_mode)

示例:

from darra_ethercat import RingMode

def on_red_change(mi, old, new):
print(f"冗余模式: {RingMode(old).name} -> {RingMode(new).name}")

master.on_redundancy_mode_changed(on_red_change)
def add_slave_port_link_changed(self,
callback: Callable[[int, int, int, bool], None]) -> None

从站 ESC 端口物理链路变化时触发(P0-P3 之一断开或恢复)。每秒诊断周期检测 DL Status 寄存器的 link bit,从 1→0 触发"断开",从 0→1 触发"恢复"。始终自动记录日志。

用于精确定位故障线缆段(相邻从站的对向端口同时报断,说明中间线缆有问题)。

回调参数:

  • master_index (int) — 主站索引
  • slave_index (int) — 从站索引(1-based)
  • port (int) — 端口号 0-3,对应 P0 / P1 / P2 / P3
  • is_up (bool) — True=link 恢复, False=link 断开

示例:

events = master.events

def on_port_link(mi, si, port, is_up):
action = "恢复" if is_up else "断开"
print(f"从站 {si} 端口 P{port} link {action}")

events.add_slave_port_link_changed(on_port_link)
配合断点定位

master.diagnostics.break_point 提供聚合后的故障点视图(断线 + CRC 故障)。slave_port_link_changed 是原始事件源,适合做实时告警。详见 主站诊断 - 冗余状态。

日志回调​

set_log_callback()​

def set_log_callback(self, callback: Optional[Callable[[int, str], None]]) -> None

设置日志回调,传 None 清除本实例的注册。

回调参数:

  • category (int) — 日志类别
  • message (str) — 日志消息

示例:

def on_log(category, message):
labels = {0: "错误", 1: "警告", 2: "消息", 3: "邮箱", 4: "PDO", 5: "调试"}
print(f"[{labels.get(category, str(category))}] {message}")

master.set_log_callback(on_log)

高级事件系统 (MasterEvents)​

MasterEvents 类提供多监听器支持和从站级事件自动路由。所有事件回调统一注册,MasterEvents 自动分发到对应的监听器。

events = master.events

# === PDO 周期回调(仅同步, 异步回调已于 2026-05-21 移除) ===
events.add_pdo_cyclic_sync(lambda mi: None) # 同步回调

# === 主站状态变化 ===
events.add_state_changed(lambda old, new:
print(f"主站: 0x{old:02X} -> 0x{new:02X}"))

# === 从站状态变化 ===
events.add_slave_state_changed(lambda mi, si, old, new:
print(f"从站 {si}: 0x{old:02X} -> 0x{new:02X}"))

# === 热插拔 ===
events.add_slave_offline(lambda si: print(f"从站 {si} 离线"))
events.add_slave_online(lambda si: print(f"从站 {si} 上线"))

# === 异常事件 ===
events.add_pdo_frame_loss(lambda mi, grp, cons, total:
print(f"组 {grp} 丢帧: {cons}"))

events.add_emergency(lambda mi, si, ec, er, b1, w1, w2:
print(f"从站 {si} 紧急: 0x{ec:04X}"))

events.add_dc_sync_lost(lambda mi, si, diff:
print(f"从站 {si} DC 同步丢失: {diff}ns"))

# 注: 输入数据变化回调 (add_input_data_changed) 已于 2026-05-21 移除,
# 改用 on_pdo_cycle 同步回调里轮询 slave.pdo.inputs 比对。

# === 冗余 ===
events.add_redundancy_mode_changed(lambda mi, old, new:
print(f"冗余: {old} -> {new}"))

# === 从站端口链路变化 (断线 / 恢复) ===
events.add_slave_port_link_changed(lambda mi, si, port, is_up:
print(f"从站 {si} P{port} {'恢复' if is_up else '断开'}"))

# === 从站身份不符 (热插拔自修复) ===
events.add_slave_identity_mismatch(lambda args:
print(f"从站 {args.slave_index} 身份不符, 实际 Vendor=0x{args.actual_vendor:08X}"))

# === 查询从站离线状态 ===
if events.is_slave_offline(2):
print("从站 2 处于离线状态")

# === 移除监听器 ===
events.remove_slave_offline(my_callback)

clear_all() (MasterEvents)​

def clear_all(self) -> bool

清除所有事件订阅,防止内存泄漏。一次性移除所有通过 add_* 注册的回调,同时停掉 PDO worker 并清空离线从站跟踪集合。返回 True 表示 PDO worker 已安全停止。

示例:

events = master.events
events.add_slave_offline(lambda si: print(f"从站 {si} 离线"))
events.add_emergency(lambda mi, si, ec, er, b1, w1, w2: None)

# 清除所有订阅
events.clear_all()

从站级事件 (SlaveEvents)​

从站级事件通过 slave.events 访问(需要先通过 master[n] 获取从站实例)。事件参数已自动过滤到当前从站,不含 master_index / slave_index。

slave = master[1]

# 注册从站状态变化
slave.events.add_state_changed(lambda old, new:
print(f"状态: 0x{old:02X} -> 0x{new:02X}"))

# 紧急消息 (error_code, error_reg, b1, w1, w2)
slave.events.add_emergency(lambda ec, er, b1, w1, w2:
print(f"紧急: 0x{ec:04X}"))

# 离线/上线
slave.events.add_offline(lambda: print("从站离线"))
slave.events.add_online(lambda: print("从站上线"))

# DC 同步丢失 (diff_ns)
slave.events.add_dc_sync_lost(lambda diff: print(f"DC 同步丢失: {diff}ns"))

# 注: 从站级输入数据变化回调 (add_input_changed) 已于 2026-05-21 移除,
# 改用 on_pdo_cycle 同步回调里轮询 slave.pdo.inputs 比对。

# 移除监听器
slave.events.remove_state_changed(my_callback)
slave.events.remove_emergency(my_callback)
slave.events.remove_offline(my_callback)
slave.events.remove_online(my_callback)
slave.events.remove_dc_sync_lost(my_callback)

clear_all() (SlaveEvents)​

def clear_all(self) -> None

清除该从站的所有事件订阅(包括 FSoE 事件),防止内存泄漏。

示例:

slave = master[1]
slave.events.add_state_changed(lambda old, new: None)
slave.events.add_emergency(lambda ec, er, b1, w1, w2: None)

# 清除该从站的所有订阅
slave.events.clear_all()

线程安全​

事件回调在非 UI 线程上触发。如需更新 GUI 或在主线程处理数据,需使用对应的线程同步机制:

import queue

# 使用 queue 跨线程传递事件(推荐)
event_queue = queue.Queue()

def on_discovery(mi, si, found):
event_queue.put(("discovery", si, found))

master.on_slave_discovery(on_discovery)

# 主线程消费
while True:
event_type, idx, found = event_queue.get()
print(f"从站 {idx} {'上线' if found else '离线'}")
GIL 与性能

Python GIL 确保回调是线程安全的,但要注意:

  • PDO 周期回调在普通 worker 线程执行(不是 PDO 周期线程),慢回调只丢周期
  • 不要在回调中进行 I/O 操作、加锁或 SDO 读写
  • 耗时处理用 queue.Queue 跨线程传递数据,回调里只做快速入队

完整示例​

from darra_ethercat import EtherCATMaster, EcState

with EtherCATMaster() as master:
# ===== 日志 =====
master.set_log_callback(lambda cat, msg: print(f"[{cat}] {msg}"))

# ===== 状态变化 =====
master.on_state_change(lambda mi, si, old, new:
print(f"从站 {si}: 0x{old:02X} -> 0x{new:02X}"))

# ===== 紧急消息 =====
master.on_emergency(lambda mi, si, ec, er, b1, w1, w2:
print(f"从站 {si} 紧急: 0x{ec:04X}"))

# ===== 从站发现/丢失 =====
master.on_slave_discovery(lambda mi, si, found:
print(f"从站 {si} {'上线' if found else '离线'}"))

# ===== PDO 丢帧 =====
master.on_pdo_frame_loss(lambda mi, grp, cons, total:
print(f"组 {grp} 丢帧: 连续={cons}, 累计={total}"))

# ===== DC 同步丢失 =====
master.set_dc_sync_lost_callback(lambda mi, si, diff:
print(f"从站 {si} DC 同步丢失: {diff}ns"))

# ===== 冗余模式变化 =====
master.on_redundancy_mode_changed(lambda mi, old, new:
print(f"冗余: {old} -> {new}"))

# ===== PDO 周期回调(同步, 供 FSoE 等实时控制) =====
# 输入变化检测改在此回调里轮询 slave.pdo.inputs 比对
master.on_pdo_cycle(lambda mi: None)

# 连接和运行
master.set_network(r"\Device\NPF_{GUID}")
master.set_state(EcState.OP)
master.start()