随着现在WEB、移动互联网的快速发展,OPC UA方式用的越来越广泛。我最近项目需要,自己整理了一下,这里和大家分享。
我平常用KepServer 比较多,所以我就拿KepServer服务端自带UA服务端做说明演示。
一:KepServer UA服务端配置:
1.打开KepServer Configuration
2.选中【项目】右键-选择属性,选择OPC UA ,打开OPC UA Server功能。
3.再有电脑右下角托盘处,找到KepServer图标,右键-OPC UA配置
4.选择【添加】
这里可以配置网卡、端口号、安全策略。设置后好保存。
5.重启,测试。
二:OPC UA
OPC UA有好多开源封装好的类库。我用的OPC基金会官方开源类库,这里写一写分享这个库的用法。
OPC UA连接以Session为抽象通讯单位,几乎所有操作都是在这基础上完成的。下面分享一些经常用的方法:
1.初始化配置
private void SetDefaultConfiguration()
{
var certificateValidator = new CertificateValidator();
certificateValidator.CertificateValidation += (sender, eventArgs) =>
{
if (ServiceResult.IsGood(eventArgs.Error))
eventArgs.Accept = true;
else if ((eventArgs.Error.StatusCode.Code == StatusCodes.BadCertificateUntrusted) && true)
eventArgs.Accept = true;
else
throw new Exception(string.Format("Failed to validate certificate with error code {0}: {1}", eventArgs.Error.Code, eventArgs.Error.AdditionalInfo));
};
m_configuration = new ApplicationConfiguration()
{
ApplicationUri = "",
ApplicationName = "Client",
ApplicationType = ApplicationType.Client,
CertificateValidator = certificateValidator,
ServerConfiguration = new ServerConfiguration
{
MaxSubscriptionCount = 10000,
MaxMessageQueueSize = 10000,
MaxNotificationQueueSize = 10000,
MaxPublishRequestCount = 10000
},
SecurityConfiguration = new SecurityConfiguration
{
AutoAcceptUntrustedCertificates = true,
},
TransportQuotas = new TransportQuotas
{
OperationTimeout = 600000,
MaxStringLength = 1048576,
MaxByteStringLength = 1048576,
MaxArrayLength = 65535,
MaxMessageSize = 4194304,
MaxBufferSize = 65535,
ChannelLifetime = 600000,
SecurityTokenLifetime = 3600000
},
ClientConfiguration = new ClientConfiguration
{
DefaultSessionTimeout = 60000,
MinSubscriptionLifetime = 10000
},
DisableHiResClock = true
};
}
2.连接UA 服务器
public async Task Connect(string serverUrl, bool useSecurity)
{
// disconnect from existing session.
Disconnect();
if (m_configuration == null)
{
throw new ArgumentNullException("m_configuration");
}
// select the best endpoint.
EndpointDescription endpointDescription = CoreClientUtils.SelectEndpoint(serverUrl, useSecurity, m_discoverTimeout);
EndpointConfiguration endpointConfiguration = EndpointConfiguration.Create(m_configuration);
ConfiguredEndpoint endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration);
m_session = await Session.Create(
m_configuration,
endpoint,
false,
!DisableDomainCheck,
(String.IsNullOrEmpty(SessionName)) ? m_configuration.ApplicationName : SessionName,
60000,
UserIdentity,
PreferredLocales);
// set up keep alive callback.
m_session.KeepAlive += new KeepAliveEventHandler(Session_KeepAlive);
// raise an event.
DoConnectComplete(null);
// return the new session.
return m_session;
}
3.浏览节点
public ReferenceDescriptionCollection BrowseNodes(NodeId sourceId)
{
m_session = this.MySession;
if (m_session == null)
{
return null;
}
// set a suitable initial state.
//if (m_session != null && !m_connectedOnce)
//{
// m_connectedOnce = true;
//}
// fetch references from the server.
// find all of the components of the node.
BrowseDescription nodeToBrowse1 = new BrowseDescription();
nodeToBrowse1.NodeId = sourceId;
nodeToBrowse1.BrowseDirection = BrowseDirection.Forward;
nodeToBrowse1.ReferenceTypeId = ReferenceTypeIds.Aggregates;
nodeToBrowse1.IncludeSubtypes = true;
nodeToBrowse1.NodeClassMask = (uint)(NodeClass.Object | NodeClass.Variable);
nodeToBrowse1.ResultMask = (uint)BrowseResultMask.All;
Browser bro = new Browser(m_session);
ReferenceDescriptionCollection references = bro.Browse(sourceId);
return references;
}
4.读取节点下所有子节点
public ReferenceDescriptionCollection Browse( BrowseDescriptionCollection nodesToBrowse, bool throwOnError)
{
try
{
ReferenceDescriptionCollection references = new ReferenceDescriptionCollection();
BrowseDescriptionCollection unprocessedOperations = new BrowseDescriptionCollection();
while (nodesToBrowse.Count > 0)
{
// start the browse operation.
BrowseResultCollection results = null;
DiagnosticInfoCollection diagnosticInfos = null;
m_session.Browse(
null,
null,
0,
nodesToBrowse,
out results,
out diagnosticInfos);
ClientBase.ValidateResponse(results, nodesToBrowse);
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToBrowse);
ByteStringCollection continuationPoints = new ByteStringCollection();
for (int ii = 0; ii < nodesToBrowse.Count; ii++)
{
// check for error.
if (StatusCode.IsBad(results[ii].StatusCode))
{
// this error indicates that the server does not have enough simultaneously active
// continuation points. This request will need to be resent after the other operations
// have been completed and their continuation points released.
if (results[ii].StatusCode == StatusCodes.BadNoContinuationPoints)
{
unprocessedOperations.Add(nodesToBrowse[ii]);
}
continue;
}
// check if all references have been fetched.
if (results[ii].References.Count == 0)
{
continue;
}
// save results.
references.AddRange(results[ii].References);
// check for continuation point.
if (results[ii].ContinuationPoint != null)
{
continuationPoints.Add(results[ii].ContinuationPoint);
}
}
// process continuation points.
ByteStringCollection revisedContiuationPoints = new ByteStringCollection();
while (continuationPoints.Count > 0)
{
// continue browse operation.
m_session.BrowseNext(
null,
false,
continuationPoints,
out results,
out diagnosticInfos);
ClientBase.ValidateResponse(results, continuationPoints);
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, continuationPoints);
for (int ii = 0; ii < continuationPoints.Count; ii++)
{
// check for error.
if (StatusCode.IsBad(results[ii].StatusCode))
{
continue;
}
// check if all references have been fetched.
if (results[ii].References.Count == 0)
{
continue;
}
// save results.
references.AddRange(results[ii].References);
// check for continuation point.
if (results[ii].ContinuationPoint != null)
{
revisedContiuationPoints.Add(results[ii].ContinuationPoint);
}
}
// check if browsing must continue;
revisedContiuationPoints = continuationPoints;
}
// check if unprocessed results exist.
nodesToBrowse = unprocessedOperations;
}
// return complete list.
return references;
}
catch (Exception exception)
{
if (throwOnError)
{
throw new ServiceResultException(exception, StatusCodes.BadUnexpectedError);
}
return null;
}
}
5.同步写
public bool WriteValue(NodeId nodeId,DataValue dataValue,string oValue)
{
bool result =true;
WriteValueCollection writeValues = new WriteValueCollection();
WriteValue writeValue = new WriteValue();
writeValue.NodeId =nodeId;
writeValue.AttributeId = Attributes.Value;
writeValue.Value.Value = ChangeType(dataValue, oValue);
writeValue.Value.StatusCode = StatusCodes.Good;
writeValue.Value.ServerTimestamp = DateTime.MinValue;
writeValue.Value.SourceTimestamp = DateTime.MinValue;
writeValues.Add(writeValue);
StatusCodeCollection results;
DiagnosticInfoCollection diagnosticinfos;
MySession.Write(null, writeValues, out results, out diagnosticinfos);
ClientBase.ValidateResponse(results, writeValues);
ClientBase.ValidateDiagnosticInfos(diagnosticinfos, writeValues);
if (StatusCode.IsBad(results[0]))
{
throw new ServiceResultException(results[0]);
}
return result;
}
6.异步写
public bool BeginWrite(NodeId nodeid, DataValue dataValue, string oValue,AsyncCallback callback)
{
bool result = true;
WriteValueCollection writeValues = new WriteValueCollection();
WriteValue writeValue = new WriteValue();
writeValue.NodeId = nodeid;
writeValue.AttributeId = Attributes.Value;
writeValue.Value.Value = ChangeType(dataValue, oValue);
writeValue.Value.StatusCode = StatusCodes.Good;
writeValue.Value.ServerTimestamp = DateTime.MinValue;
writeValue.Value.SourceTimestamp = DateTime.MinValue;
writeValues.Add(writeValue);
MySession.BeginWrite(null, writeValues, callback, writeValues);
return result;
}
7.同步读
DataValue readValue = ua.MySession.ReadValue((NodeId)CurrentSelectedItem.Reference.NodeId);
CurrentSelectedItem.Value = readValue.Value.ToString();
8.异步读
ReadValueIdCollection tmpReads = new ReadValueIdCollection();
ReadValueId tmpReadId = new ReadValueId();
tmpReadId.NodeId = (NodeId)CurrentSelectedItem.Reference.NodeId;
tmpReadId.AttributeId = Attributes.Value;
tmpReads.Add(tmpReadId);
ua.MySession.BeginRead(null,0,TimestampsToReturn.Neither,tmpReads,callback, tmpReads);
9.添加订阅
public MonitoredItem CreateMonitoredItem(NodeId nodeId, string displayName)
{
if (m_subscription == null)
{
m_subscription = new Subscription(m_session.DefaultSubscription);
m_subscription.PublishingEnabled = true;
m_subscription.PublishingInterval = 1000;
m_subscription.KeepAliveCount = 10;
m_subscription.LifetimeCount = 10;
m_subscription.MaxNotificationsPerPublish = 1000;
m_subscription.Priority = 100;
m_session.AddSubscription(m_subscription);
m_subscription.Create();
}
// add the new monitored item
.
MonitoredItem monitoredItem = new MonitoredItem(m_subscription.DefaultItem);
monitoredItem.StartNodeId = nodeId;
monitoredItem.AttributeId = Attributes.Value;
monitoredItem.DisplayName = displayName;
monitoredItem.MonitoringMode = MonitoringMode.Reporting;
monitoredItem.SamplingInterval = 1000;
monitoredItem.QueueSize = 0;
monitoredItem.DiscardOldest = true;
monitoredItem.Handle = nodeId;
monitoredItem.Notification += m_MonitoredItem_Notification;
m_subscription.AddItem(monitoredItem);
m_subscription.ApplyChanges();
if (ServiceResult.IsBad(monitoredItem.Status.Error))
{
string tmpStr = monitoredItem.Status.Error.StatusCode.ToString();
}
return monitoredItem;
}
//订阅回调
protected virtual void m_MonitoredItem_Notification(MonitoredItem monitoreditem, MonitoredItemNotificationEventArgs e)
{
this.SubscriptionDataChangeEvent?.Invoke(monitoreditem, e);
}
三、自己学习时测试源码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
namespace UAClient
{
public class UAHelper
{
#region Private Fields
//private ApplicationInstance application;
private ApplicationConfiguration m_configuration;
private Session m_session;
private int m_reconnectPeriod = 10;
private int m_discoverTimeout = 5000;
private SessionReconnectHandler m_reconnectHandler;
private CertificateValidationEventHandler m_CertificateValidation;
private EventHandler m_ReconnectComplete;
private EventHandler m_ReconnectStarting;
private EventHandler m_KeepAliveComplete;
private EventHandler m_ConnectComplete;
public event MonitoredItemNotificationEventHandler SubscriptionDataChangeEvent = null;
private Subscription m_subscription;
//private StatusStrip m_StatusStrip;
//private ToolStripItem m_ServerStatusLB;
//private ToolStripItem m_StatusUpateTimeLB;
#endregion
#region Public Members
//public string OpcUaName { get; set; }
///
/// The name of the session to create.
///
public string SessionName { get; set; }
///
/// Gets or sets a flag indicating that the domain checks should be ignored when connecting.
///
public bool DisableDomainCheck { get; set; }
///
/// The locales to use when creating the session.
///
public string[] PreferredLocales { get; set; }
///
/// The user identity to use when creating the session.
///
public IUserIdentity UserIdentity { get; set; }
///
/// The client application configuration.
///
public ApplicationConfiguration Configuration
{
get { return m_configuration; }
set
{
if (!Object.ReferenceEquals(m_configuration, value))
{
if (m_configuration != null)
{
m_configuration.CertificateValidator.CertificateValidation -= m_CertificateValidation;
}
m_configuration = value;
if (m_configuration != null)
{
m_configuration.CertificateValidator.CertificateValidation += m_CertificateValidation;
}
}
}
}
///
/// The currently active session.
///
public Session MySession
{
get { return m_session; }
}
///
/// The number of seconds between reconnect attempts (0 means reconnect is disabled).
///
[DefaultValue(10)]
public int ReconnectPeriod
{
get { return m_reconnectPeriod; }
set { m_reconnectPeriod = value; }
}
///
/// Raised when a good keep alive from the server arrives.
///
public event EventHandler KeepAliveComplete
{
add { m_KeepAliveComplete += value; }
remove { m_KeepAliveComplete -= value; }
}
///
/// Raised when a reconnect operation starts.
///
public event EventHandler ReconnectStarting
{
add { m_ReconnectStarting += value; }
remove { m_ReconnectStarting -= value; }
}
///
/// Raised when a reconnect operation completes.
///
public event EventHandler ReconnectComplete
{
add { m_ReconnectComplete += value; }
remove { m_ReconnectComplete -= value; }
}
///
/// Raised after successfully connecting to or disconnecing from a server.
///
public event EventHandler ConnectComplete
{
add { m_ConnectComplete += value; }
remove { m_ConnectComplete -= value; }
}
public UAHelper(string OpcUaName):this(OpcUaName,string.Empty)
{
}
public UAHelper(string sessionName, ApplicationConfiguration cfg)
{
this.SessionName = sessionName;
if (cfg == null)
{
SetDefaultConfiguration();
}
else
{
this.m_configuration = cfg;
}
}
public UAHelper(string sessionName, string cfgPath)
{
this.SessionName = sessionName;
if (string.IsNullOrEmpty(cfgPath))
{
SetDefaultConfiguration();
}
else
{
LoadConfiguration(cfgPath);
m_configuration.CertificateValidator = new CertificateValidator();
}
}
private async void LoadConfiguration(string cfgPath)
{
m_configuration = await ApplicationConfiguration.Load("Client", ApplicationType.Client);
}
private void SetDefaultConfiguration()
{
var certificateValidator = new CertificateValidator();
certificateValidator.CertificateValidation += (sender, eventArgs) =>
{
if (ServiceResult.IsGood(eventArgs.Error))
eventArgs.Accept = true;
else if ((eventArgs.Error.StatusCode.Code == StatusCodes.BadCertificateUntrusted) && true)
eventArgs.Accept = true;
else
throw new Exception(string.Format("Failed to validate certificate with error code {0}: {1}", eventArgs.Error.Code, eventArgs.Error.AdditionalInfo));
};
m_configuration = new ApplicationConfiguration()
{
ApplicationUri = "",
ApplicationName = "Client",
ApplicationType = ApplicationType.Client,
CertificateValidator = certificateValidator,
ServerConfiguration = new ServerConfiguration
{
MaxSubscriptionCount = 10000,
MaxMessageQueueSize = 10000,
MaxNotificationQueueSize = 10000,
MaxPublishRequestCount = 10000
},
SecurityConfiguration = new SecurityConfiguration
{
AutoAcceptUntrustedCertificates = true,
},
TransportQuotas = new TransportQuotas
{
OperationTimeout = 600000,
MaxStringLength = 1048576,
MaxByteStringLength = 1048576,
MaxArrayLength = 65535,
MaxMessageSize = 4194304,
MaxBufferSize = 65535,
ChannelLifetime = 600000,
SecurityTokenLifetime = 3600000
},
ClientConfiguration = new ClientConfiguration
{
DefaultSessionTimeout = 60000,
MinSubscriptionLifetime = 10000
},
DisableHiResClock = true
};
}
///
/// Sets the URLs shown in the control.
///
public void SetAvailableUrls(IList urls)
{
//UrlCB.Items.Clear();
if (urls != null)
{
foreach (string url in urls)
{
int index = url.LastIndexOf("/discovery", StringComparison.InvariantCultureIgnoreCase);
if (index != -1)
{
//UrlCB.Items.Add(url.Substring(0, index));
continue;
}
//UrlCB.Items.Add(url);
}
//if (UrlCB.Items.Count > 0)
//{
// UrlCB.SelectedIndex = 0;
//}
}
}
///
/// Creates a new session.
///
/// The new session object.
public async Task Connect(string serverUrl)
{
Session session = await Connect(serverUrl,false);
return session;
}
///
/// Creates a new session.
///
/// The URL of a server endpoint.
/// Whether to use security.
/// The new session object.
public async Task Connect(string serverUrl, bool useSecurity)
{
// disconnect from existing session.
Disconnect();
if (m_configuration == null)
{
throw new ArgumentNullException("m_configuration");
}
// select the best endpoint.
EndpointDescription endpointDescription = CoreClientUtils.SelectEndpoint(serverUrl, useSecurity, m_discoverTimeout);
EndpointConfiguration endpointConfiguration = EndpointConfiguration.Create(m_configuration);
ConfiguredEndpoint endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration);
m_session = await Session.Create(
m_configuration,
endpoint,
false,
!DisableDomainCheck,
(String.IsNullOrEmpty(SessionName)) ? m_configuration.ApplicationName : SessionName,
60000,
UserIdentity,
PreferredLocales);
// set up keep alive callback.
m_session.KeepAlive += new KeepAliveEventHandler(Session_KeepAlive);
// raise an event.
DoConnectComplete(null);
// return the new session.
return m_session;
}
///
/// Disconnects from the server.
///
public void Disconnect()
{
UpdateStatus(false, DateTime.UtcNow, "Disconnected");
// stop any reconnect operation.
if (m_reconnectHandler != null)
{
m_reconnectHandler.Dispose();
m_reconnectHandler = null;
}
// disconnect any existing session.
if (m_session != null)
{
m_session.Close(10000);
m_session = null;
}
// raise an event.
//DoConnectComplete(null); 2018.08.15 注释,没有明白断开连接时为什么要触发连接成功
}
/////
///// Prompts the user to choose a server on another host.
/////
//public void Discover(string hostName)
//{
// string endpointUrl = new DiscoverServerDlg().ShowDialog(m_configuration, hostName);
// if (endpointUrl != null)
// {
// ServerUrl = endpointUrl;
// }
//}
#endregion
#region Private Methods
///
/// Raises the connect complete event on the main GUI thread.
///
private void DoConnectComplete(object state)
{
if (m_ConnectComplete != null)
{
m_ConnectComplete(this, null);
}
}
///
/// Finds the endpoint that best matches the current settings.
///
private EndpointDescription SelectEndpoint(string discoveryUrl,bool useSecurity)
{
Cursor cuisour = Cursors.Wait;
try
{
// return the selected endpoint.
return CoreClientUtils.SelectEndpoint(discoveryUrl, useSecurity, m_discoverTimeout);
}
finally
{
cuisour = Cursors.Arrow;
}
}
#endregion
#region Event Handlers
///
/// Updates the status control.
///
/// Whether the status represents an error.
/// The time associated with the status.
/// The status message.
/// Arguments used to format the status message.
private void UpdateStatus(bool error, DateTime time, string status, params object[] args)
{
//if (m_ServerStatusLB != null)
//{
// m_ServerStatusLB.Text = String.Format(status, args);
// m_ServerStatusLB.ForeColor = (error) ? Color.Red : Color.Empty;
//}
//if (m_StatusUpateTimeLB != null)
//{
// m_StatusUpateTimeLB.Text = time.ToLocalTime().ToString("hh:mm:ss");
// m_StatusUpateTimeLB.ForeColor = (error) ? Color.Red : Color.Empty;
//}
}
///
/// Handles a keep alive event from a session.
///
private void Session_KeepAlive(Session session, KeepAliveEventArgs e)
{
//if (this.InvokeRequired)
//{
// this.BeginInvoke(new KeepAliveEventHandler(Session_KeepAlive), session, e);
// return;
//}
try
{
// check for events from discarded sessions.
if (!Object.ReferenceEquals(session, m_session))
{
return;
}
// start reconnect sequence on communication error.
if (ServiceResult.IsBad(e.Status))
{
if (m_reconnectPeriod <= 0)
{
UpdateStatus(true, e.CurrentTime, "Communication Error ({0})", e.Status);
return;
}
UpdateStatus(true, e.CurrentTime, "Reconnecting in {0}s", m_reconnectPeriod);
if (m_reconnectHandler == null)
{
if (m_ReconnectStarting != null)
{
m_ReconnectStarting(this, e);
}
m_reconnectHandler = new SessionReconnectHandler();
m_reconnectHandler.BeginReconnect(m_session, m_reconnectPeriod * 1000, Server_ReconnectComplete);
}
return;
}
// update status.
UpdateStatus(false, e.CurrentTime, "Connected [{0}]", session.Endpoint.EndpointUrl);
// raise any additional notifications.
if (m_KeepAliveComplete != null)
{
m_KeepAliveComplete(this, e);
}
}
catch (Exception exception)
{
//ClientUtils.HandleException("Error", exception);
}
}
/////
///// Handles a click on the connect button.
/////
//private async void Server_ConnectMI_Click(object sender, EventArgs e)
//{
// try
// {
// await Connect();
// }
// catch (Exception exception)
// {
// ClientUtils.HandleException(this.Text, exception);
// }
//}
///
/// Handles a reconnect event complete from the reconnect handler.
///
private void Server_ReconnectComplete(object sender, EventArgs e)
{
//if (this.InvokeRequired)
//{
// this.BeginInvoke(new EventHandler(Server_ReconnectComplete), sender, e);
// return;
//}
try
{
// ignore callbacks from discarded objects.
if (!Object.ReferenceEquals(sender, m_reconnectHandler))
{
return;
}
m_session = m_reconnectHandler.Session;
m_reconnectHandler.Dispose();
m_reconnectHandler = null;
// raise any additional notifications.
if (m_ReconnectComplete != null)
{
m_ReconnectComplete(this, e);
}
}
catch (Exception exception)
{
//ClientUtils.HandleException("Error", exception);
}
}
///
/// Handles a certificate validation error.
///
private void CertificateValidator_CertificateValidation(CertificateValidator sender, CertificateValidationEventArgs e)
{
//if (this.InvokeRequired)
//{
// this.Invoke(new CertificateValidationEventHandler(CertificateValidator_CertificateValidation), sender, e);
// return;
//}
try
{
e.Accept = m_configuration.SecurityConfiguration.AutoAcceptUntrustedCertificates;
if (!m_configuration.SecurityConfiguration.AutoAcceptUntrustedCertificates)
{
MessageBoxResult result = MessageBox.Show(
e.Certificate.Subject,
"Untrusted Certificate",
MessageBoxButton.YesNo,
MessageBoxImage.Error
);
e.Accept = (result == MessageBoxResult.Yes);
}
}
catch (Exception exception)
{
//ClientUtils.HandleException("Error", exception);
}
}
#endregion
public ReferenceDescriptionCollection BrowseNodes(NodeId sourceId)
{
m_session = this.MySession;
if (m_session == null)
{
return null;
}
// set a suitable initial state.
//if (m_session != null && !m_connectedOnce)
//{
// m_connectedOnce = true;
//}
// fetch references from the server.
// find all of the components of the node.
BrowseDescription nodeToBrowse1 = new BrowseDescription();
nodeToBrowse1.NodeId = sourceId;
nodeToBrowse1.BrowseDirection = BrowseDirection.Forward;
nodeToBrowse1.ReferenceTypeId = ReferenceTypeIds.Aggregates;
nodeToBrowse1.IncludeSubtypes = true;
nodeToBrowse1.NodeClassMask = (uint)(NodeClass.Object | NodeClass.Variable);
nodeToBrowse1.ResultMask = (uint)BrowseResultMask.All;
Browser bro = new Browser(m_session);
ReferenceDescriptionCollection references = bro.Browse(sourceId);
return references;
}
///
/// Browses the address space and returns the references found.
///
/// The session.
/// The set of browse operations to perform.
/// if set to true a exception will be thrown on an error.
///
/// The references found. Null if an error occurred.
///
public ReferenceDescriptionCollection Browse( BrowseDescriptionCollection nodesToBrowse, bool throwOnError)
{
try
{
ReferenceDescriptionCollection references = new ReferenceDescriptionCollection();
BrowseDescriptionCollection unprocessedOperations = new BrowseDescriptionCollection();
while (nodesToBrowse.Count > 0)
{
// start the browse operation.
BrowseResultCollection results = null;
DiagnosticInfoCollection diagnosticInfos = null;
m_session.Browse(
null,
null,
0,
nodesToBrowse,
out results,
out diagnosticInfos);
ClientBase.ValidateResponse(results, nodesToBrowse);
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToBrowse);
ByteStringCollection continuationPoints = new ByteStringCollection();
for (int ii = 0; ii < nodesToBrowse.Count; ii++)
{
// check for error.
if (StatusCode.IsBad(results[ii].StatusCode))
{
// this error indicates that the server does not have enough simultaneously active
// continuation points. This request will need to be resent after the other operations
// have been completed and their continuation points released.
if (results[ii].StatusCode == StatusCodes.BadNoContinuationPoints)
{
unprocessedOperations.Add(nodesToBrowse[ii]);
}
continue;
}
// check if all references have been fetched.
if (results[ii].References.Count == 0)
{
continue;
}
// save results.
references.AddRange(results[ii].References);
// check for continuation point.
if (results[ii].ContinuationPoint != null)
{
continuationPoints.Add(results[ii].ContinuationPoint);
}
}
// process continuation points.
ByteStringCollection revisedContiuationPoints = new ByteStringCollection();
while (continuationPoints.Count > 0)
{
// continue browse operation.
m_session.BrowseNext(
null,
false,
continuationPoints,
out results,
out diagnosticInfos);
ClientBase.ValidateResponse(results, continuationPoints);
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, continuationPoints);
for (int ii = 0; ii < continuationPoints.Count; ii++)
{
// check for error.
if (StatusCode.IsBad(results[ii].StatusCode))
{
continue;
}
// check if all references have been fetched.
if (results[ii].References.Count == 0)
{
continue;
}
// save results.
references.AddRange(results[ii].References);
// check for continuation point.
if (results[ii].ContinuationPoint != null)
{
revisedContiuationPoints.Add(results[ii].ContinuationPoint);
}
}
// check if browsing must continue;
revisedContiuationPoints = continuationPoints;
}
// check if unprocessed results exist.
nodesToBrowse = unprocessedOperations;
}
// return complete list.
return references;
}
catch (Exception exception)
{
if (throwOnError)
{
throw new ServiceResultException(exception, StatusCodes.BadUnexpectedError);
}
return null;
}
}
///
/// Changes the value in the text box to the data type required for the write operation.
///
/// A value with the correct type.
public object ChangeType(DataValue m_value,string v)
{
object value = (m_value != null) ? m_value.Value : null;
switch (m_value.WrappedValue.TypeInfo.BuiltInType)
{
case BuiltInType.Boolean:
{
value = Convert.ToBoolean(v);
break;
}
case BuiltInType.SByte:
{
value = Convert.ToSByte(v);
break;
}
case BuiltInType.Byte:
{
value = Convert.ToByte(v);
break;
}
case BuiltInType.Int16:
{
value = Convert.ToInt16(v);
break;
}
case BuiltInType.UInt16:
{
value = Convert.ToUInt16(v);
break;
}
case BuiltInType.Int32:
{
value = Convert.ToInt32(v);
break;
}
case BuiltInType.UInt32:
{
value = Convert.ToUInt32(v);
break;
}
case BuiltInType.Int64:
{
value = Convert.ToInt64(v);
break;
}
case BuiltInType.UInt64:
{
value = Convert.ToUInt64(v);
break;
}
case BuiltInType.Float:
{
value = Convert.ToSingle(v);
break;
}
case BuiltInType.Double:
{
value = Convert.ToDouble(v);
break;
}
default:
{
value = v;
break;
}
}
return value;
}
///
/// Creates the monitored item.
///
public MonitoredItem CreateMonitoredItem(NodeId nodeId, string displayName)
{
if (m_subscription == null)
{
m_subscription = new Subscription(m_session.DefaultSubscription);
m_subscription.PublishingEnabled = true;
m_subscription.PublishingInterval = 1000;
m_subscription.KeepAliveCount = 10;
m_subscription.LifetimeCount = 10;
m_subscription.MaxNotificationsPerPublish = 1000;
m_subscription.Priority = 100;
m_session.AddSubscription(m_subscription);
m_subscription.Create();
}
// add the new monitored item.
MonitoredItem monitoredItem = new MonitoredItem(m_subscription.DefaultItem);
monitoredItem.StartNodeId = nodeId;
monitoredItem.AttributeId = Attributes.Value;
monitoredItem.DisplayName = displayName;
monitoredItem.MonitoringMode = MonitoringMode.Reporting;
monitoredItem.SamplingInterval = 1000;
monitoredItem.QueueSize = 0;
monitoredItem.DiscardOldest = true;
monitoredItem.Handle = nodeId;
monitoredItem.Notification += m_MonitoredItem_Notification;
m_subscription.AddItem(monitoredItem);
m_subscription.ApplyChanges();
if (ServiceResult.IsBad(monitoredItem.Status.Error))
{
string tmpStr = monitoredItem.Status.Error.StatusCode.ToString();
}
return monitoredItem;
}
public bool DeleteMonitoredItem(List monitoredItems)
{
bool result = true;
for (int i = 0; i < monitoredItems.Count; i++)
{
monitoredItems[i].Notification -= m_MonitoredItem_Notification;
}
if (m_subscription != null)
{
m_subscription.RemoveItems(monitoredItems);
m_subscription.ApplyChanges();
}
return result;
}
public bool WriteValue(NodeId nodeId,DataValue dataValue,string oValue)
{
bool result =true;
WriteValueCollection writeValues = new WriteValueCollection();
WriteValue writeValue = new WriteValue();
writeValue.NodeId =nodeId;
writeValue.AttributeId = Attributes.Value;
writeValue.Value.Value = ChangeType(dataValue, oValue);
writeValue.Value.StatusCode = StatusCodes.Good;
writeValue.Value.ServerTimestamp = DateTime.MinValue;
writeValue.Value.SourceTimestamp = DateTime.MinValue;
writeValues.Add(writeValue);
StatusCodeCollection results;
DiagnosticInfoCollection diagnosticinfos;
MySession.Write(null, writeValues, out results, out diagnosticinfos);
ClientBase.ValidateResponse(results, writeValues);
ClientBase.ValidateDiagnosticInfos(diagnosticinfos, writeValues);
if (StatusCode.IsBad(results[0]))
{
throw new ServiceResultException(results[0]);
}
return result;
}
public bool BeginWrite(NodeId nodeid, DataValue dataValue, string oValue,AsyncCallback callback)
{
bool result = true;
WriteValueCollection writeValues = new WriteValueCollection();
WriteValue writeValue = new WriteValue();
writeValue.NodeId = nodeid;
writeValue.AttributeId = Attributes.Value;
writeValue.Value.Value = ChangeType(dataValue, oValue);
writeValue.Value.StatusCode = StatusCodes.Good;
writeValue.Value.ServerTimestamp = DateTime.MinValue;
writeValue.Value.SourceTimestamp = DateTime.MinValue;
writeValues.Add(writeValue);
MySession.BeginWrite(null, writeValues, callback, writeValues);
return result;
}
protected virtual void m_MonitoredItem_Notification(MonitoredItem monitoreditem, MonitoredItemNotificationEventArgs e)
{
this.SubscriptionDataChangeEvent?.Invoke(monitoreditem, e);
}
}
}
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Net.Mime;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using Opc.Ua;
using Opc.Ua.Client;
//using Opc.Ua.Client.Controls;
using Opc.Ua.Configuration;
using UAClient.Annotations;
namespace UAClient
{
///
/// MainWindow.xaml 的交互逻辑
///
public partial class MainWindow : Window
{
private UAHelper ua = null;
private ItemInfo CurrentSelectedItem = null;
private List SubSelectedItems = new List();
private ObservableCollection ItemsConlection = new ObservableCollection();
private ObservableCollection SubcriptionItemsConlection = new ObservableCollection();
public MainWindow()
{
InitializeComponent();
ua = new UAHelper("Test");
ua.SubscriptionDataChangeEvent += Ua_SubscriptionDataChange;
ua.ConnectComplete += ua_ConnectComplete;
ua.KeepAliveComplete += ua_KeepAliveComplete;
ua.ReconnectComplete += ua_ReconnectComplete;
}
private void Ua_SubscriptionDataChange(MonitoredItem monitoredItem, MonitoredItemNotificationEventArgs e)
{
MonitoredItemNotification notification = e.NotificationValue as MonitoredItemNotification;
if (notification == null)
{
return;
}
ItemInfo tmpItemInfo = GetItemInfo((NodeId)monitoredItem.Handle);
tmpItemInfo.Value = Utils.Format("{0}", notification.Value.WrappedValue);
tmpItemInfo.StatusCode = Utils.Format("{0}", notification.Value.StatusCode);
tmpItemInfo.TimeStamp = Utils.Format("{0:HH:mm:ss.fff}", notification.Value.SourceTimestamp.ToLocalTime());
}
private ItemInfo GetItemInfo(NodeId nodeid)
{
ItemInfo tmpItemInfo = null;
lock (SubcriptionItemsConlection)
{
foreach (var i in SubcriptionItemsConlection)
{
if (i.Reference.NodeId.ToString().Equals(nodeid.ToString(),StringComparison.CurrentCultureIgnoreCase))
{
tmpItemInfo = i;
break;
}
}
}
return tmpItemInfo;
}
private void ua_ReconnectComplete(object sender, EventArgs e)
{
//throw new NotImplementedException();
}
private void ua_KeepAliveComplete(object sender, EventArgs e)
{
//MessageBox.Show("连接成功!");
ReferenceDescriptionCollection referenceDescriptionCollection = ua.BrowseNodes(ObjectIds.ObjectsFolder);
}
private void ua_ConnectComplete(object sender, EventArgs e)
{
//MessageBox.Show("连接成功!");
btnConnect.IsEnabled = false;
ReferenceDescriptionCollection referenceDescriptionCollection = ua.BrowseNodes(ObjectIds.ObjectsFolder);
FillTree(referenceDescriptionCollection);
}
private async void btnConnect_Click(object sender, RoutedEventArgs e)
{
string tmpUrl = tbUrl.Text.Trim();
if (string.IsNullOrEmpty(tmpUrl))
{
MessageBox.Show("地址不能为空", "提示", MessageBoxButton.OK);
return;
}
try
{
await ua.Connect(tmpUrl);
}
catch (Exception ex)
{
ShowMessage("error",ex);
}
}
private void ShowMessage(string caption, Exception e)
{
MessageBox.Show(e.Message,caption,MessageBoxButton.OK);
}
private void btnBrowse_Click(object sender, RoutedEventArgs e)
{
ReferenceDescriptionCollection collection = ua.BrowseNodes(ObjectIds.ObjectsFolder);
//throw new NotImplementedException();
}
private void FillTree(List ls)
{
tvNodes.Items.Clear();
for (int i = 0; i < ls.Count; i++)
{
TreeViewItem item = new TreeViewItem();
item.Header = ls[i].DisplayName;
item.Tag = ls[i];
item.Items.Add("*");//占位符
item.Expanded += Item_Expanded;
item.Selected += SubItem_Selected;
tvNodes.Items.Add(item);
}
}
private void Item_Expanded(object sender, RoutedEventArgs e)
{
TreeViewItem item = e.OriginalSource as TreeViewItem;
item.Items.Clear();
ReferenceDescription refDes = item.Tag as ReferenceDescription;
if (refDes == null || refDes.NodeId.IsAbsolute)
{
return;
}
ReferenceDescriptionCollection nodes = ua.BrowseNodes((NodeId)refDes.NodeId);
for (int i = 0; i < nodes.Count; i++)
{
TreeViewItem subItem = new TreeViewItem();
subItem.Header = nodes[i].DisplayName;
subItem.Tag = nodes[i];
subItem.Items.Add("*");
subItem.Expanded += Item_Expanded;
subItem.Selected += SubItem_Selected;
item.Items.Add(subItem);
}
}
private void SubItem_Selected(object sender, RoutedEventArgs e)
{
ItemsConlection.Clear();
TreeViewItem item = e.OriginalSource as TreeViewItem;
ReferenceDescription reference = item.Tag as ReferenceDescription;
DisplayAttributes((NodeId)reference.NodeId);
//throw new NotImplementedException();
}
///
/// Displays the attributes and properties in the attributes view.
///
/// The NodeId of the Node to browse.
private void DisplayAttributes(NodeId sourceId)
{
//try
//{
ReadValueIdCollection nodesToRead = new ReadValueIdCollection();
// attempt to read all possible attributes.
for (uint ii = Attributes.NodeClass; ii <= Attributes.UserExecutable; ii++)
{
ReadValueId nodeToRead = new ReadValueId();
nodeToRead.NodeId = sourceId;
nodeToRead.AttributeId = ii;
nodesToRead.Add(nodeToRead);
}
int startOfProperties = nodesToRead.Count;
//// find all of the pror of the node.
//BrowseDescription nodeToBrowse1 = new BrowseDescription();
//nodeToBrowse1.NodeId = sourceId;
//nodeToBrowse1.BrowseDirection = BrowseDirection.Forward;
//nodeToBrowse1.ReferenceTypeId = ReferenceTypeIds.HasProperty;
//nodeToBrowse1.IncludeSubtypes = true;
//nodeToBrowse1.NodeClassMask = 0;
//nodeToBrowse1.ResultMask = (uint)BrowseResultMask.All;
//BrowseDescriptionCollection nodesToBrowse = new BrowseDescriptionCollection();
//nodesToBrowse.Add(nodeToBrowse1);
//// fetch property references from the server.
//ReferenceDescriptionCollection references = ua.Browse(nodesToBrowse, true);
ReferenceDescriptionCollection references = ua.BrowseNodes(sourceId);
if (references == null)
{
return;
}
for (int ii = 0; ii < references.Count; ii++)
{
// ignore external references.
if (references[ii].NodeId.IsAbsolute)
{
continue;
}
ReadValueId nodeToRead = new ReadValueId();
nodeToRead.NodeId = (NodeId)references[ii].NodeId;
nodeToRead.AttributeId = Attributes.Value;
nodesToRead.Add(nodeToRead);
}
// read all values.
DataValueCollection results = null;
DiagnosticInfoCollection diagnosticInfos = null;
ua.MySession.Read(
null,
0,
TimestampsToReturn.Neither,
nodesToRead,
out results,
out diagnosticInfos);
ClientBase.ValidateResponse(results, nodesToRead);
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToRead);
// process results.
for (int ii = 0; ii < results.Count; ii++)
{
string name = null;
string datatype = null;
string value = null;
ReferenceDescription tmpReferencedes = null;
DataValue tmpDataValue = results[ii];
// process attribute value.
if (ii < startOfProperties)
{
// ignore attributes which are invalid for the node.
if (results[ii].StatusCode == StatusCodes.BadAttributeIdInvalid)
{
continue;
}
// get the name of the attribute.
name = Attributes.GetBrowseName(nodesToRead[ii].AttributeId);
// display any unexpected error.
if (StatusCode.IsBad(results[ii].StatusCode))
{
datatype = Utils.Format("{0}", Attributes.GetDataTypeId(nodesToRead[ii].AttributeId));
value = Utils.Format("{0}", results[ii].StatusCode);
}
// display the value.
else
{
TypeInfo typeInfo = TypeInfo.Construct(results[ii].Value);
datatype = typeInfo.BuiltInType.ToString();
if (typeInfo.ValueRank >= ValueRanks.OneOrMoreDimensions)
{
datatype += "[]";
}
value = Utils.Format("{0}", results[ii].Value);
}
}
// process property value.
else
{
// ignore properties which are invalid for the node.
if (results[ii].StatusCode == StatusCodes.BadNodeIdUnknown)
{
continue;
}
// get the name of the property.
name = Utils.Format("{0}", references[ii - startOfProperties]);
tmpReferencedes = references[ii - startOfProperties];
// display any unexpected error.
if (StatusCode.IsBad(results[ii].StatusCode))
{
datatype = String.Empty;
value = Utils.Format("{0}", results[ii].StatusCode);
}
// display the value.
else
{
TypeInfo typeInfo = TypeInfo.Construct(results[ii].Value);
datatype = typeInfo.BuiltInType.ToString();
if (typeInfo.ValueRank >= ValueRanks.OneOrMoreDimensions)
{
datatype += "[]";
}
value = Utils.Format("{0}", results[ii].Value);
}
}
// add the attribute name/value to the list view.
ItemInfo tmpModel = new ItemInfo()
{
Name = name,
DataType = datatype,
Value = value,
Reference = tmpReferencedes,
ODataValue = tmpDataValue
};
ItemsConlection.Add(tmpModel);
}
//}
//catch (Exception e)
//{
// Utils.Trace(e, "Unexpected error in '{0}'.", "error"); //待修改
// return;
//}
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
dgDetial.ItemsSource = ItemsConlection;
dgSub.ItemsSource = SubcriptionItemsConlection;
//dgDetial.DataContext = ItemsConlection;
}
private void DisConnect_Click(object sender, RoutedEventArgs e)
{
ua.Disconnect();
btnConnect.IsEnabled = true;
tvNodes.Items.Clear();
ItemsConlection.Clear();
}
private void dgDetial_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
DataGrid tmpDg = sender as DataGrid;
int tmpSelectedIndex = tmpDg.SelectedIndex;
if (tmpSelectedIndex == -1) return;
CurrentSelectedItem = ItemsConlection[tmpSelectedIndex]; ;
}
private void mi_Click(object sender, RoutedEventArgs e)
{
if (CurrentSelectedItem == null)
{
MessageBox.Show("请选择要操作的条目!");
return;
}
MenuItem tmpMi = sender as MenuItem;
switch (tmpMi.Tag)
{
case "0": //同步读
DataValue readValue = ua.MySession.ReadValue((NodeId)CurrentSelectedItem.Reference.NodeId);
CurrentSelectedItem.Value = readValue.Value.ToString();
break;
case "1": //异步读
ReadValueIdCollection tmpReads = new ReadValueIdCollection();
ReadValueId tmpReadId = new ReadValueId();
tmpReadId.NodeId = (NodeId)CurrentSelectedItem.Reference.NodeId;
tmpReadId.AttributeId = Attributes.Value;
tmpReads.Add(tmpReadId);
ua.MySession.BeginRead(null,0,TimestampsToReturn.Neither,tmpReads,callback, tmpReads);
break;
case "2": //同步写
WriteValueWin f = new WriteValueWin(CurrentSelectedItem,ua);
f.ShowDialog();
break;
case "3": //异步写
break;
case "4": //订阅
lock (SubcriptionItemsConlection)
{
if (SubcriptionItemsConlection.Contains(CurrentSelectedItem))
{
MessageBox.Show("订阅列表中已经存在着项!");
return;
}
MonitoredItem tmpMonitoredItem = ua.CreateMonitoredItem((NodeId) CurrentSelectedItem.Reference.NodeId,
CurrentSelectedItem.Name);
CurrentSelectedItem.MonitoredItem = tmpMonitoredItem;
SubcriptionItemsConlection.Add(CurrentSelectedItem);
}
break;
case "5": //取消订阅
break;
default:
CurrentSelectedItem = null;
break;
}
}
private void callback(IAsyncResult ar)
{
if (ar.IsCompleted)
{
DataValueCollection results;
DiagnosticInfoCollection diagnosticinfos;
ReadValueIdCollection tmpReadIds = ar.AsyncState as ReadValueIdCollection;
ResponseHeader responseHeader = ua.MySession.EndRead(ar, out results, out diagnosticinfos);
ClientBase.ValidateResponse(results, tmpReadIds);
ClientBase.ValidateDiagnosticInfos(diagnosticinfos,tmpReadIds);
CurrentSelectedItem.Value = results[0].Value.ToString();
}
//throw new NotImplementedException();
}
private bool SubCollectionIsPro = false;
private void BtnDeleteSub_OnClick(object sender, RoutedEventArgs e)
{
SubCollectionIsPro = true;
if (SubSelectedItems.Count < 1)
{
MessageBox.Show("请选择取消订阅的条目!");
return;
}
List tmpMonitors = new List();
for (int i=0;i< SubSelectedItems.Count;i++)
{
tmpMonitors.Add(SubSelectedItems[i].MonitoredItem);
lock (SubcriptionItemsConlection)
{
SubcriptionItemsConlection.Remove(SubSelectedItems[i]);
}
}
ua.DeleteMonitoredItem(tmpMonitors);
SubSelectedItems.Clear();
SubCollectionIsPro = false;
}
private void dgSub_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (SubCollectionIsPro) return;
SubSelectedItems.Clear();
var eOriginalSource = (DataGrid)e.OriginalSource;
foreach (var item in eOriginalSource.SelectedItems)
{
ItemInfo tmpRow = item as ItemInfo;
if (!SubSelectedItems.Contains(tmpRow))
{
SubSelectedItems.Add(tmpRow);
}
}
}
}
public class ItemInfo : INotifyPropertyChanged
{
private string _name;
private string _dataType;
private string _value;
public string Name
{
get => _name;
set
{
if (value == _name) return;
_name = value;
OnPropertyChanged();
}
}
public string DataType
{
get => _dataType;
set
{
if (value == _dataType) return;
_dataType = value;
OnPropertyChanged();
}
}
public string Value
{
get => _value;
set
{
if (value == _value) return;
_value = value;
OnPropertyChanged();
}
}
private string statueCode;
public string StatusCode
{
get { return statueCode; }
set
{
if (value == statueCode) return;
statueCode = value;
OnPropertyChanged();
}
}
private string timeStamp;
public string TimeStamp
{
get { return timeStamp; }
set
{
if (value == timeStamp) return;
timeStamp = value;
OnPropertyChanged();
}
}
public MonitoredItem MonitoredItem { get; set; }
public ReferenceDescription Reference { get; set; }
public DataValue ODataValue { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
opc.tcp://127.0.0.1:49320
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Opc.Ua;
namespace UAClient
{
///
/// WriteValueWin.xaml 的交互逻辑
///
public partial class WriteValueWin : Window
{
private ItemInfo Item = null;
private UAHelper Ua = null;
public WriteValueWin(ItemInfo item,UAHelper ua)
{
InitializeComponent();
this.Item = item;
this.Ua = ua;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
string tmpValue = tbValue.Text.Trim();
Button tmpBtn = sender as Button;
if (tmpBtn.Tag.Equals("0"))
{
Ua.WriteValue((NodeId)Item.Reference.NodeId, Item.ODataValue, tmpValue);
}
else
{
//异步写
Ua.BeginWrite((NodeId)Item.Reference.NodeId,Item.ODataValue, tmpValue,calback);
}
}
private void calback(IAsyncResult ar)
{
StatusCodeCollection results = null;
DiagnosticInfoCollection diagnosticinfos =null;
if (ar.IsCompleted)
{
WriteValueCollection writeValues = ar.AsyncState as WriteValueCollection;
Ua.MySession.EndWrite(ar, out results, out diagnosticinfos);
ClientBase.ValidateResponse(results, writeValues);
ClientBase.ValidateDiagnosticInfos(diagnosticinfos, writeValues);
}
if (StatusCode.IsBad(results[0]))
{
throw new ServiceResultException(results[0]);
}
//throw new NotImplementedException();
}
}
}
四:做了简单的封装,为了测试用,大家可以直接安装nuget ,搜UAClient.zlw
或者Package Manager安装 Install-Package UAClient.zlw -Version 1.0.0
NET CLI dotnet add package UAClient.zlw --version 1.0.0
注:仅供大家一起学习。