# PLC 模块使用指南 | PLC Module Usage Guide ## 1. 模块注册 | Module Registration PLC 模块已在 `App.xaml.cs` 中注册,通过 Prism 的模块化系统自动加载。 ```csharp protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) { moduleCatalog.AddModule(); 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 ``` > **注意**:`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 对应一个 DB 块: ```xml ``` ### XML 格式说明 | XML Format Notes | 节点/属性 | 说明 | |-----------|------| | `` | 分组唯一标识,不可重复 | | `` | 该组信号所属的 PLC DB 块号(整数) | | `` | 信号逻辑名称,全局唯一(跨所有 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(); } } } ``` ### 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("PlcLive"); // 批量缓存(在范围内) bool isReady = _signalService.GetValueByName("SystemReady"); // 批量缓存(在范围内) byte scanMode = _signalService.GetValueByName("ScanMode"); // 单点读取(DB100) byte softLive = _signalService.GetValueByName("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("DB1.0.0"); byte byteValue = await _plcService.ReadValueAsync("DB1.0"); int intValue = await _plcService.ReadValueAsync("DB1.4"); float floatValue = await _plcService.ReadValueAsync("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("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?.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(); } 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("PlcLive"); // ScanMode 在 DB100 → 单点读取 byte scanMode = _signalService.GetValueByName("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 中是否包含该信号名称(``) - 注意信号名称区分大小写 ### 信号名称重复导致加载失败 - 检查 PlcAddrDfn.xml 中是否有跨 Group 的重名信号 - 信号名称必须在所有 Group 中全局唯一 ### EnqueueWrite 返回 false - 检查 PlcWriteQueue 是否已启动(IsRunning) - 确认队列未满(默认容量 1000) ### WriteDirectWithVerify 返回 false - 检查直接写入通道的连接状态 - 查看日志中的具体错误信息(写入失败 / 回读失败 / 回读值不一致)