AoE (ADS over EtherCAT)
AoE 协议实现了 ADS (Automation Device Specification) over EtherCAT 通信,支持 Beckhoff TwinCAT 和类似设备的 ADS 通信。
通过 slave.aoe() 获取 AoEInstance 实例。
属性
| 方法 | 类型 | 说明 |
|---|---|---|
| default_timeout() | i32 | 默认超时时间(微秒),500000 |
数据读写
read()
pub fn read(&self, index_group: u32, index_offset: u32, length: u32) -> Result<Option<Vec<u8>>>
读取 ADS 数据(使用内部默认超时 500ms)。
参数:
index_group(u32) — 索引组index_offset(u32) — 索引偏移length(u32) — 读取长度
返回值:
Result<Option<Vec<u8>>>— 读取的数据,无数据返回Ok(None)
示例:
let aoe = slave.aoe();
if let Some(status) = aoe.read(0x4020, 0, 4)? {
let value = u32::from_le_bytes(status[0..4].try_into()?);
println!("状态值: 0x{:08X}", value);
}
read_with_timeout()
pub fn read_with_timeout(&self, index_group: u32, index_offset: u32,
length: u32, timeout_us: i32) -> Result<Option<Vec<u8>>>
读取 ADS 数据(指定超时)。
write()
pub fn write(&self, index_group: u32, index_offset: u32, data: &[u8]) -> Result<()>
写入 ADS 数据(使用内部默认超时 500ms)。
参数:
index_group(u32) — 索引组index_offset(u32) — 索引偏移data(&[u8]) — 写入数据
返回值:
Result<()>— 成功或错误
示例:
aoe.write(0x4020, 0, &0x0006u32.to_le_bytes())?;
read_write()
pub fn read_write(&self, index_group: u32, index_offset: u32, read_length: u32,
write_data: Option<&[u8]>, timeout_us: i32) -> Result<Option<Vec<u8>>>
同时读写数据(ADS ReadWrite 命令)。
参数:
index_group(u32) — 索引组index_offset(u32) — 索引偏移read_length(u32) — 读取长度,0 表示纯写入write_data(Option<&[u8]>) — 写入数据,为空表示纯读取timeout_us(i32) — 超时时间(微秒)
返回值:
Result<Option<Vec<u8>>>— 读取的数据
设备信息
read_device_info()
pub fn read_device_info(&self) -> Result<(u8, u8, u16, String)>
读取 ADS 设备信息(使用内部默认超时)。
返回值:
Result<(u8, u8, u16, String)>— (major_version, minor_version, build, device_name)
相关结构:
pub struct AdsDeviceInfo {
pub major_ver: u8, // 主版本号
pub minor_ver: u8, // 次版本号
pub build: u16, // 编译号
pub device_name: String, // 设备名称
}
示例:
let (major, minor, build, name) = aoe.read_device_info()?;
println!("设备: {} v{}.{}.{}", name, major, minor, build);
read_state()
pub fn read_state(&self) -> Result<(u16, u16)>
读取 ADS 状态(使用内部默认超时)。
返回值:
Result<(u16, u16)>— (ads_state, device_state) 元组
相关结构:
#[repr(u16)]
pub enum AdsState {
Invalid = 0, // 无效状态
Idle = 1, // 空闲
Reset = 2, // 复位
Init = 3, // 初始化
Start = 4, // 启动
Run = 5, // 运行
Stop = 6, // 停止
SaveConfig = 7, // 保存配置
LoadConfig = 8, // 加载配置
PowerFailure = 9, // 电源故障
PowerGood = 10, // 电源正常
Error = 11, // 错误
Shutdown = 12, // 关闭
Suspend = 13, // 挂起
Resume = 14, // 恢复
Config = 15, // 配置
Reconfig = 16, // 重新配置
}
示例:
let state = aoe.read_state(500_000)?;
println!("ADS 状态: {} ({})", aoe.get_ads_state_name(state.ads_state), state.ads_state);
write_control()
pub fn write_control(&self, ads_state: u16, device_state: u16,
data: Option<&[u8]>) -> Result<()>
写入控制命令(切换设备状态,使用内部默认超时)。
参数:
ads_state(u16) — 目标 ADS 状态device_state(u16) — 目标设备状态data(Option<&[u8]>) — 附加数据(可选)
返回值:
Result<()>— 成功或错误
示例:
aoe.write_control(5, 0, None)?; // 切换到 Run 状态
ads_state_name()
pub fn ads_state_name(ads_state: u16) -> &'static str
获取 ADS 状态名称。
数据订阅
subscribe()
pub fn subscribe<F>(&self, index_group: u32, index_offset: u32,
data_length: u32, callback: F,
mode: AoETransmissionMode, cycle_time_ms: i32)
-> Result<AoESubscription, DarraError>
where F: Fn(&[u8]) + Send + 'static
订阅数据变化通知。
参数:
index_group(u32) — 索引组index_offset(u32) — 索引偏移data_length(u32) — 数据长度callback(Fn(&[u8])) — 数据变化回调mode(AoETransmissionMode) — 传输模式(默认OnChange)cycle_time_ms(i32) — 检查周期(毫秒,默认 100)
返回值:
Result<AoESubscription, DarraError>— 订阅对象
#[repr(u32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AoETransmissionMode {
NoTransmission = 0, // 无通知
Cyclic = 1, // 循环通知 - 按指定周期发送
OnChange = 2, // 变化通知 - 数据变化时发送
CyclicInDevice = 3, // 设备端循环通知
OnChangeInDevice = 4, // 设备端变化通知
}
示例:
let _sub = aoe.subscribe(0x4020, 0, 4, |data| {
let value = u32::from_le_bytes(data[0..4].try_into().unwrap());
println!("数据变化: 0x{:08X}", value);
}, AoETransmissionMode::OnChange, 100)?;
unsubscribe() / unsubscribe_all()
pub fn unsubscribe(&self, subscription: AoESubscription) -> Result<(), DarraError>
pub fn unsubscribe_all(&self)
取消订阅。
AoE 配置
set_config()
pub fn set_config(&self, target_net_id: &[u8; 6], target_port: u16,
source_net_id: &[u8; 6], source_port: u16) -> Result<()>
设置 AoE 路由配置(AMS NetID 和端口)。
示例:
aoe.set_config(&[5, 80, 187, 177, 1, 1], 851,
&[192, 168, 1, 100, 1, 1], 32768)?;
config()
pub fn config(&self) -> Result<([u8; 6], u16, [u8; 6], u16)>
获取 AoE 路由配置,返回 (target_net_id, target_port, source_net_id, source_port) 元组。
initialize_slave_net_id()
pub fn initialize_slave_net_id(&self, net_id: &[u8; 6]) -> Result<()>
初始化从站 AoE Net ID(ETG.1020 §9.4)。在 INIT→PreOp 状态切换期间调用。
示例:
aoe.initialize_slave_net_id(&[5, 80, 187, 177, 1, 1])?;
跨协议网关
通过 AoE 路由访问其他邮箱协议(ETG.1020),支持 CoE 和 SoE 协议的透明转发。
read_coe_via_aoe()
pub fn read_coe_via_aoe(&self, index: u16, subindex: u8,
read_length: u32) -> Result<Option<Vec<u8>>>
通过 AoE 路由读取 CoE 对象(IndexGroup=0xF302,IndexOffset=(index << 16) | subindex)。
write_coe_via_aoe()
pub fn write_coe_via_aoe(&self, index: u16, subindex: u8,
data: &[u8]) -> Result<()>
通过 AoE 路由写入 CoE 对象(IndexGroup=0xF302)。
read_soe_via_aoe()
pub fn read_soe_via_aoe(&self, idn: u32, read_length: u32) -> Result<Option<Vec<u8>>>
通过 AoE 路由读取 SoE IDN(IndexGroup=0xF420,IndexOffset=IDN 编号)。
write_soe_via_aoe()
pub fn write_soe_via_aoe(&self, idn: u32, data: &[u8]) -> Result<()>
通过 AoE 路由写入 SoE IDN(IndexGroup=0xF420)。
示例:
// 通过 AoE 读取 CoE 对象 0x6041:0(状态字)
if let Some(data) = aoe.read_coe_via_aoe(0x6041, 0, 2)? {
let status = u16::from_le_bytes(data[0..2].try_into()?);
println!("状态字: 0x{:04X}", status);
}
// 通过 AoE 写入 CoE 对象 0x6040:0(控制字)
aoe.write_coe_via_aoe(0x6040, 0, &0x000Fu16.to_le_bytes())?;
// 通过 AoE 读取 SoE IDN
if let Some(idn_data) = aoe.read_soe_via_aoe(32, 4)? {
println!("IDN 32: {:?}", idn_data);
}
错误码
AoEResultCode 枚举
ADS over EtherCAT 标准错误码 (ETG.5001-2). 出现 ADS 错误时透过 DarraError::AoEError(AoEResultCode)
返回, 也可通过 last_result_code() 直接查询最近一次 ADS Result.
#[repr(u32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AoEResultCode {
NoError = 0x0000_0000,
InternalError = 0x0000_0001,
NoRtime = 0x0000_0002,
AllocLockedMem = 0x0000_0003,
InsertMailbox = 0x0000_0004,
WrongReceiveHmsg = 0x0000_0005,
TargetPortNotFound = 0x0000_0006,
TargetMachineNotFound = 0x0000_0007,
UnknownCmdId = 0x0000_0008,
BadTaskId = 0x0000_0009,
NoIo = 0x0000_000A,
UnknownAmsCmd = 0x0000_000B,
Win32Error = 0x0000_000C,
PortNotConnected = 0x0000_000D,
InvalidAmsLength = 0x0000_000E,
InvalidAmsNetId = 0x0000_000F,
InvalidPort = 0x0000_0010,
ServiceNotSupp = 0x0000_0011,
InvalidGrp = 0x0000_0012,
InvalidOffset = 0x0000_0013,
InvalidAccess = 0x0000_0014,
InvalidSize = 0x0000_0015,
InvalidData = 0x0000_0016,
NotReady = 0x0000_0017,
Busy = 0x0000_0018,
InvalidContext = 0x0000_0019,
OutOfMemory = 0x0000_001A,
InvalidParm = 0x0000_001B,
NotFound = 0x0000_001C,
Syntax = 0x0000_001D,
Incompatible = 0x0000_001E,
Exists = 0x0000_001F,
SymbolNotFound = 0x0000_0020,
SymbolVersionInvalid = 0x0000_0021,
InvalidState = 0x0000_0022,
TransmodeNotSupp = 0x0000_0023,
NotifyHndInvalid = 0x0000_0024,
ClientUnknown = 0x0000_0025,
NoMoreHdls = 0x0000_0026,
InvalidWatchsize = 0x0000_0027,
NotInitialized = 0x0000_0028,
Timeout = 0x0000_0029,
NoInterface = 0x0000_002A,
InvalidInterface = 0x0000_002B,
InvalidAmsPort = 0x0000_002C,
/// 其它未在枚举中的厂商私有码会以 `Other(code)` 透传
Other = 0xFFFF_FFFF,
}
impl AoEResultCode {
/// 中文描述
pub fn description(self) -> &'static str;
/// 是否成功 (NoError)
pub fn is_ok(self) -> bool;
}
last_result_code()
pub fn last_result_code(&self) -> AoEResultCode
最近一次 AoE 操作的 ADS Result. 成功后自动重置为 NoError.
示例:
match aoe.read(0x4020, 0, 4) {
Ok(_) => {}
Err(_) => {
let code = aoe.last_result_code();
eprintln!("AoE 失败: {:?} - {}", code, code.description());
}
}
异步读写
ADS 报文往返通常 1ms ~ 数十 ms, 读写多个变量时建议异步并发. SDK 提供两条路径:
- std::thread 双轨 — 不依赖 tokio, 用
read_blocking_in_thread()/write_blocking_in_thread()返回JoinHandle, 适合不引入异步运行时的场景. - tokio 异步 — 启用
async-tokiofeature 后暴露read_async()/write_async(), 可在async fn内.await并tokio::join!并发.
read_async() / write_async() (tokio)
pub async fn read_async(
&self,
index_group: u32,
index_offset: u32,
length: u32,
) -> Result<Vec<u8>, DarraError>
pub async fn write_async(
&self,
index_group: u32,
index_offset: u32,
data: &[u8],
) -> Result<(), DarraError>
启用 async-tokio feature 后可用. 内部派发到阻塞线程池, 不会饿死 tokio 调度器.
示例:
use tokio::join;
let (a, b, c) = join!(
aoe.read_async(0x4020, 0, 4),
aoe.read_async(0x4020, 4, 4),
aoe.read_async(0x4020, 8, 4),
);
let (a, b, c) = (a?, b?, c?);
println!("并发读取完成: {} {} {} 字节", a.len(), b.len(), c.len());
read_blocking_in_thread() / write_blocking_in_thread()
pub fn read_blocking_in_thread(
&self,
index_group: u32,
index_offset: u32,
length: u32,
) -> std::thread::JoinHandle<Result<Vec<u8>, DarraError>>
pub fn write_blocking_in_thread(
&self,
index_group: u32,
index_offset: u32,
data: Vec<u8>,
) -> std::thread::JoinHandle<Result<(), DarraError>>
不依赖 tokio 的阻塞 + 后台线程版本. 内部自行 clone 句柄到子线程, 调用方 join() 等待.
示例:
let h1 = aoe.read_blocking_in_thread(0x4020, 0, 4);
let h2 = aoe.read_blocking_in_thread(0x4020, 4, 4);
let a = h1.join().unwrap()?;
let b = h2.join().unwrap()?;
项目已经引入 tokio → 用 read_async/write_async; 否则用 _in_thread 版本.
完整示例
数据读写
let aoe = slave.aoe().ok_or("不支持 AoE")?;
// 读取数据
let status = aoe.read(0x4020, 0, 4, 500_000)?;
println!("状态: 0x{:08X}", u32::from_le_bytes(status[0..4].try_into()?));
// 写入数据
aoe.write(0x4020, 0, &0x0006u32.to_le_bytes(), 500_000)?;
// 设备信息
let info = aoe.read_device_info(500_000)?;
println!("设备: {} v{}.{}.{}", info.device_name, info.major_ver, info.minor_ver, info.build);
数据订阅
let aoe = slave.aoe().ok_or("不支持 AoE")?;
let _sub = aoe.subscribe(0x4020, 0, 4, |data| {
let value = u32::from_le_bytes(data[0..4].try_into().unwrap());
println!("数据变化: 0x{:08X}", value);
}, AoETransmissionMode::OnChange, 100)?;
std::thread::sleep(std::time::Duration::from_secs(10));
aoe.unsubscribe_all();
跨协议网关
let aoe = slave.aoe().ok_or("不支持 AoE")?;
// 通过 AoE 读取 CoE 对象
if let Some(data) = aoe.read_coe_via_aoe(0x6041, 0, 2)? {
let status = u16::from_le_bytes(data[0..2].try_into()?);
println!("状态字: 0x{:04X}", status);
}
// 通过 AoE 写入 CoE 对象
aoe.write_coe_via_aoe(0x6040, 0, &0x000Fu16.to_le_bytes())?;
// 通过 AoE 读写 SoE IDN
if let Some(idn_data) = aoe.read_soe_via_aoe(32, 4)? {
println!("IDN 32: {:?}", idn_data);
}
aoe.write_soe_via_aoe(32, &[0x01, 0x00, 0x00, 0x00])?;
跨协议网关 API
四个便捷方法, 通过 ETG.1020 标准 IndexGroup 直接走 AoE 邮箱路由读写 CoE / SoE 对象, 不走从站 CoE 邮箱通道, 适合 AoE 主控的网关型设备。
read_coe_via_aoe / write_coe_via_aoe
pub fn read_coe_via_aoe(&self, index: u16, subindex: u8, read_length: u32) -> Result<Option<Vec<u8>>>
pub fn write_coe_via_aoe(&self, index: u16, subindex: u8, data: &[u8]) -> Result<()>
IndexGroup = 0xF302, IndexOffset = (index << 16) | subindex, 默认超时 500 ms。
read_soe_via_aoe / write_soe_via_aoe
pub fn read_soe_via_aoe(&self, idn: u32, read_length: u32) -> Result<Option<Vec<u8>>>
pub fn write_soe_via_aoe(&self, idn: u32, data: &[u8]) -> Result<()>
IndexGroup = 0xF420, IndexOffset = idn。
initialize_slave_net_id
pub fn initialize_slave_net_id(&self, net_id: &[u8; 6]) -> Result<()>
ETG.1020 §9.4: 在 IP→PreOp 状态切换期间将本机 Net ID 写入从站 ADS 路由表 (IndexGroup=1, IndexOffset=3)。绑定 AoE 通信前必须调用一次。
AoE 订阅管理器
[AoeSubscriptionManager] 封装多订阅注册 / 注销与 Drop 时自动清理, 内部用 HashMap 按字符串 key 索引订阅。
创建与注册
pub fn new(master_index: u16) -> Self
pub fn subscribe(
&mut self,
name: impl Into<String>,
config: AoeSubscription,
callback: AOENotificationCallback,
user_data: *mut std::ffi::c_void,
) -> Result<u32>
name 在管理器内全局唯一, 重名返回 Err。config 通过 AoeSubscription::on_change 或 AoeSubscription::cyclic 构造。
注销
pub fn unsubscribe(&mut self, name: &str) -> Result<()>
pub fn unsubscribe_all(&mut self)
unsubscribe 走 AOEUnregisterNotification + AOEDelNotification 双步注销; unsubscribe_all 由 Drop 自动调用, 也可手动批量清空。
查询
pub fn has_subscription(&self, name: &str) -> bool
pub fn subscription_count(&self) -> usize
pub fn subscription_names(&self) -> Vec<&str>
pub fn active_subscriptions(&self) -> Vec<&str>
pub fn subscription_config(&self, name: &str) -> Option<&AoeSubscription>
pub fn subscription_handle(&self, name: &str) -> Option<u32>
active_subscriptions 与 subscription_names 当前实现等价, 留作 C# GetActiveSubscriptions 兼容名。
AoeSubscription
pub struct AoeSubscription {
pub slave_index: u16,
pub index_group: u32,
pub index_offset: u32,
pub length: u32,
pub trans_mode: AoeTransMode,
pub max_delay_us: u32,
pub cycle_time_us: u32,
pub timeout_us: i32,
}
#[repr(u32)]
pub enum AoeTransMode {
NoTransmission = 0,
Cyclic = 1,
OnChange = 2,
CyclicInDevice = 3,
OnChangeInDevice = 4,
}
构造函数:
AoeSubscription::on_change(slave, ig, io, length, timeout_us)
AoeSubscription::cyclic(slave, ig, io, length, cycle_time_us, timeout_us)
完整示例
use ethercat::slave::aoe::{
AoeSubscriptionManager, AoeSubscription, AoeTransMode,
};
let mut mgr = AoeSubscriptionManager::new(master.index());
extern "C" fn on_data(
slave: u16, _ig: u32, _io: u32,
data: *const u8, len: u32, _user: *mut std::ffi::c_void,
) {
let bytes = unsafe { std::slice::from_raw_parts(data, len as usize) };
println!("Slave {} 通知 {} 字节: {:?}", slave, len, bytes);
}
let cfg = AoeSubscription::on_change(slave.index(), 0x4020, 0, 4, 500_000);
mgr.subscribe("driver_status", cfg, Some(on_data), std::ptr::null_mut())?;
println!("订阅数: {}", mgr.subscription_count());
println!("活跃订阅: {:?}", mgr.active_subscriptions());
// 退出前可显式 unsubscribe_all (Drop 也会自动调用)
mgr.unsubscribe_all();