347 lines
13 KiB
Markdown
347 lines
13 KiB
Markdown
# XP.Hardware.PLC
|
||
|
||
工业 PLC 通讯模块 | Industrial PLC Communication Module
|
||
|
||
---
|
||
|
||
## 项目概述 | Project Overview
|
||
|
||
XP.Hardware.PLC 是 XplorePlane X 射线检测系统的 PLC 通讯模块,提供与西门子 S7 系列 PLC 的实时数据交互能力。该模块采用模块化设计,支持多种 PLC 型号,提供统一的异步读写接口、自动重连机制,以及基于信号逻辑名称的高级数据交互服务。
|
||
|
||
### 主要特性 | Key Features
|
||
|
||
- 支持西门子 S7 系列 PLC(S200Smart/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
|