C# 使用OPCUA 与CODESYS进行标签通讯

目录

[1.导出的标签 识别标签名称](#1.导出的标签 识别标签名称)

2.引用OPCUA的包

3.读写方法的封装

4.完整的业务模块封装


1.导出的标签 识别标签名称

从CODESYS导出使用标签通讯的模块文档

大概是这样子的

XML 复制代码
<?xml version="1.0" encoding="utf-8"?>
<Symbolconfiguration xmlns="http://www.3s-software.com/schemas/Symbolconfiguration.xsd">
  <Header>
    <Version>3.5.14.0</Version>
    <SymbolConfigObject version="4.5.1.0" runtimeid="3.5.18.40" libversion="4.5.0.0" compiler="3.5.20.0" lmm="3.5.20.0" profile="CODESYS V3.5 SP20+" settings="SupportOPCUA, XmlIncludeComments, LayoutCalculator=OptimizedClientSideLayoutCalculator" />
    <ProjectInfo name="CheckWeek1" devicename="Device" appname="Application" />
  </Header>
  <TypeList>
    <TypeSimple name="T_BOOL" size="1" swapsize="0" typeclass="Bool" iecname="BOOL" />
    <TypeSimple name="T_REAL" size="4" swapsize="4" typeclass="Real" iecname="REAL" />
    <TypeSimple name="T_STRING" size="81" swapsize="0" typeclass="String" iecname="STRING" />
  </TypeList>
  <NodeList>
    <Node name="Application">
      <Node name="GVL">
        <Comment>{attribute 'qualified_only'}</Comment>
        <Node name="Button01" type="T_BOOL" access="ReadWrite" />
        <Node name="Button02" type="T_BOOL" access="ReadWrite" />
        <Node name="now_wei" type="T_REAL" access="ReadWrite">
          <Comment>当前重量值</Comment>
        </Node>
        <Node name="overweight" type="T_BOOL" access="ReadWrite">
          <Comment>超重</Comment>
        </Node>
        <Node name="pass" type="T_BOOL" access="ReadWrite">
          <Comment>合格</Comment>
        </Node>
        <Node name="print_wei" type="T_REAL" access="ReadWrite">
          <Comment>显示重量</Comment>
        </Node>
        <Node name="Reset_ON" type="T_BOOL" access="ReadWrite" />
        <Node name="underweight" type="T_BOOL" access="ReadWrite">
          <Comment>欠重</Comment>
        </Node>
        <Node name="检重结果" type="T_STRING" access="ReadWrite" />
      </Node>
      <Node name="PersistentVars">
        <Comment>{attribute 'qualified_only'}</Comment>
        <Node name="over_wei" type="T_REAL" access="ReadWrite">
          <Comment>上限</Comment>
        </Node>
        <Node name="pass_wei" type="T_REAL" access="ReadWrite" />
        <Node name="under_wei" type="T_REAL" access="ReadWrite">
          <Comment>下限</Comment>
        </Node>
        <Node name="合格数" type="T_REAL" access="ReadWrite" />
        <Node name="总数" type="T_REAL" access="ReadWrite" />
        <Node name="欠重数" type="T_REAL" access="ReadWrite" />
        <Node name="超重数" type="T_REAL" access="ReadWrite" />
      </Node>
    </Node>
  </NodeList>
</Symbolconfiguration>

进行通讯的标签 包含在NodeList 节点下的Node标签

标签有层级关系,下面的当前重量值,标签索引就是 GVL.now_wei

<Node name="GVL">
<Node name="now_wei" type="T_REAL" access="ReadWrite">
<Comment>当前重量值</Comment>
</Node>

</Node>

但实际还有前缀和sn,前缀跟设备型号有关,每台设备都可能不一样,最后完整的标签信息是:

ns=4;s=|var|Sinsegye x86_64-Linux-SM-CNC.Application.GVL.now_wei

还有要注意类型:

type="T_BOOL" 对应的就是Bool类型

type="T_REAL" 对应的就是float类型

类型不一致,会导致读写出现异常

2.引用OPCUA的包

Nuget 搜索Opc.Ua.Client 找到: OPCFoundation.NetStandard.Opc.Ua.Client

然后安装合适自己程序的版本

我的程序目标框架是.NET Framework 4.6.1 ; OPCUA安装的版本是1.5.376.235

3.读写方法的封装

先定义一个读写通用的数据模型,方便数据交互

cs 复制代码
  /// <summary>
  /// 节点数据模型
  /// 读写共用
  /// </summary>
  public class NodeModel
  {
      public NodeModel() { }
      public NodeModel(string name, ushort index) { Name = name; Index = index; }
      public NodeModel(string name, ushort index, object value) { Name = name; Index = index; Value = value; }
      /// <summary>
      /// 标签名
      /// </summary>
      public string Name { get; set; }
      /// <summary>
      /// NS 索引值
      /// </summary>
      public ushort Index { get; set; }
      /// <summary>
      /// 写入的值
      /// </summary>
      public object Value { get; set; }
  }

先建立连接-OPCUA

Application 的名字自行定义,需要提供OPC的IP和端口,才能进行连接

cs 复制代码
        private ISession _session;
        public ISessionFactory SessionFactory { get; set; } = DefaultSessionFactory.Instance;
        /// <summary>
        /// 建立连接
        /// </summary>
        /// <returns></returns>
        public async Task<BaseResult> Connect()
        {
            try
            {
                if (_session != null && _session.Connected)
                {
                    return BaseResult.Successed;
                }
                var endpointUrl = $"opc.tcp://{_ip}:{_port}";
                //var endpointUrl = "opc.tcp://DESKTOP-DGM4E64:53530/OPCUA/SimulationServer";

                // 创建客户端应用程序实例
                var application = new ApplicationInstance
                {
                    ApplicationName = "LSOpcUaClient",
                    ApplicationType = ApplicationType.Client
                };

                // 动态创建配置
                var config = new ApplicationConfiguration
                {
                    ApplicationName = "LSOpcUaClient",
                    ApplicationType = ApplicationType.Client,
                    ApplicationUri = "urn:localhost:LSOpcUaClient",
                    ProductUri = "urn:example.com:LSOpcUaClient",

                    SecurityConfiguration = new SecurityConfiguration
                    {
                        AutoAcceptUntrustedCertificates = true,
                        ApplicationCertificate = new CertificateIdentifier
                        {
                            StoreType = "Directory",
                            StorePath = "./PKI/own",
                            SubjectName = "CN=LSOpcUaClient, O=Example, C=US",
                        },
                        TrustedIssuerCertificates = new CertificateTrustList
                        {
                            StoreType = "Directory",
                            StorePath = "./PKI/issuers"
                        },
                        TrustedPeerCertificates = new CertificateTrustList
                        {
                            StoreType = "Directory",
                            StorePath = "./PKI/trusted"
                        },
                    },
                    ClientConfiguration = new ClientConfiguration
                    {
                        DefaultSessionTimeout = 60000,
                        MinSubscriptionLifetime = 60000,
                    }
                };

                // 验证配置并应用
                await config.Validate(ApplicationType.Client);
                application.ApplicationConfiguration = config;

                // 检查并创建客户端证书
                bool haveAppCertificate = await application.CheckApplicationInstanceCertificate(false, 0);
                if (!haveAppCertificate)
                {
                    return new BaseResult(false, "无法创建客户端证书");
                }

                // 创建并连接会话
                var endpointDescription = CoreClientUtils.SelectEndpoint(endpointUrl, useSecurity: false);
                var endpointConfiguration = EndpointConfiguration.Create(config);
                var endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration);
                endpoint.Configuration.OperationTimeout = 2000;

                _session = await SessionFactory.CreateAsync(config, endpoint, false, false,
                        "urn:DESKTOP-DGM4E64:UnifiedAutomation:UaExpert", 60000, new UserIdentity(), null).ConfigureAwait(false);

                _session.KeepAliveInterval = 30000; // 30秒心跳
                _session.KeepAlive += (sender, e) =>
                {
                    if (e.Status != null && StatusCode.IsBad(e.Status.Code))
                        Connect(); // 触发重连逻辑
                };
                _session.SessionClosing += (s, e) =>
                {
                };


                //Console.WriteLine("成功连接到服务器");
                if (_session.Connected)
                {
                    isConnect = true;
                    return BaseResult.Successed;
                }
                else
                {
                    isConnect = false;
                    return new BaseResult(false, "连接失败");
                }
            }
            catch (Exception ex)
            {
                isConnect = false;
                return new BaseResult(false, "连接失败");
            }
        }

建立连接后,使用连接对象 _session进行读写的操作。

读取单个数据的方法,使用泛型来决定读取数据的类型,读取T_BOOL就传入bool,读取T_REAL就传入float

cs 复制代码
        /// <summary>
        /// 读取单个数据
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="name"></param>
        /// <param name="index"></param>
        /// <returns></returns>
        public async Task<T> Read<T>(NodeModel data)
        {
            if (isConnect)
            {
                var nodeId = NodeId.Parse($"ns={data.Index};s={data.Name}");
                var node = await _session.ReadValueAsync(nodeId);
                return (T)node.Value;
            }
            else
            {
                return default(T);
            }
        }

也可以一次性读取多个标签

cs 复制代码
       /// <summary>
       /// 读取多个数据
       /// </summary>
       /// <typeparam name="T"></typeparam>
       /// <param name="name"></param>
       /// <param name="index"></param>
       /// <returns></returns>
       public async ValueTask<List<T>> ReadMultiple<T>(List<NodeModel> datas)
       {
           if (isConnect)
           {
               List<NodeId> ids = new List<NodeId>();
               foreach (var data in datas)
               {
                   var nodeId = NodeId.Parse($"ns={data.Index};s={data.Name}");
                   ids.Add(nodeId);
               }


               var nodes = await _session.ReadValuesAsync(ids);

               List<T> res = new List<T>();
               foreach (var node in nodes.Item1)
               {
                   res.Add((T)node.Value);
               }
               return res;
           }
           else
           {
               return default(List<T>);
           }
       }

写入数据到标签的方法实现:

cs 复制代码
/// <summary>
/// 通用的写入方法
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
public async Task<BaseResult> Write(NodeModel data)
{
    if (isConnect)
    {
        var requestHeader = new RequestHeader()
        {

        };
        var nodeId = NodeId.Parse($"ns={data.Index};s={data.Name}");
        var writeValue = new WriteValue
        {
            NodeId = nodeId, // 目标节点ID
            AttributeId = 13,
            Value = new DataValue() { Value = data.Value } // 写入值(自动类型转换)
        };

        var writeRequest = new WriteValueCollection();
        writeRequest.Add(writeValue);

        var node = _session.ReadNode(nodeId);
        if (node is VariableNode variableNode)
        {
            if ((variableNode.UserAccessLevel & AccessLevels.CurrentWrite) != 0)
            {
                // 5. 执行写入                
                var writeResponse = await _session.WriteAsync(requestHeader, writeRequest, CancellationToken.None);
                var statusCode = writeResponse.Results[0]; // 获取首个节点的写入状态                

                if (StatusCode.IsGood(statusCode))
                    return BaseResult.Successed;
                else
                    return new BaseResult(false, $"❌ 写入失败: {statusCode}");
            }
            else
            {
                return new BaseResult(false, $"节点 {nodeId} 不可写!");
            }
        }
        else
        {
            return new BaseResult(false, $"节点 {nodeId} 不可写!");
        }
    }
    else
    {
        return new BaseResult(false, "未建立连接");
    }
}

4.完整的业务模块封装

最后,再整合一些自动重连,读写方法的封装等,一个方便调用的OPCUA通讯模块就封装好了,程序中使用就很便捷了

下面提供完整的封装代码:

cs 复制代码
using Google.Protobuf.WellKnownTypes;
using LS.Standard.Data;
using LS.WPF.MVVM;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Shapes;

namespace WPFClient.Controls
{
    public class OPCUAControl
    {
        /// <summary>
        /// OPCUA 通讯
        /// </summary>
        /// <param name="ip">PLC ip</param>
        /// <param name="port">PLC 端口</param>
        /// <param name="name">名称标识</param>
        public OPCUAControl(string ip, int port, string name = "OPCUA")
        {
            _ip = ip;
            _port = port;
            _name = name;
        }
        #region 属性
        //重连的时间间隔
        private int _reconnectTime = 1000;
        //获取数据的间隔
        private int _runTime = 100;
        //避免数据快速丢失,每次发指令循环写入次数
        private int RecycleWriteTime = 1;
        //循环写入的睡眠时间 默认取数据间隔+100ms
        private int RecycleTime { get => 20; }
        private bool isConnect = false;
        private bool _isRunning = false;
        private string _ip;
        private int _port;
        private string _name;
        private string _prefix = "|var|Sinsegye x86_64-Linux-SM-CNC.Application.";//标签前缀  
        private ushort _index = 4;
        private bool isInit = false;
        private ISession _session;
        public ISessionFactory SessionFactory { get; set; } = DefaultSessionFactory.Instance;

        private WeighModel WeighData { get; set; } = new WeighModel();

        /// <summary>
        /// 运行后执行一次的方法
        /// 运行完返回True  返回False会再次执行
        /// </summary>
        public event DelegateRunStart OnRunStart;
        /// <summary>
        /// 委托  启动时运行一次
        /// </summary>
        public delegate bool DelegateRunStart();
        #endregion

        /// <summary>
        /// 建立连接
        /// </summary>
        /// <returns></returns>
        public async Task<BaseResult> Connect()
        {
            try
            {
                if (_session != null && _session.Connected)
                {
                    return BaseResult.Successed;
                }
                var endpointUrl = $"opc.tcp://{_ip}:{_port}";
                //var endpointUrl = "opc.tcp://DESKTOP-DGM4E64:53530/OPCUA/SimulationServer";

                // 创建客户端应用程序实例
                var application = new ApplicationInstance
                {
                    ApplicationName = "LSOpcUaClient",
                    ApplicationType = ApplicationType.Client
                };

                // 动态创建配置
                var config = new ApplicationConfiguration
                {
                    ApplicationName = "LSOpcUaClient",
                    ApplicationType = ApplicationType.Client,
                    ApplicationUri = "urn:localhost:LSOpcUaClient",
                    ProductUri = "urn:example.com:LSOpcUaClient",

                    SecurityConfiguration = new SecurityConfiguration
                    {
                        AutoAcceptUntrustedCertificates = true,
                        ApplicationCertificate = new CertificateIdentifier
                        {
                            StoreType = "Directory",
                            StorePath = "./PKI/own",
                            SubjectName = "CN=LSOpcUaClient, O=Example, C=US",
                        },
                        TrustedIssuerCertificates = new CertificateTrustList
                        {
                            StoreType = "Directory",
                            StorePath = "./PKI/issuers"
                        },
                        TrustedPeerCertificates = new CertificateTrustList
                        {
                            StoreType = "Directory",
                            StorePath = "./PKI/trusted"
                        },
                    },
                    ClientConfiguration = new ClientConfiguration
                    {
                        DefaultSessionTimeout = 60000,
                        MinSubscriptionLifetime = 60000,
                    }
                };

                // 验证配置并应用
                await config.Validate(ApplicationType.Client);
                application.ApplicationConfiguration = config;

                // 检查并创建客户端证书
                bool haveAppCertificate = await application.CheckApplicationInstanceCertificate(false, 0);
                if (!haveAppCertificate)
                {
                    return new BaseResult(false, "无法创建客户端证书");
                }

                // 创建并连接会话
                var endpointDescription = CoreClientUtils.SelectEndpoint(endpointUrl, useSecurity: false);
                var endpointConfiguration = EndpointConfiguration.Create(config);
                var endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration);
                endpoint.Configuration.OperationTimeout = 2000;

                _session = await SessionFactory.CreateAsync(config, endpoint, false, false,
                        "urn:DESKTOP-DGM4E64:UnifiedAutomation:UaExpert", 60000, new UserIdentity(), null).ConfigureAwait(false);

                _session.KeepAliveInterval = 30000; // 30秒心跳
                _session.KeepAlive += (sender, e) =>
                {
                    if (e.Status != null && StatusCode.IsBad(e.Status.Code))
                        Connect(); // 触发重连逻辑
                };
                _session.SessionClosing += (s, e) =>
                {
                };


                //Console.WriteLine("成功连接到服务器");
                if (_session.Connected)
                {
                    isConnect = true;
                    return BaseResult.Successed;
                }
                else
                {
                    isConnect = false;
                    return new BaseResult(false, "连接失败");
                }
            }
            catch (Exception ex)
            {
                isConnect = false;
                return new BaseResult(false, "连接失败");
            }
        }

        /// <summary>
        /// 获取称重数据
        /// </summary>
        /// <returns></returns>
        public WeighModel GetWeighData()
        {
            return WeighData;
        }

        /*

        /// <summary>
        /// 连接到OPC UA服务器
        /// </summary>
        /// <param name="serverUrl">服务器地址 (e.g. opc.tcp://localhost:4840)</param>
        /// <param name="useAuth">是否使用用户名/密码认证</param>
        /// <param name="username">用户名(可选)</param>
        /// <param name="password">密码(可选)</param>
        public async Task ConnectAsync(string serverUrl, bool useAuth = false, string username = "", string password = "")
        {
            try
            {
                // 选择安全策略 (Basic256Sha256 或 None)
                var endpointDescription = CoreClientUtils.SelectEndpoint(serverUrl, useSecurity: false);
                endpointDescription.SecurityPolicyUri = SecurityPolicies.Basic256Sha256;

                // 配置端点
                var endpointConfiguration = EndpointConfiguration.Create(_config);
                var endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration);

                // 设置用户身份
                IUserIdentity identity = useAuth ?
                    new UserIdentity(username, password) :
                    new UserIdentity(new AnonymousIdentityToken());

                // 创建会话
                _session = await Session.Create(
                    configuration: _config,
                    endpoint: endpoint,
                    updateBeforeConnect: true,
                    checkDomain: false,
                    sessionName: "OPCUA_Client_Session",
                    sessionTimeout: SessionTimeout,
                    identity: identity,
                    preferredLocales: null
                );

                Console.WriteLine($"✅ 已连接到服务器: {serverUrl}");
                Console.WriteLine($"🔒 安全策略: {_session.Endpoint.SecurityPolicyUri}");

                // 设置保活回调
                _session.KeepAliveInterval = 5000;
                _session.KeepAlive += (session, e) =>
                {
                    if (e.Status != null && ServiceResult.IsNotGood(e.Status))
                        Console.WriteLine($"⚠️ 连接状态: {e.Status}");
                };
            }
            catch (Exception ex)
            {
                Console.WriteLine($"❌ 连接失败: {ex.Message}");
                throw;
            }
        }

        */

        /// <summary>
        /// 设置一些参数
        /// 在Start之前调用才有效
        /// </summary>
        /// <param name="reconnectTime">重连的时间间隔</param>
        /// <param name="runTime">获取数据的间隔</param>
        /// <param name="recycleWriteTime">避免数据快速丢失,每次发指令循环写入次数</param>
        public void SetProperty(int reconnectTime, int runTime, int recycleWriteTime)
        {
            //重连的时间间隔
            _reconnectTime = reconnectTime;
            //获取数据的间隔
            _runTime = runTime;
            //避免数据快速丢失,每次发指令循环写入次数
            RecycleWriteTime = recycleWriteTime;
        }

        /// <summary>
        /// 判断是否连接上PLC
        /// </summary>
        /// <returns></returns>
        public bool IsConnect()
        {
            return isConnect;
        }

        /// <summary>
        /// 自动运行
        /// </summary>
        private void AutoRun()
        {
            if (isInit)
            {
                return;
            }
            _isRunning = true;
            new Thread(() =>
            {
                while (true)
                {
                    if (!_isRunning)
                        break;
                    //重连
                    RunConnect();
                    Thread.Sleep(_reconnectTime);
                }
            })
            { IsBackground = true }.Start();
            new Thread(() =>
            {
                while (true)
                {
                    if (!_isRunning)
                        break;

                    UpdateData();
                    Thread.Sleep(_runTime);
                }
            })
            { IsBackground = true }.Start();
            new Thread(() =>
            {
                while (true)
                {
                    if (!_isRunning)
                        break;

                    GetStart();
                    Thread.Sleep(_runTime);
                }
            })
            { IsBackground = true }.Start();
            RunStatr();
            isInit = true;
        }

        /// <summary>
        /// 断线重连
        /// </summary>
        private void RunConnect()
        {
            try
            {
                if (!isConnect)
                {
                    if (_session != null)
                    {
                        _session.CloseAsync();
                        _session = null;
                    }
                    if (string.IsNullOrEmpty(this._ip) || _port <= 0)
                    {
                        Thread.Sleep(1000);
                        return;
                    }
                    Start().Wait();
                    if (isConnect)
                    {
                        _reconnectTime = 1000;
                    }
                }
            }
            catch (Exception ex)
            {
                if (isConnect)
                {
                    LogOperate.Error($"[{_name}] 断线重连", ex);
                }
                else
                {
                    _reconnectTime = 10 * 1000;
                }
                isConnect = false;
            }
        }

        /// <summary>
        /// 更新一次数据
        /// </summary>
        private async Task UpdateData()
        {
            try
            {
                if (_session != null && _session.Connected && isConnect)
                {
                    try
                    {
                        NodeModel node = new NodeModel();
                        node.Name = _prefix + "GVL.now_wei";
                        node.Index = _index;
                        var value =await Read<float>(node);
                        WeighData.Weigh = Math.Round(value,3);
                    }
                    catch (Exception ex)
                    {
                        LogOperate.Error("GetWeigh", ex);
                    }
                    try
                    {
                        NodeModel node = new NodeModel();
                        node.Name = _prefix + "GVL.print_wei";
                        node.Index = _index;
                        var value =await Read<float>(node);
                        WeighData.BoxWeigh= Math.Round(value, 3);
                    }
                    catch (Exception ex)
                    {
                        LogOperate.Error("GetBoxWeigh", ex);
                    }
                    try
                    {
                        NodeModel node = new NodeModel();
                        node.Name = _prefix + "GVL.检重结果";
                        node.Index = _index;
                        var value =await Read<string>(node);
                        WeighData.WeighResult= value;
                    }
                    catch (Exception ex)
                    {
                        LogOperate.Error("GetWeighResult", ex);
                    }
                    try
                    {
                        NodeModel node = new NodeModel();
                        node.Name = _prefix + "GVL.overweight";
                        node.Index = _index;
                        var value =await Read<bool>(node);
                        WeighData.IsOverWeight = value;
                    }
                    catch (Exception ex)
                    {
                        LogOperate.Error("IsOverWeight", ex);
                    }
                    try
                    {
                        NodeModel node = new NodeModel();
                        node.Name = _prefix + "GVL.pass";
                        node.Index = _index;
                        var value =await Read<bool>(node);
                        WeighData.IsPass= value;
                    }
                    catch (Exception ex)
                    {
                        LogOperate.Error("IsPass", ex);
                    }
                    try
                    {
                        NodeModel node = new NodeModel();
                        node.Name = _prefix + "GVL.underweight";
                        node.Index = _index;
                        var value =await Read<bool>(node);
                        WeighData.IsUnderWeight= value;
                    }
                    catch (Exception ex)
                    {
                        LogOperate.Error("IsUnderWeight", ex);
                    }
                    try
                    {
                        NodeModel node = new NodeModel();
                        node.Name = _prefix + "PersistentVars.pass_wei";
                        node.Index = _index;
                        var value =await Read<float>(node);
                        WeighData.StandardWeight= Math.Round(value, 2);
                    }
                    catch (Exception ex)
                    {
                        LogOperate.Error("GetStandardWeight", ex);
                    }
                    try
                    {
                        NodeModel node = new NodeModel();
                        node.Name = _prefix + "PersistentVars.over_wei";
                        node.Index = _index;
                        var value = await Read<float>(node);
                        WeighData.UpperLimit= Math.Round(value, 2);
                    }
                    catch (Exception ex)
                    {
                        LogOperate.Error("GetUpperLimit", ex);
                    }
                    try
                    {
                        NodeModel node = new NodeModel();
                        node.Name = _prefix + "PersistentVars.under_wei";
                        node.Index = _index;
                        var value = await Read<float>(node);
                        WeighData.LowerLimit= Math.Round(value, 2);
                    }
                    catch (Exception ex)
                    {
                        LogOperate.Error("GetLowerLimit", ex);
                    }

                }
                else
                {
                    if (isConnect)
                    {
                        LogOperate.Error($"[{_name}] -- 更新数据时通讯 异常");
                        isConnect = false;
                    }
                    Thread.Sleep(1000);
                }
            }
            catch (Exception ex)
            {
                if (isConnect)
                {
                    LogOperate.Error($"[{_name}] -- UpdateData 异常", ex);
                }
                isConnect = false;
                Thread.Sleep(1000);
            }
        }


        /// <summary>
        /// 运行在刚启动的时候
        /// </summary>
        private void RunStatr()
        {
            if (!isInit)
            {
                while (true)
                {
                    try
                    {
                        if (OnRunStart == null)
                        {
                            isInit = true;
                        }
                        else
                        {
                            if (OnRunStart.Invoke())
                            {
                                isInit = true;
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        LogOperate.Error("RunStatr", ex);
                    }
                    if (isInit)
                    {
                        return;
                    }
                    Thread.Sleep(1000);
                }
            }
        }


        public delegate void DelegateOnTrigger();
        public event DelegateOnTrigger OnTrigger;

        #region 方法

        /// <summary>
        /// 打开通讯,并开始轮询数据
        /// </summary>
        /// <returns></returns>
        public async Task<BaseResult> Start()
        {
            try
            {
                LogOperate.General(_name, "开始建立通讯");
                BaseResult res = Stop();
                if (res)
                {
                    //赋值后IP依然为空,则不再进行连接
                    if (string.IsNullOrEmpty(_ip) && _port <= 0)
                    {
                        LogOperate.General(_name, "IP 未配置,不再进行连接");
                        return new BaseResult(false, "IP 未配置,不再进行连接");
                    }

                    res = await Connect();
                }
                if (res)
                {
                    isConnect = true;
                    LogOperate.General(_name, "建立通讯成功");
                    Thread.Sleep(500);
                }
                return res;
            }
            catch (Exception ex)
            {
                LogOperate.Error($"[{_name}]-Start 异常", ex);
                return new BaseResult(false, ex.Message);
            }
            finally
            {
                AutoRun();
            }
        }

        /// <summary>
        /// 停止通讯
        /// </summary>
        /// <returns></returns>
        public BaseResult Stop()
        {
            isConnect = false;
            _isRunning = false;
            Thread.Sleep(20);
            _session?.CloseAsync();
            return BaseResult.Successed;
        }


        #region 业务方法

        private bool isStart = false;
        /// <summary>
        /// 获取两个按钮是否都按下
        /// </summary>
        /// <returns></returns>
        public async Task<bool> GetStart()
        {
            try
            {
                NodeModel node1 = new NodeModel();
                node1.Name = _prefix + "GVL.Button01";
                node1.Index = _index;
                NodeModel node2 = new NodeModel();
                node2.Name = _prefix + "GVL.Button02";
                node2.Index = _index;

                List<NodeModel> nodes = new List<NodeModel>();
                nodes.Add(node1);
                nodes.Add(node2);
                var value = await ReadMultiple<bool>(nodes);
                if (value != null)
                {
                    WeighData.IsStart = !value.Contains(false);
                    if (WeighData.IsStart && !isStart)
                    {
                        isStart = true;
                        OnTrigger?.Invoke();
                    }
                    if (!WeighData.IsStart && isStart)
                    {
                        isStart = false;
                    }
                }
                return WeighData.IsStart;
            }
            catch (Exception ex)
            {
                LogOperate.Error("GetStart", ex);
                return false;
            }
        }

        /// <summary>
        /// 读取当前实时重量
        /// </summary>
        /// <returns></returns>
        public double GetWeigh()
        {
            return WeighData.Weigh;
        }


        /// <summary>
        /// 读取显示重量
        /// 称重重量
        /// </summary>
        /// <returns></returns>
        public double GetBoxWeigh()
        {
            return WeighData.BoxWeigh;
        }

        /// <summary>
        /// 获取减重结果
        /// </summary>
        /// <returns></returns>
        public string GetWeighResult()
        {
            return WeighData.WeighResult;            
        }


        /// <summary>
        /// 获取是否超重
        /// </summary>
        /// <returns></returns>
        public bool IsOverWeight()
        {
            return WeighData.IsOverWeight;
        }

        /// <summary>
        /// 读取是否Pass
        /// </summary>
        /// <returns></returns>
        public bool IsPass()
        {
            return WeighData.IsPass;
        }

        /// <summary>
        /// 读取是否欠重
        /// </summary>
        /// <returns></returns>
        public bool IsUnderWeight()
        {
            return WeighData.IsUnderWeight;
        }

        /// <summary>
        /// 获取标准重量
        /// </summary>
        /// <returns></returns>
        public double GetStandardWeight()
        {
            return WeighData.StandardWeight;
        }

        /// <summary>
        /// 获取上限 公差
        /// </summary>
        /// <returns></returns>
        public double GetUpperLimit()
        {
            return WeighData.UpperLimit;
        }

        /// <summary> 
        /// 获取下限 公差
        /// </summary>
        /// <returns></returns>
        public double GetLowerLimit()
        {
            return WeighData.LowerLimit;
        }

        /// <summary>
        /// 获取标准重量
        /// </summary>
        /// <returns></returns>
        public BaseResult SetStandardWeight(float num)
        {
            try
            {
                NodeModel node = new NodeModel();
                node.Name = _prefix + "PersistentVars.pass_wei";
                node.Index = _index;
                node.Value = num;
                var value = Write(node);
                value.Wait();
                return value.Result;
            }
            catch (Exception ex)
            {
                LogOperate.Error("SetStandardWeight", ex);
                return new BaseResult(false, ex.Message);
            }
        }

        /// <summary>
        /// 设置上限 公差
        /// </summary>
        /// <returns></returns>
        public BaseResult SetUpperLimit(float num)
        {
            try
            {
                NodeModel node = new NodeModel();
                node.Name = _prefix + "PersistentVars.over_wei";
                node.Index = _index;
                node.Value = num;
                var value = Write(node);
                value.Wait();
                return value.Result;
            }
            catch (Exception ex)
            {
                LogOperate.Error("SetUpperLimit", ex);
                return new BaseResult(false,ex.Message);
            }
        }

        /// <summary>
        /// 设置下限 公差
        /// </summary>
        /// <returns></returns>
        public BaseResult SetLowerLimit(float num)
        {
            try
            {
                NodeModel node = new NodeModel();
                node.Name = _prefix + "PersistentVars.under_wei";
                node.Index = _index;
                node.Value = num;
                var value = Write(node);
                value.Wait();
                return value.Result;
            }
            catch (Exception ex)
            {
                LogOperate.Error("SetLowerLimit", ex);
                return new BaseResult(false, ex.Message);
            }
        }


        #endregion


        /// <summary>
        /// 通用的写入方法
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        public async Task<BaseResult> Write(NodeModel data)
        {
            if (isConnect)
            {
                var requestHeader = new RequestHeader()
                {

                };
                var nodeId = NodeId.Parse($"ns={data.Index};s={data.Name}");
                var writeValue = new WriteValue
                {
                    NodeId = nodeId, // 目标节点ID
                    AttributeId = 13,
                    Value = new DataValue() { Value = data.Value } // 写入值(自动类型转换)
                };

                var writeRequest = new WriteValueCollection();
                writeRequest.Add(writeValue);

                var node = _session.ReadNode(nodeId);
                if (node is VariableNode variableNode)
                {
                    if ((variableNode.UserAccessLevel & AccessLevels.CurrentWrite) != 0)
                    {
                        // 5. 执行写入                
                        var writeResponse = await _session.WriteAsync(requestHeader, writeRequest, CancellationToken.None);
                        var statusCode = writeResponse.Results[0]; // 获取首个节点的写入状态                

                        if (StatusCode.IsGood(statusCode))
                            return BaseResult.Successed;
                        else
                            return new BaseResult(false, $"❌ 写入失败: {statusCode}");
                    }
                    else
                    {
                        return new BaseResult(false, $"节点 {nodeId} 不可写!");
                    }
                }
                else
                {
                    return new BaseResult(false, $"节点 {nodeId} 不可写!");
                }
            }
            else
            {
                return new BaseResult(false, "未建立连接");
            }
        }

        /// <summary>
        /// 读取单个数据
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="name"></param>
        /// <param name="index"></param>
        /// <returns></returns>
        public async Task<T> Read<T>(NodeModel data)
        {
            if (isConnect)
            {
                var nodeId = NodeId.Parse($"ns={data.Index};s={data.Name}");
                var node = await _session.ReadValueAsync(nodeId);
                return (T)node.Value;
            }
            else
            {
                return default(T);
            }
        }

        /// <summary>
        /// 读取多个数据
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="name"></param>
        /// <param name="index"></param>
        /// <returns></returns>
        public async ValueTask<List<T>> ReadMultiple<T>(List<NodeModel> datas)
        {
            if (isConnect)
            {
                List<NodeId> ids = new List<NodeId>();
                foreach (var data in datas)
                {
                    var nodeId = NodeId.Parse($"ns={data.Index};s={data.Name}");
                    ids.Add(nodeId);
                }


                var nodes = await _session.ReadValuesAsync(ids);

                List<T> res = new List<T>();
                foreach (var node in nodes.Item1)
                {
                    res.Add((T)node.Value);
                }
                return res;
            }
            else
            {
                return default(List<T>);
            }
        }

        #endregion

    }

    /// <summary>
    /// 节点数据模型
    /// 读写共用
    /// </summary>
    public class NodeModel
    {
        public NodeModel() { }
        public NodeModel(string name, ushort index) { Name = name; Index = index; }
        public NodeModel(string name, ushort index, object value) { Name = name; Index = index; Value = value; }
        /// <summary>
        /// 标签名
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// NS 索引值
        /// </summary>
        public ushort Index { get; set; }
        /// <summary>
        /// 写入的值
        /// </summary>
        public object Value { get; set; }
    }


    /// <summary>
    /// 称重平台数据模型
    /// </summary>
    public class WeighModel
    {
        /// <summary>
        /// 是否启动
        /// </summary>
        public bool IsStart { get; set; }

        /// <summary>
        /// 实时重量
        /// </summary>
        public double Weigh { get; set; }
        /// <summary>
        /// 称重重量
        /// </summary>
        public double BoxWeigh { get; set; }
        /// <summary>
        /// 称重结果
        /// </summary>
        public string WeighResult { get; set; }

        /// <summary>
        /// 是否超重
        /// </summary>
        public bool IsOverWeight { get; set; }

        /// <summary>
        /// 是否合格
        /// </summary>
        public bool IsPass { get; set; }

        /// <summary>
        /// 是否欠重
        /// </summary>
        public bool IsUnderWeight { get; set; }

        /// <summary>
        /// 标准重量
        /// </summary>
        public double StandardWeight { get; set; }

        /// <summary>
        /// 上限公差
        /// </summary>

        public double UpperLimit { get; set; }

        /// <summary>
        /// 下限公差
        /// </summary>
        public double LowerLimit { get; set; }
    }
}