将Feature/XP.Common和Feature/XP.Hardware分支合并至Develop/XP.forHardwareAndCommon,完善XPapp注册和相关硬件类库通用类库功能。
This commit is contained in:
@@ -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>
|
||||
@@ -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.
|
||||
-->
|
||||
@@ -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 系列 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
|
||||
Reference in New Issue
Block a user