跳到主要内容

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 提供两条路径:

  1. std::thread 双轨 — 不依赖 tokio, 用 read_blocking_in_thread() / write_blocking_in_thread() 返回 JoinHandle, 适合不引入异步运行时的场景.
  2. tokio 异步 — 启用 async-tokio feature 后暴露 read_async() / write_async(), 可在 async fn.awaittokio::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 在管理器内全局唯一, 重名返回 Errconfig 通过 AoeSubscription::on_changeAoeSubscription::cyclic 构造。

注销

pub fn unsubscribe(&mut self, name: &str) -> Result<()>
pub fn unsubscribe_all(&mut self)

unsubscribeAOEUnregisterNotification + AOEDelNotification 双步注销; unsubscribe_allDrop 自动调用, 也可手动批量清空。

查询

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_subscriptionssubscription_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();