Files

17 KiB
Raw Permalink Blame History

PLC 模块使用指南 | PLC Module Usage Guide

1. 模块注册 | Module Registration

PLC 模块已在 App.xaml.cs 中注册,通过 Prism 的模块化系统自动加载。

protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
    moduleCatalog.AddModule<PLCModule>();
    base.ConfigureModuleCatalog(moduleCatalog);
}

DI 注册清单 | DI Registration Summary

PLCModule 在 RegisterTypes 中注册以下服务:

服务 生命周期 说明
IPlcClientS7PlcClient 瞬态 每次 Resolve 创建独立实例
PlcService 单例 PLC 连接管理 + 批量读取缓存 + 统一信号字典
IPlcServicePlcService 单例(同实例) 跨模块 PLC 连接状态查询接口
PlcWriteQueue 单例 后台单线程顺序写入队列
ISignalDataServiceSignalDataService 单例 信号数据交互服务(对外接口)
XmlSignalParser 瞬态 XML 信号定义解析器
ConfigLoader 瞬态 配置加载器

2. 配置文件设置 | Configuration File Setup

App.config 中添加 PLC 连接配置:

<?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 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.1 概述 | Overview

ISignalDataService 是外部模块与 PLC 交互的推荐接口。外部模块仅需通过信号逻辑名称(如 "PlcLive""StartCmd")即可完成数据读写,无需关心物理地址和数据类型转换。

该服务提供三类数据交互方式:

  1. 自动路由读取GetValueByName):根据信号 DB 块和地址范围自动选择批量缓存或单点读取
  2. 队列写入EnqueueWrite):将写入任务提交到 PlcWriteQueue,后台顺序处理
  3. 直接写入+回读校验WriteDirectWithVerify):绕过队列,高优先级写入并回读验证

4.2 注入 ISignalDataService

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 根据信号的 DBNumberStartAddr 自动选择读取路径:

  • 批量缓存路径signal.DBNumber == ReadDbBlock 块号signal.StartAddr[ReadStartAddress, ReadStartAddress+ReadLength-1] 范围内
  • 单点读取路径:其他情况(不同 DB 块,或同 DB 块但地址超出缓存范围)
// 非泛型读取,返回 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}

// 写入字节值 | 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

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

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 保留底层泛型读写方法,适用于不通过信号定义的场景:

// 泛型读取 | 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 内部根据信号的 DBNumberStartAddrIndexOrLength 自动生成:

信号类型 生成格式 示例
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

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

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

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

  • 检查直接写入通道的连接状态
  • 查看日志中的具体错误信息(写入失败 / 回读失败 / 回读值不一致)