Exemplo n.º 1
0
        /// <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.");
            }
        }
Exemplo n.º 2
0
        /// <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);
            }
        }
Exemplo n.º 3
0
 /// <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}");
     }
 }
Exemplo n.º 4
0
        /// <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."));
        }
Exemplo n.º 7
0
        /// <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);
        }
Exemplo n.º 8
0
        /// <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}"));
            }
        }
Exemplo n.º 9
0
        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);
        }
Exemplo n.º 12
0
        /// <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);
        }