using Hylasoft.Opc.Common;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Hylasoft.Opc.Ua
{
///
/// Client Implementation for UA
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling",
Justification = "Doesn't make sense to split this class")]
public class UaClient : IClient
{
private readonly UaClientOptions _options = new UaClientOptions();
private readonly Uri _serverUrl;
private Session _session;
private readonly IDictionary _nodesCache = new Dictionary();
private readonly IDictionary> _folderCache = new Dictionary>();
///
/// Creates a server object
///
/// the url of the server to connect to
public UaClient(Uri serverUrl)
{
_serverUrl = serverUrl;
Status = OpcStatus.NotConnected;
}
///
/// Creates a server object
///
/// the url of the server to connect to
/// custom options to use with ua client
public UaClient(Uri serverUrl, UaClientOptions options)
{
_serverUrl = serverUrl;
_options = options;
Status = OpcStatus.NotConnected;
}
///
/// Options to configure the UA client session
///
public UaClientOptions Options
{
get { return _options; }
}
///
/// OPC Foundation underlying session object
///
protected Session Session
{
get
{
return _session;
}
}
private void PostInitializeSession()
{
var node = _session.NodeCache.Find(ObjectIds.ObjectsFolder);
RootNode = new UaNode(string.Empty, node.NodeId.ToString());
AddNodeToCache(RootNode);
Status = OpcStatus.Connected;
}
///
/// Connect the client to the OPC Server
///
public async Task Connect()
{
if (Status == OpcStatus.Connected)
return;
_session = await InitializeSession(_serverUrl);
_session.KeepAlive += SessionKeepAlive;
_session.SessionClosing += SessionClosing;
PostInitializeSession();
}
///
/// Gets the datatype of an OPC tag
///
/// Tag to get datatype of
/// System Type
public System.Type GetDataType(string tag)
{
var nodesToRead = BuildReadValueIdCollection(tag, Attributes.Value);
DataValueCollection results;
DiagnosticInfoCollection diag;
_session.Read(
requestHeader: null,
maxAge: 0,
timestampsToReturn: TimestampsToReturn.Neither,
nodesToRead: nodesToRead,
results: out results,
diagnosticInfos: out diag);
var type = results[0].WrappedValue.TypeInfo.BuiltInType;
return System.Type.GetType("System." + type.ToString());
}
private void SessionKeepAlive(ISession session, KeepAliveEventArgs e)
{
if (e.CurrentState != ServerState.Running)
{
if (Status == OpcStatus.Connected)
{
Status = OpcStatus.NotConnected;
NotifyServerConnectionLost();
}
}
else if (e.CurrentState == ServerState.Running)
{
if (Status == OpcStatus.NotConnected)
{
Status = OpcStatus.Connected;
NotifyServerConnectionRestored();
}
}
}
private void SessionClosing(object? sender, EventArgs e)
{
Status = OpcStatus.NotConnected;
NotifyServerConnectionLost();
}
///
/// Reconnect the OPC session
///
public void ReConnect()
{
Status = OpcStatus.NotConnected;
_session.Reconnect();
Status = OpcStatus.Connected;
}
///
/// Create a new OPC session, based on the current session parameters.
///
public void RecreateSession()
{
Status = OpcStatus.NotConnected;
_session = Session.Recreate(_session);
PostInitializeSession();
}
///
/// Gets the current status of the OPC Client
///
public OpcStatus Status { get; private set; }
private ReadValueIdCollection BuildReadValueIdCollection(string tag, uint attributeId)
{
var n = FindNode(tag, RootNode);
var readValue = new ReadValueId
{
NodeId = n.NodeId,
AttributeId = attributeId
};
return new ReadValueIdCollection { readValue };
}
///
/// Read a tag
///
/// The type of tag to read
/// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name.
/// E.g: the tag `foo.bar` reads the tag `bar` on the folder `foo`
/// The value retrieved from the OPC
public ReadEvent Read(string tag)
{
var nodesToRead = BuildReadValueIdCollection(tag, Attributes.Value);
DataValueCollection results;
DiagnosticInfoCollection diag;
_session.Read(
requestHeader: null,
maxAge: 0,
timestampsToReturn: TimestampsToReturn.Neither,
nodesToRead: nodesToRead,
results: out results,
diagnosticInfos: out diag);
var val = results[0];
var readEvent = new ReadEvent();
readEvent.Value = (T)val.Value;
readEvent.SourceTimestamp = val.SourceTimestamp;
readEvent.ServerTimestamp = val.ServerTimestamp;
if (StatusCode.IsGood(val.StatusCode)) readEvent.Quality = Quality.Good;
if (StatusCode.IsBad(val.StatusCode)) readEvent.Quality = Quality.Bad;
return readEvent;
}
///
/// Read a tag asynchronously
///
/// The type of tag to read
/// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name.
/// E.g: the tag `foo.bar` reads the tag `bar` on the folder `foo`
/// The value retrieved from the OPC
public Task> ReadAsync(string tag)
{
var nodesToRead = BuildReadValueIdCollection(tag, Attributes.Value);
// Wrap the ReadAsync logic in a TaskCompletionSource, so we can use C# async/await syntax to call it:
var taskCompletionSource = new TaskCompletionSource>();
_session.BeginRead(
requestHeader: null,
maxAge: 0,
timestampsToReturn: TimestampsToReturn.Neither,
nodesToRead: nodesToRead,
callback: ar =>
{
DataValueCollection results;
DiagnosticInfoCollection diag;
var response = _session.EndRead(
result: ar,
results: out results,
diagnosticInfos: out diag);
try
{
CheckReturnValue(response.ServiceResult);
var val = results[0];
var readEvent = new ReadEvent();
readEvent.Value = (T)val.Value;
readEvent.SourceTimestamp = val.SourceTimestamp;
readEvent.ServerTimestamp = val.ServerTimestamp;
if (StatusCode.IsGood(val.StatusCode)) readEvent.Quality = Quality.Good;
if (StatusCode.IsBad(val.StatusCode)) readEvent.Quality = Quality.Bad;
taskCompletionSource.TrySetResult(readEvent);
}
catch (Exception ex)
{
taskCompletionSource.TrySetException(ex);
}
},
asyncState: null);
return taskCompletionSource.Task;
}
private WriteValueCollection BuildWriteValueCollection(string tag, uint attributeId, object dataValue)
{
var n = FindNode(tag, RootNode);
var writeValue = new WriteValue
{
NodeId = n.NodeId,
AttributeId = attributeId,
Value = { Value = dataValue }
};
return new WriteValueCollection { writeValue };
}
///
/// Write a value on the specified opc tag
///
/// The type of tag to write on
/// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name.
/// E.g: the tag `foo.bar` writes on the tag `bar` on the folder `foo`
/// The value for the item to write
public void Write(string tag, T item)
{
var nodesToWrite = BuildWriteValueCollection(tag, Attributes.Value, item);
StatusCodeCollection results;
DiagnosticInfoCollection diag;
_session.Write(
requestHeader: null,
nodesToWrite: nodesToWrite,
results: out results,
diagnosticInfos: out diag);
CheckReturnValue(results[0]);
}
///
/// Write a value on the specified opc tag asynchronously
///
/// The type of tag to write on
/// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name.
/// E.g: the tag `foo.bar` writes on the tag `bar` on the folder `foo`
/// The value for the item to write
public Task WriteAsync(string tag, T item)
{
var nodesToWrite = BuildWriteValueCollection(tag, Attributes.Value, item);
// Wrap the WriteAsync logic in a TaskCompletionSource, so we can use C# async/await syntax to call it:
var taskCompletionSource = new TaskCompletionSource();
_session.BeginWrite(
requestHeader: null,
nodesToWrite: nodesToWrite,
callback: ar =>
{
StatusCodeCollection results;
DiagnosticInfoCollection diag;
var response = _session.EndWrite(
result: ar,
results: out results,
diagnosticInfos: out diag);
try
{
CheckReturnValue(response.ServiceResult);
CheckReturnValue(results[0]);
taskCompletionSource.SetResult(response.ServiceResult);
}
catch (Exception ex)
{
taskCompletionSource.TrySetException(ex);
}
},
asyncState: null);
return taskCompletionSource.Task;
}
///
/// Monitor the specified tag for changes
///
/// the type of tag to monitor
/// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name.
/// E.g: the tag `foo.bar` monitors the tag `bar` on the folder `foo`
/// the callback to execute when the value is changed.
/// The first parameter is a MonitorEvent object which represents the data point, the second is an `unsubscribe` function to unsubscribe the callback
public void Monitor(string tag, Action, Action> callback)
{
var node = FindNode(tag);
var sub = new Subscription
{
PublishingInterval = _options.DefaultMonitorInterval,
PublishingEnabled = true,
LifetimeCount = _options.SubscriptionLifetimeCount,
KeepAliveCount = _options.SubscriptionKeepAliveCount,
DisplayName = tag,
Priority = byte.MaxValue
};
var item = new MonitoredItem
{
StartNodeId = node.NodeId,
AttributeId = Attributes.Value,
DisplayName = tag,
SamplingInterval = _options.DefaultMonitorInterval
};
sub.AddItem(item);
_session.AddSubscription(sub);
sub.Create();
sub.ApplyChanges();
item.Notification += (monitoredItem, args) =>
{
var p = (MonitoredItemNotification)args.NotificationValue;
var t = p.Value.WrappedValue.Value;
Action unsubscribe = () =>
{
sub.RemoveItems(sub.MonitoredItems);
sub.Delete(true);
_session.RemoveSubscription(sub);
sub.Dispose();
};
var monitorEvent = new ReadEvent();
monitorEvent.Value = (T)t;
monitorEvent.SourceTimestamp = p.Value.SourceTimestamp;
monitorEvent.ServerTimestamp = p.Value.ServerTimestamp;
if (StatusCode.IsGood(p.Value.StatusCode)) monitorEvent.Quality = Quality.Good;
if (StatusCode.IsBad(p.Value.StatusCode)) monitorEvent.Quality = Quality.Bad;
callback(monitorEvent, unsubscribe);
};
}
///
/// Explore a folder on the Opc Server
///
/// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name.
/// E.g: the tag `foo.bar` finds the sub nodes of `bar` on the folder `foo`
/// The list of sub-nodes
public IEnumerable ExploreFolder(string tag)
{
IList nodes;
_folderCache.TryGetValue(tag, out nodes);
if (nodes != null)
return nodes;
var folder = FindNode(tag);
nodes = ClientUtils.Browse(_session, folder.NodeId)
.GroupBy(n => n.NodeId) //this is to select distinct
.Select(n => n.First())
.Where(n => n.NodeClass == NodeClass.Variable || n.NodeClass == NodeClass.Object)
.Select(n => n.ToHylaNode(folder))
.ToList();
//add nodes to cache
if (!_folderCache.ContainsKey(tag))
_folderCache.Add(tag, nodes);
foreach (var node in nodes)
AddNodeToCache(node);
return nodes;
}
///
/// Explores a folder asynchronously
///
public async Task> ExploreFolderAsync(string tag)
{
return await Task.Run(() => ExploreFolder(tag));
}
///
/// Finds a node on the Opc Server
///
/// The fully-qualified identifier of the tag. You can specify a subfolder by using a comma delimited name.
/// E.g: the tag `foo.bar` finds the tag `bar` on the folder `foo`
/// If there is a tag, it returns it, otherwise it throws an
public UaNode FindNode(string tag)
{
// if the tag already exists in cache, return it
if (_nodesCache.ContainsKey(tag))
return _nodesCache[tag];
// try to find the tag otherwise
var found = FindNode(tag, RootNode);
if (found != null)
{
AddNodeToCache(found);
return found;
}
// throws an exception if not found
throw new OpcException(string.Format("The tag \"{0}\" doesn't exist on the Server", tag));
}
///
/// Find node asynchronously
///
public async Task FindNodeAsync(string tag)
{
return await Task.Run(() => FindNode(tag));
}
///
/// Gets the root node of the server
///
public UaNode RootNode { get; private set; }
///
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
///
public void Dispose()
{
if (_session != null)
{
_session.RemoveSubscriptions(_session.Subscriptions.ToList());
_session.Close();
_session.Dispose();
}
GC.SuppressFinalize(this);
}
private void CheckReturnValue(StatusCode status)
{
if (!StatusCode.IsGood(status))
throw new OpcException(string.Format("Invalid response from the server. (Response Status: {0})", status), status);
}
///
/// Adds a node to the cache using the tag as its key
///
/// the node to add
private void AddNodeToCache(UaNode node)
{
if (!_nodesCache.ContainsKey(node.Tag))
_nodesCache.Add(node.Tag, node);
}
///
/// Return identity login object for a given URI.
///
/// Login URI
/// AnonUser or User with name and password
private UserIdentity GetIdentity(Uri url)
{
if (_options.UserIdentity != null)
{
return _options.UserIdentity;
}
var uriLogin = new UserIdentity();
if (!string.IsNullOrEmpty(url.UserInfo))
{
var uis = url.UserInfo.Split(':');
uriLogin = new UserIdentity(uis[0], uis[1]);
}
return uriLogin;
}
///
/// Crappy method to initialize the session. I don't know what many of these things do, sincerely.
///
private async Task InitializeSession(Uri url)
{
var certificateValidator = new CertificateValidator();
certificateValidator.CertificateValidation += (sender, eventArgs) =>
{
if (ServiceResult.IsGood(eventArgs.Error))
eventArgs.Accept = true;
else if ((eventArgs.Error.StatusCode.Code == StatusCodes.BadCertificateUntrusted) && _options.AutoAcceptUntrustedCertificates)
eventArgs.Accept = true;
else
throw new OpcException(string.Format("Failed to validate certificate with error code {0}: {1}", eventArgs.Error.Code, eventArgs.Error.AdditionalInfo), eventArgs.Error.StatusCode);
};
// Build the application configuration
var appInstance = new ApplicationInstance
{
ApplicationType = ApplicationType.Client,
ConfigSectionName = _options.ConfigSectionName,
ApplicationConfiguration = new ApplicationConfiguration
{
ApplicationUri = url.ToString(),
ApplicationName = _options.ApplicationName,
ApplicationType = ApplicationType.Client,
CertificateValidator = certificateValidator,
ServerConfiguration = new ServerConfiguration
{
MaxSubscriptionCount = _options.MaxSubscriptionCount,
MaxMessageQueueSize = _options.MaxMessageQueueSize,
MaxNotificationQueueSize = _options.MaxNotificationQueueSize,
MaxPublishRequestCount = _options.MaxPublishRequestCount
},
SecurityConfiguration = new SecurityConfiguration
{
AutoAcceptUntrustedCertificates = _options.AutoAcceptUntrustedCertificates
},
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
}
};
// Assign a application certificate (when specified)
if (_options.ApplicationCertificate != null)
appInstance.ApplicationConfiguration.SecurityConfiguration.ApplicationCertificate = new CertificateIdentifier(_options.ApplicationCertificate);
// Find the endpoint to be used
var endpoints = ClientUtils.SelectEndpoint(url, _options.UseMessageSecurity);
// Create the OPC session:
var session = await Session.Create(
configuration: appInstance.ApplicationConfiguration,
endpoint: new ConfiguredEndpoint(
collection: null,
description: endpoints,
configuration: EndpointConfiguration.Create(applicationConfiguration: appInstance.ApplicationConfiguration)),
updateBeforeConnect: false,
checkDomain: false,
sessionName: _options.SessionName,
sessionTimeout: _options.SessionTimeout,
identity: GetIdentity(url),
preferredLocales: new string[] { });
return session;
}
///
/// Finds a node starting from the specified node as the root folder
///
/// the tag to find
/// the root node
///
private UaNode FindNode(string tag, UaNode node)
{
var folders = tag.Split('.');
var head = folders.FirstOrDefault();
UaNode found;
try
{
var subNodes = ExploreFolder(node.Tag);
found = subNodes.Single(n => n.Name == head);
}
catch (Exception ex)
{
throw new OpcException(string.Format("The tag \"{0}\" doesn't exist on folder \"{1}\"", head, node.Tag), ex);
}
// remove an array element by converting it to a list
var folderList = folders.ToList();
folderList.RemoveAt(0); // remove the first node
folders = folderList.ToArray();
return folders.Length == 0
? found // last node, return it
: FindNode(string.Join(".", folders), found); // find sub nodes
}
private void NotifyServerConnectionLost()
{
if (ServerConnectionLost != null)
ServerConnectionLost(this, EventArgs.Empty);
}
private void NotifyServerConnectionRestored()
{
if (ServerConnectionRestored != null)
ServerConnectionRestored(this, EventArgs.Empty);
}
///
/// This event is raised when the connection to the OPC server is lost.
///
public event EventHandler ServerConnectionLost;
///
/// This event is raised when the connection to the OPC server is restored.
///
public event EventHandler ServerConnectionRestored;
}
}