事件与回调
通过 master.Events() 获取 MasterEvents& 引用,用 On*() 注册方法添加 std::function 回调。
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 Emergency | Events().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, 不应直接操作。
临时启动一段周期通信时, 可用 darra::sugar::ScopedStart guard(master); RAII 守护, 离开作用域自动 Stop(). 详见 RAII 作用域守护.
所有事件(PDO 周期回调除外)触发时系统均自动记录日志,无论是否订阅。确保关键运行状态不会被静默丢失。
PDO 周期回调(ProcessDataCyclicSync)为高频回调,不记录日志。
功能概览
| 类别 | 事件字段 | 说明 | 自动日志 |
|---|---|---|---|
| PDO 周期回调 | ProcessDataCyclicSync | PDO 同步周期回调(供 FSoE 等实时控制) | -- |
| 状态事件 | StateChanged | 主站 EtherCAT 状态变化 | yes |
| SlaveStateChanged | 从站 EtherCAT 状态变化 | yes | |
| 热插拔事件 | SlaveOffline | 从站离线(断开) | yes |
| SlaveOnline | 从站上线(恢复) | yes | |
| 热插拔事件 | SlaveIdentityMismatch | 从站身份不符(换成错误型号/低版本) | yes |
| 异常事件 | EmergencyEvent | CoE Emergency 紧急消息 | yes |
| PDOFrameLoss | PDO 连续丢帧 | yes | |
| DCSyncLost | DC 同步丢失 | yes | |
| 冗余事件 | RedundancyModeChanged | 冗余模式变化 | yes |
| SlavePortLinkChanged | 从站端口 link 变化(P0-P3 断开/恢复) | yes |
PDO 周期回调
ProcessDataCyclicSync
using ProcessDataCyclicCallback = std::function<void(uint16_t masterIndex)>;
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) — 配置期望的厂商 IDexpectedProduct(uint32_t) — 配置期望的产品代码expectedRevision(uint32_t) — 配置期望的最低修订号actualVendor(uint32_t) — 当前实际厂商 IDactualProduct(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)
返回值:
bool—true表示已接受并复位 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 事件靠 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 / P3isUp(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::mutex 或 std::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;
}