将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
+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