跳到主要内容

事件与回调

通过 master.Events() 获取 MasterEvents& 引用,用 On*() 注册方法添加 std::function 回调。

多订阅者模型 — 用 On*() 注册, 不是字段赋值

MasterEvents 每个事件是 多订阅者std::vector<std::function<...>>, 不能用 = 直接赋值。 必须调用对应的 On*() 注册方法 (可多次调用注册多个回调)。常见映射:

事件注册方法
PDO 周期回调 (同步)Events().SetPDOCallbackSync(cb)
主站状态变化Events().OnStateChanged(cb)
从站状态变化Events().OnSlaveStateChanged(cb)
从站上线 / 离线Events().OnSlaveDiscovered(cb) / Events().OnSlaveLost(cb)
从站身份不符Events().OnSlaveIdentityMismatch(cb)
CoE EmergencyEvents().OnEmergency(cb)
PDO 连续丢帧Events().OnPDOFrameLoss(cb)
DC 同步丢失Events().OnDCSyncLost(cb)
冗余模式变化Events().OnRedundancyModeChanged(cb)
从站端口链路变化Events().OnSlavePortLinkChanged(cb)
错误 / 警告Events().OnError(cb) / Events().OnWarning(cb)
从站 PreOP 重配置Events().OnSlavePreOpReconfig(cb)

MasterEvents 没有 ProcessDataCyclicSync / StateChanged 这类可直接赋值的字段; 底层数据成员名为 pdoCyclicSync / stateChanged / ... 且类型是 std::vector, 不应直接操作。

C++ 特有语法糖

临时启动一段周期通信时, 可用 darra::sugar::ScopedStart guard(master); RAII 守护, 离开作用域自动 Stop(). 详见 RAII 作用域守护.

自动日志

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

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

功能概览

类别事件字段说明自动日志
PDO 周期回调ProcessDataCyclicSyncPDO 同步周期回调(供 FSoE 等实时控制)--
状态事件StateChanged主站 EtherCAT 状态变化yes
SlaveStateChanged从站 EtherCAT 状态变化yes
热插拔事件SlaveOffline从站离线(断开)yes
SlaveOnline从站上线(恢复)yes
热插拔事件SlaveIdentityMismatch从站身份不符(换成错误型号/低版本)yes
异常事件EmergencyEventCoE Emergency 紧急消息yes
PDOFrameLossPDO 连续丢帧yes
DCSyncLostDC 同步丢失yes
冗余事件RedundancyModeChanged冗余模式变化yes
SlavePortLinkChanged从站端口 link 变化(P0-P3 断开/恢复)yes

PDO 周期回调

ProcessDataCyclicSync

using ProcessDataCyclicCallback = std::function<void(uint16_t masterIndex)>;
PDO 数据通常无需回调

PDO 过程数据为纯内核 RT 收发,应用层经内核共享内存指针零拷贝直接读写过程映像。一般情况下直接轮询过程映像指针Slave::InputDataPointer() / OutputDataPointer()SlavePdo::BindPdoStruct<T>())即可,无需事件回调。ProcessDataCyclicSync 仅在需要 FSoE 等当周期实时控制时使用。

异步周期回调(旧 ProcessDataCyclicAsync)已随 PDO 纯内核化移除:PDO 数据已是内核共享内存指针零拷贝,不再有"用户态每周期通知"环节。

每个 PDO 周期触发一次(同步模式)。回调在 PDO 实时线程中直接执行,延迟最低但回调必须快速返回。 用 SetPDOCallbackSync() 注册。

示例:

// 同步模式(在 PDO 实时线程内执行, 供 FSoE 等当周期实时控制)
master.Events().SetPDOCallbackSync([&](uint16_t mi) {
auto& servo = master.GetSlave(1);
void* input = servo.InputDataPointer();
void* output = servo.OutputDataPointer();
// 零拷贝读写 PDO 数据
});
注意

同步回调中不要执行耗时操作(如 SDO 读写、文件 I/O、锁等待),否则会阻塞实时线程导致丢帧。

状态事件

StateChanged

std::function<void(EcState oldState, EcState newState)>

主站 EtherCAT 状态变化时触发(包括主动切换和异常降级)。始终自动记录日志。 用 OnStateChanged() 注册 (也可用 master.OnStateChanged())。

示例:

master.Events().OnStateChanged([](EcState oldS, EcState newS) {
printf("主站状态: %s -> %s\n",
EcStateFormat::Format(oldS).c_str(),
EcStateFormat::Format(newS).c_str());
});

SlaveStateChanged

using SlaveStateChangeCallback = std::function<void(
uint16_t masterIndex, uint16_t slaveIndex,
EcState oldState, EcState newState)>;

从站 EtherCAT 状态变化时触发(包括主动切换和异常降级)。用 OnSlaveStateChanged() 注册。

示例:

master.Events().OnSlaveStateChanged([](uint16_t mi, uint16_t si,
EcState oldS, EcState newS) {
printf("从站 %d 状态: %s -> %s\n", si,
EcStateFormat::Format(oldS).c_str(),
EcStateFormat::Format(newS).c_str());
});

从站上线/离线事件

SlaveOnline / SlaveOffline

using SlaveOnlineCallback = std::function<void(uint16_t slaveIndex)>;
using SlaveOfflineCallback = std::function<void(uint16_t slaveIndex)>;

从站上线或离线时触发。分别用 OnSlaveDiscovered() (上线) / OnSlaveLost() (离线) 注册。

示例:

master.Events().OnSlaveDiscovered([](uint16_t si) {
printf("从站 %d 上线\n", si);
});

master.Events().OnSlaveLost([](uint16_t si) {
printf("从站 %d 离线!\n", si);
});

IsSlaveOffline()

bool IsSlaveOffline(uint16_t slaveIndex) const;

查询从站是否已被事件系统确认为离线状态。

参数:

  • slaveIndex (uint16_t) -- 从站索引

返回值:

  • bool -- 从站离线返回 true

示例:

if (master.Events().IsSlaveOffline(2)) {
printf("从站 2 处于离线状态\n");
}

SlaveIdentityMismatch

using SlaveIdentityMismatchCallback = std::function<void(
uint16_t masterIndex, uint16_t slaveIndex,
uint32_t expectedVendor, uint32_t expectedProduct, uint32_t expectedRevision,
uint32_t actualVendor, uint32_t actualProduct, uint32_t actualRevision)>;

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

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

回调参数:

  • masterIndex (uint16_t) — 主站索引
  • slaveIndex (uint16_t) — 从站索引(1-based)
  • expectedVendor (uint32_t) — 配置期望的厂商 ID
  • expectedProduct (uint32_t) — 配置期望的产品代码
  • expectedRevision (uint32_t) — 配置期望的最低修订号
  • actualVendor (uint32_t) — 当前实际厂商 ID
  • actualProduct (uint32_t) — 当前实际产品代码
  • actualRevision (uint32_t) — 当前实际修订号

示例:

master.Events().OnSlaveIdentityMismatch(
[&](uint16_t mi, uint16_t si,
uint32_t eV, uint32_t eP, uint32_t eR,
uint32_t aV, uint32_t aP, uint32_t aR) {
printf("从站 %u 身份不符:\n", si);
printf(" 期望: V=0x%08X P=0x%08X R>=0x%08X\n", eV, eP, eR);
printf(" 实际: V=0x%08X P=0x%08X R=0x%08X\n", aV, aP, aR);
// UI 提示用户换回正确设备, 确认后调用:
// master_other::acknowledge_slave_replacement(master, si);
});
去重机制

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

AcknowledgeSlaveReplacement()

namespace darra { namespace ethercat { namespace master_other {
bool acknowledge_slave_replacement(EtherCATMaster& m, uint16_t slaveIndex);
} } }

操作员检查 / 更换从站完毕后调用, 让 SDK 复位识别状态机重新探测该从站。配套 SlaveIdentityMismatch 事件使用。

参数:

  • m (EtherCATMaster&) — 主站引用
  • slaveIndex (uint16_t) — 从站索引 (1-based)

返回值:

  • booltrue 表示已接受并复位 FSM; false 表示从站不在 IDENT_REJECTED / FAILED 状态, 或参数无效

行为:

  • 身份纠正 (换回正确设备 / 同型号升级 Revision) → 自动恢复, 触发 SlaveOnline
  • 身份仍不匹配 → 再次触发 SlaveIdentityMismatch, 回到 IDENT_REJECTED
必须先收到事件

未触发 SlaveIdentityMismatch 之前调用本函数无效, 返回 false

示例:

using namespace darra::ethercat;

master.Events().OnSlaveIdentityMismatch(
[&](uint16_t mi, uint16_t si,
uint32_t eV, uint32_t eP, uint32_t eR,
uint32_t aV, uint32_t aP, uint32_t aR) {
printf("从站 %u 身份不符\n", si);

if (ShowReplacementDialog(si)) {
bool ok = master_other::acknowledge_slave_replacement(master, si);
if (!ok) printf("确认失败: 从站不在 IDENT_REJECTED 状态\n");
}
});

紧急消息事件

EmergencyEvent

using EmergencyEventCallback = std::function<void(
uint16_t masterIndex, uint16_t slaveIndex,
uint16_t errorCode, uint16_t errorReg,
uint8_t b1, uint16_t w1, uint16_t w2)>;

CoE Emergency 紧急消息事件。数据格式遵循 CANopen Emergency 协议(CiA 301)。用 OnEmergency() 注册。

示例:

master.Events().OnEmergency([](uint16_t mi, uint16_t si,
uint16_t code, uint16_t reg,
uint8_t b1, uint16_t w1, uint16_t w2) {
printf("从站 %d 紧急消息: 错误码=0x%04X, 寄存器=0x%04X\n",
si, code, reg);
});

PDO 丢帧事件

PDOFrameLoss

using PDOFrameLossCallback = std::function<void(
uint16_t masterIndex, uint8_t group,
uint32_t consecutiveLost, uint32_t totalLost)>;

PDO 连续丢帧事件。用 OnPDOFrameLoss() 注册。

示例:

master.Events().OnPDOFrameLoss([](uint16_t mi, uint8_t grp,
uint32_t consec, uint32_t total) {
printf("组 %d 丢帧: 连续=%u, 累计=%u\n", grp, consec, total);
});

DC 同步丢失事件

DCSyncLost

using DCSyncLostCallback = std::function<void(
uint16_t masterIndex, uint16_t slaveIndex, int diffNs)>;

DC 同步丢失事件。用 OnDCSyncLost() 注册。

示例:

master.Events().OnDCSyncLost([](uint16_t mi, uint16_t si, int diff) {
printf("从站 %d DC 同步丢失: 偏差 %d ns\n", si, diff);
});

输入数据变化

InputDataChanged 事件已移除

InputDataChanged 事件靠 SDK 层每周期逐字节检测输入 PDO 变化。PDO 数据通路改为纯内核 RT 收发 + 内核共享内存指针零拷贝后,该机制不再驱动。

需要感知输入变化时,请自行轮询过程映像指针并比对上一周期快照

// 周期轮询输入 PDO 指针, 与上一周期快照比对检测变化
auto& slave = master.GetSlave(1);
auto [inPtr, inSize] = slave.Pdo().InputSpan();
std::vector<uint8_t> prev(inSize, 0);

while (running) {
if (inPtr && std::memcmp(inPtr, prev.data(), inSize) != 0) {
std::memcpy(prev.data(), inPtr, inSize);
printf("从站 1 输入数据变化\n");
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}

冗余事件

RedundancyModeChanged

using RedundancyModeChangedCallback = std::function<void(
uint16_t masterIndex, int oldMode, int newMode)>;

冗余运行模式变化时触发。模式值:0=Inactive, 1=Dual, 2=Degraded。用 OnRedundancyModeChanged() 注册。

示例:

master.Events().OnRedundancyModeChanged([](uint16_t mi, int oldM, int newM) {
const char* names[] = {"Inactive", "Dual", "Degraded"};
printf("冗余模式: %s -> %s\n", names[oldM], names[newM]);
});

SlavePortLinkChanged

using SlavePortLinkChangedCallback = std::function<void(
uint16_t masterIndex, uint16_t slaveIndex, uint8_t port, bool isUp)>;

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

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

回调参数:

  • masterIndex (uint16_t) — 主站索引
  • slaveIndex (uint16_t) — 从站索引(1-based)
  • port (uint8_t) — 端口号 0-3,对应 P0 / P1 / P2 / P3
  • isUp (bool) — true=link 恢复, false=link 断开

示例:

master.Events().OnSlavePortLinkChanged(
[](uint16_t mi, uint16_t si, uint8_t port, bool isUp) {
const char* state = isUp ? "恢复" : "断开";
printf("从站 %u 端口 P%u link %s\n", si, port, state);
});
配合断点定位

master.GetDiagnostics().GetBreakPoints() 提供聚合后的故障点视图(断线 + CRC 故障,返回 std::vector<BreakPoint>)。SlavePortLinkChanged 是原始事件源,适合做实时告警。详见 主站诊断 - 冗余状态

ClearAll()

void ClearAll();

清除所有事件订阅,防止内存泄漏。在销毁主站或重新初始化前调用。

示例:

// 清除主站所有事件
master.Events().ClearAll();
提示

ClearAll() 等价于 RemoveAll(),同时清除从站离线状态跟踪。建议在 Dispose() 前调用,确保回调中的捕获引用不会产生悬垂指针。

从站事件 (SlaveEvents)

SDK 另有独立的从站事件集合 SlaveEvents 类 (master/events.hpp),事件参数已自动过滤到当前从站。 同样用 On*() 方法注册 (多订阅者 std::vector<std::function>),并用 ClearAll() / RemoveAll() 清除:

事件注册方法回调签名
状态变化OnStateChanged(cb)void(EcState, EcState)
紧急消息OnEmergency(cb)void(uint16_t, uint16_t, uint8_t, uint16_t, uint16_t)
离线OnOffline(cb)void()
上线OnOnline(cb)void()
DC 同步丢失OnDCSyncLost(cb)void(int)
备注

Slave 类本身不暴露 Events() 访问器。SlaveEvents 由主站事件路由层内部通过 MasterEvents::RegisterSlaveEvents(slaveIndex, &events) 关联; 一般使用直接订阅主站级 master.Events().OnSlaveStateChanged(...) 等事件即可, 回调参数含 slaveIndex 可自行过滤。

线程安全

回调线程

所有事件回调在非 UI 线程上触发。在回调中访问共享数据时必须使用同步机制。

线程模型:

  • ProcessDataCyclicSync -- 在 PDO 实时线程中执行,不要执行任何耗时操作
  • 其他所有事件 -- 在事件分发线程中依次执行,不要长时间阻塞

注意事项:

  • 回调中不要调用 SetState()Stop() 等可能导致死锁的方法
  • 同步回调中不要进行 SDO 读写、文件 I/O 等阻塞操作
  • 使用 std::atomic 处理简单标志,使用 std::mutex 保护复杂数据

C++ 中建议使用 std::mutexstd::atomic 保护共享数据:

#include <mutex>
#include <atomic>

std::atomic<bool> g_slave_offline{false};

master.Events().OnSlaveLost([&](uint16_t si) {
g_slave_offline.store(true); // 原子操作
});

// 主线程中安全读取
if (g_slave_offline.load()) {
printf("有从站离线\n");
}

完整示例

#include "ethercat.hpp"
#include <cstdio>
#include <atomic>

using namespace darra;

int main() {
std::atomic<bool> running{true};

try {
EtherCATMaster master(dll);

// 注册所有回调 (一律用 On*() / SetPDOCallback*() 注册, 不是字段赋值)
auto& ev = master.Events();

// ===== 状态事件 =====
ev.OnStateChanged([](EcState oldS, EcState newS) {
printf("主站: %s -> %s\n",
EcStateFormat::Format(oldS).c_str(),
EcStateFormat::Format(newS).c_str());
});

ev.OnSlaveStateChanged([](uint16_t mi, uint16_t si,
EcState oldS, EcState newS) {
printf("从站 %d: %s -> %s\n", si,
EcStateFormat::Format(oldS).c_str(),
EcStateFormat::Format(newS).c_str());
});

// ===== 热插拔 =====
ev.OnSlaveDiscovered([](uint16_t si) {
printf("从站 %d 上线\n", si);
});

ev.OnSlaveLost([](uint16_t si) {
printf("从站 %d 离线\n", si);
});

// ===== 异常事件 =====
ev.OnEmergency([](uint16_t mi, uint16_t si, uint16_t code,
uint16_t reg, uint8_t b1, uint16_t w1, uint16_t w2) {
printf("从站 %d 紧急: 0x%04X\n", si, code);
});

ev.OnPDOFrameLoss([](uint16_t mi, uint8_t grp, uint32_t c, uint32_t t) {
printf("组 %d 丢帧: %u/%u\n", grp, c, t);
});

ev.OnDCSyncLost([](uint16_t mi, uint16_t si, int diff) {
printf("从站 %d DC 偏差: %dns\n", si, diff);
});

// ===== 冗余事件 =====
ev.OnRedundancyModeChanged([](uint16_t mi, int oldM, int newM) {
const char* names[] = {"Inactive", "Dual", "Degraded"};
printf("冗余模式: %s -> %s\n", names[oldM], names[newM]);
});

// ===== PDO 周期回调 (供 FSoE 等实时控制; PDO 数据通常直接轮询过程映像指针) =====
ev.SetPDOCallbackSync([&](uint16_t mi) {
if (!running.load()) return;
// PDO 数据处理
});

// 启动
master.SetNetwork("\\Device\\NPF_{...}");
master.Build();
master.SetState(EcState::OP);
master.Start();

printf("运行中... 按 Enter 停止\n");
getchar();
running.store(false);

} catch (const ethercat::DarraException& e) {
printf("错误: %s\n", e.what());
return -1;
}
return 0;
}