跳到主要内容

C++ 特有语法糖

C++ SDK 在 C++17/C++20 标准之上,提供了一组完全可选的"语法糖"组件,集中放在 include/sugar/ 子目录、命名空间 darra::sugar 下。它们不修改主 SDK 任何头文件,完全是 free function / 独立类,按需引入即可。

设计原则

  1. 不修改主 SDK: 所有 sugar 都是 free function 或独立类,主 hpp 字节不变。
  2. 零强制依赖: 不引入新第三方库,仅用 STL(<future> <thread> <optional> <tuple> <string_view> <concepts>)。
  3. C++17 兼容: 默认 C++17 可用,C++20 特性(concepts)通过宏开关条件编译。
  4. 按需引入: 一文件一主题,也可一行 #include "sugar/sugar.hpp" 全收。
  5. 不破坏现有 API: 所有原有 PascalCase 属性方法保留不变。

一行引入全部

#include "ethercat.hpp"
#include "sugar/sugar.hpp"

using namespace darra::ethercat;
using namespace darra::sugar;

也可以按主题单独引入:

#include "sugar/scoped.hpp"      // 仅 RAII
#include "sugar/iter.hpp" // 仅 range-based for / STL 算法
#include "sugar/snapshot.hpp" // 仅快照视图

模块概览

头文件主题关键符号
sugar/scoped.hppRAII 守护ScopedStart / ScopedState / ScopedLockIO / MakeMaster()
sugar/iter.hpp迭代视图SlavesView / slaves(master)
sugar/index.hpp索引访问at() / slave_opt() / find_by_id() / find_by_name() / idx()
sugar/snapshot.hpp数据快照SlaveSnapshot / slaves_snapshot() / master_snapshot()
sugar/async.hpp异步操作StartAsync() / StopAsync() / BuildAsync() / WaitForState()
sugar/tuple.hpp结构化绑定identity_tuple() / pdo_size() / wkc_stats()
sugar/string_view.hppstring_view 入参set_network() / set_eni()
sugar/concepts.hppC++20 类型约束SlaveLike / MasterLike

RAII 作用域守护 (scoped.hpp)

现状

EtherCATMaster 自身已实现 RAII:析构函数会自动调用 Dispose(),依次 Stop()dll_.Dispose() → 释放从站缓存。普通用户已经不需要手动调用 Stop/Dispose

{
EtherCATMaster master(dll);
master.SetNetwork("\\Device\\NPF_{...}").Build();
master.SetState(EcState::OP);
master.Start();
// ... 周期通信 ...
} // 离开作用域: 析构 → Dispose() → Stop() → dll_.Dispose()

ScopedStart:进入 Start, 离开 Stop

适合"临时启动一段周期通信"的场景:

master.Build();
master.SetState(EcState::OP);
{
darra::sugar::ScopedStart guard(master);
if (!guard) {
std::cerr << "Start 失败\n";
return -1;
}
// 周期通信进行中
std::this_thread::sleep_for(std::chrono::seconds(5));
}
// 自动 Stop()

如果不希望离开作用域时 Stop(例如转交给另一个对象),调用 release()

ScopedStart guard(master);
if (!guard) return;
guard.release(); // 放弃守护, 之后由用户负责 Stop

ScopedState:进入切换状态, 离开恢复

master.SetState(EcState::PreOp);

{
darra::sugar::ScopedState op(master, EcState::OP);
if (!op) {
std::cerr << "无法切到 OP\n";
return;
}
// 临时 OP, 做一段事
}
// 离开作用域自动切回 PreOp(即 ScopedState 构造时记录的 old_)

ScopedLockIO:IOmap 互斥锁守护

MutexProtection=false 的高性能模式下,用户必须手动 LockIOmap/UnlockIOmap 配对。ScopedLockIO 替代手写:

master.MutexProtection(false);   // 关闭自动锁

void HighFreqCallback(uint16_t mi) {
darra::sugar::ScopedLockIO lk(master);
auto* in = master.GetSlave(1).InputDataPointer();
auto* out = master.GetSlave(1).OutputDataPointer();
// 临界区 - 离开自动 Unlock
}

MakeMaster:一行工厂

避免 Builder + Validate + Build 三步重复样板:

try {
auto master = darra::sugar::MakeMaster(dll, "\\Device\\NPF_{...}");
master.SetState(EcState::OP);
master.Start();
} catch (const darra::ethercat::DarraException& e) {
std::cerr << "MakeMaster 失败: " << e.what() << "\n";
}

冗余模式:

auto master = darra::sugar::MakeMaster(dll,
"\\Device\\NPF_{primary}",
"\\Device\\NPF_{redundant}");

range-based for / STL 算法 (iter.hpp)

内置迭代器

EtherCATMaster::SlaveIterator 已经支持 range-based for:

for (auto s : master) {
std::cout << s.SlaveNum() << " " << s.Name() << "\n";
}

注意 *it 返回 Slave 按值(轻量句柄)。

SlavesView 视图

sugar::SlavesView 提供更明确的视图语义,适合与 STL 算法配合:

#include <algorithm>

auto v = darra::sugar::slaves(master);

// 计算支持 DC 的从站数量
auto dc_count = std::count_if(v.begin(), v.end(),
[](Slave& s) { return s.HasDC(); });

// 查找首个丢失的从站
auto it = std::find_if(v.begin(), v.end(),
[](Slave& s) { return s.IsLost(); });
if (it != v.end()) {
std::cout << "丢失从站: " << it->Name() << " idx=" << it.index() << "\n";
}

// 视图大小
std::cout << "size=" << v.size() << " empty=" << v.empty() << "\n";

SlavesView::iterator 实现了 input_iterator 要求:operator* / operator-> / operator++ / operator== / operator!=,并提供 STL 期望的 value_type / iterator_category 等 typedef。


索引访问 (index.hpp)

越界检查

EtherCATMaster::GetSlave(idx) 是 lazy-create 的,越界时行为取决于底层。sugar::at() 提供显式越界检查:

try {
auto& s = darra::sugar::at(master, 99); // 越界抛 DarraException
} catch (const darra::ethercat::DarraException& e) {
std::cerr << e.what() << "\n";
// sugar::at 越界: idx=99 当前从站数=8
}

std::optional null safety

if (auto opt = darra::sugar::slave_opt(master, 1)) {
Slave& s = opt->get();
std::cout << s.Name() << "\n";
} else {
std::cout << "从站不存在\n";
}

按 ID 或名称查找

// 按 vendor:product 查找首个匹配
auto found = darra::sugar::find_by_id(master, 0x000000ABu, 0x12345678u);
if (found) std::cout << "在槽位 " << found->get().Index() << "\n";

// 按名称查找
auto el2008 = darra::sugar::find_by_name(master, "EL2008");
if (el2008) {
el2008->get().GetCoE(); // 链式调用
}

简短调用糖

auto& s = darra::sugar::idx(master, 1);   // 等价 master.GetSlave(1)

数据快照 (snapshot.hpp)

Slave 类持有 dll_t& 引用,不可拷贝/不可序列化。当用户需要:

  • 把当前从站状态发到 HMI / 写入日志
  • 在另一个线程异步处理 (避免持有 dll_t)
  • 单元测试断言

需要先"快照成纯数据"。

SlaveSnapshot POD

struct SlaveSnapshot {
uint16_t index;
std::string name;
uint32_t vendor_id;
uint32_t product_id;
uint32_t revision;
uint32_t serial_number;
uint16_t config_addr;
uint16_t alias_addr;
uint16_t input_bits;
uint16_t output_bits;
bool has_dc;
bool is_lost;
EcState state;
};
auto snaps = darra::sugar::slaves_snapshot(master);
for (auto& s : snaps) {
std::cout << "[" << s.index << "] " << s.name
<< " VID=0x" << std::hex << s.vendor_id
<< " PID=0x" << s.product_id
<< " state=" << static_cast<int>(s.state) << "\n";
}

// 跨线程传递 - 拷贝/move 安全
std::async(std::launch::async, [snaps]() {
upload_to_hmi(snaps);
});

MasterSnapshot

auto m = darra::sugar::master_snapshot(master);
log("WKC=%u/%u err_cnt=%u dc=%lld",
m.wkc, m.expected_wkc, m.error_cnt, (long long)m.dc_time_ns);

std::future 异步 (async.hpp)

EtherCATMaster::SetStateAsync 已内置返回 std::future<bool>

auto fut = master.SetStateAsync(EcState::OP, 10000);
// ... 同时做其他工作 ...
bool ok = fut.get(); // 阻塞等待

sugar/async.hpp 补充更多异步包装。

StartAsync / StopAsync / BuildAsync

auto fut = darra::sugar::BuildAsync(master);
// UI 主线程继续刷新,避免阻塞
while (fut.wait_for(std::chrono::milliseconds(50)) != std::future_status::ready) {
pump_ui_messages();
}
bool ok = fut.get();

SlaveSetStateAsync:单从站异步切状态

auto& s = master.GetSlave(1);
auto fut = darra::sugar::SlaveSetStateAsync(s, EcState::SafeOp, 3000);
if (!fut.get()) std::cerr << s.Name() << " 切 SafeOp 失败\n";

WaitForState:被动轮询等待

适合"由其他线程发起切状态,本线程被动等待"的场景:

auto fut = darra::sugar::WaitForState(master, EcState::OP, 5000, 50);
if (fut.get()) std::cout << "已进入 OP\n";
else std::cerr << "超时\n";

structured binding (tuple.hpp)

C++17 结构化绑定让多返回值更清晰:

// 从站身份
auto [vendor, product, rev] = darra::sugar::identity_tuple(slave);

// PDO 尺寸
auto [in_bytes, out_bytes] = darra::sugar::pdo_size(slave);

// PDO 偏移 (字节, 起始位)
auto [io, ib, oo, ob] = darra::sugar::pdo_offsets(slave);

// 主站 WKC 三件套
auto [wkc, ewkc, errs] = darra::sugar::wkc_stats(master);

// 冗余 WKC
auto [pri_wkc, sec_wkc] = darra::sugar::redundancy_wkc(master);

// 主站状态三件套
auto [st, al_err, link] = darra::sugar::state_tuple(master);

写日志/格式化时不用再频繁 master.WKC() master.ExpectedWKC() 串调一长串。


std::string_view 入参 (string_view.hpp)

SetNetwork / SetENI 原型接收 const std::string&。如果用户已经持有 std::string_view(例如配置解析得到子串),需要构造临时 std::string,写起来啰嗦。

using namespace std::string_view_literals;

darra::sugar::set_network(master, "\\Device\\NPF_{primary}"sv);
darra::sugar::set_eni(master, "C:/cfg/site.eni"sv);

Tradeoff:DLL 边界依然要 c_str() 终止符,所以 sugar 内部仍然 std::string(view) 一次拷贝;价值在调用风格统一,调用者无需手动转换。


C++20 Concepts (concepts.hpp)

__cplusplus >= 202002L 时启用,C++17 模式下 concepts.hpp 体内是空文件,不报错。

提供"鸭子类型"约束,方便用户写适配 mock / 测试桩:

#include "sugar/concepts.hpp"

template<darra::sugar::SlaveLike S>
void log_slave(const S& s) {
std::cout << s.Name() << " VID=" << s.VendorId() << "\n";
}

// 真实从站
log_slave(master.GetSlave(1));

// 用户自定义 mock
struct FakeSlave {
std::string Name() const { return "fake"; }
uint32_t VendorId() const { return 0x42; }
uint32_t ProductId() const { return 0x1234; }
};
log_slave(FakeSlave{});

SlaveLike 要求类型暴露 Name() VendorId() ProductId() 三个方法(返回值可转 std::string / uint32_t)。MasterLike 类似要求 SlaveCount() / Build()


constexpr 枚举常量

EcState / EcALState / EcLinkState 是普通枚举,自带 constexpr 语义,无需额外语法糖:

constexpr EcState target = EcState::OP;
static_assert(target == EcState::OP);

如果需要 switch 全枚举,建议 #include <type_traits> 配合 static_cast<int> 显式转:

switch (master.State()) {
case EcState::Init: ...; break;
case EcState::PreOp: ...; break;
case EcState::SafeOp: ...; break;
case EcState::OP: ...; break;
default: ...;
}

std::function 回调 (现状)

C++ SDK 已经全面采用 std::function 作为回调载体, 通过 On*() / SetPDOCallback*() 注册:

master.Events().SetPDOCallbackSync([&](uint16_t mi) {
// Lambda 捕获 - 完全 OK
auto& s = master.GetSlave(1);
/* ... */
});

master.OnStateChanged([](EcState old_st, EcState new_st) {
std::cout << "state " << (int)old_st << " -> " << (int)new_st << "\n";
});

MasterEvents 每个事件是 多订阅者std::vector<std::function<...>>, 用 On*() / SetPDOCallbackSync() 注册 (可注册多个回调), 不能用 = 直接赋值。 回调可以是 Lambda、std::bind 结果、函数指针、Functor。这是已有特性,sugar 中无重复包装。


完整示例:所有糖组合用

#include "ethercat.hpp"
#include "sugar/sugar.hpp"

#include <algorithm>
#include <iostream>

using namespace darra::ethercat;
using namespace darra::sugar;

int main() {
dll_t dll;
if (!load_darra_dll(dll, "Darra.Core.dll")) return -1;

try {
// 1. 一行工厂 + RAII
auto master = MakeMaster(dll, "\\Device\\NPF_{...}");

// 2. structured binding 取状态
auto [st, err, link] = state_tuple(master);
std::cout << "init state=" << (int)st << "\n";

// 3. 异步切 OP
auto fut_op = master.SetStateAsync(EcState::OP, 10000);
if (!fut_op.get()) {
std::cerr << "OP 失败\n";
return -2;
}

// 4. 作用域 Start
ScopedStart run(master);
if (!run) return -3;

// 5. STL 找 DC 从站
auto v = slaves(master);
auto dc_count = std::count_if(v.begin(), v.end(),
[](Slave& s) { return s.HasDC(); });
std::cout << dc_count << " 个 DC 从站\n";

// 6. 快照上报
auto snaps = slaves_snapshot(master);
std::async(std::launch::async, [snaps]() {
// 在 worker 线程把快照发给 HMI
upload(snaps);
});

// 7. 周期回调
master.Events().SetPDOCallbackSync([&master](uint16_t mi) {
auto& s = master.GetSlave(1);
(void)s.InputDataPointer();
});

std::this_thread::sleep_for(std::chrono::seconds(10));

} catch (const DarraException& e) {
std::cerr << "异常: " << e.what() << " code=" << e.code() << "\n";
return -1;
}
// master 析构 → Stop → Dispose
return 0;
}

不在 sugar 中的内容

为避免误解,以下不属于 sugar,由主 SDK 直接提供:

  • 移动语义(EtherCATMaster(EtherCATMaster&&)):主类已实现
  • 拷贝禁止:主类已 = delete
  • std::optional<EtherCATMaster> 工厂:InitializeSpecificMaster 已返回
  • range-based for 基础版:master.begin()/end() 已可直接用
  • 异常类:DarraExceptionutils/dll.hpp
  • 自动 Dispose:~EtherCATMaster 已实现

sugar 是附加层,所有原生用法保持完整有效。


编译要求

头文件最低标准
scoped.hpp / iter.hpp / index.hpp / snapshot.hpp / tuple.hppC++17
async.hppC++17 (<future> <thread> <chrono>)
string_view.hppC++17 (<string_view>)
concepts.hppC++20 (__cplusplus >= 202002L) — C++17 下退化为空文件
sugar.hpp (伞形)C++17 起,自动跳过 concepts

CMake 示例:

target_compile_features(my_app PRIVATE cxx_std_17)
# 如需 concepts:
# target_compile_features(my_app PRIVATE cxx_std_20)
target_include_directories(my_app PRIVATE ${SDK_PATH}/include)

引用关系图

ethercat.hpp                  (主 SDK 伞形, 原状态)
└── master/core.hpp (EtherCATMaster - 已 RAII / 已 begin-end / 已 SetStateAsync)
└── slave/core.hpp (Slave - 已 PascalCase 属性)

│ (sugar 是叠加层, 不修改任何上述文件)

sugar/sugar.hpp (sugar 伞形)
├── scoped.hpp ──┐
├── iter.hpp ──┤
├── index.hpp ──┼─ 仅 #include 主 SDK 头文件 + STL
├── snapshot.hpp ──┤
├── async.hpp ──┤
├── tuple.hpp ──┤
├── string_view.hpp ──┤
└── concepts.hpp ──┘ (条件编译)

常见问答

Q: sugar 是否影响二进制 ABI? A: 不影响。所有 sugar 是 inline free function / class template,不导出符号到 DLL。

Q: 不引 sugar 是否完整可用? A: 完整可用。ethercat.hpp 单独引就是全功能 SDK。sugar 只是写法层的便利。

Q: 跨编译器兼容性? A: MSVC 19.20+ / GCC 9+ / Clang 10+ 的 C++17 模式下均测试通过;C++20 concepts 部分需要 MSVC 19.30+ / GCC 10+ / Clang 13+。

Q: 与 C# / Java SDK 的对应关系? A: sugar 是 C++ 独有特性。其他语言 SDK 通过本身语言机制提供等价能力(C# using / Java try-with-resources / Python with / Rust Drop / Go defer)。