目录
1.导出的标签 识别标签名称
2.引用OPCUA的包
3.读写方法的封装
4.完整的业务模块封装
1.导出的标签 识别标签名称
从CODESYS导出使用标签通讯的模块文档
大概是这样子的
<?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.读写方法的封装
先定义一个读写通用的数据模型,方便数据交互
/// <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和端口,才能进行连接
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
/// <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>);}}
写入数据到标签的方法实现:
/// <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, // 目标节点IDAttributeId = 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;elsereturn new BaseResult(false, $"❌ 写入失败: {statusCode}");}else{return new BaseResult(false, $"节点 {nodeId} 不可写!");}}else{return new BaseResult(false, $"节点 {nodeId} 不可写!");}}else{return new BaseResult(false, "未建立连接");}
}
4.完整的业务模块封装
最后,再整合一些自动重连,读写方法的封装等,一个方便调用的OPCUA通讯模块就封装好了,程序中使用就很便捷了
下面提供完整的封装代码:
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;//循环写入的睡眠时间 默认取数据间隔+100msprivate 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, // 目标节点IDAttributeId = 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;elsereturn 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; }}
}