Merged PR 140: 新增对倍福Beckoff PLC的调研集成文档,为集成倍福PLC做准备。

新增对倍福Beckoff PLC的调研集成文档,为集成倍福PLC做准备。
This commit is contained in:
QI Mingxuan
2026-06-30 15:01:24 +08:00
committed by SONG Tian
@@ -0,0 +1,895 @@
# 倍福 Beckhoff PLC 适配指南
## 概述 | Overview
本文档描述在 XP.Hardware.PLC 模块中增加对倍福(BeckhoffTwinCAT 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