跳到主要内容

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); // 整条线程停掉, 仅在退出时用
不要在多个 manager 上重复 Start

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);