Merged PR 140: 新增对倍福Beckoff PLC的调研集成文档,为集成倍福PLC做准备。
新增对倍福Beckoff PLC的调研集成文档,为集成倍福PLC做准备。
This commit is contained in:
@@ -0,0 +1,895 @@
|
||||
# 倍福 Beckhoff PLC 适配指南
|
||||
|
||||
## 概述 | Overview
|
||||
|
||||
本文档描述在 XP.Hardware.PLC 模块中增加对倍福(Beckhoff)TwinCAT ADS PLC 支持所需的全部改动工作。
|
||||
|
||||
适配目标:**对外接口(IPlcService、ISignalDataService)保持不变**,仅通过配置文件切换 PLC 品牌。
|
||||
|
||||
### 通讯库选型结论
|
||||
|
||||
| 方案 | 可行性 |
|
||||
|------|--------|
|
||||
| HslCommunication(当前 v7.0.1) | ❌ 不包含 `BeckhoffAdsNet` 类,需升级至 v11.0+ |
|
||||
| Beckhoff 官方 TwinCAT.Ads NuGet 包 | ✅ **推荐方案**,免费、功能完整、原生异步支持 |
|
||||
|
||||
**最终选型:使用 Beckhoff 官方 `Beckhoff.TwinCAT.Ads` NuGet 包(当前最新版 7.0.172)**
|
||||
|
||||
优势:
|
||||
- 免费开源,无授权限制
|
||||
- 官方维护,API 稳定
|
||||
- 原生支持 .NET 8.0
|
||||
- 原生 async/await 异步 API(`ReadValueAsync<T>`、`WriteValueAsync<T>`)
|
||||
- 支持符号路径(Symbolic Path)直接读写
|
||||
- 支持 ADS SumCommand 批量读写优化
|
||||
- 不影响现有 HslCommunication v7.0.1 的西门子通讯稳定性
|
||||
|
||||
---
|
||||
|
||||
## 背景差异分析 | Key Differences
|
||||
|
||||
### 西门子 vs 倍福 对比
|
||||
|
||||
| 维度 | 西门子 S7 | 倍福 Beckhoff TwinCAT |
|
||||
|------|-----------|----------------------|
|
||||
| 通讯协议 | S7 协议 (TCP/102) | ADS 协议 (TCP/48898) |
|
||||
| 通讯库 | HslCommunication v7.0.1 | Beckhoff.TwinCAT.Ads v7.0.172 |
|
||||
| 连接参数 | IP + Rack + Slot | IP + AMS Net ID + AMS Port |
|
||||
| 地址格式 | 绝对地址:`DB1.200`、`DB1.0.5`(bool) | 符号路径(Symbolic Path):`MAIN.bStart`、`GVL.nCounter` |
|
||||
| 字节序 | 大端(Big-Endian) | 小端(Little-Endian) |
|
||||
| 数据块概念 | DB 块(DB1, DB31...) | 无 DB 块概念,使用全局变量列表(GVL)或程序变量 |
|
||||
| 批量读取 | 按 DB 块 + 起始地址 + 长度读取连续字节 | ADS SumCommand 批量按符号名读取,或逐变量读取 |
|
||||
| 数据类型映射 | 通过地址格式隐含 | 通过符号信息自动获取或手动指定 |
|
||||
|
||||
### 核心挑战
|
||||
|
||||
1. **地址体系完全不同**:西门子使用 `DB{N}.{Addr}` 绝对地址,倍福使用符号变量路径(如 `GVL_HMI.bSystemReady`)
|
||||
2. **无 DB 块概念**:倍福没有"数据块号 + 偏移地址"的概念,信号定义 XML 格式需要重新设计
|
||||
3. **无需批量读取缓存**:ADS 协议单次读取延迟极低(亚毫秒~几毫秒),不需要西门子那样的批量缓存机制(详见下方性能分析)
|
||||
|
||||
### 批量读取缓存:倍福不需要 | Bulk Read Cache: Not Needed for Beckhoff
|
||||
|
||||
**西门子为什么需要批量读取?**
|
||||
|
||||
西门子 S7 协议每次单点通讯往返延迟约 10-50ms(受协议握手开销大影响)。如果有 30 个高频信号逐一读取,100ms 周期根本来不及。因此必须用 `ReadBytesAsync` 一次读取整块连续内存(200字节),然后在本地用 `PlcDataBlock` 按偏移解析各信号值。
|
||||
|
||||
**倍福为什么不需要?**
|
||||
|
||||
| 指标 | 西门子 S7 | 倍福 ADS |
|
||||
|------|-----------|----------|
|
||||
| 单变量读取往返延迟 | 10-50ms | <1ms(本地)/ 1-5ms(局域网) |
|
||||
| 100ms 内可读取变量数 | 2-10 个 | 50-100+ 个 |
|
||||
| 推送机制 | 无 | ADS Notification(变量变化时主动推送) |
|
||||
|
||||
根据 Beckhoff 官方文档和社区实测数据:
|
||||
- ADS 在 Beckhoff 控制器上可稳定做到 1ms 周期通讯
|
||||
- 局域网远程通讯 10ms 周期无压力
|
||||
- Beckhoff 官方明确建议:对于需要连续监控的变量,优先使用 **ADS Notification 推送模式** 而非轮询
|
||||
|
||||
**结论**:
|
||||
- 批量读取缓存(`PlcDataBlock` + Timer + byte[] 缓存)是 **西门子 S7 协议特有的优化**
|
||||
- 倍福模式下 `GetValueByName` 直接走单点 `ReadValueAsync<T>` 即可满足 100ms 级性能要求
|
||||
- 后续如需进一步优化,可使用 ADS Notification 实现变量变化推送,比轮询更高效
|
||||
|
||||
---
|
||||
|
||||
## NuGet 包引用 | Package Reference
|
||||
|
||||
在 `XP.Hardware.PLC.csproj` 中添加:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Beckhoff.TwinCAT.Ads" Version="7.0.172" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
> 注意:如果运行环境未安装 TwinCAT XAE/XAR,还需额外引用 ADS Router 包以支持独立运行:
|
||||
> ```xml
|
||||
> <PackageReference Include="Beckhoff.TwinCAT.Ads.TcpRouter" Version="7.0.172" />
|
||||
> ```
|
||||
|
||||
---
|
||||
|
||||
## 改动清单 | Change List
|
||||
|
||||
### 1. PlcConfig.cs — 扩展配置模型
|
||||
|
||||
**改动类型**:修改现有文件
|
||||
**工作量**:⭐(约15分钟)
|
||||
|
||||
#### 1.1 扩展 PlcType 枚举
|
||||
|
||||
```csharp
|
||||
public enum PlcType
|
||||
{
|
||||
// 西门子系列 | Siemens series
|
||||
S200Smart,
|
||||
S300,
|
||||
S400,
|
||||
S1200,
|
||||
S1500,
|
||||
|
||||
// 倍福系列 | Beckhoff series
|
||||
Beckhoff
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 增加倍福特有配置属性
|
||||
|
||||
```csharp
|
||||
public class PlcConfig
|
||||
{
|
||||
// === 现有字段保持不变 ===
|
||||
|
||||
// === 倍福专用配置 | Beckhoff-specific configuration ===
|
||||
|
||||
/// <summary>
|
||||
/// 倍福 AMS Net ID(仅 Beckhoff 使用)| Beckhoff AMS Net ID (Beckhoff only)
|
||||
/// 格式: "x.x.x.x.x.x",例如 "192.168.1.100.1.1"
|
||||
/// </summary>
|
||||
public string AmsNetId { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 倍福 AMS Port(仅 Beckhoff 使用)| Beckhoff AMS Port (Beckhoff only)
|
||||
/// 默认值 851 对应 TwinCAT 3 PLC Runtime 1
|
||||
/// </summary>
|
||||
public int AmsPort { get; set; } = 851;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. ConfigLoader.cs — 增加倍福配置读取和验证
|
||||
|
||||
**改动类型**:修改现有文件
|
||||
**工作量**:⭐(约15分钟)
|
||||
|
||||
#### 2.1 LoadPlcConfig() 增加读取
|
||||
|
||||
```csharp
|
||||
// 读取倍福 AMS 配置 | Read Beckhoff AMS configuration
|
||||
var amsNetId = ConfigurationManager.AppSettings[$"{prefix}:AmsNetId"];
|
||||
if (!string.IsNullOrEmpty(amsNetId))
|
||||
{
|
||||
config.AmsNetId = amsNetId;
|
||||
}
|
||||
|
||||
var amsPort = ConfigurationManager.AppSettings[$"{prefix}:AmsPort"];
|
||||
if (int.TryParse(amsPort, out var amsPortValue))
|
||||
{
|
||||
config.AmsPort = amsPortValue;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 ValidateConfig() 增加倍福验证逻辑
|
||||
|
||||
```csharp
|
||||
if (config.PlcType == PlcType.Beckhoff)
|
||||
{
|
||||
// 倍福必须配置 AmsNetId | Beckhoff requires AmsNetId
|
||||
if (string.IsNullOrWhiteSpace(config.AmsNetId))
|
||||
{
|
||||
throw new PlcException("倍福 PLC 配置错误: AmsNetId 不能为空");
|
||||
}
|
||||
if (config.AmsPort <= 0)
|
||||
{
|
||||
throw new PlcException($"倍福 PLC 配置错误: AmsPort 无效: {config.AmsPort}");
|
||||
}
|
||||
// 倍福不需要 Rack/Slot 验证,跳过 | Beckhoff doesn't need Rack/Slot validation
|
||||
}
|
||||
else
|
||||
{
|
||||
// 现有西门子验证逻辑 | Existing Siemens validation logic
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 新建 Core/BeckhoffPlcClient.cs — 倍福 PLC 客户端实现
|
||||
|
||||
**改动类型**:新增文件
|
||||
**工作量**:⭐⭐⭐(约3-4小时,含调试)
|
||||
|
||||
使用 `Beckhoff.TwinCAT.Ads` 官方库的 `AdsClient` 类实现 `IPlcClient` 接口。
|
||||
|
||||
#### 核心实现框架
|
||||
|
||||
```csharp
|
||||
using TwinCAT.Ads;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Hardware.Plc.Abstractions;
|
||||
using XP.Hardware.Plc.Exceptions;
|
||||
using XP.Hardware.PLC.Configs;
|
||||
|
||||
namespace XP.Hardware.Plc.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 倍福 Beckhoff ADS PLC 客户端实现 | Beckhoff ADS PLC client implementation
|
||||
/// 使用 Beckhoff.TwinCAT.Ads 官方库 | Uses official Beckhoff.TwinCAT.Ads library
|
||||
/// </summary>
|
||||
public class BeckhoffPlcClient : IPlcClient
|
||||
{
|
||||
private AdsClient _adsClient;
|
||||
private PlcConfig _config;
|
||||
private readonly ILoggerService _logger;
|
||||
private bool _isConnected = false;
|
||||
|
||||
public bool IsConnected => _adsClient != null && _isConnected && _adsClient.IsConnected;
|
||||
|
||||
public BeckhoffPlcClient(ILoggerService logger)
|
||||
{
|
||||
_logger = logger.ForModule("BeckhoffPlcClient");
|
||||
}
|
||||
|
||||
public async Task<bool> ConnectAsync(PlcConfig config)
|
||||
{
|
||||
_config = config;
|
||||
Disconnect();
|
||||
|
||||
try
|
||||
{
|
||||
_logger.Info("正在连接倍福 PLC: {IpAddress}, AmsNetId={AmsNetId}, AmsPort={AmsPort}",
|
||||
config.IpAddress, config.AmsNetId, config.AmsPort);
|
||||
|
||||
_adsClient = new AdsClient();
|
||||
|
||||
// 解析 AMS Net ID | Parse AMS Net ID
|
||||
var amsNetId = AmsNetId.Parse(config.AmsNetId);
|
||||
|
||||
// 连接到 PLC | Connect to PLC
|
||||
_adsClient.Connect(amsNetId, config.AmsPort);
|
||||
|
||||
// 验证连接状态 | Verify connection state
|
||||
var stateInfo = await _adsClient.ReadStateAsync(CancellationToken.None);
|
||||
if (stateInfo.AdsState == AdsState.Run || stateInfo.AdsState == AdsState.Stop)
|
||||
{
|
||||
_isConnected = true;
|
||||
_logger.Info("倍福 PLC 连接成功, ADS State={State}", stateInfo.AdsState);
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new PlcException($"倍福 PLC 状态异常: {stateInfo.AdsState}");
|
||||
}
|
||||
catch (PlcException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "倍福 PLC 连接失败: {Message}", ex.Message);
|
||||
throw new PlcException($"倍福 PLC 连接失败: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
if (_adsClient != null)
|
||||
{
|
||||
_adsClient.Disconnect();
|
||||
_adsClient.Dispose();
|
||||
_adsClient = null;
|
||||
_isConnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取变量值(address 为符号路径)| Read variable (address is symbol path)
|
||||
/// </summary>
|
||||
public async Task<T> ReadAsync<T>(string address)
|
||||
{
|
||||
if (!IsConnected)
|
||||
throw new PlcException("倍福 PLC 未连接");
|
||||
|
||||
try
|
||||
{
|
||||
// 使用符号路径直接读取 | Read directly via symbol path
|
||||
var result = await _adsClient.ReadValueAsync<T>(address, CancellationToken.None);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new PlcException($"倍福读取失败: 符号={address}, 错误={ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入变量值(address 为符号路径)| Write variable (address is symbol path)
|
||||
/// </summary>
|
||||
public async Task<bool> WriteAsync<T>(string address, T value)
|
||||
{
|
||||
if (!IsConnected)
|
||||
throw new PlcException("倍福 PLC 未连接");
|
||||
|
||||
try
|
||||
{
|
||||
await _adsClient.WriteValueAsync(address, value, CancellationToken.None);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "倍福写入失败: 符号={Address}, 错误={Message}", address, ex.Message);
|
||||
throw new PlcException($"倍福写入失败: 符号={address}, 错误={ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> ReadStringAsync(string address, ushort length)
|
||||
{
|
||||
if (!IsConnected)
|
||||
throw new PlcException("倍福 PLC 未连接");
|
||||
|
||||
try
|
||||
{
|
||||
// TwinCAT 字符串变量可直接按 string 类型读取 | TwinCAT string variables can be read as string type directly
|
||||
var result = await _adsClient.ReadValueAsync<string>(address, CancellationToken.None);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new PlcException($"倍福读取字符串失败: 符号={address}, 错误={ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> WriteStringAsync(string address, string value, ushort length)
|
||||
{
|
||||
if (!IsConnected)
|
||||
throw new PlcException("倍福 PLC 未连接");
|
||||
|
||||
try
|
||||
{
|
||||
await _adsClient.WriteValueAsync(address, value ?? string.Empty, CancellationToken.None);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "倍福写入字符串失败: 符号={Address}, 错误={Message}", address, ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量读取字节数据 | Read bytes in bulk
|
||||
/// 倍福模式下该方法不适用于常规场景,保留接口兼容性
|
||||
/// In Beckhoff mode this method is not used in normal scenarios, kept for interface compatibility
|
||||
/// </summary>
|
||||
public async Task<byte[]> ReadBytesAsync(string dbBlock, int startAddress, int length)
|
||||
{
|
||||
if (!IsConnected)
|
||||
throw new PlcException("倍福 PLC 未连接");
|
||||
|
||||
try
|
||||
{
|
||||
// 使用 IndexGroup + IndexOffset 方式读取原始内存(如需要)
|
||||
// Use IndexGroup + IndexOffset for raw memory read (if needed)
|
||||
var buffer = new byte[length];
|
||||
var memory = new Memory<byte>(buffer);
|
||||
|
||||
// 0x4020 = PLC Memory Area (INDEXGROUP_MEMORYAREA)
|
||||
// 此方法仅为保持接口兼容,倍福推荐方案使用字典缓存而非字节块读取
|
||||
var result = await _adsClient.ReadAsync(0x4020, (uint)startAddress, memory, CancellationToken.None);
|
||||
return buffer;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new PlcException($"倍福批量读取失败: 错误={ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> WriteBytesAsync(string dbBlock, int startAddress, byte[] data)
|
||||
{
|
||||
if (!IsConnected)
|
||||
throw new PlcException("倍福 PLC 未连接");
|
||||
|
||||
try
|
||||
{
|
||||
var memory = new ReadOnlyMemory<byte>(data);
|
||||
await _adsClient.WriteAsync(0x4020, (uint)startAddress, memory, CancellationToken.None);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "倍福批量写入失败: 错误={Message}", ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 关键 API 说明(Beckhoff.TwinCAT.Ads)
|
||||
|
||||
| 方法 | 用途 |
|
||||
|------|------|
|
||||
| `AdsClient.Connect(AmsNetId, int port)` | 建立 ADS 连接 |
|
||||
| `AdsClient.ReadValueAsync<T>(string symbolPath, CancellationToken)` | 按符号路径读取变量值 |
|
||||
| `AdsClient.WriteValueAsync<T>(string symbolPath, T value, CancellationToken)` | 按符号路径写入变量值 |
|
||||
| `AdsClient.ReadStateAsync(CancellationToken)` | 读取 PLC 运行状态 |
|
||||
| `AdsClient.ReadAsync(uint indexGroup, uint indexOffset, Memory<byte>, CancellationToken)` | 原始内存读取 |
|
||||
| `AdsClient.IsConnected` | 连接状态属性 |
|
||||
|
||||
---
|
||||
|
||||
### 4. 信号定义 XML 格式适配 — 核心改动
|
||||
|
||||
**改动类型**:新增解析逻辑 + 修改信号模型
|
||||
**工作量**:⭐⭐⭐(约3-4小时)
|
||||
|
||||
#### 4.1 SignalEntry 模型扩展
|
||||
|
||||
在 `Models/SignalEntry.cs` 中增加符号路径字段:
|
||||
|
||||
```csharp
|
||||
public class SignalEntry
|
||||
{
|
||||
// === 现有字段(西门子使用)===
|
||||
public string Name { get; set; } // 信号逻辑名称(全局唯一)
|
||||
public string Type { get; set; } // 数据类型
|
||||
public int StartAddr { get; set; } // 起始地址(西门子)
|
||||
public string IndexOrLength { get; set; } // 位索引或字符串长度
|
||||
public string GroupId { get; set; } // 所属 Group ID
|
||||
public int DBNumber { get; set; } // DB 块号(西门子)
|
||||
|
||||
// === 新增字段(倍福使用)===
|
||||
|
||||
/// <summary>
|
||||
/// 符号变量路径(倍福 Beckhoff 使用)| Symbol variable path (Beckhoff only)
|
||||
/// 例如: "MAIN.bStart", "GVL_Process.nTemperature"
|
||||
/// 西门子模式下此字段为空 | Empty in Siemens mode
|
||||
/// </summary>
|
||||
public string SymbolPath { get; set; } = "";
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2 倍福信号定义 XML 格式设计
|
||||
|
||||
设计一个兼容现有 Group 结构的 XML 格式,通过根节点 `PlcType` 属性区分:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<Config PlcType="Beckhoff">
|
||||
<!--
|
||||
倍福信号定义格式说明:
|
||||
- 使用 SymbolPath 指定 TwinCAT 中的变量完整路径
|
||||
- 不使用 DBNumber 和 StartAddr
|
||||
- Type 保持与西门子一致的类型名称
|
||||
- IndexOrLength 仅 string 类型使用(指定长度)
|
||||
- 倍福不需要区分"缓存读取"和"单点读取",所有信号统一单点读取
|
||||
-->
|
||||
|
||||
<!-- 读取信号组 | Read signals -->
|
||||
<Group ID="SignalList_Read">
|
||||
<Signal Name="PlcLive"
|
||||
Type="byte"
|
||||
SymbolPath="GVL_HMI.nPlcHeartbeat"
|
||||
Remark="PLC 心跳" />
|
||||
<Signal Name="SystemReady"
|
||||
Type="bool"
|
||||
SymbolPath="GVL_HMI.bSystemReady"
|
||||
Remark="系统就绪" />
|
||||
<Signal Name="CurrentVoltage"
|
||||
Type="single"
|
||||
SymbolPath="GVL_Process.fVoltage"
|
||||
Remark="当前电压" />
|
||||
<Signal Name="ScanMode"
|
||||
Type="byte"
|
||||
SymbolPath="GVL_Process.nScanMode"
|
||||
Remark="扫描模式" />
|
||||
</Group>
|
||||
|
||||
<!-- 写入信号组 | Write signals -->
|
||||
<Group ID="SignalList_Write">
|
||||
<Signal Name="SoftLive"
|
||||
Type="byte"
|
||||
SymbolPath="GVL_HMI.nSoftHeartbeat"
|
||||
Remark="软件心跳" />
|
||||
<Signal Name="StartCmd"
|
||||
Type="bool"
|
||||
SymbolPath="GVL_HMI.bStartCmd"
|
||||
Remark="启动命令" />
|
||||
<Signal Name="TargetVoltage"
|
||||
Type="single"
|
||||
SymbolPath="GVL_Process.fTargetVoltage"
|
||||
Remark="目标电压" />
|
||||
<Signal Name="DeviceName"
|
||||
Type="string"
|
||||
SymbolPath="GVL_HMI.sDeviceName"
|
||||
IndexOrLength="20"
|
||||
Remark="设备名称" />
|
||||
</Group>
|
||||
|
||||
<!-- 状态信号组 | Status signals -->
|
||||
<Group ID="Status">
|
||||
<Signal Name="ErrorCode"
|
||||
Type="int"
|
||||
SymbolPath="GVL_Alarm.nErrorCode"
|
||||
Remark="错误码" />
|
||||
<Signal Name="DoorState"
|
||||
Type="bool"
|
||||
SymbolPath="GVL_Safety.bDoorClosed"
|
||||
Remark="安全门状态" />
|
||||
</Group>
|
||||
</Config>
|
||||
```
|
||||
|
||||
#### 4.3 XmlSignalParser 解析器扩展
|
||||
|
||||
在现有 `XmlSignalParser` 中增加倍福格式解析分支:
|
||||
|
||||
```csharp
|
||||
public List<SignalGroup> LoadFromFile(string xmlFilePath)
|
||||
{
|
||||
var doc = XDocument.Load(xmlFilePath);
|
||||
var plcTypeAttr = doc.Root?.Attribute("PlcType")?.Value;
|
||||
|
||||
if (string.Equals(plcTypeAttr, "Beckhoff", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ParseBeckhoffFormat(doc);
|
||||
}
|
||||
else
|
||||
{
|
||||
return ParseSiemensFormat(doc); // 现有逻辑保持不变
|
||||
}
|
||||
}
|
||||
|
||||
private List<SignalGroup> ParseBeckhoffFormat(XDocument doc)
|
||||
{
|
||||
var groups = new List<SignalGroup>();
|
||||
|
||||
foreach (var groupElement in doc.Root.Elements("Group"))
|
||||
{
|
||||
var groupId = groupElement.Attribute("ID")?.Value ?? "";
|
||||
var group = new SignalGroup { GroupId = groupId };
|
||||
|
||||
foreach (var signalElement in groupElement.Elements("Signal"))
|
||||
{
|
||||
var signal = new SignalEntry
|
||||
{
|
||||
Name = signalElement.Attribute("Name")?.Value ?? "",
|
||||
Type = signalElement.Attribute("Type")?.Value ?? "",
|
||||
SymbolPath = signalElement.Attribute("SymbolPath")?.Value ?? "",
|
||||
IndexOrLength = signalElement.Attribute("IndexOrLength")?.Value ?? "",
|
||||
GroupId = groupId,
|
||||
// 西门子字段置为无效值 | Siemens fields set to invalid values
|
||||
DBNumber = -1,
|
||||
StartAddr = -1
|
||||
};
|
||||
group.Signals.Add(signal);
|
||||
}
|
||||
|
||||
groups.Add(group);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. PlcService 适配:倍福无需批量读取缓存
|
||||
|
||||
**改动类型**:修改 PlcService 中的初始化和读取逻辑
|
||||
**工作量**:⭐⭐(约1-2小时)
|
||||
|
||||
#### 5.1 设计原则
|
||||
|
||||
倍福模式下 **不启动** 批量读取定时器(`_bulkReadTimer`),也不使用 `PlcDataBlock` 字节缓存。
|
||||
所有信号的 `GetValueByName` 调用 **统一走单点读取**(`ReadAsync<T>(symbolPath)`),性能完全满足要求。
|
||||
|
||||
#### 5.2 PlcService.InitializeAsync() 中增加分支
|
||||
|
||||
```csharp
|
||||
public async Task<bool> InitializeAsync(PlcConfig config)
|
||||
{
|
||||
// ... 连接成功后 ...
|
||||
|
||||
if (config.PlcType == PlcType.Beckhoff)
|
||||
{
|
||||
// 倍福模式:不启动批量读取定时器,不需要 PlcDataBlock 缓存
|
||||
// Beckhoff mode: no bulk read timer needed, no PlcDataBlock cache
|
||||
_logger.Info("倍福模式: 跳过批量读取定时器(ADS 单点读取已足够快)");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 西门子模式:启动批量读取定时任务(现有逻辑不变)
|
||||
_bulkReadTimer = new Timer(BulkReadCallback, null, TimeSpan.Zero,
|
||||
TimeSpan.FromMilliseconds(_config.BulkReadIntervalMs));
|
||||
}
|
||||
|
||||
// ... 其余逻辑 ...
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.3 连接监控适配
|
||||
|
||||
西门子依赖批量读取失败计数判断断连,倍福需要使用 ADS 状态读取:
|
||||
|
||||
```csharp
|
||||
private void CheckConnectionStatus(object state)
|
||||
{
|
||||
if (_config.PlcType == PlcType.Beckhoff)
|
||||
{
|
||||
// 倍福:直接检查 AdsClient.IsConnected 属性
|
||||
var effectiveStatus = _plcClient.IsConnected;
|
||||
// ... 状态变化处理 ...
|
||||
}
|
||||
else
|
||||
{
|
||||
// 西门子:现有逻辑(批量读取失败计数)
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.4 后续优化选项:ADS Notification 推送(可选)
|
||||
|
||||
如果后续有场景需要极低延迟的变量监控(<10ms),可使用 ADS Notification 机制:
|
||||
|
||||
```csharp
|
||||
// ADS Notification 示例:变量变化时 PLC 主动推送到客户端
|
||||
// 无需轮询,延迟取决于 PLC 任务周期(通常 1-10ms)
|
||||
// 适合报警信号、紧急停止等场景
|
||||
_adsClient.AddDeviceNotification(symbolPath, dataSize, callback, cycleTime, maxDelay);
|
||||
```
|
||||
|
||||
此为后续优化方向,初始版本使用单点读取已完全满足需求。
|
||||
|
||||
---
|
||||
|
||||
### 6. SignalDataService 路由逻辑适配
|
||||
|
||||
**改动类型**:修改 SignalDataService.cs
|
||||
**工作量**:⭐⭐(约1-2小时)
|
||||
|
||||
#### 6.1 读取路由调整
|
||||
|
||||
倍福模式下所有信号统一走单点读取,不再区分"缓存"和"单点"两条路径:
|
||||
|
||||
```csharp
|
||||
public object GetValueByName(string signalName)
|
||||
{
|
||||
var signal = _plcService.FindSignal(signalName);
|
||||
|
||||
if (!string.IsNullOrEmpty(signal.SymbolPath))
|
||||
{
|
||||
// === 倍福模式:统一单点读取(ADS 延迟足够低,无需缓存)===
|
||||
var address = signal.SymbolPath;
|
||||
return ReadSinglePoint(signal, address);
|
||||
}
|
||||
else
|
||||
{
|
||||
// === 西门子模式:现有路由逻辑不变 ===
|
||||
var bulkDbNumber = ExtractDbNumber(_plcService.Config.ReadDbBlock);
|
||||
var bulkStart = _plcService.Config.ReadStartAddress;
|
||||
var bulkEnd = bulkStart + _plcService.Config.ReadLength - 1;
|
||||
|
||||
var useBulkCache = signal.DBNumber == bulkDbNumber
|
||||
&& signal.StartAddr >= bulkStart
|
||||
&& signal.StartAddr <= bulkEnd;
|
||||
|
||||
if (useBulkCache)
|
||||
{
|
||||
var dataBlock = _plcService.GetCacheSnapshot();
|
||||
if (dataBlock == null)
|
||||
throw new PlcException("数据缓存未就绪 | Data cache not ready");
|
||||
return ParseSignalValue(signal, dataBlock);
|
||||
}
|
||||
else
|
||||
{
|
||||
var address = FormatReadAddress(signal);
|
||||
return ReadSinglePoint(signal, address);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 6.2 写入地址格式化调整
|
||||
|
||||
```csharp
|
||||
private string FormatWriteAddress(SignalEntry signal)
|
||||
{
|
||||
// 倍福模式:直接返回符号路径 | Beckhoff mode: return symbol path directly
|
||||
if (!string.IsNullOrEmpty(signal.SymbolPath))
|
||||
{
|
||||
return signal.SymbolPath;
|
||||
}
|
||||
|
||||
// 西门子模式:现有逻辑 | Siemens mode: existing logic
|
||||
return signal.Type.ToLowerInvariant() switch
|
||||
{
|
||||
"bool" => $"DB{signal.DBNumber}.{signal.StartAddr}.{signal.IndexOrLength}",
|
||||
_ => $"DB{signal.DBNumber}.{signal.StartAddr}"
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. PLCModule.cs — DI 注册改为配置驱动
|
||||
|
||||
**改动类型**:修改现有文件
|
||||
**工作量**:⭐(约20分钟)
|
||||
|
||||
```csharp
|
||||
public void RegisterTypes(IContainerRegistry containerRegistry)
|
||||
{
|
||||
// 注册配置加载器
|
||||
containerRegistry.Register<ConfigLoader>();
|
||||
|
||||
// IPlcClient 瞬态注册:根据配置选择实现 | IPlcClient transient: select implementation by config
|
||||
containerRegistry.Register<IPlcClient>(container =>
|
||||
{
|
||||
var logger = container.Resolve<ILoggerService>();
|
||||
var configLoader = container.Resolve<ConfigLoader>();
|
||||
var config = configLoader.LoadPlcConfig();
|
||||
|
||||
return config.PlcType switch
|
||||
{
|
||||
PlcType.Beckhoff => new BeckhoffPlcClient(logger),
|
||||
_ => new S7PlcClient(logger) // 所有西门子型号
|
||||
};
|
||||
});
|
||||
|
||||
// ... 其余注册保持不变 ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. App.config 配置示例
|
||||
|
||||
**改动类型**:在 App.config.example 中补充倍福配置段
|
||||
**工作量**:⭐(约5分钟)
|
||||
|
||||
```xml
|
||||
<!-- ============================================================ -->
|
||||
<!-- 倍福 Beckhoff PLC 配置示例 | Beckhoff PLC Configuration -->
|
||||
<!-- ============================================================ -->
|
||||
<add key="Plc:IpAddress" value="192.168.1.200" />
|
||||
<add key="Plc:Port" value="48898" />
|
||||
<!-- PlcType 可选值 | Available values: S200Smart, S300, S400, S1200, S1500, Beckhoff -->
|
||||
<add key="Plc:PlcType" value="Beckhoff" />
|
||||
|
||||
<!-- 倍福专用参数 | Beckhoff-specific parameters -->
|
||||
<add key="Plc:AmsNetId" value="192.168.1.200.1.1" />
|
||||
<add key="Plc:AmsPort" value="851" />
|
||||
|
||||
<!-- 批量读取周期(与西门子共用)| Bulk read interval (shared with Siemens) -->
|
||||
<add key="Plc:BulkReadIntervalMs" value="100" />
|
||||
|
||||
<!-- 超时配置 | Timeout configuration -->
|
||||
<add key="Plc:ConnectTimeoutMs" value="5000" />
|
||||
<add key="Plc:ReadTimeoutMs" value="3000" />
|
||||
<add key="Plc:WriteTimeoutMs" value="3000" />
|
||||
<add key="Plc:bReConnect" value="true" />
|
||||
|
||||
<!--
|
||||
注意:倍福模式下以下西门子专用参数不使用(可保留但忽略):
|
||||
Plc:Rack, Plc:Slot, Plc:ReadDbBlock, Plc:ReadStartAddress, Plc:ReadLength
|
||||
-->
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 不需要改动的部分 | No Changes Required
|
||||
|
||||
| 组件 | 原因 |
|
||||
|------|------|
|
||||
| `IPlcClient` 接口 | 方法签名足够通用,倍福可完整实现 |
|
||||
| `IPlcService` 接口 | 对外接口不变 |
|
||||
| `ISignalDataService` 接口 | 对外接口不变 |
|
||||
| `PlcWriteQueue.cs` | 仅依赖 IPlcClient 接口,地址格式由上层决定 |
|
||||
| `PlcDataBlock.cs` | 西门子专用,倍福模式不使用 |
|
||||
| `PlcException.cs` | 通用异常类 |
|
||||
| `PlcHelper.cs` / `HexFormatter.cs` | 通用工具类 |
|
||||
| 上层模块(MotionControl、Detector 等) | 仅通过 ISignalDataService 交互,完全解耦 |
|
||||
|
||||
---
|
||||
|
||||
## 工作量汇总 | Effort Summary
|
||||
|
||||
| 序号 | 改动项 | 文件 | 工作量 | 预估时间 |
|
||||
|------|--------|------|--------|----------|
|
||||
| 1 | PlcConfig.cs 扩展枚举和属性 | Configs/PlcConfig.cs | ⭐ | 15分钟 |
|
||||
| 2 | ConfigLoader.cs 增加读取和验证 | Configs/ConfigLoader.cs | ⭐ | 15分钟 |
|
||||
| 3 | 新建 BeckhoffPlcClient.cs | Core/BeckhoffPlcClient.cs | ⭐⭐⭐ | 3-4小时 |
|
||||
| 4 | SignalEntry 模型增加 SymbolPath 字段 | Models/SignalEntry.cs | ⭐ | 10分钟 |
|
||||
| 5 | XmlSignalParser 增加倍福格式解析 | Helpers/XmlSignalParser.cs | ⭐⭐ | 1-2小时 |
|
||||
| 6 | PlcService 初始化分支(跳过批量读取) | Services/PlcService.cs | ⭐⭐ | 1-2小时 |
|
||||
| 7 | SignalDataService 路由和地址逻辑适配 | Services/SignalDataService.cs | ⭐⭐ | 1-2小时 |
|
||||
| 8 | PLCModule.cs DI 注册修改 | PLCModule.cs | ⭐ | 20分钟 |
|
||||
| 9 | App.config 配置示例 | Documents/App.config.example | ⭐ | 10分钟 |
|
||||
| 10 | 倍福信号定义 XML 示例文件 | Documents/ | ⭐ | 15分钟 |
|
||||
| 11 | 多语言资源文件补充 | Resources/ | ⭐ | 30分钟 |
|
||||
| 12 | 集成测试和联调 | — | ⭐⭐⭐ | 2-3小时 |
|
||||
|
||||
**总预估工作量:2-2.5个工作日**
|
||||
|
||||
---
|
||||
|
||||
## 实施建议 | Implementation Recommendations
|
||||
|
||||
### 推荐实施顺序
|
||||
|
||||
```
|
||||
阶段 1:基础框架(约半天)
|
||||
├── PlcConfig 扩展枚举 + 倍福属性
|
||||
├── ConfigLoader 增加读取/验证
|
||||
├── PLCModule DI 注册改造
|
||||
└── SignalEntry 模型增加 SymbolPath
|
||||
|
||||
阶段 2:通讯层实现(约1天)
|
||||
├── 添加 Beckhoff.TwinCAT.Ads NuGet 引用
|
||||
├── 新建 BeckhoffPlcClient 实现 IPlcClient
|
||||
└── 基本连接/读/写功能验证
|
||||
|
||||
阶段 3:信号体系和服务层适配(约半天)
|
||||
├── 倍福 XML 信号定义格式设计
|
||||
├── XmlSignalParser 增加倍福解析分支
|
||||
├── PlcService 初始化分支(倍福跳过批量读取定时器)
|
||||
└── SignalDataService 路由逻辑适配(倍福统一单点读取)
|
||||
|
||||
阶段 4:集成验证(约半天)
|
||||
├── 端到端功能测试(读/写/连接状态/断线重连)
|
||||
├── 确认单点读取性能满足业务需求
|
||||
└── 上层模块(MotionControl)透明切换验证
|
||||
```
|
||||
|
||||
### 倍福侧配合要求
|
||||
|
||||
1. **TwinCAT 项目配置**
|
||||
- 定义规范化的全局变量列表(GVL),如 `GVL_HMI`、`GVL_Process`、`GVL_Safety`
|
||||
- 确保变量符号下载已开启:Project → Settings → PLC Settings → 勾选 "Download Symbol Descriptions"
|
||||
- 确认 ADS 路由配置正确,远程 PC 的 AMS Net ID 已在 TwinCAT Router 中注册
|
||||
|
||||
2. **网络环境**
|
||||
- 倍福 ADS 默认端口 48898 需要防火墙放行
|
||||
- 如果运行环境未安装 TwinCAT XAE/XAR,需额外部署 `Beckhoff.TwinCAT.Ads.TcpRouter` 包作为独立 ADS 路由
|
||||
|
||||
3. **变量命名规范建议**
|
||||
- 前缀约定:`GVL_HMI.` 用于与上位机交互的变量
|
||||
- 布尔类型以 `b` 开头,整数以 `n` 开头,浮点以 `f` 开头,字符串以 `s` 开头
|
||||
- 与信号定义 XML 中的 `SymbolPath` 保持完全一致
|
||||
|
||||
### 运行环境要求
|
||||
|
||||
| 环境 | 要求 |
|
||||
|------|------|
|
||||
| 已安装 TwinCAT XAE/XAR | 直接可用,ADS Router 由系统服务提供 |
|
||||
| 未安装 TwinCAT | 需引用 `Beckhoff.TwinCAT.Ads.TcpRouter` NuGet 包并在启动时初始化 TCP Router |
|
||||
|
||||
未安装 TwinCAT 时的初始化代码:
|
||||
|
||||
```csharp
|
||||
// 在应用启动时初始化 ADS TCP Router(仅在未安装 TwinCAT 的环境需要)
|
||||
// Initialize ADS TCP Router at app startup (only needed when TwinCAT is not installed)
|
||||
using TwinCAT.Ads.TcpRouter;
|
||||
|
||||
// 在 App.xaml.cs 或 Bootstrapper 中
|
||||
AmsTcpIpRouter.Start(); // 启动内嵌 ADS Router
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 风险与注意事项 | Risks & Notes
|
||||
|
||||
1. **ReadValueAsync<T> 泛型约束**:`AdsClient.ReadValueAsync<T>` 要求 T 为 unmanaged 类型或有特定约束,string 类型需要使用 `ReadAnyAsync` 或非泛型重载方式读取,具体需在实现阶段验证 API 行为。
|
||||
|
||||
2. **NuGet 包大小**:`Beckhoff.TwinCAT.Ads` 包及依赖约 5-8MB,会增加最终部署包体积。
|
||||
|
||||
3. **向后兼容**:所有改动保持西门子模式完全不受影响。信号定义 XML 不带 `PlcType` 属性时默认走西门子解析路径。
|
||||
|
||||
4. **PlcDataBlock 和字节序**:倍福模式下完全不使用 `PlcDataBlock`(大端字节解析器),该类仅服务于西门子批量缓存场景,因此字节序差异不构成问题。
|
||||
|
||||
5. **IPlcClient.ReadBytesAsync / WriteBytesAsync**:这两个方法在倍福模式下基本不会被调用(无批量读取定时器),但为保持接口兼容性仍需实现。可使用 ADS IndexGroup/IndexOffset 方式提供基本实现,或在内部抛出 `NotSupportedException` 并在调用层做好判断。
|
||||
|
||||
---
|
||||
|
||||
**文档版本 | Document Version**: 2.0
|
||||
**创建日期 | Created**: 2026-06-04
|
||||
**最后更新 | Last Updated**: 2026-06-04
|
||||
**作者 | Author**: XplorePlane Development Team
|
||||
Reference in New Issue
Block a user