AoE (ADS over EtherCAT)
AoE 协议实现了 ADS (Automation Device Specification) over EtherCAT 通信,支持与实现 ADS 协议的设备进行通信。
通过 slave.aoe() 获取 AoEInstance 实例 —— 该方法直接返回 AoEInstance(不是 Option),是否真正支持 AoE 可用 aoe.is_supported() 判断。
便捷读写方法 (read() / write()) 内部使用固定默认超时 (500 ms),需要自定义超时用 read_with_timeout() 或 read_write()。
数据读写
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)
示例:
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)元组
ADS 状态值含义(标准 ADS 状态码):0=Invalid 1=Idle 2=Reset 3=Init 4=Start 5=Run 6=Stop 7=SaveConfig 8=LoadConfig 9=PowerFailure 10=PowerGood 11=Error 12=Shutdown 13=Suspend 14=Resume 15=Config 16=Reconfig。
可用关联函数 AoEInstance::ads_state_name(ads_state) 取状态名(见 ads_state_name())。
示例:
let (ads_state, device_state) = aoe.read_state()?;
println!("ADS 状态: {} ({}), 设备状态: {}",
AoEInstance::ads_state_name(ads_state), ads_state, device_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 状态名称。
数据订阅
AoEInstance 自身提供底层的 add_notification() / del_notification(),但推荐用 AoeSubscriptionManager 做多订阅注册、注销和 Drop 自动清理。订阅传输模式枚举为 AoeTransMode(见下文「AoE 订阅管理器」一节,不存在 AoETransmissionMode 类型)。
add_notification() / del_notification()
pub fn add_notification(
&self,
index_group: u32,
index_offset: u32,
length: u32,
trans_mode: u32,
max_delay: u32,
cycle_time: u32,
) -> Result<u32>
pub fn del_notification(&self, handle: u32) -> Result<()>
add_notification() 注册一个 ADS 设备通知,返回通知句柄 (u32);del_notification() 按句柄注销。trans_mode 取 AoeTransMode 的数值 (AoeTransMode::OnChange as u32 等)。回调的注册与分发由 AoeSubscriptionManager 统一管理(它持有 AOENotificationCallback),日常使用建议直接用管理器。
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.1020 Table 16)。#[repr(u32)],数值与 C#/C++/Java/Python 一致:高字节 = 错误类 (0x0700 = ADS device,0x0740 = client),低字节 = 具体错误码。
AoEInstance 的读写方法在 ADS 失败时返回 Err(DarraError::AoeFailed);AoEResultCode 是 ADS 协议层的具体子码,用 AoEResultCode::from_u32(code) 从原始数值匹配。
#[repr(u32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AoEResultCode {
NoError = 0x0000,
InternalError = 0x0001,
NoRealTime = 0x0002,
AllocationLockedMemoryError = 0x0003,
InsertMailboxError = 0x0004,
WrongReceiveHMsg = 0x0005,
TargetPortNotFound = 0x0006,
TargetMachineNotFound = 0x0007,
UnknownCommandId = 0x0008,
BadTaskId = 0x0009,
NoIO = 0x000A,
UnknownAmsCommand = 0x000B,
Win32Error = 0x000C,
PortNotConnected = 0x000D,
InvalidAmsLength = 0x000E,
InvalidAmsNetId = 0x000F,
LowInstLevel = 0x0010,
NoDebugAvailable = 0x0011,
PortDisabled = 0x0012,
PortAlreadyConnected = 0x0013,
AmsSyncWin32Error = 0x0014,
AmsSyncTimeout = 0x0015,
AmsSyncAmsError = 0x0016,
AmsSyncNoIndexMap = 0x0017,
InvalidAmsPort = 0x0018,
NoMemory = 0x0019,
TcpSendError = 0x001A,
HostUnreachable = 0x001B,
InvalidAmsFragment = 0x001C,
WsaServiceNotStarted = 0x274D,
DeviceInvalidGroup = 0x0700,
DeviceInvalidOffset = 0x0701,
DeviceInvalidAccess = 0x0702,
DeviceInvalidSize = 0x0703,
DeviceInvalidData = 0x0704,
DeviceNotReady = 0x0705,
DeviceBusy = 0x0706,
DeviceInvalidContext = 0x0707,
DeviceNotFound = 0x0708,
DeviceAlreadyExists = 0x0709,
DeviceSymbolNotFound = 0x070A,
DeviceSymbolVersionInvalid = 0x070B,
DeviceInvalidState = 0x070C,
DeviceTimeout = 0x0712,
DeviceNoInterfaceQuery = 0x0713,
DeviceInvalidInterface = 0x0714,
DeviceInvalidClassId = 0x0715,
DeviceInvalidObjectId = 0x0716,
DevicePending = 0x0717,
DeviceAborted = 0x0718,
DeviceWarning = 0x0719,
DeviceInvalidArrayIndex = 0x071A,
DeviceSymbolNotActive = 0x071B,
DeviceAccessDenied = 0x071C,
ClientError = 0x0740,
ClientInvalidParam = 0x0741,
ClientListEmpty = 0x0742,
ClientVarAlreadyInUse = 0x0743,
ClientInvokeTimeout = 0x0744,
ClientPortNotOpen = 0x0745,
ClientNotStarted = 0x0746,
ClientQueueFull = 0x0750,
}
impl AoEResultCode {
/// 从原始 u32 数值匹配枚举 (未匹配返回 None)
pub fn from_u32(code: u32) -> Option<Self>;
/// 数值
pub fn value(self) -> u32;
}
示例:
if let Err(e) = aoe.read(0x4020, 0, 4) {
eprintln!("AoE 失败: {:?}", e); // DarraError::AoeFailed
}
// 从 DLL 错误结构拆出的原始 ADS Result 码可比对枚举
if let Some(code) = AoEResultCode::from_u32(0x0705) {
println!("ADS 错误: {:?}", code); // DeviceNotReady
}
异步读写
ADS 报文往返通常 1ms ~ 数十 ms, 读写多个变量时建议异步并发. SDK 提供两条路径:
- std::thread 双轨 — 不依赖 tokio, 用关联函数
AoEInstance::read_blocking()/write_blocking()返回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<Option<Vec<u8>>>
pub async fn write_async(
&self,
index_group: u32,
index_offset: u32,
data: Vec<u8>,
) -> Result<()>
启用 async-tokio feature 后可用. 内部用 tokio::task::spawn_blocking 派发到阻塞线程池, 不会饿死 tokio 调度器. read_async 与同步 read() 一致返回 Result<Option<Vec<u8>>>;write_async 取 owned data: Vec<u8>。
示例:
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, b, c);
read_blocking() / write_blocking()
// 关联函数 (非方法), 显式传入 master_index / slave_index
pub fn read_blocking(
master_index: u16,
slave_index: u16,
index_group: u32,
index_offset: u32,
length: u32,
) -> std::thread::JoinHandle<Result<Option<Vec<u8>>>>
pub fn write_blocking(
master_index: u16,
slave_index: u16,
index_group: u32,
index_offset: u32,
data: Vec<u8>,
) -> std::thread::JoinHandle<Result<()>>
不依赖 tokio 的阻塞 + 后台线程版本. 是 AoEInstance 上的关联函数, 直接传 master/slave 索引, 调用方 join() 等待.
示例:
let h1 = AoEInstance::read_blocking(master.index(), 1, 0x4020, 0, 4);
let h2 = AoEInstance::read_blocking(master.index(), 1, 0x4020, 4, 4);
let a = h1.join().unwrap()?;
let b = h2.join().unwrap()?;
项目已经引入 tokio → 用 read_async/write_async; 否则用 read_blocking/write_blocking.
完整示例
数据读写
let aoe = slave.aoe();
if !aoe.is_supported() {
return Ok(());
}
// 读取数据 (read 取 3 个参数, 使用内部默认超时; 返回 Result<Option<Vec<u8>>>)
if let Some(status) = aoe.read(0x4020, 0, 4)? {
let value = u32::from_le_bytes(status[0..4].try_into()?);
println!("状态: 0x{:08X}", value);
}
// 写入数据 (write 取 3 个参数)
aoe.write(0x4020, 0, &0x0006u32.to_le_bytes())?;
// 设备信息 (read_device_info 不带参数, 返回元组)
let (major, minor, build, name) = aoe.read_device_info()?;
println!("设备: {} v{}.{}.{}", name, major, minor, build);
跨协议网关
let aoe = slave.aoe();
if !aoe.is_supported() {
return Ok(());
}
// 通过 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 darra_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();