跳到主要内容

Rust 特有语法糖

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

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

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


1. 迭代器 (SlaveIterExt)

1.1 标准 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();

1.2 语法糖: 链式过滤

加上 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.3 何时用?

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

2. 索引 (Indexable)

2.1 标准 API

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

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

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

由于 SlaveCopy 句柄 (不持有资源, 仅是 (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

2.3 实现说明

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

2.4 何时用?

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

3. RAII (Drop / IomapGuard)

3.1 已经实现

主 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 时自动解锁

3.2 注意

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

3.3 sugar 模块未触碰的部分

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


4. 错误传播 (? 友好)

4.1 标准 API

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

4.2 语法糖: 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);
}

4.3 状态切换的 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(())
}

4.4 注意

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

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

5. 状态变化通道 (mpsc::channel)

5.1 标准 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 边界 (异步代码) 难处理

5.2 语法糖: 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);
}
});

5.3 设计要点

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

5.4 紧急事件 (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 — 这类事件量级低, 不会触发实时线程压力.


6. 异步流 (tokio, 仅 async-tokio feature)

6.1 启用方式

Cargo.toml:

[dependencies]
darra-ethercat-master = { version = "2.5", 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)。

6.2 用法

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(())
}

6.3 与同步版的关系

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

6.4 何时用?

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

7. derive 派生 / 类型转换

7.1 已有派生

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

7.2 sugar 增补的 From 转换

源类型目标类型用途
&SlaveSlaveIdentitylet id: SlaveIdentity = (&slave).into(); 一键提身份
SlaveSlaveIdentity同上, owned 版本
u8EcState从 raw 字节直拿枚举
EcStateu8 / i32写回 DLL 接口
u8LinkStateraw 字节直拿枚举
i32RedundancyStateDLL 返回值直拿枚举
i32CiA402State同上
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");

7.3 何时用?

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

8. Display 增强

8.1 sugar 补齐的 Display

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

类型输出例
LinkStateConnected / Redundancy
RedundancyStatePrimaryOnly / Both
CiA402StateOperationEnabled / Fault
CiA402ModeCSP / PP
EcALStateNoError / InvalidStateChange / AL(0x00XX) 兜底
SlaveIdentityVendor=0x... Product=0x... Rev=0x... Serial=0x...

8.2 用法

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

8.3 与 Debug 的区别

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

9. Builder 模式

9.1 已有 Builder

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

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

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

BuildResult 实现 Deref<Target = EtherCATMaster>, 直接当 master 用.

9.2 sugar 模块未触碰

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


10. 完整示例: 综合用法

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 / CiA402StateDisplay 补齐
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)

附录: 与其它语言 SDK 的差异

SDK等价于本章语法糖的能力
C#LINQ (.Where(...).Select(...)), IDisposable, event
JavaStream API, AutoCloseable, Listener interface
Python内置迭代, context manager (with), callback
Rust本章 sugar 模块: Iterator + adapter / Drop / mpsc::channel

Rust 的语法糖建立在 零成本抽象 上 — 上述所有 adapter 在编译期都被 inline 展开, 运行期开销与手写循环等价. C# / Java / Python 的对应物多少有装箱/分配/虚调用开销, 而 Rust sugar 不引入任何额外指令.