跳到主要内容

Rust 特有语法糖

本章列出 Darra EtherCAT Rust SDK 在 crate::sugar 模块下提供的 Rust 特有语法糖. 这些 API 完全可选, 不引入任何破坏性变更, 不影响向后兼容。 只要在文件顶部加一行:

use darra_ethercat::sugar::prelude::*;

就能解锁所有便利写法。 本章按主题组织, 每节给出 标准 API ↔ 语法糖 的对照, 并说明何时该用语法糖, 何时直接用主 API.

迭代器 (SlaveIterExt)​

标准 API​

主 SDK 已经为 [EtherCATMaster] 实现 IntoIterator, 可以直接 for s in &master:

let m = EtherCATMaster::new()?;
for slave in &m {
println!("{}", slave.name());
}

slaves() 方法也能把所有从站收成 Vec<Slave>:

let all: Vec<Slave> = m.slaves();

语法糖: 链式过滤​

加上 use darra_ethercat::sugar::prelude::*; 后, 任何 Iterator<Item = Slave> 都自动获得四个领域感知方法:

  • .by_state(EcState::Operational) — 按 EtherCAT 状态过滤
  • .by_group(0) — 按从站组过滤 (0..=7)
  • .by_vendor(0x00000002) — 按厂商 ID 过滤
  • .with_dc() — 仅保留 DC 同步从站
  • .filter_slave(|s| ...) — 任意自定义谓词, 签名比 Iterator::filter 明确
use darra_ethercat::sugar::prelude::*;
use darra_ethercat::EcState;

// 在 OP 状态的 Beckhoff 从站
let beckhoff_op: Vec<_> = m.iter()
.by_state(EcState::Operational)
.by_vendor(0x00000002)
.collect();

// 第 0 组里有 AL 错误的从站
let bad: Vec<_> = m.iter()
.by_group(0)
.filter_slave(|s| {
s.detailed_info()
.map(|d| d.al_status_code != 0)
.unwrap_or(false)
})
.collect();

何时用?​

  • 链式过滤多于 1 步 → 用语法糖, 一行成型
  • 只过滤一次 → 直接 Iterator::filter 也行, 习惯哪种用哪种
  • 微秒级 PDO 回调内 → 不要用迭代器, 直接 for i in 1..=count
    • master.slave(i) 避免任何中间 trait 解糖开销

索引 (Indexable)​

标准 API​

主 SDK 用 slave(i) 拿从站句柄:

let s1 = m.slave(1);
let s2 = m.slave(2);

语法糖: master.indexable()[i]​

由于 Slave 是 Copy 句柄 (不持有资源, 仅是 (master_idx, slave_idx) 二元组), SDK 可以为它提供 Index 风格的访问:

use darra_ethercat::sugar::prelude::*;

let view = m.indexable();
let s1 = &view[1]; // 等价 m.slave(1)
let s2 = &view[2u16]; // 同时支持 usize 和 u16 索引
println!("{}", view.len());

// 越界会 panic, 想要安全访问请用下文的 try_slave

实现说明​

Indexable<'a> 内部缓存了 1..=N 的从站句柄, 与原 &'a EtherCATMaster 共享生命周期。 不缓存到主 EtherCATMaster 是为了避免破坏现有 &self 不可变方法的调用模式 (否则要全改 &mut self).

何时用?​

  • 演示/教学/REPL → 用 view[i] 简洁直观
  • 生产代码 → 推荐 try_slave(i)? (见 §4), 越界变错误而不是 panic
  • PDO 紧循环 → 仍然是 m.slave(i) 直调最快

RAII (Drop / IomapGuard)​

已经实现​

主 SDK 自带的 RAII 不需要 sugar 模块, 列出供参考:

{
let m = EtherCATMaster::new()?;
m.set_network("\\Device\\NPF_{...}", "")?;
// ... 使用 ...
} // ← 此处 Drop 自动 Stop + Dispose, 无须手动调用

IomapGuard 同样是 RAII 包装:

{
let _guard = m.iomap_guard(); // 加锁
// ... 安全读写 IO ...
} // ← guard drop 时自动解锁

注意​

  • 不要 mem::forget(master) — 会泄漏 DLL 资源
  • MasterBuilder::build() 返回的 BuildResult 实现 Deref<Target = EtherCATMaster>, 也享受 RAII

sugar 模块未触碰的部分​

Drop 由主类型保证, sugar 不重复实现。 IomapGuard 同理。

错误传播 (? 友好)​

标准 API​

let s = m.slave(1);   // 永不失败 (越界拿到的是悬空句柄)
let count = m.slave_count();
if 1 > count {
return Err(/* 自己拼错误 */);
}

语法糖: try_slave / slave_opt​

use darra_ethercat::sugar::prelude::*;
use darra_ethercat::Result;

fn use_first(m: &EtherCATMaster) -> Result<()> {
let s = m.try_slave(1)?; // 越界自动返回 Err
println!("vendor: 0x{:08X}", s.vendor_id());
Ok(())
}

// 或选择 Option 路径
if let Some(s) = m.slave_opt(1) {
println!("{}", s);
}

状态切换的 Result 包装​

主 API set_state 要求 &mut self, 不利于在共享场景下直接 ?:

// 标准 API (需要 &mut)
master.set_state(EcState::Operational)?;

sugar 版用 &self 直接走 FFI, 配合 ?:

use darra_ethercat::sugar::prelude::*;

fn ramp_up(m: &EtherCATMaster) -> Result<()> {
m.try_set_state(EcState::PreOp, 5000)?;
m.try_set_state(EcState::SafeOp, 5000)?;
m.try_set_state(EcState::Operational, 5000)?;
Ok(())
}

注意​

try_set_state 不会自动启动 PDO 线程 (副作用只在 &mut self 版的 set_state 中触发). 如果要进 SafeOp/OP 还想要 PDO 跑起来, 用主 API:

master.set_state(EcState::Operational)?;   // 自动调用 Start()

状态变化通道 (mpsc::channel)​

标准 API: 回调​

let m = EtherCATMaster::new()?;
let events = m.events();
events.on_slave_state_changed(|master, slave, old, new| {
println!("slave {}: {} -> {}", slave, old, new);
});

回调写法的局限:

  • 多线程消费要自己 Arc<Mutex<...>>
  • 想做事件历史回放 / 测试断言, 要自己缓存
  • 跨 await 边界 (异步代码) 难处理

语法糖: state_stream() 返回 Receiver​

use darra_ethercat::sugar::prelude::*;

let m = EtherCATMaster::new()?;
let rx = m.state_stream(); // mpsc::Receiver, 容量 1024

std::thread::spawn(move || {
for ev in rx {
println!("从站 {}: {} -> {}", ev.slave, ev.old_state, ev.new_state);
}
});

设计要点​

  • 容量 1024 (可配置), 满时丢弃最新事件 (try_send), 不阻塞实时线程
  • MasterStateStream 实现 IntoIterator, 直接 for ev in rx
  • 也能 rx.recv() 阻塞 / rx.try_recv() 非阻塞
  • 多次调用 state_stream() 返回独立的 channel, 各自接收一份事件

紧急事件 (EMCY) 同款​

let emcy = m.emergency_stream();
for ev in emcy {
println!("EMCY {}: code=0x{:04X} reg=0x{:02X}",
ev.slave, ev.error_code, ev.error_register);
}

EMCY 默认 unbounded — 这类事件量级低, 不会触发实时线程压力。

异步流 (tokio, 仅 async-tokio feature)​

启用方式​

Cargo.toml:

[dependencies]
darra-ethercat-master = { path = "../darra-ethercat-master-2.11.0", features = ["async-tokio"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync"] }

注意 tokio 必须开 sync feature (async-tokio 内部用 tokio::sync::mpsc; crate 已在 tokio 依赖中声明 sync,无需用户额外配置 crate 侧 feature)。

用法​

use darra_ethercat::sugar::prelude::*;
use darra_ethercat::EtherCATMaster;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let m = EtherCATMaster::new()?;
let mut stream = m.state_stream_async();

while let Some(ev) = stream.recv().await {
println!("从站 {}: {} -> {}", ev.slave, ev.old_state, ev.new_state);
}
Ok(())
}

与同步版的关系​

  • state_stream() (同步) 和 state_stream_async() (异步) 互不影响, 可同时用
  • 异步版基于 tokio::sync::mpsc::unbounded_channel, 满时不会丢事件 (但会占内存)
  • 如果希望对接 futures::Stream trait, 用 into_inner() 拿到 UnboundedReceiver, 再 tokio_stream::wrappers::UnboundedReceiverStream::new(rx) 即可

何时用?​

  • 整个程序已经是 tokio runtime → 用异步版本, 不再起新线程
  • 只有事件消费需要异步 → 同步 state_stream() + tokio spawn_blocking 也能凑合, 但不优雅
  • 不用 tokio → 只用同步版, 不要启用 async-tokio feature, 减少依赖

derive 派生 / 类型转换​

已有派生​

主 SDK 给绝大部分数据结构都派生了 Clone, Debug, Copy, PartialEq, Eq, Default. 如 SlaveIdentity / EsmTimeouts / RedundancyStatus / CommunicationStats 都是 #[derive(Debug, Clone, Copy)].

sugar 增补的 From 转换​

  • &Slave → SlaveIdentity — let id: SlaveIdentity = (&slave).into(); 一行提取身份
  • Slave → SlaveIdentity — 同上, owned 版本
  • u8 → EcState — 从 raw 字节直拿枚举
  • EcState → u8 / i32 — 写回 DLL 接口
  • u8 → LinkState — raw 字节直拿枚举
  • i32 → RedundancyState — DLL 返回值直拿枚举
  • i32 → CiA402State — 同上
  • EcState / LinkState / RedundancyState → &'static str — 日志/格式化
use darra_ethercat::sugar::prelude::*;

let s = m.slave(1);

// 一行提取身份, 用于配置比对
let id: SlaveIdentity = (&s).into();
println!("{}", id); // Vendor=0x00000002 Product=0x044C2C52 Rev=...

// raw -> 枚举 (例如解析 PDO 帧字节)
let st: EcState = 0x08u8.into();
assert_eq!(st, EcState::Operational);

// 枚举 -> 静态字符串 (无堆分配)
let label: &'static str = EcState::PreOp.into();
assert_eq!(label, "PreOp");

何时用?​

  • 写日志 / metrics 标签 → &'static str 转换零成本, 优于 format!
  • DLL 边界转换 → EcState::from(raw_byte) 比 EcState::from_raw(raw_byte).unwrap_or(...) 短

Display 增强​

sugar 补齐的 Display​

主 SDK 已为 EcState / DarraError / EcPortType 等实现 Display. sugar 补齐:

  • LinkState — Connected / Redundancy
  • RedundancyState — PrimaryOnly / Both
  • CiA402State — OperationEnabled / Fault
  • CiA402Mode — CSP / PP
  • EcALState — NoError / InvalidStateChange / AL(0x00XX) 兜底
  • SlaveIdentity — Vendor=0x... Product=0x... Rev=0x... Serial=0x...

用法​

use darra_ethercat::sugar::prelude::*;

use darra_ethercat::data::error::CiA402State;
use darra_ethercat::data::types::EcALState;

println!("链路: {}", m.link_status()); // 链路: Connected
println!("驱动: {}", CiA402State::OperationEnabled); // 驱动: OperationEnabled
println!("ALSC: {}", EcALState::SyncError); // ALSC: SyncError

与 Debug 的区别​

  • Debug ({:?}) — 输出风格: 完整字段, 适合排错;用于单元测试断言 / 打印整个结构
  • Display ({}) — 输出风格: 简短可读, 适合最终用户;用于日志 / GUI / 报警消息

Builder 模式​

已有 Builder​

主 SDK 已经实现了 MasterBuilder, sugar 模块不重复造:

let result = EtherCATMaster::builder()
.set_eni("config.xml")
.enable_auto_startup()
.build()?;

println!("从站数量: {}", result.slave_count());

BuildResult 实现 Deref<Target = EtherCATMaster>,所以只读方法可以直接沿用; 但它没有 DerefMut — 需要 &mut self 的方法 (set_state / start / stop / dispose) 必须先用 .master 取出 owned EtherCATMaster 并 let mut, 见下方完整示例。

sugar 模块未触碰​

我们认为现有 builder 已经覆盖大部分场景, 不必重新造一个 sugar 版本。 若需要把 builder 链式继续拓展 (例如 with_redundancy(...) / with_callback(...) 等), 应该在主 master/core.rs 里加方法, 而不是 sugar.

完整示例​

use darra_ethercat::sugar::prelude::*;
use darra_ethercat::{EtherCATMaster, EcState, Result};

fn main() -> Result<()> {
let mut m = EtherCATMaster::builder()
.set_eni("config.xml")
.enable_auto_startup()
.build()?
.master;

// 启动状态流 (后台线程消费)
let rx = m.state_stream();
std::thread::spawn(move || {
for ev in rx {
log::info!("[state] slave {} {} -> {}", ev.slave, ev.old_state, ev.new_state);
}
});

// 进 OP
m.set_state(EcState::Operational)?;

// 索引 + 错误传播
let s = m.try_slave(1)?;
println!("{}", s);

// 链式过滤: 列出所有 OP 状态的 Beckhoff 从站
let beckhoff_op: Vec<_> = m.iter()
.by_state(EcState::Operational)
.by_vendor(0x00000002)
.collect();
println!("Beckhoff OP 从站数: {}", beckhoff_op.len());

// RAII 自动 Stop + Dispose
Ok(())
}

附录: 模块文件索引​

文件职责
src/sugar/mod.rs模块入口, re-export 所有子模块
src/sugar/prelude.rs一行 use 引入全部语法糖
src/sugar/index.rsIndexable + MasterIndexExt
src/sugar/conversions.rs跨类型 From / Into
src/sugar/display.rsLinkState / RedundancyState / CiA402State 等 Display 补齐
src/sugar/state_stream.rsmpsc 状态流 + 紧急流
src/sugar/iter_ext.rsSlaveIterExt (by_state / by_group / by_vendor / with_dc)
src/sugar/try_slave.rstry_slave / slave_opt / try_set_state
src/sugar/async_stream.rstokio 异步流 (async-tokio feature)

附录: 与手写循环的差异​

本章 sugar 模块建立在 Iterator + adapter / Drop / mpsc::channel 上。

Rust 的语法糖建立在 零成本抽象 上 — 上述所有 adapter 在编译期都被 inline 展开, 运行期开销与手写循环等价, 不引入任何额外指令。