将Feature/XP.Common和Feature/XP.Hardware分支合并至Develop/XP.forHardwareAndCommon,完善XPapp注册和相关硬件类库通用类库功能。

This commit is contained in:
QI Mingxuan
2026-04-16 17:31:13 +08:00
parent 6ec4c3ddaa
commit 2bd6e566c3
581 changed files with 74600 additions and 222 deletions
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<!-- PLC 连接配置 | PLC Connection Configuration -->
<add key="Plc:IpAddress" value="192.168.1.100" />
<add key="Plc:Port" value="102" />
<add key="Plc:Rack" value="0" />
<add key="Plc:Slot" value="1" />
<!-- PlcType 可选值 | Available values: S200Smart, S300, S400, S1200, S1500 -->
<add key="Plc:PlcType" value="S1200" />
<!-- 批量读取 DB 块配置 | Bulk Read DB Block Configuration -->
<!--
ReadDbBlock: 周期性批量读取的 DB 块标识(如 "DB1"
信号地址落在 [ReadStartAddress, ReadStartAddress+ReadLength-1] 范围内时走缓存读取
其他 DB 块或超出范围的信号自动走单点读取
ReadDbBlock: DB block identifier for periodic bulk read (e.g. "DB1")
Signals within [ReadStartAddress, ReadStartAddress+ReadLength-1] use cache read
Other DB blocks or out-of-range signals use single-point read automatically
-->
<add key="Plc:ReadDbBlock" value="DB1" />
<add key="Plc:ReadStartAddress" value="200" />
<add key="Plc:ReadLength" value="200" />
<!--
BulkReadIntervalMs: 批量读取周期(毫秒),控制 PLC 数据缓存刷新频率
BulkReadIntervalMs: Bulk read interval (ms), controls PLC data cache refresh frequency
-->
<add key="Plc:BulkReadIntervalMs" value="100" />
<!--
注意:WriteDbBlock 配置项已移除。
写入地址由信号所属 Group 的 DBNumber 自动决定,无需单独配置。
Note: WriteDbBlock config has been removed.
Write address is determined automatically by the signal's Group DBNumber.
-->
<!-- PLC 超时配置 | PLC Timeout Configuration -->
<add key="Plc:ConnectTimeoutMs" value="5000" />
<add key="Plc:ReadTimeoutMs" value="3000" />
<add key="Plc:WriteTimeoutMs" value="3000" />
<!-- 自动重连(每 5 秒检测连接状态)| Auto Reconnection (checks every 5 seconds) -->
<add key="Plc:bReConnect" value="true" />
<!--
信号定义文件路径(可选,默认为运行目录下的 PlcAddrDfn.xml
Signal definition file path (optional, defaults to PlcAddrDfn.xml in app directory)
-->
<!-- <add key="PlcAddrDfnXmlPath" value="C:\Config\PlcAddrDfn.xml" /> -->
</appSettings>
</configuration>
+445
View File
@@ -0,0 +1,445 @@
# PLC 模块使用指南 | PLC Module Usage Guide
## 1. 模块注册 | Module Registration
PLC 模块已在 `App.xaml.cs` 中注册,通过 Prism 的模块化系统自动加载。
```csharp
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
moduleCatalog.AddModule<PLCModule>();
base.ConfigureModuleCatalog(moduleCatalog);
}
```
### DI 注册清单 | DI Registration Summary
PLCModule 在 `RegisterTypes` 中注册以下服务:
| 服务 | 生命周期 | 说明 |
|------|---------|------|
| `IPlcClient``S7PlcClient` | 瞬态 | 每次 Resolve 创建独立实例 |
| `PlcService` | 单例 | PLC 连接管理 + 批量读取缓存 + 统一信号字典 |
| `IPlcService``PlcService` | 单例(同实例) | 跨模块 PLC 连接状态查询接口 |
| `PlcWriteQueue` | 单例 | 后台单线程顺序写入队列 |
| `ISignalDataService``SignalDataService` | 单例 | 信号数据交互服务(对外接口) |
| `XmlSignalParser` | 瞬态 | XML 信号定义解析器 |
| `ConfigLoader` | 瞬态 | 配置加载器 |
## 2. 配置文件设置 | Configuration File Setup
`App.config` 中添加 PLC 连接配置:
```xml
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<!-- PLC 连接配置 | PLC Connection Configuration -->
<add key="Plc:IpAddress" value="192.168.1.100" />
<add key="Plc:Port" value="102" />
<add key="Plc:Rack" value="0" />
<add key="Plc:Slot" value="1" />
<add key="Plc:PlcType" value="S1200" />
<!-- 批量读取 DB 块配置 | Bulk Read DB Block Configuration -->
<!-- ReadDbBlock: 周期性批量读取的 DB 块,地址落在此范围内的信号走缓存读取 -->
<!-- ReadDbBlock: DB block for periodic bulk read; signals within this range use cache -->
<add key="Plc:ReadDbBlock" value="DB1" />
<add key="Plc:ReadStartAddress" value="200" />
<add key="Plc:ReadLength" value="200" />
<!-- PLC 超时配置 | PLC Timeout Configuration -->
<add key="Plc:ConnectTimeoutMs" value="3000" />
<add key="Plc:ReadTimeoutMs" value="3000" />
<add key="Plc:WriteTimeoutMs" value="3000" />
<!-- 自动重连(每 5 秒检测连接状态)| Auto Reconnection (checks every 5 seconds) -->
<add key="Plc:bReConnect" value="true" />
</appSettings>
</configuration>
```
> **注意**`WriteDbBlock` 配置项已移除。写入地址由信号所属 Group 的 `DBNumber` 自动决定。
### 支持的 PLC 类型 | Supported PLC Types
- `S200Smart` - S7-200Smart 系列
- `S300` - S7-300 系列
- `S400` - S7-400 系列
- `S1200` - S7-1200 系列
- `S1500` - S7-1500 系列
## 3. 信号定义文件格式 | Signal Definition File Format
`PlcAddrDfn.xml` 采用 `<Group>` 分组结构,每个 Group 对应一个 DB 块:
```xml
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Config>
<!-- 批量读取 DB(高频缓存信号)-->
<Group ID="SignalList_Read" DBNumber="1">
<Signal Name="PlcLive" Type="byte" StartAddr="200" IndexOrLength="" Remark="PLC 心跳" />
<Signal Name="SystemReady" Type="bool" StartAddr="221" IndexOrLength="1" Remark="系统就绪" />
</Group>
<!-- 写入 DB -->
<Group ID="SignalList_Write" DBNumber="31">
<Signal Name="SoftLive" Type="byte" StartAddr="0" IndexOrLength="" Remark="软件心跳" />
<Signal Name="StartCmd" Type="bool" StartAddr="221" IndexOrLength="0" Remark="启动命令" />
<Signal Name="DeviceName" Type="string" StartAddr="240" IndexOrLength="20" Remark="设备名称" />
</Group>
<!-- 其他 DB(单点读取)-->
<Group ID="Status" DBNumber="100">
<Signal Name="ScanMode" Type="byte" StartAddr="201" IndexOrLength="" Remark="扫描模式" />
</Group>
</Config>
```
### XML 格式说明 | XML Format Notes
| 节点/属性 | 说明 |
|-----------|------|
| `<Group ID="...">` | 分组唯一标识,不可重复 |
| `<Group DBNumber="...">` | 该组信号所属的 PLC DB 块号(整数) |
| `<Signal Name="...">` | 信号逻辑名称,全局唯一(跨所有 Group) |
| `Type` | 数据类型:bool / byte / short / int / single / double / string |
| `StartAddr` | 起始字节地址(相对于数据块) |
| `IndexOrLength` | bool 类型为位索引(0-7),string 类型为字符串长度,其他类型留空 |
| `Remark` | 备注说明(可选) |
### 信号名称全局唯一性约束 | Global Signal Name Uniqueness
所有 Group 中的信号名称必须全局唯一。`LoadSignalDefinitions` 加载时自动检查,重复时抛出 `PlcException`,包含重复名称和所属 Group ID。
## 4. 信号数据交互服务(推荐方式)| Signal Data Service (Recommended)
### 4.1 概述 | Overview
`ISignalDataService` 是外部模块与 PLC 交互的推荐接口。外部模块仅需通过信号逻辑名称(如 `"PlcLive"``"StartCmd"`)即可完成数据读写,无需关心物理地址和数据类型转换。
该服务提供三类数据交互方式:
1. **自动路由读取**GetValueByName):根据信号 DB 块和地址范围自动选择批量缓存或单点读取
2. **队列写入**EnqueueWrite):将写入任务提交到 PlcWriteQueue,后台顺序处理
3. **直接写入+回读校验**WriteDirectWithVerify):绕过队列,高优先级写入并回读验证
### 4.2 注入 ISignalDataService
```csharp
using XP.Hardware.Plc.Abstractions;
namespace YourNamespace.ViewModels
{
public class YourViewModel : BindableBase
{
private readonly ISignalDataService _signalService;
private readonly ILoggerService _logger;
public YourViewModel(ISignalDataService signalService, ILoggerService logger)
{
_signalService = signalService;
_logger = logger.ForModule<YourViewModel>();
}
}
}
```
### 4.3 读取信号(自动路由)| Read Signal (Auto-Routed)
`GetValueByName` 根据信号的 `DBNumber``StartAddr` 自动选择读取路径:
- **批量缓存路径**`signal.DBNumber == ReadDbBlock 块号``signal.StartAddr``[ReadStartAddress, ReadStartAddress+ReadLength-1]` 范围内
- **单点读取路径**:其他情况(不同 DB 块,或同 DB 块但地址超出缓存范围)
```csharp
// 非泛型读取,返回 object | Non-generic read, returns object
object value = _signalService.GetValueByName("PlcLive");
// 泛型读取,自动类型转换 | Generic read with auto type conversion
byte plcLive = _signalService.GetValueByName<byte>("PlcLive"); // 批量缓存(在范围内)
bool isReady = _signalService.GetValueByName<bool>("SystemReady"); // 批量缓存(在范围内)
byte scanMode = _signalService.GetValueByName<byte>("ScanMode"); // 单点读取(DB100
byte softLive = _signalService.GetValueByName<byte>("SoftLive"); // 单点读取(地址 0 不在缓存范围)
```
**注意事项**
- 批量缓存数据来自 100ms 周期读取,存在最大 100ms 延迟
- 单点读取每次发起 PLC 通讯,适合低频或非批量 DB 的信号
- PLC 未连接或缓存未就绪时抛出 `PlcException`
- 单点读取失败时抛出 `PlcException`(含信号名、DB 块号、错误原因)
### 4.4 队列写入 | Queue Write
写入地址由信号所属 Group 的 `DBNumber` 自动拼接,格式为 `DB{DBNumber}.{StartAddr}`
```csharp
// 写入字节值 | Write byte value
bool enqueued = _signalService.EnqueueWrite("SoftLive", (byte)1);
// 写入布尔值 | Write boolean value
bool enqueued2 = _signalService.EnqueueWrite("StartCmd", true);
// 写入整型值 | Write int value
bool enqueued3 = _signalService.EnqueueWrite("TargetPosition", 5000);
// 写入字符串 | Write string
bool enqueued4 = _signalService.EnqueueWrite("DeviceName", "XplorePlane-CT");
```
**返回值说明**
- `true`:成功入队,等待后台处理
- `false`:队列未运行或已满
### 4.5 直接写入+回读校验 | Direct Write with Verify
```csharp
bool verified = await _signalService.WriteDirectWithVerify("EmergencyStop", true);
if (verified)
{
_logger.Info("紧急停止信号写入成功并已校验 | Emergency stop signal written and verified");
}
else
{
_logger.Error(null, "紧急停止信号写入校验失败 | Emergency stop signal verification failed");
}
```
## 5. 初始化流程 | Initialization Flow
```csharp
public async Task InitializeAsync()
{
// 1. 加载配置 | Load configuration
var config = _configLoader.LoadPlcConfig();
// 2. 连接 PLC(连接成功后自动启动批量读取定时任务)
bool success = await _plcService.InitializeAsync(config);
if (!success) return;
// 3. 加载信号定义文件 | Load signal definition file
// 建立统一信号字典,检查信号名称全局唯一性
_plcService.LoadSignalDefinitions("PlcAddrDfn.xml");
_logger.Info("PLC 初始化完成,信号数据服务就绪 | PLC initialized, signal data service ready");
}
```
**初始化后的自动行为**
- PlcService 以 100ms 周期批量读取 `ReadDbBlock` 指定的 DB 块,更新 Bulk_Read_Cache
- 连接监控每 5 秒检查一次,断开时自动重连(需配置 `bReConnect=true`
## 6. 底层直接读写 | Low-Level Direct Read/Write
PlcService 保留底层泛型读写方法,适用于不通过信号定义的场景:
```csharp
// 泛型读取 | Generic read
bool boolValue = await _plcService.ReadValueAsync<bool>("DB1.0.0");
byte byteValue = await _plcService.ReadValueAsync<byte>("DB1.0");
int intValue = await _plcService.ReadValueAsync<int>("DB1.4");
float floatValue = await _plcService.ReadValueAsync<float>("DB1.8");
// 字符串读写 | String read/write
string strValue = await _plcService.ReadStringAsync("DB1.20", 10);
bool success = await _plcService.WriteStringAsync("DB1.20", "Hello", 10);
// 泛型写入 | Generic write
bool success2 = await _plcService.WriteValueAsync("DB1.4", 12345);
```
## 7. PLC 地址格式说明 | PLC Address Format
使用 `ISignalDataService` 时无需手动拼接地址,地址由 SignalDataService 内部根据信号的 `DBNumber``StartAddr``IndexOrLength` 自动生成:
| 信号类型 | 生成格式 | 示例 |
|---------|---------|------|
| bool | `DB{DBNumber}.{StartAddr}.{IndexOrLength}` | `DB1.221.1` |
| 其他 | `DB{DBNumber}.{StartAddr}` | `DB31.0` |
底层直接读写时的地址格式:
| 数据类型 | 地址格式 | 示例 |
|---------|---------|------|
| bool | `DB{n}.{byte}.{bit}` | `DB1.0.0` |
| byte/short/int/float/double | `DB{n}.{byte}` | `DB1.4` |
| string | `DB{n}.{byte}` | `DB1.20` |
## 8. 异常处理 | Exception Handling
```csharp
using XP.Hardware.Plc.Exceptions;
try
{
var value = _signalService.GetValueByName<int>("SomeSignal");
}
catch (PlcException ex)
{
_logger.Error(ex, "PLC 操作失败: {Message} | PLC operation failed: {Message}", ex.Message);
}
```
### 异常场景汇总 | Exception Scenarios
| 场景 | 方法 | 行为 |
|------|------|------|
| 信号名称不存在 | GetValueByName / EnqueueWrite / WriteDirectWithVerify | 抛出 PlcException |
| 缓存未就绪 | GetValueByName(批量缓存路径) | 抛出 PlcException |
| 单点读取失败 | GetValueByName(单点路径) | 抛出 PlcException(含信号名、DB 块号、错误原因) |
| 类型不兼容 | EnqueueWrite / WriteDirectWithVerify | 抛出 PlcException |
| 信号名称全局重复 | LoadSignalDefinitions | 抛出 PlcException(含重复名称和 Group ID |
| 写入队列未运行 | EnqueueWrite | 返回 false |
| 直接写入通道未连接 | WriteDirectWithVerify | 返回 false |
| 回读值不一致 | WriteDirectWithVerify | 返回 false |
## 9. 在应用退出时关闭连接 | Close Connection on Application Exit
```csharp
protected override void OnExit(ExitEventArgs e)
{
try
{
var plcService = Container.Resolve<PlcService>();
plcService?.Dispose();
}
catch (Exception ex)
{
Log.Error(ex, "PLC 资源释放失败 | Failed to release PLC resources");
}
Log.CloseAndFlush();
base.OnExit(e);
}
```
## 10. 自动重连机制 | Auto-Reconnection Mechanism
- 每 5 秒检查一次连接状态
- 检测到断开时自动尝试重连
- 通过配置 `bReConnect=true` 启用
- 重连状态会更新到 `StatusText` 属性(可绑定到 UI
## 11. 完整示例 | Complete Example
```csharp
using Prism.Commands;
using Prism.Mvvm;
using System.Threading.Tasks;
using XP.Common.Logging.Interfaces;
using XP.Hardware.Plc.Abstractions;
using XP.Hardware.Plc.Exceptions;
using XP.Hardware.Plc.Services;
using XP.Hardware.PLC.Helpers;
namespace YourNamespace.ViewModels
{
public class PlcDemoViewModel : BindableBase
{
private readonly PlcService _plcService;
private readonly ISignalDataService _signalService;
private readonly ConfigLoader _configLoader;
private readonly ILoggerService _logger;
public PlcDemoViewModel(
PlcService plcService,
ISignalDataService signalService,
ConfigLoader configLoader,
ILoggerService logger)
{
_plcService = plcService;
_signalService = signalService;
_configLoader = configLoader;
_logger = logger.ForModule<PlcDemoViewModel>();
}
public async Task InitAsync()
{
var config = _configLoader.LoadPlcConfig();
bool connected = await _plcService.InitializeAsync(config);
if (connected)
{
_plcService.LoadSignalDefinitions("PlcAddrDfn.xml");
}
}
public void ReadSignals()
{
try
{
// PlcLive 在批量读取范围内 → 缓存读取
byte plcLive = _signalService.GetValueByName<byte>("PlcLive");
// ScanMode 在 DB100 → 单点读取
byte scanMode = _signalService.GetValueByName<byte>("ScanMode");
_logger.Info("PlcLive={PlcLive}, ScanMode={ScanMode}", plcLive, scanMode);
}
catch (PlcException ex)
{
_logger.Error(ex, "读取信号失败 | Failed to read signals");
}
}
public void WriteSignal()
{
// 写入地址自动使用 SoftLive 所属 Group 的 DBNumber
bool enqueued = _signalService.EnqueueWrite("SoftLive", (byte)1);
_logger.Debug("SoftLive 入队结果: {Result} | SoftLive enqueue result: {Result}", enqueued);
}
public async Task DirectWriteAsync()
{
bool verified = await _signalService.WriteDirectWithVerify("EmergencyStop", true);
_logger.Info("EmergencyStop 写入校验: {Result} | EmergencyStop write verify: {Result}", verified);
}
}
}
```
## 12. 注意事项 | Notes
1. **线程安全**PlcService 的 Bulk_Read_Cache 使用 lock 保护,GetCacheSnapshot 返回独立副本
2. **异步操作**WriteDirectWithVerify 是异步方法,GetValueByName 和 EnqueueWrite 是同步方法
3. **三通道独立**PlcService、PlcWriteQueue、SignalDataService 各持有独立的 IPlcClient 实例
4. **信号定义加载**:必须在 `InitializeAsync` 成功后调用 `LoadSignalDefinitions`
5. **缓存延迟**:批量缓存路径的数据最大延迟为 100ms;单点读取路径实时但有通讯开销
6. **WriteDbBlock 已移除**:写入地址由信号所属 Group 的 DBNumber 自动决定,无需配置
7. **信号名称全局唯一**:跨 Group 的信号名称不能重复,否则 LoadSignalDefinitions 抛出异常
8. **资源释放**:应用退出时调用 `PlcService.Dispose()` 停止定时任务并释放连接
9. **跨模块连接状态**:其他硬件模块应通过 `IPlcService` 接口查询 PLC 连接状态,而非直接依赖 `PlcService` 具体类
## 13. 故障排查 | Troubleshooting
### 连接失败
- 检查 IP 地址和端口是否正确
- 确认 PLC 型号配置是否匹配
- 验证 Rack 和 Slot 参数
- 检查网络连接和防火墙设置
### GetValueByName 抛出"数据缓存未就绪"
- 确认 PLC 已连接且 InitializeAsync 返回 true
- 等待首次批量读取完成(约 100ms)
- 检查 ReadDbBlock / ReadStartAddress / ReadLength 配置是否正确
### GetValueByName 走单点读取但卡住
- 已通过 `Task.Run` 包装解决 UI 线程死锁问题
- 如仍有问题,检查 PLC 连接状态和网络延迟
### 信号地址超出缓存范围走了单点读取
- 这是正常行为:同 DB 块但地址不在 `[ReadStartAddress, ReadStartAddress+ReadLength-1]` 范围内的信号自动走单点读取
- 如需走缓存,调整 ReadStartAddress 和 ReadLength 使信号地址落在范围内
### 信号名称未找到
- 确认已调用 `LoadSignalDefinitions` 加载信号定义文件
- 检查 PlcAddrDfn.xml 中是否包含该信号名称(`<Signal Name="...">`
- 注意信号名称区分大小写
### 信号名称重复导致加载失败
- 检查 PlcAddrDfn.xml 中是否有跨 Group 的重名信号
- 信号名称必须在所有 Group 中全局唯一
### EnqueueWrite 返回 false
- 检查 PlcWriteQueue 是否已启动(IsRunning
- 确认队列未满(默认容量 1000
### WriteDirectWithVerify 返回 false
- 检查直接写入通道的连接状态
- 查看日志中的具体错误信息(写入失败 / 回读失败 / 回读值不一致)
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Config>
<!-- 组 1: 通用高频通讯参数-软件读取 DB -->
<Group ID="SignalList_ReadCommon" DBNumber="1">
<Signal Name="SoftLive" Type="byte" StartAddr="0" IndexOrLength="" Remark="软件心跳" />
<Signal Name="EmergencyStop" Type="byte" StartAddr="5" IndexOrLength="" Remark="急停" />
</Group>
<!-- 组 2: 通用高频通讯参数-软件写入 DB -->
<Group ID="SignalList_WriteCommon" DBNumber="31">
<Signal Name="ProbeA" Type="single" StartAddr="190" IndexOrLength="" Remark="测座角度A" />
<Signal Name="ProbeB" Type="string" StartAddr="194" IndexOrLength="20" Remark="测座角度B" />
</Group>
<!-- 组 3: 圆周扫描-软件读写 DB -->
<Group ID="Status" DBNumber="100">
<Signal Name="ScanMode" Type="byte" StartAddr="201" IndexOrLength="" Remark="扫描模式" />
</Group>
</Config>
<!--
信号定义文件说明 | Signal Definition File Note
==================================================
PLC 信号地址定义文件 PlcAddrDfn.xml 采用 <Group> 分组结构,每个 Group 对应一个 DB 块。
通过 PlcService.LoadSignalDefinitions("PlcAddrDfn.xml") 加载后,
外部模块可通过 ISignalDataService 使用信号名称进行读写操作。
示例格式 | Example format:
<Config>
<Group ID="SignalList_Read" DBNumber="1">
<Signal Name="PlcLive" Type="byte" StartAddr="200" IndexOrLength="" Remark="PLC 心跳" />
</Group>
<Group ID="SignalList_Write" DBNumber="31">
<Signal Name="SoftLive" Type="byte" StartAddr="0" IndexOrLength="" Remark="软件心跳" />
</Group>
</Config>
The PlcAddrDfn.xml uses <Group> structure, each Group maps to one DB block.
Load via PlcService.LoadSignalDefinitions("PlcAddrDfn.xml").
External modules use ISignalDataService to read/write by signal name.
-->
+346
View File
@@ -0,0 +1,346 @@
# XP.Hardware.PLC
工业 PLC 通讯模块 | Industrial PLC Communication Module
---
## 项目概述 | Project Overview
XP.Hardware.PLC 是 XplorePlane X 射线检测系统的 PLC 通讯模块,提供与西门子 S7 系列 PLC 的实时数据交互能力。该模块采用模块化设计,支持多种 PLC 型号,提供统一的异步读写接口、自动重连机制,以及基于信号逻辑名称的高级数据交互服务。
### 主要特性 | Key Features
- 支持西门子 S7 系列 PLCS200Smart/S300/S400/S1200/S1500
- 异步读写操作,不阻塞 UI 线程
- 自动连接监控和断线重连
- 类型安全的泛型读写接口
- **信号数据交互服务(ISignalDataService**:通过 XML 信号逻辑名称读写 PLC 数据,业务模块无需关心物理地址
- **跨模块连接状态接口(IPlcService)**:供其他硬件模块查询 PLC 连接状态,支持 INotifyPropertyChanged
- **多 DB 块支持**:XML 信号配置基于 `<Group>` 分组结构,每个 Group 对应一个 DB 块,支持同时读写多个 DB 块
- **自动读取路由**:信号地址落在批量读取范围内走缓存解析,否则自动走单点读取
- **周期性批量读取缓存(Bulk_Read_Cache**PlcService 以 100ms 周期批量读取指定 DB 块并缓存,供高频信号解析
- **写入队列(PlcWriteQueue)**:后台单线程顺序写入,避免并发冲突
- **直接写入+回读校验**:高优先级写入通道,绕过队列并回读验证数据准确性
- XML 信号地址定义解析(XmlSignalParser)和数据块解析(PlcDataBlock
- 完整的日志记录和异常处理
- 基于 Prism 的依赖注入和模块化架构
---
## 框架架构 | Architecture
```
XP.Hardware.PLC/
├── Abstractions/ # 抽象层 | Abstraction Layer
│ ├── IPlcClient.cs # PLC 客户端接口
│ ├── IPlcService.cs # PLC 服务接口(跨模块连接状态查询)
│ └── ISignalDataService.cs # 信号数据交互服务接口(对外暴露)
├── Core/ # 核心实现 | Core Implementation
│ └── S7PlcClient.cs # 西门子 S7 PLC 客户端
├── Services/ # 服务层 | Service Layer
│ ├── PlcService.cs # PLC 业务服务(单例):连接管理 + 批量读取缓存 + 统一信号字典,实现 IPlcService
│ ├── PlcWriteQueue.cs # PLC 写入队列(单例):后台单线程顺序写入
│ └── SignalDataService.cs # 信号数据交互服务实现(单例):自动路由读取 / 队列写入 / 直接写入+回读
├── Configs/ # 配置层 | Configuration Layer
│ └── PlcConfig.cs # PLC 配置模型(已移除 WriteDbBlock
├── Models/ # 数据模型 | Data Models
│ ├── SignalEntry.cs # 信号条目模型(Name, Type, StartAddr, IndexOrLength, GroupId, DBNumber
│ └── SignalGroup.cs # 信号分组模型(GroupId, DBNumber, Signals
├── Helpers/ # 辅助工具 | Helpers
│ ├── ConfigLoader.cs # 配置加载器
│ ├── PlcHelper.cs # PLC 工具类
│ ├── PlcDataBlock.cs # PLC 数据块解析器(大端字节序)
│ ├── XmlSignalParser.cs # XML 信号地址定义解析器(Group 分组格式)
│ └── HexFormatter.cs # 十六进制格式化工具
├── Exceptions/ # 异常定义 | Exceptions
│ └── PlcException.cs # PLC 自定义异常
├── Resources/ # 多语言资源 | Localization Resources
├── ViewModels/ # 视图模型 | View Models
├── Views/ # WPF 视图 | WPF Views
├── Documents/ # 文档 | Documentation
└── PLCModule.cs # Prism 模块注册
```
### 三通道架构 | Three-Channel Architecture
模块采用三个独立的 IPlcClient 实例(S7PlcClient),各自管理独立的 PLC 连接,避免并发竞争:
| 通道 | 持有者 | 用途 |
|------|--------|------|
| 主通讯通道 | PlcService | 周期性批量读取(100ms 间隔) |
| 写入队列通道 | PlcWriteQueue | 后台单线程顺序写入 |
| 直接写入通道 | SignalDataService | 高优先级写入+回读校验 |
### 设计模式 | Design Patterns
- **策略模式**`IPlcClient` 接口支持多种 PLC 实现
- **单例模式**`PlcService``PlcWriteQueue``SignalDataService` 作为全局单例
- **依赖注入**:通过 Prism 容器管理服务生命周期
- **异步模式**:所有 I/O 操作采用 async/await
- **生产者-消费者模式**PlcWriteQueue 使用 BlockingCollection 实现队列写入
---
## 核心功能 | Core Features
### 1. 连接管理 | Connection Management
```csharp
// 初始化连接 | Initialize connection
var config = _configLoader.LoadPlcConfig();
bool success = await _plcService.InitializeAsync(config);
// 加载信号定义(连接成功后)| Load signal definitions (after connection)
_plcService.LoadSignalDefinitions("PlcAddrDfn.xml");
// 自动重连(配置启用后)| Auto-reconnect (when enabled)
// 每 5 秒检测连接状态,断开时自动重连
```
### 2. 信号数据交互(推荐方式)| Signal Data Interaction (Recommended)
通过 `ISignalDataService` 接口,外部模块仅需使用信号逻辑名称即可完成读写,无需关心物理地址:
```csharp
// 注入 ISignalDataService | Inject ISignalDataService
private readonly ISignalDataService _signalService;
// 读取信号:自动路由(批量缓存 or 单点读取)| Read signal: auto-routed (cache or single-point)
object value = _signalService.GetValueByName("PlcLive");
byte plcLive = _signalService.GetValueByName<byte>("PlcLive");
bool isReady = _signalService.GetValueByName<bool>("SystemReady");
// 队列写入:提交到写入队列,后台顺序处理 | Queue write: enqueue for background sequential processing
bool enqueued = _signalService.EnqueueWrite("SoftLive", (byte)1);
bool enqueued2 = _signalService.EnqueueWrite("TargetVoltage", 150);
// 直接写入+回读校验:高优先级,绕过队列 | Direct write + read-back verify: high priority, bypasses queue
bool verified = await _signalService.WriteDirectWithVerify("EmergencyStop", true);
```
### 3. 底层直接读写 | Low-Level Direct Read/Write
PlcService 仍保留底层泛型读写方法,适用于不通过信号定义的场景:
```csharp
// 泛型读取 | Generic read
int intValue = await _plcService.ReadValueAsync<int>("DB1.4");
string strValue = await _plcService.ReadStringAsync("DB1.20", 10);
// 泛型写入 | Generic write
bool success = await _plcService.WriteValueAsync("DB1.4", 12345);
bool success2 = await _plcService.WriteStringAsync("DB1.20", "Hello", 10);
```
### 4. 状态监控 | Status Monitoring
```csharp
// 连接状态(可绑定到 UI| Connection status (bindable to UI)
bool isConnected = _plcService.IsConnected;
// 状态文本(可绑定到 UI| Status text (bindable to UI)
string statusText = _plcService.StatusText;
```
---
## 信号定义文件 | Signal Definition File
信号地址通过 `PlcAddrDfn.xml` 文件定义,采用 `<Group>` 分组结构,由 `XmlSignalParser` 解析:
```xml
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Config>
<!-- 组 1: 批量读取 DB(高频缓存信号)-->
<Group ID="SignalList_Read" DBNumber="1">
<Signal Name="PlcLive" Type="byte" StartAddr="200" IndexOrLength="" Remark="PLC 心跳" />
<Signal Name="SystemReady" Type="bool" StartAddr="221" IndexOrLength="1" Remark="系统就绪" />
</Group>
<!-- 组 2: 写入 DB -->
<Group ID="SignalList_Write" DBNumber="31">
<Signal Name="SoftLive" Type="byte" StartAddr="0" IndexOrLength="" Remark="软件心跳" />
<Signal Name="StartCmd" Type="bool" StartAddr="221" IndexOrLength="0" Remark="启动命令" />
<Signal Name="DeviceName" Type="string" StartAddr="240" IndexOrLength="20" Remark="设备名称" />
</Group>
<!-- 组 3: 其他 DB(单点读取)-->
<Group ID="Status" DBNumber="100">
<Signal Name="ScanMode" Type="byte" StartAddr="201" IndexOrLength="" Remark="扫描模式" />
</Group>
</Config>
```
### 读取路由规则 | Read Routing Rules
`GetValueByName` 根据以下条件自动选择读取路径:
| 条件 | 路径 |
|------|------|
| `signal.DBNumber == ReadDbBlock 块号``signal.StartAddr``[ReadStartAddress, ReadStartAddress+ReadLength-1]` 范围内 | 批量缓存读取(无 PLC 通讯) |
| 其他情况(不同 DB 块,或同 DB 块但地址超出缓存范围) | 单点读取(`IPlcClient.ReadAsync<T>` |
### 支持的信号类型 | Supported Signal Types
| SignalEntry.Type | CLR 类型 | 字节长度 | IndexOrLength 含义 |
|-----------------|----------|---------|-------------------|
| `bool` | bool | 1 bit | 位索引(0-7 |
| `byte` | byte | 1 | 不使用 |
| `short` | short | 2 | 不使用 |
| `int` | int | 4 | 不使用 |
| `single` | float | 4 | 不使用 |
| `double` | double | 8 | 不使用 |
| `string` | string | 可变 | 字符串长度(字节数) |
### 信号名称全局唯一性 | Global Signal Name Uniqueness
所有 Group 中的信号名称必须全局唯一。`LoadSignalDefinitions` 加载时会检查,重复时抛出 `PlcException`
---
## 技术要求 | Technical Requirements
### 运行环境 | Runtime Environment
- **.NET 8.0** (net8.0-windows7.0)
- **Windows 操作系统**WPF 依赖)
- **Visual Studio 2022** 或更高版本
### 核心依赖 | Core Dependencies
| 依赖库 | 版本 | 用途 |
|--------|------|------|
| **Prism.Wpf** | 9.0.537 | MVVM 框架和依赖注入 |
| **HslCommunication** | - | 西门子 PLC 通讯库 |
| **Serilog** | - | 结构化日志记录(通过 XP.Common) |
### PLC 硬件要求 | PLC Hardware Requirements
- 支持的 PLC 型号:S7-200Smart / S7-300 / S7-400 / S7-1200 / S7-1500
- 网络连接:TCP/IP(默认端口 102)
- PLC 需开启 S7 通讯功能
---
## 快速开始 | Quick Start
### 1. 配置文件设置
参见 [App.config.example](./App.config.example)
### 2. 注册模块
```csharp
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
moduleCatalog.AddModule<PLCModule>();
}
```
### 3. 使用信号数据服务(推荐)
```csharp
public class YourViewModel : BindableBase
{
private readonly ISignalDataService _signalService;
public YourViewModel(ISignalDataService signalService)
{
_signalService = signalService;
}
public byte ReadPlcLive()
{
// 自动路由:PlcLive 在批量读取范围内走缓存,否则走单点读取
return _signalService.GetValueByName<byte>("PlcLive");
}
public bool WriteSoftLive(byte value)
{
return _signalService.EnqueueWrite("SoftLive", value);
}
}
```
### 4. 跨模块查询 PLC 连接状态
其他硬件模块可通过 `IPlcService` 接口查询 PLC 连接状态,无需依赖 `PlcService` 具体类:
```csharp
using XP.Hardware.Plc.Abstractions;
public class YourHardwareService
{
private readonly IPlcService _plcService;
public YourHardwareService(IPlcService plcService)
{
_plcService = plcService;
}
public void DoWork()
{
// 检查 PLC 连接状态 | Check PLC connection status
if (!_plcService.IsConnected) return;
// 监听连接状态变化(INotifyPropertyChanged| Listen for connection changes
_plcService.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(IPlcService.IsConnected))
{
// 连接状态变化处理 | Handle connection change
}
};
}
}
```
---
## 异常处理 | Exception Handling
模块使用自定义 `PlcException` 异常,覆盖以下场景:
| 场景 | 行为 |
|------|------|
| 信号名称未找到 | 抛出 PlcException |
| 数据缓存未就绪 | 抛出 PlcException |
| 单点读取失败 | 抛出 PlcException(含信号名、DB 块号、错误原因) |
| 类型转换失败 | 抛出 PlcException |
| 信号名称全局重复 | LoadSignalDefinitions 抛出 PlcException |
| 批量读取失败 | 保留上次缓存,记录 Error 日志 |
| 写入队列已满/已停止 | EnqueueWrite 返回 false |
| 直接写入通道未连接 | WriteDirectWithVerify 返回 false |
| 回读值与写入值不一致 | WriteDirectWithVerify 返回 false |
---
## 日志记录 | Logging
- **Debug**:信号读取结果(含路由路径)、队列写入详情
- **Info**:连接成功、信号定义加载、直接写入校验结果
- **Warn**:连接断开、重连尝试、队列未运行
- **Error**:连接失败、批量读取失败、单点读取失败、类型转换失败、回读校验失败
---
## 资源释放 | Resource Disposal
```csharp
protected override void OnExit(ExitEventArgs e)
{
var plcService = Container.Resolve<PlcService>();
plcService?.Dispose(); // 停止批量读取定时任务、清空缓存、释放连接
base.OnExit(e);
}
```
---
## 文档索引 | Documentation Index
- **[GUIDENCE.md](./GUIDENCE.md)** - 详细使用指南,包含完整代码示例
- **[App.config.example](./App.config.example)** - 配置文件示例
- **[README.md](./README.md)** - 本文档,项目概述和快速参考
---
**最后更新 | Last Updated**: 2026-04-14