/// <summary> /// Handler for the standard "keep alive" event sent by all OPC UA servers /// </summary> private static void StandardClient_KeepAlive(Session session, KeepAliveEventArgs e) { // Ignore if we are shutting down. if (PublisherShutdownInProgress == true) { return; } if (e != null && session != null && session.ConfiguredEndpoint != null) { OpcSession opcSession = null; try { OpcSessionsSemaphore.Wait(); var opcSessions = OpcSessions.Where(s => s.Session != null); opcSession = opcSessions.Where(s => s.Session.ConfiguredEndpoint.EndpointUrl.Equals(session.ConfiguredEndpoint.EndpointUrl)).FirstOrDefault(); if (!ServiceResult.IsGood(e.Status)) { Trace($"Session endpoint: {session.ConfiguredEndpoint.EndpointUrl} has Status: {e.Status}"); Trace($"Outstanding requests: {session.OutstandingRequestCount}, Defunct requests: {session.DefunctRequestCount}"); Trace($"Good publish requests: {session.GoodPublishRequestCount}, KeepAlive interval: {session.KeepAliveInterval}"); Trace($"SessionId: {session.SessionId}"); if (opcSession != null && opcSession.State == SessionState.Connected) { opcSession.MissedKeepAlives++; Trace($"Missed KeepAlives: {opcSession.MissedKeepAlives}"); if (opcSession.MissedKeepAlives >= OpcKeepAliveDisconnectThreshold) { Trace($"Hit configured missed keep alive threshold of {Program.OpcKeepAliveDisconnectThreshold}. Disconnecting the session to endpoint {session.ConfiguredEndpoint.EndpointUrl}."); session.KeepAlive -= StandardClient_KeepAlive; opcSession.Disconnect(); } } } else { if (opcSession != null && opcSession.MissedKeepAlives != 0) { // Reset missed keep alive count Trace($"Session endpoint: {session.ConfiguredEndpoint.EndpointUrl} got a keep alive after {opcSession.MissedKeepAlives} {(opcSession.MissedKeepAlives == 1 ? "was" : "were")} missed."); opcSession.MissedKeepAlives = 0; } } } finally { OpcSessionsSemaphore.Release(); } } else { Trace("Keep alive arguments seems to be wrong."); } }
/// <summary> /// Shutdown all sessions. /// </summary> public async static Task SessionShutdownAsync() { try { while (OpcSessions.Count > 0) { OpcSession opcSession = null; try { await OpcSessionsListSemaphore.WaitAsync(); opcSession = OpcSessions.ElementAt(0); OpcSessions.RemoveAt(0); } finally { OpcSessionsListSemaphore.Release(); } await opcSession?.ShutdownAsync(); } } catch (Exception e) { Trace(e, "Failed to shutdown all sessions."); } // Wait and continue after a while. uint maxTries = _publisherShutdownWaitPeriod; while (true) { int sessionCount = OpcSessions.Count; if (sessionCount == 0) { return; } if (maxTries-- == 0) { Trace($"There are still {sessionCount} sessions alive. Ignore and continue shutdown."); return; } Trace($"Publisher is shutting down. Wait {_publisherSessionConnectWaitSec} seconds, since there are stil {sessionCount} sessions alive..."); await Task.Delay(_publisherSessionConnectWaitSec * 1000); } }
/// <summary> /// write to session. /// </summary> public static void SessionWriteNode(string uri, string node, string value) { try { if (OpcSessions.Count > 0) { OpcSession opcSession = OpcSessions.Find(x => x.EndpointUri.ToString() == uri); if (opcSession != null) { opcSession.WriteNode(node, value); } } } catch (Exception e) { Trace(e, $"Failed to write value to session. (message: {e.Message}"); } }
/// <summary> /// Create the publisher data structures to manage OPC sessions, subscriptions and monitored items. /// </summary> /// <returns></returns> public static async Task <bool> CreateOpcPublishingDataAsync() { // create a list to manage sessions, subscriptions and monitored items. try { await PublisherNodeConfigurationSemaphore.WaitAsync().ConfigureAwait(false); await OpcSessionsListSemaphore.WaitAsync().ConfigureAwait(false); var uniqueEndpointUrls = _nodePublishingConfiguration.Select(n => n.EndpointUrl).Distinct(); foreach (var endpointUrl in uniqueEndpointUrls) { // create new session info. OpcSession opcSession = new OpcSession(endpointUrl, _nodePublishingConfiguration.Where(n => n.EndpointUrl == endpointUrl).First().UseSecurity, OpcSessionCreationTimeout); // create a subscription for each distinct publishing inverval var nodesDistinctPublishingInterval = _nodePublishingConfiguration.Where(n => n.EndpointUrl.Equals(endpointUrl, StringComparison.OrdinalIgnoreCase)).Select(c => c.OpcPublishingInterval).Distinct(); foreach (var nodeDistinctPublishingInterval in nodesDistinctPublishingInterval) { // create a subscription for the publishing interval and add it to the session. OpcSubscription opcSubscription = new OpcSubscription(nodeDistinctPublishingInterval); // add all nodes with this OPC publishing interval to this subscription. var nodesWithSamePublishingInterval = _nodePublishingConfiguration.Where(n => n.EndpointUrl.Equals(endpointUrl, StringComparison.OrdinalIgnoreCase)).Where(n => n.OpcPublishingInterval == nodeDistinctPublishingInterval); foreach (var nodeInfo in nodesWithSamePublishingInterval) { // differentiate if NodeId or ExpandedNodeId format is used if (nodeInfo.ExpandedNodeId != null) { // create a monitored item for the node, we do not have the namespace index without a connected session. // so request a namespace update. OpcMonitoredItem opcMonitoredItem = new OpcMonitoredItem(nodeInfo.ExpandedNodeId, opcSession.EndpointUrl, nodeInfo.OpcSamplingInterval, nodeInfo.DisplayName); opcSubscription.OpcMonitoredItems.Add(opcMonitoredItem); Interlocked.Increment(ref NodeConfigVersion); } else if (nodeInfo.NodeId != null) { // create a monitored item for the node with the configured or default sampling interval OpcMonitoredItem opcMonitoredItem = new OpcMonitoredItem(nodeInfo.NodeId, opcSession.EndpointUrl, nodeInfo.OpcSamplingInterval, nodeInfo.DisplayName); opcSubscription.OpcMonitoredItems.Add(opcMonitoredItem); Interlocked.Increment(ref NodeConfigVersion); } else { Logger.Error($"Node {nodeInfo.OriginalId} has an invalid format. Skipping..."); } } // add subscription to session. opcSession.OpcSubscriptions.Add(opcSubscription); } // add session. OpcSessions.Add(opcSession); } } catch (Exception e) { Logger.Fatal(e, "Creation of the internal OPC data managment structures failed. Exiting..."); return(false); } finally { OpcSessionsListSemaphore.Release(); PublisherNodeConfigurationSemaphore.Release(); } return(true); }
/// <summary> /// Method to remove the node from the subscription and stop publishing telemetry to IoTHub. Executes synchronously. /// </summary> private ServiceResult OnUnpublishNodeCall(ISystemContext context, MethodState method, IList <object> inputArguments, IList <object> outputArguments) { string logPrefix = "OnUnpublishNodeCall:"; if (string.IsNullOrEmpty(inputArguments[0] as string) || string.IsNullOrEmpty(inputArguments[1] as string)) { Logger.Error($"{logPrefix} Invalid arguments!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide all arguments!")); } HttpStatusCode statusCode = HttpStatusCode.InternalServerError; NodeId nodeId = null; ExpandedNodeId expandedNodeId = null; Uri endpointUri = null; bool isNodeIdFormat = true; try { string id = inputArguments[0] as string; if (id.Contains("nsu=", StringComparison.InvariantCulture)) { expandedNodeId = ExpandedNodeId.Parse(id); isNodeIdFormat = false; } else { nodeId = NodeId.Parse(id); isNodeIdFormat = true; } endpointUri = new Uri(inputArguments[1] as string); } catch (UriFormatException) { Logger.Error($"{logPrefix} The endpointUrl is invalid '{inputArguments[1] as string}'!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide a valid OPC UA endpoint URL as second argument!")); } catch (Exception e) { Logger.Error(e, $"{logPrefix} The NodeId has an invalid format '{inputArguments[0] as string}'!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide a valid OPC UA NodeId in NodeId or ExpandedNodeId format as first argument!")); } // find the session and stop monitoring the node. try { OpcSessionsListSemaphore.Wait(); if (ShutdownTokenSource.IsCancellationRequested) { return(ServiceResult.Create(StatusCodes.BadUnexpectedError, $"Publisher shutdown in progress.")); } // find the session we need to monitor the node OpcSession opcSession = null; try { opcSession = OpcSessions.FirstOrDefault(s => s.EndpointUrl.Equals(endpointUri.OriginalString, StringComparison.OrdinalIgnoreCase)); } catch { opcSession = null; } if (opcSession == null) { // do nothing if there is no session for this endpoint. Logger.Error($"{logPrefix} Session for endpoint '{endpointUri.OriginalString}' not found."); return(ServiceResult.Create(StatusCodes.BadSessionIdInvalid, "Session for endpoint of node to unpublished not found!")); } else { if (isNodeIdFormat) { // stop monitoring the node, execute syncronously Logger.Information($"{logPrefix} Request to stop monitoring item with NodeId '{nodeId.ToString()}')"); statusCode = opcSession.RequestMonitorItemRemovalAsync(nodeId, null, ShutdownTokenSource.Token).Result; } else { // stop monitoring the node, execute syncronously Logger.Information($"{logPrefix} Request to stop monitoring item with ExpandedNodeId '{expandedNodeId.ToString()}')"); statusCode = opcSession.RequestMonitorItemRemovalAsync(null, expandedNodeId, ShutdownTokenSource.Token).Result; } } } catch (Exception e) { Logger.Error(e, $"{logPrefix} Exception while trying to configure publishing node '{nodeId.ToString()}'"); return(ServiceResult.Create(e, StatusCodes.BadUnexpectedError, $"Unexpected error unpublishing node: {e.Message}")); } finally { OpcSessionsListSemaphore.Release(); } return(statusCode == HttpStatusCode.OK || statusCode == HttpStatusCode.Accepted ? ServiceResult.Good : ServiceResult.Create(StatusCodes.Bad, "Can not stop monitoring node!")); }
/// <summary> /// Method to start monitoring a node and publish the data to IoTHub. Executes synchronously. /// </summary> private ServiceResult OnPublishNodeCall(ISystemContext context, MethodState method, IList <object> inputArguments, IList <object> outputArguments) { string logPrefix = "OnPublishNodeCall:"; if (string.IsNullOrEmpty(inputArguments[0] as string) || string.IsNullOrEmpty(inputArguments[1] as string)) { Logger.Error($"{logPrefix} Invalid Arguments when trying to publish a node."); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide all arguments as strings!")); } HttpStatusCode statusCode = HttpStatusCode.InternalServerError; NodeId nodeId = null; ExpandedNodeId expandedNodeId = null; Uri endpointUri = null; bool isNodeIdFormat = true; try { string id = inputArguments[0] as string; if (id.Contains("nsu=", StringComparison.InvariantCulture)) { expandedNodeId = ExpandedNodeId.Parse(id); isNodeIdFormat = false; } else { nodeId = NodeId.Parse(id); isNodeIdFormat = true; } endpointUri = new Uri(inputArguments[1] as string); } catch (UriFormatException) { Logger.Error($"{logPrefix} The EndpointUrl has an invalid format '{inputArguments[1] as string}'!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide a valid OPC UA endpoint URL as second argument!")); } catch (Exception e) { Logger.Error(e, $"{logPrefix} The NodeId has an invalid format '{inputArguments[0] as string}'!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide a valid OPC UA NodeId in NodeId or ExpandedNodeId format as first argument!")); } // find/create a session to the endpoint URL and start monitoring the node. try { // lock the publishing configuration till we are done OpcSessionsListSemaphore.Wait(); if (ShutdownTokenSource.IsCancellationRequested) { return(ServiceResult.Create(StatusCodes.BadUnexpectedError, $"Publisher shutdown in progress.")); } // find the session we need to monitor the node OpcSession opcSession = null; opcSession = OpcSessions.FirstOrDefault(s => s.EndpointUrl.Equals(endpointUri.OriginalString, StringComparison.OrdinalIgnoreCase)); // add a new session. if (opcSession == null) { // create new session info. opcSession = new OpcSession(endpointUri.OriginalString, true, OpcSessionCreationTimeout); OpcSessions.Add(opcSession); Logger.Information($"OnPublishNodeCall: No matching session found for endpoint '{endpointUri.OriginalString}'. Requested to create a new one."); } if (isNodeIdFormat) { // add the node info to the subscription with the default publishing interval, execute syncronously Logger.Debug($"{logPrefix} Request to monitor item with NodeId '{nodeId.ToString()}' (with default PublishingInterval and SamplingInterval)"); statusCode = opcSession.AddNodeForMonitoringAsync(nodeId, null, null, null, null, null, null, ShutdownTokenSource.Token).Result; } else { // add the node info to the subscription with the default publishing interval, execute syncronously Logger.Debug($"{logPrefix} Request to monitor item with ExpandedNodeId '{expandedNodeId.ToString()}' (with default PublishingInterval and SamplingInterval)"); statusCode = opcSession.AddNodeForMonitoringAsync(null, expandedNodeId, null, null, null, null, null, ShutdownTokenSource.Token).Result; } } catch (Exception e) { Logger.Error(e, $"{logPrefix} Exception while trying to configure publishing node '{(isNodeIdFormat ? nodeId.ToString() : expandedNodeId.ToString())}'"); return(ServiceResult.Create(e, StatusCodes.BadUnexpectedError, $"Unexpected error publishing node: {e.Message}")); } finally { OpcSessionsListSemaphore.Release(); } if (statusCode == HttpStatusCode.OK || statusCode == HttpStatusCode.Accepted) { return(ServiceResult.Good); } return(ServiceResult.Create(StatusCodes.Bad, "Can not start monitoring node! Reason unknown.")); }
/// <summary> /// Method to remove the node from the subscription and stop publishing telemetry to IoTHub. /// </summary> private ServiceResult OnUnpublishNodeCall(ISystemContext context, MethodState method, IList <object> inputArguments, IList <object> outputArguments) { if (inputArguments[0] == null || inputArguments[1] == null) { Trace("UnpublishNode: Invalid arguments!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide all arguments!")); } NodeId nodeId = null; Uri endpointUri = null; try { if (string.IsNullOrEmpty(inputArguments[0] as string) || string.IsNullOrEmpty(inputArguments[1] as string)) { Trace($"UnpublishNode: Arguments (0 (nodeId), 1 (endpointUrl)) are not valid strings!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide all arguments as strings!")); } nodeId = inputArguments[0] as string; endpointUri = new Uri(inputArguments[1] as string); } catch (UriFormatException) { Trace($"UnpublishNode: The endpointUrl is invalid '{inputArguments[1] as string}'!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide a valid OPC UA endpoint URL as second argument!")); } // find the session and stop monitoring the node. try { if (PublisherShutdownInProgress) { return(ServiceResult.Create(StatusCodes.BadUnexpectedError, $"Publisher shutdown in progress.")); } // find the session we need to monitor the node OpcSession opcSession = null; try { OpcSessionsSemaphore.Wait(); opcSession = OpcSessions.FirstOrDefault(s => s.EndpointUri == endpointUri); } catch { opcSession = null; } finally { OpcSessionsSemaphore.Release(); } if (opcSession == null) { // do nothing if there is no session for this endpoint. Trace($"UnpublishNode: Session for endpoint '{endpointUri.OriginalString}' not found."); return(ServiceResult.Create(StatusCodes.BadSessionIdInvalid, "Session for endpoint of published node not found!")); } else { Trace($"UnpublishNode: Session found for endpoint '{endpointUri.OriginalString}'"); } // remove the node from the sessions monitored items list. opcSession.TagNodeForMonitoringStop(nodeId); Trace("UnpublishNode: Requested to stop monitoring of node."); // remove node from persisted config file try { PublishDataSemaphore.Wait(); var entryToRemove = PublisherConfigFileEntries.Find(l => l.NodeId == nodeId && l.EndpointUri == endpointUri); PublisherConfigFileEntries.Remove(entryToRemove); File.WriteAllText(NodesToPublishAbsFilename, JsonConvert.SerializeObject(PublisherConfigFileEntries)); } finally { PublishDataSemaphore.Release(); } } catch (Exception e) { Trace(e, $"UnpublishNode: Exception while trying to configure publishing node '{nodeId.ToString()}'"); return(ServiceResult.Create(e, StatusCodes.BadUnexpectedError, $"Unexpected error publishing node: {e.Message}")); } return(ServiceResult.Good); }
/// <summary> /// Method to start monitoring a node and publish the data to IoTHub. /// </summary> private ServiceResult OnPublishNodeCall(ISystemContext context, MethodState method, IList <object> inputArguments, IList <object> outputArguments) { if (inputArguments[0] == null || inputArguments[1] == null) { Trace("PublishNode: Invalid Arguments when trying to publish a node."); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide all arguments!")); } NodeToPublishConfig nodeToPublish; NodeId nodeId = null; Uri endpointUri = null; try { if (string.IsNullOrEmpty(inputArguments[0] as string) || string.IsNullOrEmpty(inputArguments[1] as string)) { Trace($"PublishNode: Arguments (0 (nodeId), 1 (endpointUrl)) are not valid strings!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide all arguments as strings!")); } nodeId = NodeId.Parse(inputArguments[0] as string); endpointUri = new Uri(inputArguments[1] as string); nodeToPublish = new NodeToPublishConfig(nodeId, endpointUri, OpcSamplingInterval, OpcPublishingInterval); } catch (UriFormatException) { Trace($"PublishNode: The EndpointUri has an invalid format '{inputArguments[1] as string}'!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide a valid OPC UA endpoint URL as second argument!")); } catch (Exception e) { Trace(e, $"PublishNode: The NodeId has an invalid format '{inputArguments[0] as string}'!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide a valid OPC UA NodeId in 'ns=' syntax as first argument!")); } // find/create a session to the endpoint URL and start monitoring the node. try { if (PublisherShutdownInProgress) { return(ServiceResult.Create(StatusCodes.BadUnexpectedError, $"Publisher shutdown in progress.")); } // find the session we need to monitor the node OpcSession opcSession = null; try { OpcSessionsSemaphore.Wait(); opcSession = OpcSessions.FirstOrDefault(s => s.EndpointUri == nodeToPublish.EndpointUri); // add a new session. if (opcSession == null) { // create new session info. opcSession = new OpcSession(nodeToPublish.EndpointUri, OpcSessionCreationTimeout); OpcSessions.Add(opcSession); Trace($"PublishNode: No matching session found for endpoint '{nodeToPublish.EndpointUri.OriginalString}'. Requested to create a new one."); } else { Trace($"PublishNode: Session found for endpoint '{nodeToPublish.EndpointUri.OriginalString}'"); } // add the node info to the subscription with the default publishing interval opcSession.AddNodeForMonitoring(OpcPublishingInterval, OpcSamplingInterval, nodeToPublish.NodeId); Trace($"PublishNode: Requested to monitor item with NodeId '{nodeToPublish.NodeId.ToString()}' (PublishingInterval: {OpcPublishingInterval}, SamplingInterval: {OpcSamplingInterval})"); } finally { OpcSessionsSemaphore.Release(); } // update our data try { PublishDataSemaphore.Wait(); PublishConfig.Add(nodeToPublish); // add it also to the publish file var publisherConfigFileEntry = new PublisherConfigFileEntry() { EndpointUri = endpointUri, NodeId = nodeId }; PublisherConfigFileEntries.Add(publisherConfigFileEntry); File.WriteAllText(NodesToPublishAbsFilename, JsonConvert.SerializeObject(PublisherConfigFileEntries)); } finally { PublishDataSemaphore.Release(); } return(ServiceResult.Good); } catch (Exception e) { Trace(e, $"PublishNode: Exception while trying to configure publishing node '{nodeToPublish.NodeId.ToString()}'"); return(ServiceResult.Create(e, StatusCodes.BadUnexpectedError, $"Unexpected error publishing node: {e.Message}")); } }
public static void Main(string[] args) { var opcTraceInitialized = false; try { var shouldShowHelp = false; // command line options configuration Mono.Options.OptionSet options = new Mono.Options.OptionSet { // Publishing configuration options { "pf|publishfile=", $"the filename to configure the nodes to publish.\nDefault: '{NodesToPublishAbsFilenameDefault}'", (string p) => NodesToPublishAbsFilename = p }, { "sd|shopfloordomain=", $"the domain of the shopfloor. if specified this domain is appended (delimited by a ':' to the 'ApplicationURI' property when telemetry is sent to IoTHub.\n" + "The value must follow the syntactical rules of a DNS hostname.\nDefault: not set", (string s) => { Regex domainNameRegex = new Regex("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$"); if (domainNameRegex.IsMatch(s)) { ShopfloorDomain = s; } else { throw new OptionException("The shopfloor domain is not a valid DNS hostname.", "shopfloordomain"); } } }, { "sw|sessionconnectwait=", $"specify the wait time in seconds publisher is trying to connect to disconnected endpoints and starts monitoring unmonitored items\nMin: 10\nDefault: {PublisherSessionConnectWaitSec}", (int i) => { if (i > 10) { PublisherSessionConnectWaitSec = i; } else { throw new OptionException("The sessionconnectwait must be greater than 10 sec", "sessionconnectwait"); } } }, { "vc|verboseconsole=", $"the output of publisher is shown on the console.\nDefault: {VerboseConsole}", (bool b) => VerboseConsole = b }, // IoTHub specific options { "ih|iothubprotocol=", $"the protocol to use for communication with Azure IoTHub (allowed values: {string.Join(", ", Enum.GetNames(IotHubProtocol.GetType()))}).\nDefault: {Enum.GetName(IotHubProtocol.GetType(), IotHubProtocol)}", (Microsoft.Azure.Devices.Client.TransportType p) => IotHubProtocol = p }, { "ms|iothubmessagesize=", $"the max size of a message which can be send to IoTHub. when telemetry of this size is available it will be sent.\n0 will enforce immediate send when telemetry is available\nMin: 0\nMax: 256 * 1024\nDefault: {_MaxSizeOfIoTHubMessageBytes}", (uint u) => { if (u >= 0 && u <= 256 * 1024) { _MaxSizeOfIoTHubMessageBytes = u; } else { throw new OptionException("The iothubmessagesize must be in the range between 1 and 256*1024.", "iothubmessagesize"); } } }, { "si|iothubsendinterval=", $"the interval in seconds when telemetry should be send to IoTHub. If 0, then only the iothubmessagesize parameter controls when telemetry is sent.\nDefault: '{_DefaultSendIntervalSeconds}'", (int i) => { if (i >= 0) { _DefaultSendIntervalSeconds = i; } else { throw new OptionException("The iothubsendinterval must be larger or equal 0.", "iothubsendinterval"); } } }, // opc server configuration options { "lf|logfile=", $"the filename of the logfile to use.\nDefault: './Logs/<applicationname>.log.txt'", (string l) => LogFileName = l }, { "pn|portnum=", $"the server port of the publisher OPC server endpoint.\nDefault: {PublisherServerPort}", (ushort p) => PublisherServerPort = p }, { "pa|path=", $"the enpoint URL path part of the publisher OPC server endpoint.\nDefault: '{PublisherServerPath}'", (string a) => PublisherServerPath = a }, { "lr|ldsreginterval=", $"the LDS(-ME) registration interval in ms. If 0, then the registration is disabled.\nDefault: {LdsRegistrationInterval}", (int i) => { if (i >= 0) { LdsRegistrationInterval = i; } else { throw new OptionException("The ldsreginterval must be larger or equal 0.", "ldsreginterval"); } } }, { "ot|operationtimeout=", $"the operation timeout of the publisher OPC UA client in ms.\nDefault: {OpcOperationTimeout}", (int i) => { if (i >= 0) { OpcOperationTimeout = i; } else { throw new OptionException("The operation timeout must be larger or equal 0.", "operationtimeout"); } } }, { "oi|opcsamplinginterval=", "the publisher is using this as default value in milliseconds to request the servers to sample the nodes with this interval\n" + "this value might be revised by the OPC UA servers to a supported sampling interval.\n" + "please check the OPC UA specification for details how this is handled by the OPC UA stack.\n" + "a negative value will set the sampling interval to the publishing interval of the subscription this node is on.\n" + $"0 will configure the OPC UA server to sample in the highest possible resolution and should be taken with care.\nDefault: {OpcSamplingInterval}", (int i) => OpcSamplingInterval = i }, { "op|opcpublishinginterval=", "the publisher is using this as default value in milliseconds for the publishing interval setting of the subscriptions established to the OPC UA servers.\n" + "please check the OPC UA specification for details how this is handled by the OPC UA stack.\n" + $"a value less than or equal zero will let the server revise the publishing interval.\nDefault: {OpcPublishingInterval}", (int i) => { if (i > 0 && i >= OpcSamplingInterval) { OpcPublishingInterval = i; } else { if (i <= 0) { OpcPublishingInterval = 0; } else { throw new OptionException($"The opcpublishinterval ({i}) must be larger than the opcsamplinginterval ({OpcSamplingInterval}).", "opcpublishinterval"); } } } }, { "ct|createsessiontimeout=", $"specify the timeout in seconds used when creating a session to an endpoint. On unsuccessful connection attemps a backoff up to {OpcSessionCreationBackoffMax} times the specified timeout value is used.\nMin: 1\nDefault: {OpcSessionCreationTimeout}", (uint u) => { if (u > 1) { OpcSessionCreationTimeout = u; } else { throw new OptionException("The createsessiontimeout must be greater than 1 sec", "createsessiontimeout"); } } }, { "ki|keepaliveinterval=", $"specify the interval in seconds the publisher is sending keep alive messages to the OPC servers on the endpoints it is connected to.\nMin: 2\nDefault: {OpcKeepAliveIntervalInSec}", (int i) => { if (i >= 2) { OpcKeepAliveIntervalInSec = i; } else { throw new OptionException("The keepaliveinterval must be greater or equal 2", "keepalivethreshold"); } } }, { "kt|keepalivethreshold=", $"specify the number of keep alive packets a server can miss, before the session is disconneced\nMin: 1\nDefault: {OpcKeepAliveDisconnectThreshold}", (uint u) => { if (u > 1) { OpcKeepAliveDisconnectThreshold = u; } else { throw new OptionException("The keepalivethreshold must be greater than 1", "keepalivethreshold"); } } }, { "st|opcstacktracemask=", $"the trace mask for the OPC stack. See github OPC .NET stack for definitions.\nTo enable IoTHub telemetry tracing set it to 711.\nDefault: {OpcStackTraceMask:X} ({Program.OpcStackTraceMask})", (int i) => { if (i >= 0) { OpcStackTraceMask = i; } else { throw new OptionException("The OPC stack trace mask must be larger or equal 0.", "opcstacktracemask"); } } }, { "as|autotrustservercerts=", $"the publisher trusts all servers it is establishing a connection to.\nDefault: {OpcPublisherAutoTrustServerCerts}", (bool b) => OpcPublisherAutoTrustServerCerts = b }, // trust own public cert option { "tm|trustmyself=", $"the publisher certificate is put into the trusted certificate store automatically.\nDefault: {TrustMyself}", (bool b) => TrustMyself = b }, // own cert store options { "at|appcertstoretype=", $"the own application cert store type. \n(allowed values: Directory, X509Store)\nDefault: '{OpcOwnCertStoreType}'", (string s) => { if (s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) || s.Equals(Directory, StringComparison.OrdinalIgnoreCase)) { OpcOwnCertStoreType = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? X509Store : Directory; OpcOwnCertStorePath = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? _opcOwnCertX509StorePathDefault : _opcOwnCertDirectoryStorePathDefault; } else { throw new OptionException(); } } }, { "ap|appcertstorepath=", $"the path where the own application cert should be stored\nDefault (depends on store type):\n" + $"X509Store: '{_opcOwnCertX509StorePathDefault}'\n" + $"Directory: '{_opcOwnCertDirectoryStorePathDefault}'", (string s) => OpcOwnCertStorePath = s }, // trusted cert store options { "tt|trustedcertstoretype=", $"the trusted cert store type. \n(allowed values: Directory, X509Store)\nDefault: {OpcTrustedCertStoreType}", (string s) => { if (s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) || s.Equals(Directory, StringComparison.OrdinalIgnoreCase)) { OpcTrustedCertStoreType = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? X509Store : Directory; OpcTrustedCertStorePath = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? OpcTrustedCertX509StorePathDefault : OpcTrustedCertDirectoryStorePathDefault; } else { throw new OptionException(); } } }, { "tp|trustedcertstorepath=", $"the path of the trusted cert store\nDefault (depends on store type):\n" + $"X509Store: '{OpcTrustedCertX509StorePathDefault}'\n" + $"Directory: '{OpcTrustedCertDirectoryStorePathDefault}'", (string s) => OpcTrustedCertStorePath = s }, // rejected cert store options { "rt|rejectedcertstoretype=", $"the rejected cert store type. \n(allowed values: Directory, X509Store)\nDefault: {OpcRejectedCertStoreType}", (string s) => { if (s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) || s.Equals(Directory, StringComparison.OrdinalIgnoreCase)) { OpcRejectedCertStoreType = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? X509Store : Directory; OpcRejectedCertStorePath = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? _opcRejectedCertX509StorePathDefault : _opcRejectedCertDirectoryStorePathDefault; } else { throw new OptionException(); } } }, { "rp|rejectedcertstorepath=", $"the path of the rejected cert store\nDefault (depends on store type):\n" + $"X509Store: '{_opcRejectedCertX509StorePathDefault}'\n" + $"Directory: '{_opcRejectedCertDirectoryStorePathDefault}'", (string s) => OpcRejectedCertStorePath = s }, // issuer cert store options { "it|issuercertstoretype=", $"the trusted issuer cert store type. \n(allowed values: Directory, X509Store)\nDefault: {OpcIssuerCertStoreType}", (string s) => { if (s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) || s.Equals(Directory, StringComparison.OrdinalIgnoreCase)) { OpcIssuerCertStoreType = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? X509Store : Directory; OpcIssuerCertStorePath = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? _opcIssuerCertX509StorePathDefault : _opcIssuerCertDirectoryStorePathDefault; } else { throw new OptionException(); } } }, { "ip|issuercertstorepath=", $"the path of the trusted issuer cert store\nDefault (depends on store type):\n" + $"X509Store: '{_opcIssuerCertX509StorePathDefault}'\n" + $"Directory: '{_opcIssuerCertDirectoryStorePathDefault}'", (string s) => OpcIssuerCertStorePath = s }, // device connection string cert store options { "dt|devicecertstoretype=", $"the iothub device cert store type. \n(allowed values: Directory, X509Store)\nDefault: {IotDeviceCertStoreType}", (string s) => { if (s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) || s.Equals(Directory, StringComparison.OrdinalIgnoreCase)) { IotDeviceCertStoreType = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? X509Store : Directory; IotDeviceCertStorePath = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? _iotDeviceCertX509StorePathDefault : _iotDeviceCertDirectoryStorePathDefault; } else { throw new OptionException(); } } }, { "dp|devicecertstorepath=", $"the path of the iot device cert store\nDefault Default (depends on store type):\n" + $"X509Store: '{_iotDeviceCertX509StorePathDefault}'\n" + $"Directory: '{_iotDeviceCertDirectoryStorePathDefault}'", (string s) => IotDeviceCertStorePath = s }, // misc { "h|help", "show this message and exit", h => shouldShowHelp = h != null }, }; List <string> arguments; try { // parse the command line arguments = options.Parse(args); } catch (OptionException e) { // show message WriteLine($"Error: {e.Message}"); // show usage Usage(options); return; } // Validate and parse arguments. if (arguments.Count > 2 || shouldShowHelp) { Usage(options); return; } else if (arguments.Count == 2) { ApplicationName = arguments[0]; _IotHubOwnerConnectionString = arguments[1]; } else if (arguments.Count == 1) { ApplicationName = arguments[0]; } else { ApplicationName = Utils.GetHostName(); } WriteLine("Publisher is starting up..."); // init OPC configuration and tracing Init(OpcStackTraceMask, VerboseConsole); OpcStackConfiguration opcStackConfiguration = new OpcStackConfiguration(ApplicationName); opcTraceInitialized = true; OpcConfiguration = opcStackConfiguration.Configuration; // log shopfloor domain setting if (string.IsNullOrEmpty(ShopfloorDomain)) { Trace("There is no shopfloor domain configured."); } else { Trace($"Publisher is in shopfloor domain '{ShopfloorDomain}'."); } // Set certificate validator. if (OpcPublisherAutoTrustServerCerts) { Trace("Publisher configured to auto trust server certificates of the servers it is connecting to."); OpcConfiguration.CertificateValidator.CertificateValidation += new CertificateValidationEventHandler(CertificateValidator_AutoTrustServerCerts); } else { Trace("Publisher configured to not auto trust server certificates. When connecting to servers, you need to manually copy the rejected server certs to the trusted store to trust them."); OpcConfiguration.CertificateValidator.CertificateValidation += new CertificateValidationEventHandler(CertificateValidator_Default); } // start our server interface try { Trace($"Starting server on endpoint {OpcConfiguration.ServerConfiguration.BaseAddresses[0].ToString()} ..."); _publisherServer = new PublisherServer(); _publisherServer.Start(OpcConfiguration); Trace("Server started."); } catch (Exception e) { Trace(e, $"Failed to start Publisher OPC UA server."); Trace("exiting..."); return; } // get information on the nodes to publish and validate the json by deserializing it. try { PublishDataSemaphore.Wait(); if (string.IsNullOrEmpty(NodesToPublishAbsFilename)) { // check if we have an env variable specifying the published nodes path, otherwise use the default NodesToPublishAbsFilename = NodesToPublishAbsFilenameDefault; if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("_GW_PNFP"))) { Trace("Publishing node configuration file path read from environment."); NodesToPublishAbsFilename = Environment.GetEnvironmentVariable("_GW_PNFP"); } } Trace($"Attempting to load nodes file from: {NodesToPublishAbsFilename}"); PublisherConfigFileEntries = JsonConvert.DeserializeObject <List <PublisherConfigFileEntry> >(File.ReadAllText(NodesToPublishAbsFilename)); Trace($"Loaded {PublisherConfigFileEntries.Count.ToString()} config file entry/entries."); foreach (var publisherConfigFileEntry in PublisherConfigFileEntries) { if (publisherConfigFileEntry.NodeId == null) { // new node configuration syntax. foreach (var opcNode in publisherConfigFileEntry.OpcNodes) { PublishConfig.Add(new NodeToPublishConfig(ExpandedNodeId.Parse(opcNode.ExpandedNodeId), publisherConfigFileEntry.EndpointUri, opcNode.OpcSamplingInterval ?? OpcSamplingInterval, opcNode.OpcPublishingInterval ?? OpcPublishingInterval)); } } else { // legacy (using ns=) node configuration syntax using default sampling and publishing interval. PublishConfig.Add(new NodeToPublishConfig(publisherConfigFileEntry.NodeId, publisherConfigFileEntry.EndpointUri, OpcSamplingInterval, OpcPublishingInterval)); } } } catch (Exception e) { Trace(e, "Loading of the node configuration file failed. Does the file exist and has correct syntax?"); Trace("exiting..."); return; } finally { PublishDataSemaphore.Release(); } Trace($"There are {PublishConfig.Count.ToString()} nodes to publish."); // initialize and start IoTHub messaging IotHubCommunication = new IotHubMessaging(); if (!IotHubCommunication.Init(_IotHubOwnerConnectionString, _MaxSizeOfIoTHubMessageBytes, _DefaultSendIntervalSeconds)) { return; } // create a list to manage sessions, subscriptions and monitored items. try { PublishDataSemaphore.Wait(); OpcSessionsSemaphore.Wait(); var uniqueEndpointUrls = PublishConfig.Select(n => n.EndpointUri).Distinct(); foreach (var endpointUrl in uniqueEndpointUrls) { // create new session info. OpcSession opcSession = new OpcSession(endpointUrl, OpcSessionCreationTimeout); // create a subscription for each distinct publishing inverval var nodesDistinctPublishingInterval = PublishConfig.Where(n => n.EndpointUri.Equals(endpointUrl)).Select(c => c.OpcPublishingInterval).Distinct(); foreach (var nodeDistinctPublishingInterval in nodesDistinctPublishingInterval) { // create a subscription for the publishing interval and add it to the session. OpcSubscription opcSubscription = new OpcSubscription(nodeDistinctPublishingInterval); // add all nodes with this OPC publishing interval to this subscription. var nodesWithSamePublishingInterval = PublishConfig.Where(n => n.EndpointUri.Equals(endpointUrl)).Where(n => n.OpcPublishingInterval == nodeDistinctPublishingInterval); foreach (var nodeInfo in nodesWithSamePublishingInterval) { // differentiate if legacy (using ns=) or new syntax (using nsu=) is used if (nodeInfo.NodeId == null) { // create a monitored item for the node OpcMonitoredItem opcMonitoredItem = new OpcMonitoredItem(nodeInfo.ExpandedNodeId, opcSession.EndpointUri) { RequestedSamplingInterval = nodeInfo.OpcSamplingInterval, SamplingInterval = nodeInfo.OpcSamplingInterval }; opcSubscription.OpcMonitoredItems.Add(opcMonitoredItem); } else { // give user a warning that the syntax is obsolete Trace($"Please update the syntax of the configuration file and use ExpandedNodeId instead of NodeId property name for node with identifier '{nodeInfo.NodeId.ToString()}' on EndpointUrl '{nodeInfo.EndpointUri.AbsolutePath}'."); // create a monitored item for the node with the configured or default sampling interval OpcMonitoredItem opcMonitoredItem = new OpcMonitoredItem(nodeInfo.NodeId, opcSession.EndpointUri) { RequestedSamplingInterval = nodeInfo.OpcSamplingInterval, SamplingInterval = nodeInfo.OpcSamplingInterval }; opcSubscription.OpcMonitoredItems.Add(opcMonitoredItem); } } // add subscription to session. opcSession.OpcSubscriptions.Add(opcSubscription); } // add session. OpcSessions.Add(opcSession); } } finally { OpcSessionsSemaphore.Release(); PublishDataSemaphore.Release(); } // kick off the task to maintain all sessions var cts = new CancellationTokenSource(); Task.Run(async() => await SessionConnector(cts.Token)); // Show notification on session events _publisherServer.CurrentInstance.SessionManager.SessionActivated += ServerEventStatus; _publisherServer.CurrentInstance.SessionManager.SessionClosing += ServerEventStatus; _publisherServer.CurrentInstance.SessionManager.SessionCreated += ServerEventStatus; // stop on user request WriteLine(""); WriteLine(""); WriteLine("Publisher is running. Press ENTER to quit."); WriteLine(""); WriteLine(""); ReadLine(); cts.Cancel(); WriteLine("Publisher is shuting down..."); // close all connected session PublisherShutdownInProgress = true; // stop the server _publisherServer.Stop(); // Clean up Publisher sessions Task.Run(async() => await SessionShutdown()).Wait(); // shutdown the IoTHub messaging IotHubCommunication.Shutdown(); } catch (Exception e) { if (opcTraceInitialized) { Trace(e, e.StackTrace); e = e.InnerException ?? null; while (e != null) { Trace(e, e.StackTrace); e = e.InnerException ?? null; } Trace("Publisher exiting... "); } else { WriteLine($"{DateTime.Now.ToString()}: {e.Message.ToString()}"); WriteLine($"{DateTime.Now.ToString()}: {e.StackTrace}"); e = e.InnerException ?? null; while (e != null) { WriteLine($"{DateTime.Now.ToString()}: {e.Message.ToString()}"); WriteLine($"{DateTime.Now.ToString()}: {e.StackTrace}"); e = e.InnerException ?? null; } WriteLine($"{DateTime.Now.ToString()}: Publisher exiting..."); } } }
/// <summary> /// Method to remove the node from the subscription and stop publishing telemetry to IoTHub. Executes synchronously. /// </summary> private ServiceResult OnUnpublishNodeCall(ISystemContext context, MethodState method, IList <object> inputArguments, IList <object> outputArguments) { if (string.IsNullOrEmpty(inputArguments[0] as string) || string.IsNullOrEmpty(inputArguments[1] as string)) { Trace("UnpublishNode: Invalid arguments!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide all arguments!")); } NodeId nodeId = null; Uri endpointUri = null; try { nodeId = NodeId.Parse(inputArguments[0] as string); endpointUri = new Uri(inputArguments[1] as string); } catch (UriFormatException) { Trace($"UnpublishNode: The endpointUrl is invalid '{inputArguments[1] as string}'!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide a valid OPC UA endpoint URL as second argument!")); } // find the session and stop monitoring the node. try { if (ShutdownTokenSource.IsCancellationRequested) { return(ServiceResult.Create(StatusCodes.BadUnexpectedError, $"Publisher shutdown in progress.")); } // find the session we need to monitor the node OpcSession opcSession = null; try { OpcSessionsListSemaphore.WaitAsync(); opcSession = OpcSessions.FirstOrDefault(s => s.EndpointUri.AbsoluteUri.Equals(endpointUri.AbsoluteUri, StringComparison.OrdinalIgnoreCase)); } catch { opcSession = null; } finally { OpcSessionsListSemaphore.Release(); } ExpandedNodeId expandedNodeId = null; if (opcSession == null) { // do nothing if there is no session for this endpoint. Trace($"UnpublishNode: Session for endpoint '{endpointUri.OriginalString}' not found."); return(ServiceResult.Create(StatusCodes.BadSessionIdInvalid, "Session for endpoint of node to unpublished not found!")); } else { // check if node is already published string namespaceUri = opcSession.GetNamespaceUri(nodeId.NamespaceIndex); if (string.IsNullOrEmpty(namespaceUri)) { return(ServiceResult.Create(StatusCodes.BadUnexpectedError, $"The namespace index of the node id is invalid.")); } expandedNodeId = new ExpandedNodeId(nodeId.Identifier, nodeId.NamespaceIndex, namespaceUri, 0); if (!OpcSession.IsNodePublished(nodeId, expandedNodeId, endpointUri)) { Trace($"UnpublishNode: Node with id '{nodeId.Identifier.ToString()}' on endpoint '{endpointUri.OriginalString}' is not published."); return(ServiceResult.Good); } } // remove the node from the sessions monitored items list. opcSession.RequestMonitorItemRemoval(nodeId, expandedNodeId, ShutdownTokenSource.Token).Wait(); Trace("UnpublishNode: Requested to stop monitoring of node."); } catch (Exception e) { Trace(e, $"UnpublishNode: Exception while trying to configure publishing node '{nodeId.ToString()}'"); return(ServiceResult.Create(e, StatusCodes.BadUnexpectedError, $"Unexpected error unpublishing node: {e.Message}")); } return(ServiceResult.Good); }
/// <summary> /// Method to start monitoring a node and publish the data to IoTHub. Executes synchronously. /// </summary> private ServiceResult OnPublishNodeCall(ISystemContext context, MethodState method, IList <object> inputArguments, IList <object> outputArguments) { if (string.IsNullOrEmpty(inputArguments[0] as string) || string.IsNullOrEmpty(inputArguments[1] as string)) { Trace("PublishNode: Invalid Arguments when trying to publish a node."); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide all arguments as strings!")); } NodeId nodeId = null; Uri endpointUri = null; try { nodeId = NodeId.Parse(inputArguments[0] as string); endpointUri = new Uri(inputArguments[1] as string); } catch (UriFormatException) { Trace($"PublishNode: The EndpointUri has an invalid format '{inputArguments[1] as string}'!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide a valid OPC UA endpoint URL as second argument!")); } catch (Exception e) { Trace(e, $"PublishNode: The NodeId has an invalid format '{inputArguments[0] as string}'!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide a valid OPC UA NodeId in NodeId format as first argument!")); } // find/create a session to the endpoint URL and start monitoring the node. try { // lock the publishing configuration till we are done OpcSessionsListSemaphore.WaitAsync(); if (ShutdownTokenSource.IsCancellationRequested) { return(ServiceResult.Create(StatusCodes.BadUnexpectedError, $"Publisher shutdown in progress.")); } // find the session we need to monitor the node OpcSession opcSession = null; opcSession = OpcSessions.FirstOrDefault(s => s.EndpointUri.AbsoluteUri.Equals(endpointUri.AbsoluteUri, StringComparison.OrdinalIgnoreCase)); string namespaceUri = null; ExpandedNodeId expandedNodeId = null; // add a new session. if (opcSession == null) { // create new session info. opcSession = new OpcSession(endpointUri, true, OpcSessionCreationTimeout); OpcSessions.Add(opcSession); Trace($"PublishNode: No matching session found for endpoint '{endpointUri.OriginalString}'. Requested to create a new one."); } else { Trace($"PublishNode: Session found for endpoint '{endpointUri.OriginalString}'"); // check if node is already published namespaceUri = opcSession.GetNamespaceUri(nodeId.NamespaceIndex); if (string.IsNullOrEmpty(namespaceUri)) { return(ServiceResult.Create(StatusCodes.BadUnexpectedError, $"The namespace index of the node id is invalid.")); } expandedNodeId = new ExpandedNodeId(nodeId.Identifier, nodeId.NamespaceIndex, namespaceUri, 0); } // add the node info to the subscription with the default publishing interval, execute syncronously opcSession.AddNodeForMonitoring(nodeId, expandedNodeId, OpcPublishingInterval, OpcSamplingInterval, ShutdownTokenSource.Token).Wait(); Trace($"PublishNode: Requested to monitor item with NodeId '{nodeId.ToString()}' (PublishingInterval: {OpcPublishingInterval}, SamplingInterval: {OpcSamplingInterval})"); } catch (Exception e) { Trace(e, $"PublishNode: Exception while trying to configure publishing node '{nodeId.ToString()}'"); return(ServiceResult.Create(e, StatusCodes.BadUnexpectedError, $"Unexpected error publishing node: {e.Message}")); } finally { OpcSessionsListSemaphore.Release(); } return(ServiceResult.Good); }
/// <summary> /// Handle publish event node method call. /// </summary> public virtual async Task <MethodResponse> HandlePublishEventsMethodAsync(MethodRequest methodRequest, object userContext) { var logPrefix = "HandlePublishEventsMethodAsync:"; var useSecurity = true; Guid endpointId = Guid.Empty; string endpointName = null; Uri endpointUri = null; OpcAuthenticationMode? desiredAuthenticationMode = null; EncryptedNetworkCredential desiredEncryptedCredential = null; PublishNodesMethodRequestModel publishEventsMethodData = null; PublishNodesMethodResponseModel publishedEventMethodResponse = null; var statusCode = HttpStatusCode.OK; var statusResponse = new List <string>(); string statusMessage; try { _logger.Debug($"{logPrefix} called"); publishEventsMethodData = JsonConvert.DeserializeObject <PublishNodesMethodRequestModel>(methodRequest.DataAsJson); endpointId = publishEventsMethodData.EndpointId == null ? Guid.Empty : new Guid(publishEventsMethodData.EndpointId); endpointName = publishEventsMethodData.EndpointName; endpointUri = new Uri(publishEventsMethodData.EndpointUrl); useSecurity = publishEventsMethodData.UseSecurity; if (publishEventsMethodData.OpcAuthenticationMode == OpcAuthenticationMode.UsernamePassword) { if (string.IsNullOrWhiteSpace(publishEventsMethodData.UserName) && string.IsNullOrWhiteSpace(publishEventsMethodData.Password)) { throw new ArgumentException($"If {nameof(publishEventsMethodData.OpcAuthenticationMode)} is set to '{OpcAuthenticationMode.UsernamePassword}', you have to specify '{nameof(publishEventsMethodData.UserName)}' and/or '{nameof(publishEventsMethodData.Password)}'."); } desiredAuthenticationMode = OpcAuthenticationMode.UsernamePassword; desiredEncryptedCredential = await EncryptedNetworkCredential.FromPlainCredential(publishEventsMethodData.UserName, publishEventsMethodData.Password); } if (publishEventsMethodData.OpcEvents.Count != 1) { statusMessage = $"You can only configure one Event simultaneously, but you trying to configure {publishEventsMethodData.OpcEvents.Count + 1} events"; _logger.Error($"{logPrefix} {statusMessage}"); statusResponse.Add(statusMessage); statusCode = HttpStatusCode.NotAcceptable; } } catch (UriFormatException e) { statusMessage = $"Exception ({e.Message}) while parsing EndpointUrl '{publishEventsMethodData?.EndpointUrl}'"; _logger.Error(e, $"{logPrefix} {statusMessage}"); statusResponse.Add(statusMessage); statusCode = HttpStatusCode.NotAcceptable; } catch (FormatException e) { statusMessage = $"Exception ({e.Message}) while parsing EndpointId '{publishEventsMethodData?.EndpointId}'"; _logger.Error(e, $"{logPrefix} {statusMessage}"); statusResponse.Add(statusMessage); statusCode = HttpStatusCode.NotAcceptable; } catch (Exception e) { statusMessage = $"Exception ({e.Message}) while deserializing message payload"; _logger.Error(e, $"{logPrefix} {statusMessage}"); statusResponse.Add(statusMessage); statusCode = HttpStatusCode.InternalServerError; } if (statusCode == HttpStatusCode.OK) { // find/create a session to the endpoint URL and start monitoring the node. try { // lock the publishing configuration till we are done await NodeConfiguration.OpcSessionsListSemaphore.WaitAsync(_shutdownToken).ConfigureAwait(false); if (ShutdownTokenSource.IsCancellationRequested) { statusMessage = $"Publisher is in shutdown"; _logger.Warning($"{logPrefix} {statusMessage}"); statusResponse.Add(statusMessage); statusCode = HttpStatusCode.Gone; } else { IOpcSession opcSession = null; /* we create new sessions in two cases * 1. For new endpoints * 2. For existing endpoints which do not have a OpcSession configured: * this happens if for an existing endpoint all monitored items, commands and events are removed (unused sessions are removed). */ var isNewEndpoint = endpointId == Guid.Empty; var isExistingEndpointWithoutSession = !isNewEndpoint && NodeConfiguration.OpcSessions.FirstOrDefault(s => s.EndpointId.Equals(endpointId)) == null; if (isNewEndpoint || isExistingEndpointWithoutSession) { // if the no OpcAuthenticationMode is specified, we create the new session with "Anonymous" auth if (!desiredAuthenticationMode.HasValue) { desiredAuthenticationMode = OpcAuthenticationMode.Anonymous; } if (isNewEndpoint) { endpointId = Guid.NewGuid(); } // create new session info. opcSession = new OpcSession(endpointId, endpointName, endpointUri.OriginalString, useSecurity, OpcSessionCreationTimeout, desiredAuthenticationMode.Value, desiredEncryptedCredential); NodeConfiguration.OpcSessions.Add(opcSession); Logger.Information($"{logPrefix} No matching session found for endpoint '{endpointUri.OriginalString}'. Requested to create a new one."); } else { // find the session we need to monitor the node opcSession = NodeConfiguration.OpcSessions.FirstOrDefault(s => s.EndpointUrl.Equals(endpointUri?.OriginalString, StringComparison.OrdinalIgnoreCase)); // a session already exists, so we check, if we need to change authentication settings. This is only true, if the payload contains an OpcAuthenticationMode-Property if (desiredAuthenticationMode.HasValue) { bool reconnectRequired = false; if (opcSession.OpcAuthenticationMode != desiredAuthenticationMode.Value) { opcSession.OpcAuthenticationMode = desiredAuthenticationMode.Value; reconnectRequired = true; } if (opcSession.EncryptedAuthCredential != desiredEncryptedCredential) { opcSession.EncryptedAuthCredential = desiredEncryptedCredential; reconnectRequired = true; } if (reconnectRequired) { await opcSession.Reconnect(); } } } // process all nodes if (publishEventsMethodData?.OpcEvents != null) { foreach (var eventNode in publishEventsMethodData?.OpcEvents) { NodeId nodeId = null; ExpandedNodeId expandedNodeId = null; bool isNodeIdFormat; try { if (eventNode.Id.Contains("nsu=", StringComparison.InvariantCulture)) { expandedNodeId = ExpandedNodeId.Parse(eventNode.Id); isNodeIdFormat = false; } else { nodeId = NodeId.Parse(eventNode.Id); isNodeIdFormat = true; } } catch (Exception e) { statusMessage = $"Exception in ({e.Message}) while formatting node '{eventNode.Id}'!"; Logger.Error(e, $"{logPrefix} {statusMessage}"); statusResponse.Add(statusMessage); statusCode = HttpStatusCode.NotAcceptable; continue; } try { HttpStatusCode nodeStatusCode; if (isNodeIdFormat) { // add the event node info to the subscription with the default publishing interval, execute synchronously Logger.Debug( $"{logPrefix} Request to monitor eventNode with NodeId '{eventNode.Id}'"); nodeStatusCode = await opcSession.AddEventNodeForMonitoringAsync(nodeId, null, 5000, 2000, eventNode.DisplayName, null, null, ShutdownTokenSource.Token, null, publishEventsMethodData) .ConfigureAwait(false); } else { // add the event node info to the subscription with the default publishing interval, execute synchronously Logger.Debug( $"{logPrefix} Request to monitor eventNode with ExpandedNodeId '{eventNode.Id}'"); nodeStatusCode = await opcSession.AddEventNodeForMonitoringAsync(null, expandedNodeId, 5000, 2000, eventNode.DisplayName, null, null, ShutdownTokenSource.Token, null, publishEventsMethodData) .ConfigureAwait(false); } // check and store a result message in case of an error switch (nodeStatusCode) { case HttpStatusCode.OK: statusMessage = $"'{eventNode.Id}': already monitored"; Logger.Debug($"{logPrefix} {statusMessage}"); statusResponse.Add(statusMessage); break; case HttpStatusCode.Accepted: statusMessage = $"'{eventNode.Id}': added"; Logger.Debug($"{logPrefix} {statusMessage}"); statusResponse.Add(statusMessage); break; case HttpStatusCode.Gone: statusMessage = $"'{eventNode.Id}': session to endpoint does not exist anymore"; Logger.Debug($"{logPrefix} {statusMessage}"); statusResponse.Add(statusMessage); statusCode = HttpStatusCode.Gone; break; case HttpStatusCode.InternalServerError: statusMessage = $"'{eventNode.Id}': error while trying to configure"; Logger.Debug($"{logPrefix} {statusMessage}"); statusResponse.Add(statusMessage); statusCode = HttpStatusCode.InternalServerError; break; } } catch (Exception e) { statusMessage = $"Exception ({e.Message}) while trying to configure publishing node '{eventNode.Id}'"; Logger.Error(e, $"{logPrefix} {statusMessage}"); statusResponse.Add(statusMessage); statusCode = HttpStatusCode.InternalServerError; } } } else { statusMessage = $"There are no EventConfigurations provided with the current call, provided JSON Data was: {methodRequest.DataAsJson}"; Logger.Error($"{logPrefix} {statusMessage}"); statusResponse.Add(statusMessage); statusCode = HttpStatusCode.BadRequest; } } } catch (AggregateException e) { foreach (var ex in e.InnerExceptions) { Logger.Error(ex, $"{logPrefix} Exception"); } statusMessage = $"EndpointUrl: '{publishEventsMethodData.EndpointUrl}': exception ({e.Message}) while trying to publish"; Logger.Error(e, $"{logPrefix} {statusMessage}"); statusResponse.Add(statusMessage); statusCode = HttpStatusCode.InternalServerError; } catch (Exception e) { statusMessage = $"EndpointUrl: '{publishEventsMethodData.EndpointUrl}': exception ({e.Message}) while trying to publish"; Logger.Error(e, $"{logPrefix} {statusMessage}"); statusResponse.Add(statusMessage); statusCode = HttpStatusCode.InternalServerError; } finally { NodeConfiguration.OpcSessionsListSemaphore.Release(); } } // build response publishedEventMethodResponse = new PublishNodesMethodResponseModel(endpointId.ToString()); string resultString = statusCode == HttpStatusCode.OK || statusCode == HttpStatusCode.Accepted ? JsonConvert.SerializeObject(publishedEventMethodResponse): JsonConvert.SerializeObject(statusResponse); byte[] result = Encoding.UTF8.GetBytes(resultString); if (result.Length > MaxResponsePayloadLength) { Logger.Error($"{logPrefix} Response size is too long"); Array.Resize(ref result, result.Length > MaxResponsePayloadLength ? MaxResponsePayloadLength : result.Length); } MethodResponse methodResponse = new MethodResponse(result, (int)statusCode); Logger.Information($"{logPrefix} completed with result {statusCode.ToString()}"); return(methodResponse); }