AoE (ADS over EtherCAT)
AoE 协议实现了 ADS (Automation Device Specification) over EtherCAT 通信,支持 Beckhoff TwinCAT 和类似设备的 ADS 通信。
通过 slave.AoE 访问。从站不支持 AoE 时为 null。
属性
| 属性 | 类型 | 访问 | 说明 |
|---|---|---|---|
| DefaultTimeout | int | 只读 | 默认超时时间(微秒),500000 |
数据读写
Read(uint indexGroup, uint indexOffset, uint length, int timeoutUs = DefaultTimeout)
public byte[]? Read(uint indexGroup, uint indexOffset, uint length, int timeoutUs = DefaultTimeout)
读取 ADS 数据。
参数:
indexGroup(uint) — 索引组indexOffset(uint) — 索引偏移length(uint) — 读取长度timeoutUs(int) — 超时时间(微秒)
返回值:
byte[]?— 读取的数据,失败返回null
示例:
byte[]? status = slave.AoE.Read(0x4020, 0, 4);
if (status != null)
{
uint value = BitConverter.ToUInt32(status, 0);
Console.WriteLine($"状态值: 0x{value:X8}");
}
Write(uint indexGroup, uint indexOffset, byte[] data, int timeoutUs = DefaultTimeout)
public bool Write(uint indexGroup, uint indexOffset, byte[] data, int timeoutUs = DefaultTimeout)
写入 ADS 数据。
参数:
indexGroup(uint) — 索引组indexOffset(uint) — 索引偏移data(byte[]) — 写入数据timeoutUs(int) — 超时时间(微秒)
返回值:
bool— 成功返回true
示例:
slave.AoE.Write(0x4020, 0, BitConverter.GetBytes((uint)0x0006));
ReadWrite(uint indexGroup, uint indexOffset, uint readLength, byte[]? writeData = null, int timeoutUs = DefaultTimeout)
public byte[]? ReadWrite(uint indexGroup, uint indexOffset, uint readLength, byte[]? writeData = null, int timeoutUs = DefaultTimeout)
同时读写数据(ADS ReadWrite 命令)。
参数:
indexGroup(uint) — 索引组indexOffset(uint) — 索引偏移readLength(uint) — 读取长度writeData(byte[]?) — 写入数据(可选)timeoutUs(int) — 超时时间(微秒)
返回值:
byte[]?— 读取的数据
设备信息
ReadDeviceInfo(int timeoutUs = DefaultTimeout)
public (bool success, byte majorVer, byte minorVer, ushort build, string deviceName) ReadDeviceInfo(int timeoutUs = DefaultTimeout)
读取 ADS 设备信息。
返回值:
success(bool) — 是否成功majorVer(byte) — 主版本号minorVer(byte) — 次版本号build(ushort) — 编译号deviceName(string) — 设备名称
示例:
var (success, major, minor, build, name) = slave.AoE.ReadDeviceInfo();
if (success)
Console.WriteLine($"设备: {name} v{major}.{minor}.{build}");
ReadState(int timeoutUs = DefaultTimeout)
public (bool success, ushort adsState, ushort deviceState) ReadState(int timeoutUs = DefaultTimeout)
读取 ADS 状态。
返回值:
success(bool) — 是否成功adsState(ushort) — ADS 状态deviceState(ushort) — 设备状态
相关结构:
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重新配置
示例:
var (success, adsState, deviceState) = slave.AoE.ReadState();
if (success)
Console.WriteLine($"ADS 状态: {slave.AoE.GetAdsStateName(adsState)} ({adsState})");
WriteControl(ushort adsState, ushort deviceState, byte[]? data = null, int timeoutUs = DefaultTimeout)
public bool WriteControl(ushort adsState, ushort deviceState, byte[]? data = null, int timeoutUs = DefaultTimeout)
写入控制命令(切换设备状态)。
参数:
adsState(ushort) — 目标 ADS 状态deviceState(ushort) — 目标设备状态data(byte[]?) — 附加数据(可选)timeoutUs(int) — 超时时间(微秒)
返回值:
bool— 成功返回true
示例:
slave.AoE.WriteControl(5, 0); // 切换到 Run 状态
GetAdsStateName(ushort adsState)
public string GetAdsStateName(ushort adsState)
获取 ADS 状态名称。
数据订阅
Subscribe(...)
public AoESubscription? Subscribe(
uint indexGroup, uint indexOffset, uint dataLength,
Action<AoENotificationEventArgs> callback,
AoETransmissionMode mode = AoETransmissionMode.OnChange,
int cycleTimeMs = 100)
订阅数据变化通知。
参数:
indexGroup(uint) — 索引组indexOffset(uint) — 索引偏移dataLength(uint) — 数据长度callback(Action<AoENotificationEventArgs>) — 数据变化回调mode(AoETransmissionMode) — 传输模式(默认OnChange)cycleTimeMs(int) — 检查周期(毫秒,默认 100)
返回值:
AoESubscription?— 订阅对象,失败返回null
相关结构:
public enum AoETransmissionMode : uint
{
NoTransmission = 0, // 无通知
Cyclic = 1, // 循环通知 - 按指定周期发送
OnChange = 2, // 变化通知 - 数据变化时发送
CyclicInDevice = 3, // 设备端循环通知
OnChangeInDevice = 4 // 设备端变化通知
}
示例:
var sub = slave.AoE.Subscribe(0x4020, 0, 4, e =>
{
uint value = BitConverter.ToUInt32(e.Data, 0);
Console.WriteLine($"数据变化: 0x{value:X8}");
});
Unsubscribe(AoESubscription subscription)
public bool Unsubscribe(AoESubscription subscription)
取消订阅。
UnsubscribeAll()
public void UnsubscribeAll()
取消所有订阅。
DelNotification(uint notificationHandle, int timeoutUs = DefaultTimeout)
public bool DelNotification(uint notificationHandle, int timeoutUs = DefaultTimeout)
底层删除从站设备通知句柄 (ADS Command 0x07)。常规场景应使用 Unsubscribe / UnsubscribeAll,此方法仅在持有原始 handle 且需手动清理时调用。
参数:
notificationHandle(uint) — 由 ADS AddNotification 返回的句柄timeoutUs(int) — 超时时间(微秒),默认DefaultTimeout
返回值:
bool— 成功返回true
示例:
// 已知 handle 的低层清理路径
slave.AoE.DelNotification(handle);
DelNotificationAsync(uint notificationHandle, int timeoutUs = DefaultTimeout, CancellationToken cancellationToken = default)
public Task<bool> DelNotificationAsync(uint notificationHandle, int timeoutUs = DefaultTimeout, CancellationToken cancellationToken = default)
异步版本的 DelNotification,用于 UI / 后台线程清理设备通知。
参数:
notificationHandle(uint) — 由 ADS AddNotification 返回的句柄timeoutUs(int) — 超时时间(微秒),默认DefaultTimeoutcancellationToken(CancellationToken) — 取消令牌
返回值:
Task<bool>— 成功返回true
示例:
await slave.AoE.DelNotificationAsync(handle, cancellationToken: ct);
AoE 配置
SetConfig(byte[] targetNetId, ushort targetPort, byte[] sourceNetId, ushort sourcePort)
public bool SetConfig(byte[] targetNetId, ushort targetPort, byte[] sourceNetId, ushort sourcePort)
设置 AoE 路由配置(AMS NetID 和端口)。
参数:
targetNetId(byte[]) — 目标 AMS NetID(6 字节)targetPort(ushort) — 目标 AMS 端口sourceNetId(byte[]) — 源 AMS NetID(6 字节)sourcePort(ushort) — 源 AMS 端口
返回值:
bool— 成功返回true
示例:
byte[] targetNetId = { 5, 80, 187, 177, 1, 1 };
byte[] sourceNetId = { 192, 168, 1, 100, 1, 1 };
slave.AoE.SetConfig(targetNetId, 851, sourceNetId, 32768);
InitializeSlaveNetId(byte[] netId, int timeoutUs = DefaultTimeout)
public bool InitializeSlaveNetId(byte[] netId, int timeoutUs = DefaultTimeout)
初始化从站 AoE Net ID(ETG.1020 §9.4)。在 INIT→PreOp 状态切换期间调用,将 Net ID 写入从站 ADS 路由表。
参数:
netId(byte[]) — 6 字节的 AMS Net IDtimeoutUs(int) — 超时时间(微秒)
返回值:
bool— 成功返回true
示例:
byte[] netId = { 5, 80, 187, 177, 1, 1 };
slave.AoE.InitializeSlaveNetId(netId);
InitializeSlaveNetIdAsync(byte[] netId, int timeoutUs = DefaultTimeout, CancellationToken cancellationToken = default)
public Task<bool> InitializeSlaveNetIdAsync(byte[] netId, int timeoutUs = DefaultTimeout, CancellationToken cancellationToken = default)
异步版本的 InitializeSlaveNetId,避免阻塞调用线程。
参数:
netId(byte[]) — 6 字节的 AMS Net IDtimeoutUs(int) — 超时时间(微秒)cancellationToken(CancellationToken) — 取消令牌
返回值:
Task<bool>— 成功返回true
示例:
byte[] netId = { 5, 80, 187, 177, 1, 1 };
await slave.AoE.InitializeSlaveNetIdAsync(netId, cancellationToken: ct);
跨协议网关
通过 AoE 路由访问其他邮箱协议(ETG.1020),支持 CoE 和 SoE 协议的透明转发。
ReadCoEViaAoE(ushort index, byte subindex, uint readLength, int timeoutUs = DefaultTimeout)
public byte[]? ReadCoEViaAoE(ushort index, byte subindex, uint readLength, int timeoutUs = DefaultTimeout)
通过 AoE 路由读取 CoE 对象(IndexGroup=0xF302)。
参数:
index(ushort) — CoE 对象索引subindex(byte) — CoE 子索引readLength(uint) — 期望读取长度timeoutUs(int) — 超时时间(微秒)
返回值:
byte[]?— 读取的数据,失败返回null
示例:
// 通过 AoE 网关读取 CoE 对象 0x6041:0(状态字)
byte[]? data = slave.AoE.ReadCoEViaAoE(0x6041, 0, 2);
if (data != null)
{
ushort statusWord = BitConverter.ToUInt16(data, 0);
Console.WriteLine($"状态字: 0x{statusWord:X4}");
}
WriteCoEViaAoE(ushort index, byte subindex, byte[] data, int timeoutUs = DefaultTimeout)
public bool WriteCoEViaAoE(ushort index, byte subindex, byte[] data, int timeoutUs = DefaultTimeout)
通过 AoE 路由写入 CoE 对象(IndexGroup=0xF302)。
参数:
index(ushort) — CoE 对象索引subindex(byte) — CoE 子索引data(byte[]) — 写入数据timeoutUs(int) — 超时时间(微秒)
返回值:
bool— 成功返回true
示例:
// 通过 AoE 网关写入 CoE 对象 0x6040:0(控制字)
slave.AoE.WriteCoEViaAoE(0x6040, 0, BitConverter.GetBytes((ushort)0x000F));
WriteCoEViaAoEAsync(ushort index, byte subindex, byte[] data, int timeoutUs = DefaultTimeout, CancellationToken cancellationToken = default)
public Task<bool> WriteCoEViaAoEAsync(ushort index, byte subindex, byte[] data, int timeoutUs = DefaultTimeout, CancellationToken cancellationToken = default)
异步版本的 WriteCoEViaAoE,适合 UI 主线程发起的远程 CoE 写入。
参数:
index(ushort) — CoE 对象索引subindex(byte) — CoE 子索引data(byte[]) — 写入数据timeoutUs(int) — 超时时间(微秒)cancellationToken(CancellationToken) — 取消令牌
返回值:
Task<bool>— 成功返回true
示例:
await slave.AoE.WriteCoEViaAoEAsync(0x6040, 0,
BitConverter.GetBytes((ushort)0x000F), cancellationToken: ct);
ReadSoEViaAoE(uint idn, uint readLength, int timeoutUs = DefaultTimeout)
public byte[]? ReadSoEViaAoE(uint idn, uint readLength, int timeoutUs = DefaultTimeout)
通过 AoE 路由读取 SoE IDN(IndexGroup=0xF420)。
参数:
idn(uint) — SoE IDN 编号readLength(uint) — 期望读取长度timeoutUs(int) — 超时时间(微秒)
返回值:
byte[]?— 读取的数据,失败返回null
WriteSoEViaAoE(uint idn, byte[] data, int timeoutUs = DefaultTimeout)
public bool WriteSoEViaAoE(uint idn, byte[] data, int timeoutUs = DefaultTimeout)
通过 AoE 路由写入 SoE IDN(IndexGroup=0xF420)。
参数:
idn(uint) — SoE IDN 编号data(byte[]) — 写入数据timeoutUs(int) — 超时时间(微秒)
返回值:
bool— 成功返回true
WriteSoEViaAoEAsync(uint idn, byte[] data, int timeoutUs = DefaultTimeout, CancellationToken cancellationToken = default)
public Task<bool> WriteSoEViaAoEAsync(uint idn, byte[] data, int timeoutUs = DefaultTimeout, CancellationToken cancellationToken = default)
异步版本的 WriteSoEViaAoE,可与 await / CancellationToken 协作。
参数:
idn(uint) — SoE IDN 编号data(byte[]) — 写入数据timeoutUs(int) — 超时时间(微秒)cancellationToken(CancellationToken) — 取消令牌
返回值:
Task<bool>— 成功返回true
示例:
await slave.AoE.WriteSoEViaAoEAsync(idn: 32,
BitConverter.GetBytes((uint)1500), cancellationToken: ct);
完整示例
数据读写
if (slave.AoE != null)
{
// 读取数据
byte[]? status = slave.AoE.Read(0x4020, 0, 4);
if (status != null)
Console.WriteLine($"状态: 0x{BitConverter.ToUInt32(status, 0):X8}");
// 写入数据
slave.AoE.Write(0x4020, 0, BitConverter.GetBytes((uint)0x0006));
// 设备信息
var (ok, major, minor, build, name) = slave.AoE.ReadDeviceInfo();
if (ok) Console.WriteLine($"设备: {name} v{major}.{minor}.{build}");
}
数据订阅
if (slave.AoE != null)
{
var sub = slave.AoE.Subscribe(0x4020, 0, 4, e =>
{
Console.WriteLine($"数据变化, 订阅 ID: {e.SubscriptionId}");
});
Thread.Sleep(10000);
slave.AoE.UnsubscribeAll();
}
跨协议网关
if (slave.AoE != null)
{
// 通过 AoE 读取 CoE 对象
byte[]? statusWord = slave.AoE.ReadCoEViaAoE(0x6041, 0, 2);
if (statusWord != null)
Console.WriteLine($"状态字: 0x{BitConverter.ToUInt16(statusWord, 0):X4}");
// 通过 AoE 写入 CoE 对象
slave.AoE.WriteCoEViaAoE(0x6040, 0, BitConverter.GetBytes((ushort)0x000F));
// 通过 AoE 读取 SoE IDN
byte[]? idnData = slave.AoE.ReadSoEViaAoE(32, 4);
if (idnData != null)
Console.WriteLine($"IDN 32 值: {BitConverter.ToUInt32(idnData, 0)}");
}
AoE 订阅子系统
ADS DeviceNotification (Command 0x06/0x07/0x08) 的托管包装. slave.AoE.Subscriptions 入口, 内部跨主站路由 native callback. 订阅后从设备主动推送数据变化, 不再依赖轮询.
Subscriptions
public AoESubscriptionManager Subscriptions { get; }
订阅管理器单例, 首次访问惰性创建. 同一主站的所有 slave 共享 native 回调线程.
Subscribe(uint indexGroup, uint indexOffset, uint dataLength, AoETransmissionMode, int maxDelayMs, int cycleTimeMs)
public AoESubscription? Subscribe(uint indexGroup, uint indexOffset, uint dataLength,
AoETransmissionMode mode = AoETransmissionMode.OnChange,
int maxDelayMs = 100,
int cycleTimeMs = 100)
创建并注册一个新订阅. 内部依次: 启动监听器 → AddDeviceNotification (0x06) → 注册 DLL 回调 → 加入订阅表. 失败返回 null.
参数:
indexGroup(uint) — ADS 索引组indexOffset(uint) — ADS 索引偏移dataLength(uint) — 数据长度 (字节)mode(AoETransmissionMode) — 传输模式maxDelayMs(int) — 最大延迟 (毫秒), 默认 100cycleTimeMs(int) — 循环周期 (毫秒), 默认 100
返回值:
AoESubscription?— 订阅对象 (含Id/NotificationHandle/IsActive)
相关结构:
public enum AoETransmissionMode
{
NoTransmission = 0,
Cyclic = 1,
OnChange = 2,
CyclicInDevice = 3,
OnChangeInDevice = 4
}
Unsubscribe(Guid id) / Unsubscribe(AoESubscription)
public bool Unsubscribe(Guid subscriptionId)
public bool Unsubscribe(AoESubscription subscription)
取消单个订阅: 移除事件处理器 → 注销 DLL 回调 → DeleteDeviceNotification (0x07).
UnsubscribeAll() / GetActiveSubscriptions() / GetSubscription(Guid)
public void UnsubscribeAll()
public AoESubscription[] GetActiveSubscriptions()
public AoESubscription? GetSubscription(Guid subscriptionId)
批量管理 API. UnsubscribeAll 在 master 退出前必须调用以避免设备侧句柄泄漏.
订阅事件
public event EventHandler<AoENotificationEventArgs>? DataNotification;
public event EventHandler<AoESubscriptionErrorEventArgs>? Error;
public event EventHandler<AoESubscription>? SubscriptionStateChanged;
相关结构:
public class AoENotificationEventArgs : EventArgs
{
public Guid SubscriptionId { get; }
public uint NotificationHandle { get; }
public ushort SlaveIndex { get; }
public uint IndexGroup { get; }
public uint IndexOffset { get; }
public byte[] Data { get; }
public DateTime Timestamp { get; }
public ulong DeviceTimestamp { get; } // 100ns 单位
}
public class AoESubscriptionErrorEventArgs : EventArgs
{
public Guid SubscriptionId { get; }
public string ErrorMessage { get; }
public Exception? Exception { get; }
public DateTime Timestamp { get; }
}
public class AoESubscription
{
public Guid Id { get; }
public uint NotificationHandle { get; }
public ushort SlaveIndex { get; }
public uint IndexGroup { get; }
public uint IndexOffset { get; }
public uint DataLength { get; }
public AoETransmissionMode TransmissionMode { get; }
public bool IsActive { get; }
public DateTime CreatedAt { get; }
public object? Tag { get; set; }
}
订阅管理器属性
public int SubscriptionCount { get; } // 当前订阅数量
public bool IsListenerRunning { get; } // native 监听线程是否运行
public int DefaultTimeoutMs { get; set; } // 订阅注册默认超时 (默认 500)
示例:
var sub = slave.AoE.Subscriptions.Subscribe(
indexGroup: 0xF021,
indexOffset: 0x1000,
dataLength: 4,
mode: AoETransmissionMode.OnChange,
maxDelayMs: 50,
cycleTimeMs: 100);
slave.AoE.Subscriptions.DataNotification += (s, e) =>
Console.WriteLine($"slave={e.SlaveIndex} data={BitConverter.ToInt32(e.Data,0)}");
slave.AoE.Subscriptions.Error += (s, e) =>
Console.WriteLine($"订阅错误: {e.ErrorMessage}");
// 退出前清理
slave.AoE.Subscriptions.UnsubscribeAll();
AoE 错误处理
AoEResultCode
public enum AoEResultCode : uint
{
NoError = 0x00,
InternalError = 0x01,
NoRealTime = 0x02,
InsertMailboxError = 0x04,
TargetPortNotFound = 0x06,
TargetMachineNotFound = 0x07,
UnknownCommandId = 0x08,
InvalidAmsLength = 0x0E,
InvalidAmsNetId = 0x0F,
PortDisabled = 0x12,
AmsSyncTimeout = 0x15,
DeviceInvalidGroup = 0x0700,
DeviceInvalidOffset = 0x0701,
DeviceNotReady = 0x0705,
DeviceTimeout = 0x0712,
DeviceAccessDenied = 0x071C,
// 更多见源码 (ETG.1020 Table 16)
}
ETG.1020 Table 16 定义的 AoE/ADS 结果码. 由 LastAoEError 暴露.
AoELastError
public struct AoELastError
{
public uint ResultCode; // 完整 32-bit
public ushort ErrorClass; // High word
public ushort ErrorCode; // Low word
public ushort LastCommand; // 失败的 ADS command id
public bool HasError;
public AoEResultCode ResultCodeEnum { get; }
public string Message { get; }
}
最近一次 AoE 调用失败的完整记录.
主请求接口
InitAllSlavesNetId(byte[]? masterNetId = null, int timeoutUs = 1_000_000)
public int InitAllSlavesNetId(byte[]? masterNetId = null, int timeoutUs = 1_000_000)
为所有支持 AoE 的从站派发 InitializeSlaveNetId. 返回成功初始化的从站数. masterNetId == null 时使用默认 1.1.1.1.1.1.
InitializeSlaveNetId(byte[] netId, int timeoutUs)
public bool InitializeSlaveNetId(byte[] netId, int timeoutUs = DefaultTimeout)
设置当前 slave 的 AMS NetID (6 字节).
SendFragmented(byte[] payload, ushort packetNumber, int timeoutUs)
public bool SendFragmented(byte[] payload, ushort packetNumber, int timeoutUs = DefaultTimeout)
ADS Fragmentation 发送 (ETG.1020 §9.3). 大数据自动分片, 配合 ReadWrite 内部触发使用.
SetConfig(byte[] targetNetId, ushort targetPort, byte[] sourceNetId, ushort sourcePort)
public bool SetConfig(byte[] targetNetId, ushort targetPort, byte[] sourceNetId, ushort sourcePort)
设置 AoE 通信路由 (源/目标 NetID + Port).
WriteControl(ushort adsState, ushort deviceState, byte[]? data = null, int timeoutUs)
public bool WriteControl(ushort adsState, ushort deviceState, byte[]? data = null, int timeoutUs = DefaultTimeout)
ADS WriteControl (Command 0x05): 切换 ADS 状态机 (Run/Stop/Reset/Config).
AddNotification / DelNotification (低层接口)
public uint? AddNotification(uint indexGroup, uint indexOffset, uint length,
uint transMode, uint maxDelay, uint cycleTime, int timeoutUs = DefaultTimeout)
public bool DelNotification(uint notificationHandle, int timeoutUs = DefaultTimeout)
直接注册/注销设备通知. Subscriptions 内部调用; 用户代码一般使用上层 Subscribe API. maxDelay / cycleTime 单位为 100ns.
ReadCoEViaAoE / WriteCoEViaAoE / ReadSoEViaAoE / WriteSoEViaAoE
public byte[]? ReadCoEViaAoE(ushort index, byte subindex, uint readLength, int timeoutUs = DefaultTimeout)
public bool WriteCoEViaAoE(ushort index, byte subindex, byte[] data, int timeoutUs = DefaultTimeout)
public byte[]? ReadSoEViaAoE(uint idn, uint readLength, int timeoutUs = DefaultTimeout)
public bool WriteSoEViaAoE(uint idn, byte[] data, int timeoutUs = DefaultTimeout)
跨协议网关. 通过 AoE 调用 CoE/SoE 对象, 用于多层 ADS 路由场景.
异步全套
每个同步方法都有对应 Async 版本, 后缀 Async, 多接 CancellationToken cancellationToken = default:
| 同步 | 异步 |
|---|---|
| WriteControl | WriteControlAsync |
| AddNotification | AddNotificationAsync |
| DelNotification | DelNotificationAsync |
| ReadCoEViaAoE | ReadCoEViaAoEAsync |
| WriteCoEViaAoE | WriteCoEViaAoEAsync |
| ReadSoEViaAoE | ReadSoEViaAoEAsync |
| WriteSoEViaAoE | WriteSoEViaAoEAsync |
| InitializeSlaveNetId | InitializeSlaveNetIdAsync |
示例:
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(2)).Token;
byte[]? data = await slave.AoE.ReadCoEViaAoEAsync(0x6041, 0, 2, cancellationToken: ct);