跳到主要内容

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_modeAoeTransMode 的数值 (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 提供两条路径:

  1. std::thread 双轨 — 不依赖 tokio, 用关联函数 AoEInstance::read_blocking() / write_blocking() 返回 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<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 在管理器内全局唯一, 重名返回 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 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();