446 lines
17 KiB
Markdown
446 lines
17 KiB
Markdown
# 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
|
||
- 检查直接写入通道的连接状态
|
||
- 查看日志中的具体错误信息(写入失败 / 回读失败 / 回读值不一致)
|