Beckhoff 协议驱动
包: Snet.Beckhoff | 类: BeckhoffOperate | 类型: 2
提供通过 TCP/IP 上的 ADS(自动化设备规范)协议连接 Beckhoff TwinCAT PLC 的能力。
安装
dotnet add package Snet.Beckhoff
快速开始
using Snet.Beckhoff;
var op = await BeckhoffOperate.InstanceAsync(new BeckhoffData.Basics
{
IpAddress = "192.168.1.100",
Port = 851
});
var address = new Address(new List<AddressDetails>
{
new("读数", "s=MAIN.PLCVar", DataType.Float),
new("读数2", "s=MAIN.PLCVar2", DataType.Float)
});
var result = await op.ReadAsync(address);
if (result.Status)
{
var data = result.GetSource<ConcurrentDictionary<string, AddressValue>>();
foreach (var kv in data)
Console.WriteLine($"{kv.Key} = {kv.Value.ResultValue}");
}
else
{
Console.WriteLine($"读取失败: {result.Message}");
}
// 订阅之前 绑定数据事件
op.OnDataEventAsync += async (sender, e) =>
{
if (e.Status)
{
var data = e.GetSource<ConcurrentDictionary<string, AddressValue>>();
foreach (var kv in data)
Console.WriteLine($"{kv.Key} = {kv.Value.ResultValue}");
}
else
{
Console.WriteLine($"{e.Message}");
}
};
var subAddr = new Address(new List<AddressDetails>
{
new("读数", "s=MAIN.PLCVar", DataType.Float)
});
await op.SubscribeAsync(subAddr);
// 后续:取消订阅
// await op.UnSubscribeAsync(subAddr);
await op.DisposeAsync();
配置
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
IpAddress |
string | 127.0.0.1 | PLC IP 地址 |
Port |
int | 6688 | ADS TCP 端口 |
ConnectTimeOut |
int | 1000 | 连接超时(毫秒) |
ReceiveTimeOut |
int | 1000 | 接收超时(毫秒) |
ProtocolType |
enum | BeckhoffAdsNet | 协议类型 — BeckhoffAdsNet(内置 ADS)/ BeckhoffAdsNetWithAdsRouter(官方 Beckhoff.TwinCAT.Ads 库,经本地 ADS 路由) |
SenderAMSNetId |
string | (空) | 发送方 AMS NetId — 举例 192.168.0.100.1.1;也可以是带端口号 192.168.0.100.1.1:34567 |
TargetAMSNetId |
string | (空) | 目标 AMS NetId — 举例 192.168.0.1.1.1;也可以是带端口号 192.168.0.1.1.1:801。为空时按 IpAddress 推导为 {ip}.1.1(新协议) |
UseAutoAmsNetID |
bool | false | 从 IP 自动推导 AMS NetId |
UseTagCache |
bool | true | 缓存符号句柄(旧协议:TransValueHandle;新协议:ADS 句柄缓存,OffAsync 统一释放) |
UseServerActivePush |
bool | true | 使用服务器主动推送通知 |
AmsPort |
int | 851 | AMS 端口 — TwinCAT2:801, 811, 821, 831;TwinCAT3:851, 852, 853 |
支持的类型
ADS 协议,支持符号变量访问、NetId 路由和 AMS 寻址,适用于 TwinCAT 2/3 运行时。
BeckhoffAdsNet— 内置ADS实现;底层默认连接本机路由127.0.0.1:48898,未配置sender时通过PortConnect取得身份,也可显式指定连接参数。BeckhoffAdsNetWithAdsRouter— 经官方 Beckhoff.TwinCAT.Ads 库通信(NuGetBeckhoff.TwinCAT.Ads7.0.389),支持多通路。AdsClient默认需要本地 ADS 路由(TwinCAT 或托管 TcpRouter);无 TwinCAT 的服务器上部署时需同时运行路由(Beckhoff.TwinCAT.Ads.TcpRouter包),否则OnAsync报"Check for a running TwinCAT router instance!"。
BeckhoffAdsNetWithAdsRouter 按点位类型读取对应长度的原始字节后解析为值:
| 读取长度(字节) | 类型 |
|---|---|
| 1 | Bool、Byte(Bool = data[0] != 0) |
| 2 | Int16/Short、UInt16/Ushort、Char |
| 4 | Int32/Int、UInt32/Uint、Single/Float |
| 8 | Int64/Long、UInt64/Ulong、Double |
Length |
String(按 EncodingType 解码,截断到首个 \0)、ByteArray |
Length × 2 / 4 / 8 |
ShortArray/Int16Array/UshortArray/UInt16Array / IntArray/Int32Array/UintArray/UInt32Array/FloatArray/SingleArray / LongArray/Int64Array/UlongArray/UInt64Array/DoubleArray;BoolArray = 每 bool 1 字节 |
所有值均为小端序(BitConverter)。GetBaseObjectAsync 在 BeckhoffAdsNetWithAdsRouter 下返回 TwinCAT.Ads.AdsClient(BeckhoffAdsNet 下返回底层设备对象);新协议 GetStatusAsync 读取 AdsClient.IsConnected。
底层ADS读取、符号浏览与模拟标签
本节仅指 Snet.Driver.Profinet.Beckhoff.BeckhoffAdsNet,不是统一IDaq或官方TwinCAT.Ads后端共同提供的入口;须核对安装包是否包含当前源码API。低层无参构造默认127.0.0.1:48898,显式ip/port构造仍可覆盖。未配置sender AMS ID时会请求本机路由PortConnect;显式target但未配置sender也适用。显式sender的AMS端口为0时仅补端口,不覆盖地址字节。不要把这些默认值套到BeckhoffData.Basics。
读取和浏览签名
| API | 当前契约 |
|---|---|
OResult<byte[]> Read(string address, int length) |
长度按原始字节;现有ushort入口转发至此 |
OResult<byte[]> Read(string[] address, int[] length) |
维数不一致返回失败;符号解析会原地改写调用方address数组为句柄地址 |
Task<OResult<byte[]>> ReadAsync(string address, int length) |
异步传输,无CancellationToken |
Task<OResult<byte[]>> ReadAsync(string[] address, int[] length) |
符号解析阶段仍同步调用TransValueHandle,不是全程异步 |
OResult<List<AdsSymbolInfo>> BrowseSymbolInfos(bool bufferCache = false) |
浏览符号记录 |
OResult<List<AdsSymbolInfo>> BrowseSymbolStructInfos(bool bufferCache = false) |
浏览结构记录 |
Task<OResult<List<AdsSymbolInfo>>> BrowseSymbolInfosAsync(bool bufferCache = false) |
异步符号浏览,无CancellationToken |
Task<OResult<List<AdsSymbolInfo>>> BrowseSymbolStructInfosAsync(bool bufferCache = false) |
异步结构浏览,无CancellationToken |
string GetSenderAMSNetId(bool codeGen = false) |
false格式化当前身份;true返回原配置文本,可能为空 |
GetSenderAMSNetId新增可选bool参数,AdsTagItem构造新增可选第四参数;未以重载保留原CLR签名。省略这些可选参数的源码调用仍可编译,已编译的调用方须重新构建。
int读取没有新增正数/上限校验、自动分包或任意大读取保证;批量SumRead使用0xF080,返回组合载荷仍须后续解释。null数组也无明确保护。负通信错误且UseTagCache=true会清句柄缓存,正ADS错误不触发这一新增清理条件。
浏览先读取0xF00F上传信息,符号/结构长度分别在偏移4/12,再读0xF00B/0xF00E。初始信息不足16字节返回失败;传输失败及解析异常通过OResult传播。bufferCache控制每记录原始Buffer副本,不是UseTagCache句柄缓存。
AdsSymbolInfo与模拟标签
AdsSymbolInfo 的公开get/set属性为Name:string、TypeInfo:string、Size:int、Buffer:byte[]、Children:List
Snet.Driver.Profinet.Beckhoff.Helper.AdsTagItem 新增TypeName;构造为 AdsTagItem(string name, byte[] buffer, int typeLength, string typeName = null)。Length在TypeLength>0且Buffer非null时按Buffer.Length/TypeLength取整,否则0;GetTypeName优先显式类型,否则按宽度猜1=bool/2=short/4=int/8=double,其他宽度显示{TypeLength}bytes;Length>1时追加[Length]。类型化AddTagValue重载会填写TypeName;string重载以maxLength作为TypeLength,通常Length=1。这只是显示元数据,不是安全类型推断。
虚拟BeckhoffAdsServer提供TagValueDict(实时可变字典,无自动锁)、TagCount(加锁计数)、GetTagSnapshot()(字典浅拷贝,值和Buffer仍共享)、RemoveTagValue(string key)(空/不存在返回false)。浏览上传组返回硬编码示例数据,不从TagValueDict生成,不能用Browse枚举用户动态添加的标签。
公开AdsHelper
命名空间为 Snet.Driver.Profinet.Beckhoff.Helper;AdsHelper已由internal变public,以下21个静态入口可访问。其解析函数接收已提取的ADS载荷,不是完整AMS/TCP帧。
| Source line / 源码行 | Signature / 签名 |
|---|---|
| 17 | byte[] BuildAmsHeaderCommand(ushort commandId, byte[] data) |
| 45 | OResult<byte[]> BuildReadDeviceInfoCommand() |
| 54 | OResult<byte[]> BuildReadStateCommand() |
| 66 | OResult<byte[]> BuildWriteControlCommand(short state, short deviceState, byte[] data) |
| 84 | OResult<byte[]> BuildReadCommand(string address, int length, bool isBit) |
| 105 | OResult<byte[]> BuildReadCommand(string[] address, int[] length) (previous ushort[] version was internal through its type) |
| 195 | AdsSymbolInfo CreateSymbolStructInfo(byte[] data, int offset, bool bufferCache) |
| 270 | OResult<List<AdsSymbolInfo>> ParseAdsSymbolStructInfo(byte[] data, bool bufferCache) |
| 305 | OResult<List<AdsSymbolInfo>> ParseAdsSymbolInfo(byte[] data, bool bufferCache) |
| 357 | OResult<byte[]> BuildReadWriteCommand(string address, int length, bool isBit, byte[] value) |
| 383 | OResult<byte[]> BuildWriteCommand(string[] address, List<byte[]> value) |
| 412 | OResult<byte[]> BuildWriteCommand(string address, byte[] value, bool isBit) |
| 435 | OResult<byte[]> BuildWriteCommand(string address, bool[] value, bool isBit) |
| 457 | OResult<byte[]> BuildReleaseSystemHandle(uint handle) |
| 471 | OResult<int> CheckResponse(byte[] response) |
| 504 | byte[] PackAmsTcpHelper(AmsTcpHeaderFlags headerFlags, byte[] command) |
| 530 | OResult<uint,uint> AnalysisAddress(string address, bool isBit) |
| 618 | byte[] StrToAdsBytes(string value) |
| 628 | byte[] StrToAMSNetId(string amsNetId) |
| 660 | string GetAmsNetIdString(byte[] data, int index) |
| 684 | string GetErrorCodeText(int error) |
列表解析函数捕获异常并返回失败OResult;CreateSymbolStructInfo直接调用可抛索引/转换异常,未知记录类型可返回null,不能当任意输入的安全沙箱。StrToAdsBytes使用ASCII加零终止,浏览记录的名称/类型使用UTF8;StrToAMSNetId无端口返回6字节、带端口返回8字节,非法输入可抛异常。
批量BuildWriteCommand生成0xF081,但源码明确标记实际未调试通过,不代表已验证的高级批写功能。工具方法本身不连接设备。
诊断与串口边界见Driver报文诊断。
地址格式
ADS 寻址 — 地址透传设备(无 Snet 层转换)。前缀不区分大小写(s=/S=、i=/I=、ig=/IG=)。
| 区域 | 地址格式 | 示例 |
|---|---|---|
| 符号(标签)地址 | s={符号路径} |
s=MAIN.a, s=MAIN.PLCVar, s=A |
| 绝对输入/输出/内存 | M{编号} / I{编号} / Q{编号} — Bool 用 M{编号}.{位} |
M100, I100, Q100, M100.0 |
| 内存句柄 | i={十进制句柄值} |
i=1235467, i=100000 |
| 自定义索引组 | ig=0x{十六进制};{偏移} |
ig=0xF020;0, ig=0xF080;100 |
端口 — TwinCAT2 = 801, TwinCAT3 = 851。; 是修饰符分隔符,不属于值的一部分——尾随 ;(如 i=1235467;)会进入 uint.Parse 而失败;s= 与 i= 前缀取值时不带尾分号。ig={组号};{偏移} 支持 0x 前缀十六进制(ig=0xF080;100)、含 A-F 字母的十六进制(ig=F080;100)或十进制,偏移 为十进制。两种协议共用同一地址模型——M{n} → 索引组 16416、偏移 n(字节地址);M{n}.{bit}(Bool)→ 索引组 16417、偏移 = n×8+位;I{n}/Q{n} 用组 61472/61488、偏移 n+128000/n+256000,位读用组 61473/61489、偏移 n×8+位+1024000/n×8+位+2048000;i= 映射到索引组 ValueByHandle(0xF005)。符号读写:BeckhoffAdsNet 经 TransValueHandle 解析符号;BeckhoffAdsNetWithAdsRouter 经 HandleByName(0xF003)取句柄后按句柄读写——UseTagCache = true 时句柄缓存并在 OffAsync 统一释放,否则用后即释放。
支持的数据类型
所有读取按 AddressDetails.DataType 分发 — 别名(Short = Int16、Float = Single、Int = Int32 等)共用同一分支。
| 数据类型 | 支持 | 说明 |
|---|---|---|
Bool |
✅ | 位读取 — 线圈 / 字内位 |
Int16 / Short, UInt16 / Ushort |
✅ | 16 位整数 |
Int32 / Int, UInt32 / Uint |
✅ | 32 位整数 |
Int64 / Long, UInt64 / Ulong |
✅ | 64 位整数 |
Float / Single, Double |
✅ | 32 / 64 位浮点 |
String / Char |
✅ | 带 length + 编码读取 |
ByteArray + 全部 10 种 *Array |
✅ | 带 length 的数组读取 |
Byte |
✅ | 原始字节读取 |
None、Date、DateTime、Time 在 ReadAsync 中没有分支 — 不支持(BeckhoffAdsNetWithAdsRouter 对这些类型回退返回原始字节)。
地址自动组包与解包
每个数采驱动都实现 IPacker,BeckhoffOperate 已注册自动组包(BeckhoffPacker,ProtocolFamily.Beckhoff)。内存寻址区间可组包为最小批量读取:
M{n}/I{n}/Q{n}字读取 — 偏移 = 字节地址(与西门子 S7 模型相同);M100/M102合并为一批M{n}.{bit}Bool 读取 — 字节空间模型(26.235.x 起):M100.3= 字节 100 位 3——消费层对ByteArray批走 ADS 字节区字读(索引组 16416,Offset= 字节号),返回字节 n 的 8 位;批首归一化为无点号字节地址(M100.3→M100)i={句柄}:全部透传;句柄不是可递增的内存偏移。ig={组号};{偏移}:仅 16416/0x4020、61472/0xF020、61488/0xF030 原始字节组可优化;0xF005、0xF080 等其他组透传。
降级(原样透传):
s={符号}符号地址 — 驱动经TransValueHandle/ ADS 符号句柄运行时解析,字节偏移不可预知- 无点号 Bool(
M100作 Bool)— 字节区批读返回字节 100(位 800 起)的 8 位,与位 100 的单点位读语义不一致;单点ReadBool正确 - 带点号的非 Bool 类型(
M100.3作Int/Float)— 驱动字读取无法解析点号地址
// 组包:将稀疏内存地址合并为最小批量读取
var packed = await op.PackerAsync(address, "BeckhoffAdsNet"); // "BeckhoffAdsNetWithAdsRouter" 同样映射到 ProtocolFamily.Beckhoff
if (!packed.Status) return; // 组包失败 / 协议不支持
var packedAddr = packed.GetSource<Address>();
// 单次往返读取组包批次
var result = await op.ReadAsync(packedAddr);
if (!result.Status) return; // 读取失败
var data = result.GetSource<ConcurrentDictionary<string, AddressValue>>();
// 解包:将每个批次的原始字节解析回各地址的值
var unpacked = await op.UnPackerAsync(data);
if (!unpacked.Status) return; // 解包失败
var values = unpacked.GetSource<List<ConcurrentDictionary<string, AddressValue>>>();
foreach (var dict in values)
foreach (var v in dict)
Console.WriteLine($"{v.Key} = {v.Value.ResultValue}");
引擎细节:地址自动组包。
订阅
先绑定数据事件,再通过 SubscribeAsync 添加地址;数据通过 OnDataEventAsync 到达:
BeckhoffAdsNet— 经SubscribeOperate轮询订阅(每HandleInterval毫秒读取一次)。BeckhoffAdsNetWithAdsRouter—UseServerActivePush = true且地址集合不含虚拟点时,改用 ADS 设备通知(服务端主动推送;检测周期 =max(10, HandleInterval)毫秒)。集合含虚拟点或UseServerActivePush = false时回退轮询模式。UnSubscribeAsync/OffAsync同时清理两个通道。
op.OnDataEventAsync += async (sender, e) =>
{
if (e.Status)
{
var data = e.GetSource<ConcurrentDictionary<string, AddressValue>>();
foreach (var kv in data)
Console.WriteLine($"{kv.Key} = {kv.Value.ResultValue}");
}
else
{
Console.WriteLine($"订阅错误: {e.Message}");
}
};
var subAddr = new Address(new List<AddressDetails>
{
new("读数", "s=MAIN.PLCVar", DataType.Float)
});
await op.SubscribeAsync(subAddr);
// 后续:停止监视
// await op.UnSubscribeAsync(subAddr);
支持的操作
| 操作 | 方法 |
|---|---|
| 连接 | OnAsync() |
| 断开 | OffAsync() |
| 读取 | ReadAsync(address) |
| 写入 | WriteAsync(values) |
| 订阅 | SubscribeAsync(address) |
| 组包 / 解包 | PackerAsync(address, protocolTypeKey) / UnPackerAsync(data) |
