欢迎光临
我们一直在努力

C# + OPC UA:实现跨品牌国产PLC互联与数据交互

上个月接手了一个智能仓储改造项目,现场有12台PLC,涵盖了信捷XD5、汇川H5U、台达AS300三个主流国产品牌。之前的方案是每个品牌单独写一套通信协议,代码冗余不说,维护起来简直是噩梦——改一个数据点要改三个地方,出了问题要分别排查三套逻辑,光是调试通信就花了两周时间。

痛定思痛,我决定彻底重构通信层,采用OPC UA统一标准。只用了5天时间,就完成了所有PLC的接入,代码量减少了70%,而且通信稳定性大幅提升。更重要的是,以后再新增任何支持OPC UA的设备,只需要配置一下节点地址,不用写一行通信代码。

今天就把这套经过量产验证的跨品牌PLC互联方案完整分享出来,从OPC UA核心原理、国产PLC服务配置到.NET 8客户端开发、统一通信框架设计,全程都是工业现场踩出来的干货,看完就能直接解决你项目中的多设备互联难题。

一、为什么OPC UA是跨品牌互联的唯一选择

在OPC UA普及之前,工业现场的跨设备互联一直是个老大难问题。每个PLC厂商都有自己的私有协议,而且互不兼容。

传统互联方案的三大痛点

  • 协议碎片化严重:信捷用XCP协议,汇川用H3U协议,台达用AS协议,每个协议都要单独开发和维护,开发成本极高
  • 功能不一致:不同协议支持的数据类型、指令集、安全机制都不一样,上层业务逻辑需要做大量适配
  • 扩展性差:新增一个品牌的设备,就要新增一套通信代码,系统复杂度呈指数级增长
  • OPC UA的核心优势

    OPC UA(Open Platform Communications Unified Architecture)是工业4.0的标准通信协议,它彻底解决了设备互联的标准化问题:

    • 统一标准:所有支持OPC UA的设备,不管是什么品牌,都用同一种方式通信
    • 跨平台:支持Windows、Linux、ARM等所有主流平台
    • 安全可靠:内置加密、认证、授权机制,满足工业级安全要求
    • 自描述:设备可以自动描述自己的节点结构和数据类型,客户端可以自动发现
    • 丰富的数据类型:支持布尔、整数、浮点数、字符串、数组、结构体等所有复杂数据类型

    主流国产PLC OPC UA支持情况

    好消息是,现在几乎所有主流国产PLC都已经原生支持OPC UA,不需要额外购买授权:

    品牌系列最低固件版本默认端口备注
    信捷 XD/XDH/XDM V3.5.0 4840 需手动开启服务
    汇川 H3U/H5U/AM400 V2.5.0 4840 需添加OPC UA组件
    台达 AS300/AS500/DVP50MC V1.6.0 4840 支持匿名和用户名认证
    施耐德 M241/M251 V4.0.0 4840 内置OPC UA服务器
    西门子 S7-1200/1500 V4.0.0 4840 需在TIA中配置

    二、主流国产PLC OPC UA服务配置

    这是整个方案中最容易踩坑的部分,官方文档大多写得非常简略,很多关键步骤都没提到。我整理了三个最常用国产PLC的OPC UA配置步骤,照着做就能成功。

    2.1 信捷XD/XDH系列配置

  • 打开XCP Pro编程软件,连接PLC
  • 进入【工程管理】→【PLC系统设置】→【以太网设置】
  • 勾选【启用OPC UA服务器】
  • 端口号保持默认4840
  • 关键设置:安全策略选择【无】(生产环境建议选择Basic256Sha256)
  • 勾选【允许匿名登录】(调试用,生产环境建议开启用户名认证)
  • 下载配置到PLC,断电重启生效
  • 注意:信捷PLC的OPC UA节点地址格式为ns=2;s=D100,其中D100就是寄存器地址,M区的节点地址为ns=2;s=M100。

    2.2 汇川H3U/H5U系列配置

  • 打开InoProShop编程软件,连接PLC
  • 在【设备】窗口中右键点击PLC,选择【添加组件】
  • 找到【OPC UA服务器】,点击添加
  • 双击添加的OPC UA服务器组件,进入配置界面
  • 勾选【启用服务器】,端口号保持默认4840
  • 在【安全策略】中勾选【None】和【Basic256Sha256】
  • 在【用户管理】中添加用户名和密码
  • 下载配置到PLC,重启生效
  • 注意:汇川PLC的节点地址格式为ns=3;s=%"MX100.0",其中MX100.0表示M100.0位,%DW100表示D100寄存器。

    2.3 台达AS300系列配置

  • 打开ISPSoft编程软件,连接PLC
  • 进入【系统规划】→【通信设置】→【以太网】
  • 勾选【启用OPC UA服务器】
  • 端口号保持默认4840
  • 在【安全设置】中选择【无安全策略】
  • 勾选【允许匿名访问】
  • 下载配置到PLC,重启生效
  • 注意:台达PLC的节点地址格式为ns=2;s=Device1.D100,其中Device1是设备名称,D100是寄存器地址。

    三、.NET 8 OPC UA客户端开发基础

    我们使用OPC基金会官方提供的.NET库OPC.Ua.Client,这是目前最稳定、最可靠的OPC UA客户端库,完全符合OPC UA标准。

    3.1 NuGet包安装

    Install-Package OPC.Ua.Client
    Install-Package OPC.Ua.Core
    Install-Package OPC.Ua.Configuration

    3.2 连接OPC UA服务器

    public class OpcUaClient : IDisposable
    {
    private ApplicationConfiguration _config;
    private Session _session;

    public bool IsConnected => _session?.Connected ?? false;

    public async Task<bool> ConnectAsync(string endpointUrl, string username = null, string password = null)
    {
    try
    {
    // 创建应用配置
    _config = new ApplicationConfiguration
    {
    ApplicationName = "IndustrialOpcUaClient",
    ApplicationType = ApplicationType.Client,
    SecurityConfiguration = new SecurityConfiguration
    {
    AutoAcceptUntrustedCertificates = true, // 调试用,生产环境建议关闭
    RejectSHA1SignedCertificates = false
    },
    TransportConfigurations = new TransportConfigurationCollection(),
    TransportQuotas = new TransportQuotas { OperationTimeout = 15000 }
    };

    // 验证配置
    await _config.Validate(ApplicationType.Client);

    // 获取端点
    var endpoint = CoreClientUtils.SelectEndpoint(endpointUrl, useSecurity: false);

    // 创建用户身份
    IUserIdentity identity = username != null
    ? new UserNameIdentity(username, password)
    : new AnonymousIdentity();

    // 创建会话
    _session = await Session.Create(
    _config,
    new ConfiguredEndpoint(null, endpoint, EndpointConfiguration.Create(_config)),
    updateBeforeConnect: false,
    keepAliveInterval: 5000,
    identity,
    null);

    return _session.Connected;
    }
    catch (Exception ex)
    {
    Console.WriteLine($"连接OPC UA服务器失败: {ex.Message}");
    return false;
    }
    }

    public void Disconnect()
    {
    _session?.Close();
    _session?.Dispose();
    _session = null;
    }

    public void Dispose()
    {
    Disconnect();
    }
    }

    3.3 读取和写入变量

    // 读取单个变量
    public async Task<T> ReadValueAsync<T>(string nodeId)
    {
    if (!IsConnected)
    throw new InvalidOperationException("未连接到OPC UA服务器");

    var node = new NodeId(nodeId);
    var value = await _session.ReadValueAsync(node);

    return (T)Convert.ChangeType(value.Value, typeof(T));
    }

    // 写入单个变量
    public async Task WriteValueAsync<T>(string nodeId, T value)
    {
    if (!IsConnected)
    throw new InvalidOperationException("未连接到OPC UA服务器");

    var node = new NodeId(nodeId);
    var writeValue = new WriteValue
    {
    NodeId = node,
    AttributeId = Attributes.Value,
    Value = new DataValue(new Variant(value))
    };

    var response = await _session.WriteAsync(null, new[] { writeValue });

    if (response.Results[0] != StatusCodes.Good)
    throw new Exception($"写入失败: {response.Results[0]}");
    }

    // 批量读取变量
    public async Task<Dictionary<string, object>> BatchReadAsync(IEnumerable<string> nodeIds)
    {
    if (!IsConnected)
    throw new InvalidOperationException("未连接到OPC UA服务器");

    var readValues = nodeIds.Select(id => new ReadValueId
    {
    NodeId = new NodeId(id),
    AttributeId = Attributes.Value
    }).ToList();

    var response = await _session.ReadAsync(null, 0, TimestampsToReturn.Neither, readValues);

    var result = new Dictionary<string, object>();
    for (int i = 0; i < readValues.Count; i++)
    {
    result[nodeIds.ElementAt(i)] = response.Results[i].Value;
    }

    return result;
    }

    3.4 订阅数据变化

    public async Task<Subscription> SubscribeAsync(string nodeId, Action<object> callback, int publishingInterval = 100)
    {
    if (!IsConnected)
    throw new InvalidOperationException("未连接到OPC UA服务器");

    // 创建订阅
    var subscription = new Subscription(_session.DefaultSubscription)
    {
    PublishingInterval = publishingInterval
    };

    _session.AddSubscription(subscription);
    await subscription.CreateAsync();

    // 添加监控项
    var monitoredItem = new MonitoredItem
    {
    StartNodeId = new NodeId(nodeId),
    AttributeId = Attributes.Value,
    SamplingInterval = publishingInterval
    };

    monitoredItem.Notification += (_, e) =>
    {
    if (e.Notification is DataChangeNotification dataChange)
    {
    callback(dataChange.MonitoredItems[0].Value.Value);
    }
    };

    subscription.AddItem(monitoredItem);
    await subscription.ApplyChangesAsync();

    return subscription;
    }

    四、跨品牌PLC统一通信框架设计

    虽然OPC UA是统一标准,但不同品牌PLC的节点地址格式和数据类型还是有差异。我们需要设计一个抽象层,彻底屏蔽这些差异,让上层业务逻辑完全不需要关心底层是哪个品牌的PLC。

    4.1 整体架构

    #mermaid-svg-0WcOuaqZ3ikPUNJg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0WcOuaqZ3ikPUNJg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0WcOuaqZ3ikPUNJg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0WcOuaqZ3ikPUNJg .error-icon{fill:#552222;}#mermaid-svg-0WcOuaqZ3ikPUNJg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0WcOuaqZ3ikPUNJg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0WcOuaqZ3ikPUNJg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0WcOuaqZ3ikPUNJg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0WcOuaqZ3ikPUNJg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0WcOuaqZ3ikPUNJg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0WcOuaqZ3ikPUNJg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0WcOuaqZ3ikPUNJg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0WcOuaqZ3ikPUNJg .marker.cross{stroke:#333333;}#mermaid-svg-0WcOuaqZ3ikPUNJg svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0WcOuaqZ3ikPUNJg p{margin:0;}#mermaid-svg-0WcOuaqZ3ikPUNJg .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-0WcOuaqZ3ikPUNJg .cluster-label text{fill:#333;}#mermaid-svg-0WcOuaqZ3ikPUNJg .cluster-label span{color:#333;}#mermaid-svg-0WcOuaqZ3ikPUNJg .cluster-label span p{background-color:transparent;}#mermaid-svg-0WcOuaqZ3ikPUNJg .label text,#mermaid-svg-0WcOuaqZ3ikPUNJg span{fill:#333;color:#333;}#mermaid-svg-0WcOuaqZ3ikPUNJg .node rect,#mermaid-svg-0WcOuaqZ3ikPUNJg .node circle,#mermaid-svg-0WcOuaqZ3ikPUNJg .node ellipse,#mermaid-svg-0WcOuaqZ3ikPUNJg .node polygon,#mermaid-svg-0WcOuaqZ3ikPUNJg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0WcOuaqZ3ikPUNJg .rough-node .label text,#mermaid-svg-0WcOuaqZ3ikPUNJg .node .label text,#mermaid-svg-0WcOuaqZ3ikPUNJg .image-shape .label,#mermaid-svg-0WcOuaqZ3ikPUNJg .icon-shape .label{text-anchor:middle;}#mermaid-svg-0WcOuaqZ3ikPUNJg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0WcOuaqZ3ikPUNJg .rough-node .label,#mermaid-svg-0WcOuaqZ3ikPUNJg .node .label,#mermaid-svg-0WcOuaqZ3ikPUNJg .image-shape .label,#mermaid-svg-0WcOuaqZ3ikPUNJg .icon-shape .label{text-align:center;}#mermaid-svg-0WcOuaqZ3ikPUNJg .node.clickable{cursor:pointer;}#mermaid-svg-0WcOuaqZ3ikPUNJg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0WcOuaqZ3ikPUNJg .arrowheadPath{fill:#333333;}#mermaid-svg-0WcOuaqZ3ikPUNJg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0WcOuaqZ3ikPUNJg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0WcOuaqZ3ikPUNJg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0WcOuaqZ3ikPUNJg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0WcOuaqZ3ikPUNJg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0WcOuaqZ3ikPUNJg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0WcOuaqZ3ikPUNJg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0WcOuaqZ3ikPUNJg .cluster text{fill:#333;}#mermaid-svg-0WcOuaqZ3ikPUNJg .cluster span{color:#333;}#mermaid-svg-0WcOuaqZ3ikPUNJg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0WcOuaqZ3ikPUNJg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0WcOuaqZ3ikPUNJg rect.text{fill:none;stroke-width:0;}#mermaid-svg-0WcOuaqZ3ikPUNJg .icon-shape,#mermaid-svg-0WcOuaqZ3ikPUNJg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0WcOuaqZ3ikPUNJg .icon-shape p,#mermaid-svg-0WcOuaqZ3ikPUNJg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0WcOuaqZ3ikPUNJg .icon-shape .label rect,#mermaid-svg-0WcOuaqZ3ikPUNJg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0WcOuaqZ3ikPUNJg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0WcOuaqZ3ikPUNJg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0WcOuaqZ3ikPUNJg :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    上层业务逻辑

    统一PLC接口 IPlcClient

    OPC UA实现层 OpcUaPlcClient

    品牌适配层

    信捷PLC适配器

    汇川PLC适配器

    台达PLC适配器

    信捷OPC UA服务器

    汇川OPC UA服务器

    台达OPC UA服务器

    连接管理器

    数据缓存管理器

    断线重连管理器

    4.2 抽象PLC接口定义

    public interface IPlcClient : IDisposable
    {
    bool IsConnected { get; }
    event Action<bool> ConnectionStatusChanged;

    Task<bool> ConnectAsync(CancellationToken cancellationToken = default);
    void Disconnect();

    Task<T> ReadAsync<T>(string tagName);
    Task WriteAsync<T>(string tagName, T value);
    Task<Dictionary<string, object>> BatchReadAsync(IEnumerable<string> tagNames);

    Task<IDisposable> SubscribeAsync<T>(string tagName, Action<T> callback, int intervalMs = 100);
    }

    4.3 品牌适配器实现

    适配器的作用是将通用的标签名转换为对应品牌的OPC UA节点地址,并处理数据类型转换。

    public interface IPlcBrandAdapter
    {
    string GetNodeId(string tagName);
    object ConvertValue(object value, Type targetType);
    }

    // 信捷PLC适配器
    public class XinJeAdapter : IPlcBrandAdapter
    {
    public string GetNodeId(string tagName)
    {
    // 标签名格式:D100, M100, Y0, X0
    return $"ns=2;s={tagName}";
    }

    public object ConvertValue(object value, Type targetType)
    {
    // 信捷的布尔值直接是bool类型,不需要转换
    return Convert.ChangeType(value, targetType);
    }
    }

    // 汇川PLC适配器
    public class InovanceAdapter : IPlcBrandAdapter
    {
    public string GetNodeId(string tagName)
    {
    // 标签名格式:D100, M100.0
    if (tagName.StartsWith("M") && tagName.Contains("."))
    {
    return $"ns=3;s=%\\"MX{tagName.Substring(1)}\\"";
    }
    else if (tagName.StartsWith("D"))
    {
    return $"ns=3;s=%\\"DW{tagName.Substring(1)}\\"";
    }

    throw new ArgumentException($"不支持的标签名格式: {tagName}");
    }

    public object ConvertValue(object value, Type targetType)
    {
    // 汇川的布尔值是byte类型,需要转换
    if (targetType == typeof(bool) && value is byte b)
    {
    return b != 0;
    }

    return Convert.ChangeType(value, targetType);
    }
    }

    // 台达PLC适配器
    public class DeltaAdapter : IPlcBrandAdapter
    {
    public string GetNodeId(string tagName)
    {
    // 标签名格式:D100, M100
    return $"ns=2;s=Device1.{tagName}";
    }

    public object ConvertValue(object value, Type targetType)
    {
    return Convert.ChangeType(value, targetType);
    }
    }

    4.4 OPC UA统一客户端实现

    public class OpcUaPlcClient : IPlcClient
    {
    private readonly OpcUaClient _client;
    private readonly IPlcBrandAdapter _adapter;
    private readonly string _endpointUrl;
    private readonly string _username;
    private readonly string _password;
    private readonly ConnectionManager _connectionManager;

    public bool IsConnected => _client.IsConnected;
    public event Action<bool> ConnectionStatusChanged;

    public OpcUaPlcClient(string endpointUrl, PlcBrand brand, string username = null, string password = null)
    {
    _endpointUrl = endpointUrl;
    _username = username;
    _password = password;
    _client = new OpcUaClient();

    _adapter = brand switch
    {
    PlcBrand.XinJe => new XinJeAdapter(),
    PlcBrand.Inovance => new InovanceAdapter(),
    PlcBrand.Delta => new DeltaAdapter(),
    _ => throw new ArgumentException($"不支持的PLC品牌: {brand}")
    };

    _connectionManager = new ConnectionManager(this);
    _connectionManager.ConnectionStatusChanged += (status) => ConnectionStatusChanged?.Invoke(status);
    }

    public async Task<bool> ConnectAsync(CancellationToken cancellationToken = default)
    {
    return await _connectionManager.ConnectAsync(cancellationToken);
    }

    internal async Task<bool> InternalConnectAsync()
    {
    return await _client.ConnectAsync(_endpointUrl, _username, _password);
    }

    public void Disconnect()
    {
    _connectionManager.Stop();
    _client.Disconnect();
    }

    public async Task<T> ReadAsync<T>(string tagName)
    {
    var nodeId = _adapter.GetNodeId(tagName);
    var value = await _client.ReadValueAsync<object>(nodeId);
    return (T)_adapter.ConvertValue(value, typeof(T));
    }

    public async Task WriteAsync<T>(string tagName, T value)
    {
    var nodeId = _adapter.GetNodeId(tagName);
    await _client.WriteValueAsync(nodeId, value);
    }

    public async Task<Dictionary<string, object>> BatchReadAsync(IEnumerable<string> tagNames)
    {
    var nodeIds = tagNames.Select(_adapter.GetNodeId).ToList();
    var values = await _client.BatchReadAsync(nodeIds);

    var result = new Dictionary<string, object>();
    for (int i = 0; i < tagNames.Count(); i++)
    {
    var tagName = tagNames.ElementAt(i);
    var value = values[nodeIds[i]];
    result[tagName] = _adapter.ConvertValue(value, typeof(object));
    }

    return result;
    }

    public async Task<IDisposable> SubscribeAsync<T>(string tagName, Action<T> callback, int intervalMs = 100)
    {
    var nodeId = _adapter.GetNodeId(tagName);
    return await _client.SubscribeAsync(nodeId, value =>
    callback((T)_adapter.ConvertValue(value, typeof(T))), intervalMs);
    }

    public void Dispose()
    {
    Disconnect();
    _client.Dispose();
    }
    }

    public enum PlcBrand
    {
    XinJe,
    Inovance,
    Delta
    }

    4.5 断线重连与状态管理

    public class ConnectionManager
    {
    private readonly OpcUaPlcClient _client;
    private PeriodicTimer _healthCheckTimer;
    private CancellationTokenSource _cts;
    private bool _isReconnecting;

    public event Action<bool> ConnectionStatusChanged;

    public ConnectionManager(OpcUaPlcClient client)
    {
    _client = client;
    }

    public async Task<bool> ConnectAsync(CancellationToken cancellationToken = default)
    {
    _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

    bool success = await _client.InternalConnectAsync();
    ConnectionStatusChanged?.Invoke(success);

    if (success)
    {
    StartHealthCheck();
    }

    return success;
    }

    private void StartHealthCheck()
    {
    _healthCheckTimer = new PeriodicTimer(TimeSpan.FromSeconds(5));

    _ = HealthCheckLoopAsync(_cts.Token);
    }

    private async Task HealthCheckLoopAsync(CancellationToken cancellationToken)
    {
    try
    {
    while (await _healthCheckTimer.WaitForNextTickAsync(cancellationToken))
    {
    if (!_client.IsConnected && !_isReconnecting)
    {
    ConnectionStatusChanged?.Invoke(false);
    await ReconnectAsync(cancellationToken);
    }
    }
    }
    catch (OperationCanceledException)
    {
    // 正常取消
    }
    }

    private async Task ReconnectAsync(CancellationToken cancellationToken)
    {
    _isReconnecting = true;
    int delay = 1000;

    while (!cancellationToken.IsCancellationRequested)
    {
    Console.WriteLine($"尝试重连,延迟{delay}ms");
    await Task.Delay(delay, cancellationToken);

    if (await _client.InternalConnectAsync())
    {
    Console.WriteLine("重连成功");
    ConnectionStatusChanged?.Invoke(true);
    _isReconnecting = false;
    return;
    }

    delay = Math.Min(delay * 2, 30000);
    }

    _isReconnecting = false;
    }

    public void Stop()
    {
    _cts?.Cancel();
    _healthCheckTimer?.Dispose();
    }
    }

    五、实战:多PLC数据采集与跨设备联动

    现在我们用上面的框架,实现一个同时连接三个不同品牌PLC的例子,采集它们的运行状态,并实现跨PLC联动控制。

    // 创建三个PLC客户端
    var xinJeClient = new OpcUaPlcClient("opc.tcp://192.168.1.100:4840", PlcBrand.XinJe);
    var inovanceClient = new OpcUaPlcClient("opc.tcp://192.168.1.101:4840", PlcBrand.Inovance);
    var deltaClient = new OpcUaPlcClient("opc.tcp://192.168.1.102:4840", PlcBrand.Delta);

    // 连接所有PLC
    await Task.WhenAll(
    xinJeClient.ConnectAsync(),
    inovanceClient.ConnectAsync(),
    deltaClient.ConnectAsync()
    );

    Console.WriteLine("所有PLC连接成功");

    // 订阅信捷PLC的运行状态
    await xinJeClient.SubscribeAsync<bool>("M0", isRunning =>
    {
    Console.WriteLine($"信捷PLC运行状态: {isRunning}");

    // 跨PLC联动:信捷PLC启动时,同时启动汇川和台达PLC
    if (isRunning)
    {
    _ = inovanceClient.WriteAsync("M0", true);
    _ = deltaClient.WriteAsync("M0", true);
    }
    });

    // 批量读取所有PLC的产量数据
    while (true)
    {
    var xinJeOutput = await xinJeClient.ReadAsync<int>("D100");
    var inovanceOutput = await inovanceClient.ReadAsync<int>("D100");
    var deltaOutput = await deltaClient.ReadAsync<int>("D100");

    Console.WriteLine($"总产量: {xinJeOutput + inovanceOutput + deltaOutput}");

    await Task.Delay(1000);
    }

    六、OPC UA踩坑实录(国产PLC专属)

    问题1:连接失败,提示"安全策略不支持"

    原因:不同品牌PLC支持的安全策略不同,很多国产PLC默认只支持None安全策略。

    解决方案:

    • 在客户端配置中明确指定使用None安全策略
    • 生产环境建议升级PLC固件到最新版本,支持Basic256Sha256安全策略

    问题2:读取变量返回BadNodeIdUnknown

    原因:节点地址格式不正确,这是最常见的问题。

    解决方案:

    • 使用UaExpert工具连接PLC,浏览节点树,确认正确的节点地址
    • 检查品牌适配器中的节点地址转换逻辑是否正确

    问题3:订阅一段时间后自动断开

    原因:OPC UA会话超时,或者网络不稳定。

    解决方案:

    • 增加会话超时时间
    • 实现自动重连机制
    • 适当减小订阅的发布间隔

    问题4:批量读取超过100个变量返回错误

    原因:信捷和汇川的OPC UA服务器对单次批量读取的变量数量有限制。

    解决方案:

    • 将批量读取请求拆分成多个小请求,每次最多读取50个变量
    • 增加请求之间的间隔时间

    问题5:数据类型转换错误

    原因:不同品牌PLC的相同数据类型在OPC UA中的表示方式不同。

    解决方案:

    • 在品牌适配器中统一处理数据类型转换
    • 使用UaExpert工具查看变量的实际数据类型

    七、总结

    OPC UA彻底解决了工业现场跨品牌设备互联的难题,配合.NET 8的高性能和易用性,我们可以快速开发出稳定、可扩展的工业控制系统。

    核心要点回顾:

  • 现在所有主流国产PLC都已经原生支持OPC UA,不需要额外授权
  • 不同品牌PLC的OPC UA节点地址格式和数据类型有差异,需要用适配器模式统一
  • 必须实现自动断线重连和健康检查机制,确保系统稳定运行
  • 批量读取时要注意PLC的限制,避免单次请求过大
  • 生产环境建议开启OPC UA的安全认证和加密功能
  • 最后提醒大家,OPC UA是一个非常强大的协议,本文只介绍了最常用的功能。它还支持方法调用、事件订阅、历史数据访问等高级功能,可以满足各种复杂的工业应用需求。

    赞(0)
    未经允许不得转载:171主机测评 » C# + OPC UA:实现跨品牌国产PLC互联与数据交互
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址