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— Beckhoff ADS Net 通信(内置报文协议,TCP 直连 PLC)BeckhoffAdsNetWithAdsRouter— 经官方 Beckhoff.TwinCAT.Ads 库通信(NuGetBeckhoff.TwinCAT.Ads7.0.339),支持多通路。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 层转换)。前缀不区分大小写(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={句柄}内存句柄地址 — 原样组包(句柄式ValueByHandle访问;区域MEM)ig={组号};{偏移}自定义索引组 — 原样组包
降级(原样透传):
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) |
