Files
XplorePlane/XP.Hardware.PLC/Documents/GUIDENCE.md
T

446 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
- 检查直接写入通道的连接状态
- 查看日志中的具体错误信息(写入失败 / 回读失败 / 回读值不一致)