VoE (Vendor over EtherCAT)
VoE 协议用于厂商自定义数据传输,支持 ETG.1000.6 Mailbox 通信规范(Mailbox Type 0x0F)。每个厂商可以定义自己的 VoE 头格式和数据结构。
通过 slave.GetVoE() 获取 VoE& 引用。使用 IsSupported() 检查从站是否支持 VoE。
属性
| 属性 | 类型 | 访问 | 说明 |
|---|---|---|---|
| IsSupported() | bool | 只读 | 从站是否支持 VoE 协议 |
| GetMaxVoEDataSize() | int | 只读 | VoE 最大数据载荷大小 |
基本操作
Send()
bool Send(uint32_t vendorId, uint16_t vendorType,
const uint8_t* data, int dataLen, int timeoutMs = 500) const;
发送 VoE 数据到从站。
参数:
vendorId(uint32_t) — 厂商 ID (4 字节)vendorType(uint16_t) — 厂商类型 (2 字节)data(const uint8_t*) — 数据内容dataLen(int) — 数据长度timeoutMs(int) — 超时时间(毫秒)
返回值:
bool— 是否成功
示例:
auto& voe = master.GetSlave(1).GetVoE();
uint8_t cmd[] = {0x01, 0x00};
voe.Send(0x00000002, 0x0001, cmd, sizeof(cmd));
Receive()
std::optional<VoEResponse> Receive(int timeoutMs = 500) const;
从从站接收 VoE 数据。
返回值:
std::optional<VoEResponse>— VoE 响应对象,失败返回std::nullopt
相关结构:
struct VoEResponse {
uint32_t VendorId; // 厂商 ID
uint16_t VendorType; // 厂商数据类型
std::vector<uint8_t> Data; // 数据内容
int DataLength() const; // 数据长度
std::string ToHexString() const; // 格式化为十六进制字符串(如 "01 02 03")
std::string ToString() const; // 完整描述
};
示例:
auto& voe = master.GetSlave(1).GetVoE();
auto resp = voe.Receive();
if (resp) {
printf("厂商ID: 0x%08X, 类型: 0x%04X, 数据: %zu 字节\n",
resp->VendorId, resp->VendorType, resp->Data.size());
printf("十六进制: %s\n", resp->ToHexString().c_str());
}
SendAndReceive()
std::optional<VoEResponse> SendAndReceive(uint32_t vendorId, uint16_t vendorType,
const uint8_t* data, int dataLen,
int timeoutMs = 500) const;
std::optional<VoEResponse> SendAndReceive(uint32_t vendorId, uint16_t vendorType,
const std::vector<uint8_t>& data,
int timeoutMs = 500) const;
发送数据并等待响应。支持原始指针和 std::vector 两种版本。
示例:
auto& voe = master.GetSlave(1).GetVoE();
auto response = voe.SendAndReceive(0x00000002, 0x0001,
new uint8_t[]{0x10}, 1);
if (response) {
printf("厂商ID: 0x%08X\n", response->VendorId);
printf("数据: %s\n", response->ToHexString().c_str());
}
原始帧操作
SendRaw() / ReceiveRaw()
bool SendRaw(const uint8_t* frame, int frameLen, int timeoutMs = 500) const;
std::vector<uint8_t> ReceiveRaw(int timeoutMs = 500) const;
发送/接收原始 VoE 帧(包含 VoE 头部)。
SendRawAndReceive()
std::vector<uint8_t> SendRawAndReceive(const uint8_t* frame, int frameLen,
int timeoutMs = 500) const;
std::vector<uint8_t> SendRawAndReceive(const std::vector<uint8_t>& frame,
int timeoutMs = 500) const;
发送原始帧并等待响应。支持原始指针和 std::vector 两种版本。
返回值:
std::vector<uint8_t>— 响应的原始帧数据,失败返回空 vector
辅助方法
BuildVoEFrame()
static std::vector<uint8_t> BuildVoEFrame(uint32_t vendorId, uint16_t vendorType,
const uint8_t* data, int dataLen);
构建标准 VoE 帧(VoE 头 + 数据)。
ParseVoEFrame()
static std::optional<VoEResponse> ParseVoEFrame(const uint8_t* frame, int frameLen);
解析 VoE 帧头部。
返回值:
std::optional<VoEResponse>— 解析后的 VoE 响应对象,解析失败返回std::nullopt
VoE 头部信息
GetVoEHeader() / SetVoEHeader()
std::optional<VoEHeader> GetVoEHeader(int timeoutMs = 500) const;
bool SetVoEHeader(uint32_t vendorId, uint16_t vendorType, int timeoutMs = 500) const;
获取/设置 VoE 头部信息。GetVoEHeader() 从最近接收的帧中提取头部。SetVoEHeader() 发送空数据帧以设定头部。
相关结构:
struct VoEHeader {
uint32_t VendorId; // 厂商 ID
uint16_t VendorType; // 厂商类型
};
协议支持检测
IsSupported()
bool IsSupported() const;
检查从站是否支持 VoE 协议(通过邮箱协议位检测)。
GetMaxVoEDataSize()
int GetMaxVoEDataSize() const;
获取 VoE 最大数据载荷大小(基于邮箱写长度减去 VoE 头部和邮箱头部)。
VoENotificationManager
部分厂商设备会以 VoE 帧主动上报"事件 / 报警"(不走 SDO,不走 EMCY),订阅这种主动上报需要在 SDK 内部注册一个回调。VoENotificationManager 是 RAII 风格的多从站订阅管理器,对齐 C# VoENotificationManager / Python VoENotificationManager。
class VoENotificationManager {
public:
explicit VoENotificationManager(dll_t& dll, uint16_t masterIndex);
~VoENotificationManager(); // 自动注销所有订阅
using Callback = std::function<void(uint16_t slaveIndex,
uint32_t vendorId,
uint16_t vendorType,
const std::vector<uint8_t>& data)>;
/// 订阅指定从站的 VoE 主动上报。返回订阅 ID(< 0 表示失败)
int Subscribe(uint16_t slaveIndex, Callback cb);
/// 取消单个订阅
bool Unsubscribe(int subscriptionId);
/// 取消所有订阅
void UnsubscribeAll();
/// 当前活跃订阅数
int ActiveCount() const;
};
特点:
- RAII 注销:管理器析构时自动注销所有底层 hook,避免回调悬垂指针
- 多从站 / 多回调:每个从站可挂多个
std::function回调,按订阅顺序触发 - 后台线程:回调在 SDK 通知线程上执行,操作 UI 或共享状态需
std::mutex/std::atomic - Trampoline:内部用静态注册表把 C 风格
user_data指针路由回 C++ 实例,调用方无需关心
示例:
darra::ethercat::VoENotificationManager voeMgr(dll, master.MasterNumber());
int sub = voeMgr.Subscribe(1, [](uint16_t si, uint32_t vid, uint16_t vt,
const std::vector<uint8_t>& data) {
printf("[VoE] 从站 %u VID=0x%08X type=0x%04X (%zu B)\n",
si, vid, vt, data.size());
});
// ... 业务运行 ...
// 退出前 (可选)
voeMgr.Unsubscribe(sub);
// 或 voeMgr.UnsubscribeAll();
// 或直接让 voeMgr 离开作用域 (RAII)
Receive() 的区别Receive()是主动拉取:调用线程阻塞等待一帧,到了就返回VoENotificationManager是事件订阅:从站任何时候上报都触发回调,不阻塞业务线程
需要"事件驱动"时用 NotificationManager,需要"请求-应答"时用 SendAndReceive()。
完整示例
厂商命令交互
#include "ethercat.hpp"
using namespace darra;
int main() {
EtherCATMaster master(dll);
master.SetNetwork("\\Device\\NPF_{...}")
.SetENI("config.deni")
.Build();
master.SetState(EcState::OP);
master.Start();
auto& voe = master.GetSlave(1).GetVoE();
// 检查支持
if (!voe.IsSupported()) {
printf("从站不支持 VoE\n");
return 1;
}
printf("VoE 最大载荷: %d 字节\n", voe.GetMaxVoEDataSize());
// 发送命令并接收响应
uint8_t cmd[] = {0x01, 0x00};
auto resp = voe.SendAndReceive(0x00000002, 0x0001, cmd, sizeof(cmd));
if (resp)
printf("%s\n", resp->ToString().c_str());
getchar();
return 0;
}
批量数据传输
auto& voe = master.GetSlave(1).GetVoE();
// 发送大数据块
std::vector<uint8_t> payload(1024, 0);
voe.Send(0x00000002, 0x1000, payload.data(), static_cast<int>(payload.size()));
// 接收响应
auto reply = voe.Receive();
if (reply)
printf("收到 %d 字节响应\n", reply->DataLength());
NotificationManager 真实使用流程
上方 Subscribe(slaveIndex, cb) 是 C# / Python 的高层封装写法. C++ SDK 的实际接口分两步, 与 DLL 导出 dx_voe_start_notification_listener + dx_voe_register_notification 一致:
StartNotificationListener()
class VoENotificationManager {
public:
/// 启动 master 维度的监听线程, 并按 (slaveIndex / vendorId=0 / vendorType=0) 注册一条通配订阅
bool StartNotificationListener(uint16_t slaveIndex = 0);
/// 停止本实例的订阅 (析构自动调用)
void StopNotificationListener();
/// 添加事件监听器 (可重复调用注册多个)
void AddListener(VoENotificationCallback cb);
void ClearListeners();
size_t ListenerCount() const;
/// 整 master 监听线程级控制 (静态)
static bool IsListenerRunning(dll_t& dll);
static bool StopAllListeners(dll_t& dll);
};
StartNotificationListener(slaveIndex) — slaveIndex=0 表示通配 (任意从站 VoE 帧都触发), 否则只匹配指定从站. 内部会先启动 master 监听线程, 再调用 dx_voe_register_notification 注册一条通配 vendorId=0 / vendorType=0 的订阅, 失败返回 false (DLL 未导出 / 注册被拒).
AddListener(cb) — cb 签名 void(const VoENotificationEventArgs&), 同一 manager 可多次调用注册多个回调, 触发时按注册顺序依次执行 (单 handler 抛异常不影响后续 handler). 想动态启停某个回调, 在 lambda 内用 std::atomic_bool 自行过滤 — SDK 不提供按句柄移除单个 listener.
静态控制方法
IsListenerRunning(dll) 返回当前进程内 VoE 监听线程是否在跑 — DLL 侧的监听线程是进程级单例, 多个 master 共享一条线程, 不要每个 master 都自己 start.
StopAllListeners(dll) 强制停掉整条监听线程, 调用后所有 manager 的订阅都失效, 必须由调用方自行清理. 析构 VoENotificationManager 不会触发 StopAllListeners, 只 Unregister 自己那一条订阅.
真实示例
using darra::ethercat::VoENotificationManager;
using darra::ethercat::VoENotificationEventArgs;
VoENotificationManager voeMgr(dll, master.MasterNumber());
voeMgr.AddListener([](const VoENotificationEventArgs& ev) {
printf("[VoE] 从站=%u VID=0x%08X type=0x%04X 时间戳=%lld us, %zu B\n",
ev.SlaveIndex, ev.VendorId, ev.VendorType,
static_cast<long long>(ev.TimestampUs), ev.Data.size());
});
if (!voeMgr.StartNotificationListener(/*slaveIndex=*/0)) {
printf("VoE 监听启动失败 (DLL 未导出?)\n");
}
// ... 业务运行 ...
// 退出前
voeMgr.StopNotificationListener(); // 取消本实例订阅
// VoENotificationManager::StopAllListeners(dll); // 整条线程停掉, 仅在退出时用
DLL 侧监听线程是 master_index 维度单例 — 一个 master 内只跑一条线程, 多个 manager 都调 StartNotificationListener 不会启动多条线程, 但每个 manager 都会注册自己的订阅 (subscription_index 不同), 触发时按 subscription_index 路由回各自的 Dispatch. 业务上每个 master 维护一个 manager 是最干净的写法.
Subscribe(si, cb) 的关系C# / Python / Rust 把"订阅"包装成一个调用, 内部就是 Start + AddListener. C++ 把两步分开, 让你能区分"按从站过滤启动监听"和"叠加多个回调", 也方便诊断 DLL 导出缺失场景 (Start 失败 vs Listener 加成功但回调不触发). 高层 Subscribe() 的等价写法:
mgr.AddListener(cb);
mgr.StartNotificationListener(slaveIndex);