public async Task Shutdown() { _opcSessionSemaphore.Wait(); try { // if the session is connected, close it. if (State == SessionState.Connected) { try { Trace($"Removing {Session.DefaultSubscription.MonitoredItemCount} monitored items from default subscription from session to endpoint URI '{EndpointUri.AbsoluteUri}'."); Session.DefaultSubscription.RemoveItems(Session.DefaultSubscription.MonitoredItems); Trace($"Closing session to endpoint URI '{EndpointUri.AbsoluteUri}' closed successfully."); Session.Close(); State = SessionState.Disconnected; Trace($"Session to endpoint URI '{EndpointUri.AbsoluteUri}' closed successfully."); } catch (Exception e) { Trace(e, $"Error while closing session to endpoint '{EndpointUri.AbsoluteUri}'."); State = SessionState.Disconnected; return; } } } finally { _opcSessionSemaphore.Release(); if (OpcSessions.Count(s => s.State == SessionState.Connected) == 0) { _opcSessionSemaphore.Dispose(); } } }
/// <summary> /// Ctor to initialize resources for the telemetry configuration. /// </summary> public PublisherNodeConfiguration() { OpcSessionsListSemaphore = new SemaphoreSlim(1); PublisherNodeConfigurationSemaphore = new SemaphoreSlim(1); PublisherNodeConfigurationFileSemaphore = new SemaphoreSlim(1); OpcSessions.Clear(); _nodePublishingConfiguration = new List <NodePublishingConfigurationModel>(); _configurationFileEntries = new List <PublisherConfigurationFileEntryLegacyModel>(); // read the configuration from the configuration file if (!ReadConfigAsync().Result) { string errorMessage = $"Error while reading the node configuration file '{PublisherNodeConfigurationFilename}'"; Logger.Error(errorMessage); throw new Exception(errorMessage); } // create the configuration data structures if (!CreateOpcPublishingDataAsync().Result) { string errorMessage = $"Error while creating node configuration data structures."; Logger.Error(errorMessage); throw new Exception(errorMessage); } }
/// <summary> /// Kicks of the work horse of the publisher regularily for all sessions. /// </summary> public static async Task SessionConnectorAsync(CancellationToken ct) { while (true) { try { // get tasks for all disconnected sessions and start them IEnumerable <Task> singleSessionHandlerTaskList; try { await OpcSessionsListSemaphore.WaitAsync(); singleSessionHandlerTaskList = OpcSessions.Select(s => s.ConnectAndMonitorAsync(ct)); } finally { OpcSessionsListSemaphore.Release(); } await Task.WhenAll(singleSessionHandlerTaskList); } catch (Exception e) { Trace(e, $"Failed to connect and monitor a disconnected server. {(e.InnerException != null ? e.InnerException.Message : "")}"); } try { await Task.Delay(_publisherSessionConnectWaitSec * 1000, ct); } catch { } if (ct.IsCancellationRequested) { return; } } }
/// <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> /// Initialize resources for the node configuration. /// </summary> public static void Init() { OpcSessionsListSemaphore = new SemaphoreSlim(1); PublisherNodeConfigurationSemaphore = new SemaphoreSlim(1); PublisherNodeConfigurationFileSemaphore = new SemaphoreSlim(1); OpcSessions.Clear(); _nodePublishingConfiguration = new List <NodePublishingConfigurationModel>(); _configurationFileEntries = new List <PublisherConfigurationFileEntryLegacyModel>(); }
/// <summary> /// Frees resources for the node configuration. /// </summary> public static void Deinit() { OpcSessions.Clear(); _nodePublishingConfiguration = null; OpcSessionsListSemaphore.Dispose(); OpcSessionsListSemaphore = null; PublisherNodeConfigurationSemaphore.Dispose(); PublisherNodeConfigurationSemaphore = null; PublisherNodeConfigurationFileSemaphore.Dispose(); PublisherNodeConfigurationFileSemaphore = null; }
/// <summary> /// Shutdown all sessions. /// </summary> public async static Task SessionShutdownAsync() { if (HaveToWriteCsv) { csvWriter.Flush(); csvWriter.Dispose(); } 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) { Logger.Fatal(e, "Failed to shutdown all sessions."); } // wait and continue after a while uint maxTries = ShutdownRetryCount; while (true) { int sessionCount = OpcSessions.Count; if (sessionCount == 0) { return; } if (maxTries-- == 0) { Logger.Information($"There are still {sessionCount} sessions alive. Ignore and continue shutdown."); return; } Logger.Information($"{ProgramName} is shutting down. Wait {SessionConnectWait} seconds, since there are stil {sessionCount} sessions alive..."); await Task.Delay(SessionConnectWait * 1000); } }
/// <summary> /// Create the data structures to manage actions. /// </summary> public static async Task <bool> CreateOpcActionDataAsync() { try { await OpcActionListSemaphore.WaitAsync(); await OpcSessionsListSemaphore.WaitAsync(); // create actions out of the configuration var uniqueSessionInfo = _actionConfiguration.Select(n => new Tuple <Uri, bool>(n.EndpointUrl, n.UseSecurity)).Distinct(); foreach (var sessionInfo in uniqueSessionInfo) { // create new session info. OpcSession opcSession = new OpcSession(sessionInfo.Item1, sessionInfo.Item2, OpcSessionCreationTimeout); // add all actions to the session List <OpcAction> actionsOnEndpoint = new List <OpcAction>(); var endpointConfigs = _actionConfiguration.Where(c => c.EndpointUrl == sessionInfo.Item1 && c.UseSecurity == sessionInfo.Item2); foreach (var config in endpointConfigs) { config?.Read.ForEach(a => opcSession.OpcActions.Add(new OpcReadAction(config.EndpointUrl, a))); config?.Test.ForEach(a => opcSession.OpcActions.Add(new OpcTestAction(config.EndpointUrl, a))); config?.HistoryRead.ForEach(a => opcSession.OpcActions.Add(new OpcHistoryReadAction(config.EndpointUrl, a))); } // report actions Logger.Information($"Actions on '{opcSession.EndpointUrl.AbsoluteUri}' {(opcSession.UseSecurity ? "with" : "without")} security."); foreach (var action in opcSession.OpcActions) { Logger.Information($"{action.Description}, recurring each: {action.Interval} sec"); } // add session OpcSessions.Add(opcSession); } } catch (Exception e) { Logger.Fatal(e, "Error: creation of the internal OPC management structures failed. Exiting..."); Environment.ExitCode = 1; return(false); } finally { OpcSessionsListSemaphore.Release(); OpcActionListSemaphore.Release(); } return(true); }
/// <summary> /// Start all sessions. /// </summary> public async static Task SessionStartAsync() { try { await OpcSessionsListSemaphore.WaitAsync(); OpcSessions.ForEach(s => s.ConnectAndMonitorSession.Set()); } catch (Exception e) { Logger.Fatal(e, "Failed to start all sessions."); } finally { OpcSessionsListSemaphore.Release(); } }
/// <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 data structures to manage OPC sessions and actions. /// </summary> /// <returns></returns> public static async Task <bool> CreateOpcActionDataAsync() { try { await OpcActionListSemaphore.WaitAsync(); await OpcSessionsListSemaphore.WaitAsync(); // create actions out of the configuration var uniqueEndpointUrls = _actionConfiguration.Select(n => n.EndpointUrl).Distinct(); foreach (var endpointUrl in uniqueEndpointUrls) { // create new session info. OpcSession opcSession = new OpcSession(endpointUrl, _actionConfiguration.Where(n => n.EndpointUrl == endpointUrl).First().UseSecurity, OpcSessionCreationTimeout); // add all actions to the session List <OpcAction> actionsOnEndpoint = new List <OpcAction>(); var endpointConfigs = _actionConfiguration.Where(c => c.EndpointUrl == endpointUrl); foreach (var config in endpointConfigs) { config?.Read.ForEach(r => opcSession.OpcActions.Add(new OpcReadAction(r))); config?.Write.ForEach(w => opcSession.OpcActions.Add(new OpcWriteAction(w))); } // add session. OpcSessions.Add(opcSession); } } catch (Exception e) { Logger.Fatal(e, "Creation of the internal OPC management structures failed. Exiting..."); return(false); } finally { OpcSessionsListSemaphore.Release(); OpcActionListSemaphore.Release(); } return(true); }
/// <summary> /// Implement IDisposable. /// </summary> protected virtual void Dispose(bool disposing) { if (disposing) { OpcSessionsListSemaphore.Wait(); foreach (var opcSession in OpcSessions) { opcSession.Dispose(); } OpcSessions?.Clear(); OpcSessionsListSemaphore?.Dispose(); OpcSessionsListSemaphore = null; PublisherNodeConfigurationSemaphore?.Dispose(); PublisherNodeConfigurationSemaphore = null; PublisherNodeConfigurationFileSemaphore?.Dispose(); PublisherNodeConfigurationFileSemaphore = null; _nodePublishingConfiguration?.Clear(); _nodePublishingConfiguration = null; lock (_singletonLock) { _instance = null; } } }
/// <summary> /// Implement IDisposable. /// </summary> protected virtual void Dispose(bool disposing) { if (disposing) { //OpcSessionsListSemaphore.Wait(); avoid get semaphore here, otherwise there will be deadlock during OpcSession disposing foreach (var opcSession in OpcSessions) { opcSession.Dispose(); } OpcSessions?.Clear(); OpcSessionsListSemaphore?.Dispose(); OpcSessionsListSemaphore = null; PublisherNodeConfigurationSemaphore?.Dispose(); PublisherNodeConfigurationSemaphore = null; PublisherNodeConfigurationFileSemaphore?.Dispose(); PublisherNodeConfigurationFileSemaphore = null; _nodePublishingConfiguration?.Clear(); _nodePublishingConfiguration = null; lock (_singletonLock) { _instance = null; } } }
/// <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> /// Asynchronous part of the main method of the app. /// </summary> public async static Task MainAsync(string[] args) { try { var shouldShowHelp = false; // shutdown token sources ShutdownTokenSource = new CancellationTokenSource(); // command line options Mono.Options.OptionSet options = new Mono.Options.OptionSet { { "cf|configfile=", $"the filename containing action configuration.\nDefault: '{OpcActionConfigurationFilename}'", (string p) => OpcActionConfigurationFilename = p }, { "tc|testconnectivity", $"tests connectivity with the server.\nDefault: {TestConnectivity}", b => TestConnectivity = b != null }, { "tu|testunsecureconnectivity", $"tests connectivity with the server using an unsecured endpoint.\nDefault: {TestUnsecureConnectivity}", b => TestUnsecureConnectivity = b != null }, { "de|defaultendpointurl=", $"endpoint OPC UA server used as default.\nDefault: {DefaultEndpointUrl}", (string s) => DefaultEndpointUrl = s }, { "sw|sessionconnectwait=", $"specify the wait time in seconds we try to connect to disconnected endpoints and starts monitoring unmonitored items\nMin: 10\nDefault: {SessionConnectWait}", (int i) => { if (i > 10) { SessionConnectWait = i; } else { throw new OptionException("The sessionconnectwait must be greater than 10 sec", "sessionconnectwait"); } } }, { "di|diagnosticsinterval=", $"shows diagnostic info at the specified interval in seconds (need log level info). 0 disables diagnostic output.\nDefault: {DiagnosticsInterval}", (uint u) => DiagnosticsInterval = u }, { "lf|logfile=", $"the filename of the logfile to use.\nDefault: don't write logfile.", (string l) => _logFileName = l }, { "lt|logflushtimespan=", $"the timespan in seconds when the logfile should be flushed.\nDefault: {_logFileFlushTimeSpanSec} sec", (int s) => { if (s > 0) { _logFileFlushTimeSpanSec = TimeSpan.FromSeconds(s); } else { throw new Mono.Options.OptionException("The logflushtimespan must be a positive number.", "logflushtimespan"); } } }, { "ll|loglevel=", $"the loglevel to use (allowed: fatal, error, warn, info, debug, verbose).\nDefault: info", (string l) => { List <string> logLevels = new List <string> { "fatal", "error", "warn", "info", "debug", "verbose" }; if (logLevels.Contains(l.ToLowerInvariant())) { _logLevel = l.ToLowerInvariant(); } else { throw new Mono.Options.OptionException("The loglevel must be one of: fatal, error, warn, info, debug, verbose", "loglevel"); } } }, // opc configuration options { "ol|opcmaxstringlen=", $"the max length of a string opc can transmit/receive.\nDefault: {OpcMaxStringLength}", (int i) => { if (i > 0) { OpcMaxStringLength = i; } else { throw new OptionException("The max opc string length must be larger than 0.", "opcmaxstringlen"); } } }, { "ot|operationtimeout=", $"the operation timeout of the 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"); } } }, { "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 se send keep alive messages to the OPC servers on the endpoints it is connected to.\nMin: 2\nDefault: {OpcKeepAliveInterval}", (int i) => { if (i >= 2) { OpcKeepAliveInterval = 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"); } } }, { "csvOutput=|csvoutput=", $"filename to store readed values in CSV format.\nDefault: './{_csvFileName}'", (string path) => _csvFileName = path }, { "aa|autoaccept", $"trusts all servers we establish a connection to.\nDefault: {AutoAcceptCerts}", b => AutoAcceptCerts = b != null }, { "to|trustowncert", $"our own certificate is put into the trusted certificate store automatically.\nDefault: {TrustMyself}", t => TrustMyself = t != null }, // cert store options { "at|appcertstoretype=", $"the own application cert store type. \n(allowed values: Directory, X509Store)\nDefault: '{OpcOwnCertStoreType}'", (string s) => { if (s.Equals(CertificateStoreType.X509Store, StringComparison.OrdinalIgnoreCase) || s.Equals(CertificateStoreType.Directory, StringComparison.OrdinalIgnoreCase)) { OpcOwnCertStoreType = s.Equals(CertificateStoreType.X509Store, StringComparison.OrdinalIgnoreCase) ? CertificateStoreType.X509Store : CertificateStoreType.Directory; OpcOwnCertStorePath = s.Equals(CertificateStoreType.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 }, { "tp|trustedcertstorepath=", $"the path of the trusted cert store\nDefault '{OpcTrustedCertDirectoryStorePathDefault}'", (string s) => OpcTrustedCertStorePath = s }, { "rp|rejectedcertstorepath=", $"the path of the rejected cert store\nDefault '{OpcRejectedCertDirectoryStorePathDefault}'", (string s) => OpcRejectedCertStorePath = s }, { "ip|issuercertstorepath=", $"the path of the trusted issuer cert store\nDefault '{OpcIssuerCertDirectoryStorePathDefault}'", (string s) => OpcIssuerCertStorePath = s }, { "csr", $"show data to create a certificate signing request\nDefault '{ShowCreateSigningRequestInfo}'", c => ShowCreateSigningRequestInfo = c != null }, { "ab|applicationcertbase64=", $"update/set this applications certificate with the certificate passed in as bas64 string", (string s) => { NewCertificateBase64String = s; } }, { "af|applicationcertfile=", $"update/set this applications certificate with the certificate file specified", (string s) => { if (File.Exists(s)) { NewCertificateFileName = s; } else { throw new OptionException($"The file '{s}' does not exist.", "applicationcertfile"); } } }, { "pb|privatekeybase64=", $"initial provisioning of the application certificate (with a PEM or PFX fomat) requires a private key passed in as base64 string", (string s) => { PrivateKeyBase64String = s; } }, { "pk|privatekeyfile=", $"initial provisioning of the application certificate (with a PEM or PFX fomat) requires a private key passed in as file", (string s) => { if (File.Exists(s)) { PrivateKeyFileName = s; } else { throw new OptionException($"The file '{s}' does not exist.", "privatekeyfile"); } } }, { "cp|certpassword="******"the optional password for the PEM or PFX or the installed application certificate", (string s) => { CertificatePassword = s; } }, { "tb|addtrustedcertbase64=", $"adds the certificate to the applications trusted cert store passed in as base64 string (multiple strings supported)", (string s) => { TrustedCertificateBase64Strings = ParseListOfStrings(s); } }, { "tf|addtrustedcertfile=", $"adds the certificate file(s) to the applications trusted cert store passed in as base64 string (multiple filenames supported)", (string s) => { TrustedCertificateFileNames = ParseListOfFileNames(s, "addtrustedcertfile"); } }, { "ib|addissuercertbase64=", $"adds the specified issuer certificate to the applications trusted issuer cert store passed in as base64 string (multiple strings supported)", (string s) => { IssuerCertificateBase64Strings = ParseListOfStrings(s); } }, { "if|addissuercertfile=", $"adds the specified issuer certificate file(s) to the applications trusted issuer cert store (multiple filenames supported)", (string s) => { IssuerCertificateFileNames = ParseListOfFileNames(s, "addissuercertfile"); } }, { "rb|updatecrlbase64=", $"update the CRL passed in as base64 string to the corresponding cert store (trusted or trusted issuer)", (string s) => { CrlBase64String = s; } }, { "uc|updatecrlfile=", $"update the CRL passed in as file to the corresponding cert store (trusted or trusted issuer)", (string s) => { if (File.Exists(s)) { CrlFileName = s; } else { throw new OptionException($"The file '{s}' does not exist.", "updatecrlfile"); } } }, { "rc|removecert=", $"remove cert(s) with the given thumbprint(s) (multiple thumbprints supported)", (string s) => { ThumbprintsToRemove = ParseListOfStrings(s); } }, // misc { "h|help", "show this message and exit", h => shouldShowHelp = h != null }, }; List <string> extraArgs = new List <string>(); try { // parse the command line extraArgs = options.Parse(args); } catch (OptionException e) { // initialize logging InitLogging(); // show message Logger.Fatal(e, "Error in command line options"); Environment.ExitCode = 1; Logger.Error($"Command line arguments: {String.Join(" ", args)}"); // show usage Usage(options); return; } // initialize logging InitLogging(); // show usage if requested if (shouldShowHelp) { Usage(options); return; } // validate and parse extra arguments if (extraArgs.Count > 0) { Logger.Error("Error in command line options"); Environment.ExitCode = 1; Logger.Error($"Command line arguments: {String.Join(" ", args)}"); Usage(options); return; } //show version Logger.Information($"{ProgramName} V{FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion} starting up..."); Logger.Debug($"Informational version: V{(Attribute.GetCustomAttribute(Assembly.GetEntryAssembly(), typeof(AssemblyInformationalVersionAttribute)) as AssemblyInformationalVersionAttribute).InformationalVersion}"); // allow canceling the application var quitEvent = new ManualResetEvent(false); try { Console.CancelKeyPress += (sender, eArgs) => { quitEvent.Set(); eArgs.Cancel = true; ShutdownTokenSource.Cancel(); }; } catch { } // init OPC configuration and tracing OpcApplicationConfiguration applicationStackConfiguration = new OpcApplicationConfiguration(); await applicationStackConfiguration.ConfigureAsync(); // read OPC action configuration OpcConfiguration.Init(); if (!await ReadOpcConfigurationAsync()) { return; } // add well known actions if (!await CreateOpcActionDataAsync()) { return; } // kick off OPC client activities await SessionStartAsync(); // initialize diagnostics Diagnostics.Init(); await OpcSessionsListSemaphore.WaitAsync(); var pendingOperations = OpcSessions.Sum(s => s.OpcActions.Count); OpcSessionsListSemaphore.Release(); while (pendingOperations > 0 && ShutdownTokenSource.IsCancellationRequested != true) { Logger.Information($"{pendingOperations} pending operation(s)."); Logger.Information(""); Logger.Information($"{ProgramName} is running. Press CTRL-C to quit."); Thread.Sleep(1000); await OpcSessionsListSemaphore.WaitAsync(); pendingOperations = OpcSessions.Sum(s => s.OpcActions.Count); OpcSessionsListSemaphore.Release(); } Logger.Information("No pending operations."); Logger.Information(""); ShutdownTokenSource.Cancel(); Logger.Information($"{ProgramName} is shutting down..."); // shutdown all OPC sessions await SessionShutdownAsync(); // shutdown diagnostics await ShutdownAsync(); ShutdownTokenSource = null; } catch (Exception e) { Logger.Fatal(e, e.StackTrace); Environment.ExitCode = 1; e = e.InnerException ?? null; while (e != null) { Logger.Fatal(e, e.StackTrace); e = e.InnerException ?? null; } Logger.Fatal($"{ProgramName} exiting... "); } }
/// <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 exposed as a node in the server to stop monitoring it and no longer publish telemetry of it. /// </summary> private ServiceResult UnPublishNodeMethod(ISystemContext context, MethodState method, IList <object> inputArguments, IList <object> outputArguments) { if (inputArguments[0] == null || inputArguments[1] == null) { Trace("UnPublishNodeMethod: Invalid arguments!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide all arguments!")); } string nodeId = inputArguments[0] as string; string endpointUrl = inputArguments[1] as string; if (string.IsNullOrEmpty(nodeId) || string.IsNullOrEmpty(endpointUrl)) { Trace($"UnPublishNodeMethod: Arguments (0 (nodeId): '{nodeId}', 1 (endpointUrl):'{endpointUrl}') are not valid strings!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide all arguments as strings!")); } NodeToPublish nodeToUnpublish = new NodeToPublish(); try { nodeToUnpublish = new NodeToPublish(nodeId, endpointUrl); } catch (UriFormatException) { Trace($"UnPublishNodeMethod: The endpointUrl is invalid (0 (nodeId): '{nodeId}', 1 (endpointUrl):'{endpointUrl}')!"); 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 { // find the session we need to monitor the node OpcSession opcSession = OpcSessions.First(s => s.EndpointUri == nodeToUnpublish.EndPointUri); if (opcSession == null) { // do nothing if there is no session for this endpoint. Trace($"UnPublishNodeMethod: Session for endpoint '{nodeToUnpublish.EndPointUri.AbsolutePath}' not found."); return(ServiceResult.Create(StatusCodes.BadSessionIdInvalid, "Session for endpoint of published node not found!")); } else { Trace($"UnPublishNodeMethod: Session found for endpoint '{nodeToUnpublish.EndPointUri.AbsolutePath}'"); } // remove the node from the sessions monitored items list. opcSession.TagNodeForMonitoringStop(nodeToUnpublish.NodeId); Trace("UnPublishNodeMethod: Requested to stop monitoring of node."); // stop monitoring the node Task monitorTask = Task.Run(async() => await opcSession.ConnectAndMonitor()); monitorTask.Wait(); Trace("UnPublishNodeMethod: Session processing completed."); // remove node from our persisted data set. var itemToRemove = NodesToPublish.Find(l => l.NodeId == nodeToUnpublish.NodeId && l.EndPointUri == nodeToUnpublish.EndPointUri); NodesToPublish.Remove(itemToRemove); // persist data File.WriteAllText(NodesToPublishAbsFilename, JsonConvert.SerializeObject(NodesToPublish)); } catch (Exception e) { Trace(e, $"DoPublish: Exception while trying to configure publishing node '{nodeToUnpublish.ToString()}'"); return(ServiceResult.Create(e, StatusCodes.BadUnexpectedError, $"Unexpected error publishing node: {e.Message}")); } return(ServiceResult.Good); }
/// <summary> /// Method exposed as a node in the server to publish a node to IoT Hub that it is connected to /// </summary> private ServiceResult PublishNodeMethod(ISystemContext context, MethodState method, IList <object> inputArguments, IList <object> outputArguments) { if (inputArguments[0] == null || inputArguments[1] == null) { Trace("PublishNodeMethod: Invalid Arguments!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide all arguments!")); } NodeToPublish nodeToPublish; string nodeId = inputArguments[0] as string; string endpointUrl = inputArguments[1] as string; if (string.IsNullOrEmpty(nodeId) || string.IsNullOrEmpty(endpointUrl)) { Trace($"PublishNodeMethod: Arguments (0 (nodeId): '{nodeId}', 1 (endpointUrl):'{endpointUrl}') are not valid strings!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide all arguments as strings!")); } try { nodeToPublish = new NodeToPublish(nodeId, endpointUrl); } catch (UriFormatException) { Trace($"PublishNodeMethod: The endpointUrl is invalid (0 (nodeId): '{nodeId}', 1 (endpointUrl):'{endpointUrl}')!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide a valid OPC UA endpoint URL as second argument!")); } // Create session and add item to monitor, what ever is needed. try { // find the session we need to monitor the node OpcSession opcSession = OpcSessions.First(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($"DoPublish: No matching session found for endpoint '{nodeToPublish.EndPointUri.AbsolutePath}'. Requested to create a new one."); } else { Trace($"DoPublish: Session found for endpoint '{nodeToPublish.EndPointUri.AbsolutePath}'"); } // add the node info to the sessions monitored items list. opcSession.AddNodeForMonitoring(nodeToPublish.NodeId); Trace("DoPublish: Requested to monitor item."); // start monitoring the node Task monitorTask = Task.Run(async() => await opcSession.ConnectAndMonitor()); monitorTask.Wait(); Trace("DoPublish: Session processing completed."); // update our data NodesToPublish.Add(nodeToPublish); // persist it to disk File.WriteAllText(NodesToPublishAbsFilename, JsonConvert.SerializeObject(NodesToPublish)); Trace($"DoPublish: Now publishing: {nodeToPublish.ToString()}"); return(ServiceResult.Good); } catch (Exception e) { Trace(e, $"DoPublish: Exception while trying to configure publishing node '{nodeToPublish.ToString()}'"); return(ServiceResult.Create(e, StatusCodes.BadUnexpectedError, $"Unexpected error publishing node: {e.Message}")); } }
/// <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); }
public async Task ConnectAndMonitor() { _opcSessionSemaphore.Wait(); Trace($"Connect and monitor session and nodes on endpoint '{EndpointUri.AbsoluteUri}'."); try { // if the session is disconnected, create it. if (State == SessionState.Disconnected) { EndpointDescription selectedEndpoint = CoreClientUtils.SelectEndpoint(EndpointUri.AbsoluteUri, true); ConfiguredEndpoint configuredEndpoint = new ConfiguredEndpoint(selectedEndpoint.Server, EndpointConfiguration.Create(OpcConfiguration)); configuredEndpoint.Update(selectedEndpoint); try { uint timeout = SessionTimeout * ((UnsuccessfulConnectionCount >= OpcSessionCreationBackoffMax) ? OpcSessionCreationBackoffMax : UnsuccessfulConnectionCount + 1); Trace($"Create session for endpoint URI '{EndpointUri.AbsoluteUri}' with timeout of {timeout} ms."); Session = await Session.Create( OpcConfiguration, configuredEndpoint, true, false, OpcConfiguration.ApplicationName, timeout, new UserIdentity(new AnonymousIdentityToken()), null); if (Session != null) { Trace($"Session successfully created with Id {Session.SessionId}."); if (!selectedEndpoint.EndpointUrl.Equals(configuredEndpoint.EndpointUrl)) { Trace($"the Server has updated the EndpointUrl to '{selectedEndpoint.EndpointUrl}'"); } // init object state and install keep alive UnsuccessfulConnectionCount = 0; State = SessionState.Connected; Session.KeepAliveInterval = OpcKeepAliveIntervalInSec * 1000; Session.KeepAlive += new KeepAliveEventHandler((sender, e) => StandardClient_KeepAlive(sender, e, Session)); // fetch the namespace array and cache it. it will not change as long the session exists. NodeId namespaceArrayNodeId = new NodeId(Variables.Server_NamespaceArray, 0); DataValue namespaceArrayNodeValue = Session.ReadValue(namespaceArrayNodeId); _namespaceTable.Update(namespaceArrayNodeValue.GetValue <string[]>(null)); // show the available namespaces Trace($"The session to endpoint '{selectedEndpoint.EndpointUrl}' has {_namespaceTable.Count} entries in its namespace array:"); int i = 0; foreach (var ns in _namespaceTable.ToArray()) { Trace($"Namespace index {i++}: {ns}"); } } } catch (Exception e) { Trace(e, $"Session creation to endpoint '{EndpointUri.AbsoluteUri}' failed {++UnsuccessfulConnectionCount} time(s). Please verify if server is up and Publisher configuration is correct."); State = SessionState.Disconnected; Session = null; return; } } // stop monitoring of nodes if requested and remove them from the monitored items list. var itemsToRemove = MonitoredItemsInfo.Where(i => i.State == MonitoredItemInfo.MonitoredItemState.StopMonitoring); if (itemsToRemove.GetEnumerator().MoveNext()) { itemsToRemove.GetEnumerator().Reset(); Trace($"Remove nodes on endpoint '{EndpointUri.AbsoluteUri}'"); Session.DefaultSubscription.RemoveItems(itemsToRemove.Select(i => i.MonitoredItem)); } // ensure all nodes on this session are monitored. Trace($"Start monitoring nodes on endpoint '{EndpointUri.AbsoluteUri}'"); var unmonitoredItems = MonitoredItemsInfo.Where(i => i.State == MonitoredItemInfo.MonitoredItemState.Unmonitored); foreach (var item in unmonitoredItems) { // if the session is disconnected, we stop trying and wait for the next cycle if (State == SessionState.Disconnected) { break; } NodeId currentNodeId; try { Subscription subscription = Session.DefaultSubscription; if (Session.AddSubscription(subscription)) { Trace("Create default subscription."); subscription.Create(); } // lookup namespace index if ExpandedNodeId format has been used and build NodeId identifier. if (!string.IsNullOrEmpty(item.StartNodeId.NamespaceUri)) { currentNodeId = NodeId.Create(item.StartNodeId.Identifier, item.StartNodeId.NamespaceUri, _namespaceTable); } else { currentNodeId = new NodeId((NodeId)item.StartNodeId); } // get the DisplayName for the node, otherwise use the nodeId Node node = Session.ReadNode(currentNodeId); item.DisplayName = node.DisplayName.Text ?? currentNodeId.ToString(); // add the new monitored item. MonitoredItem monitoredItem = new MonitoredItem(subscription.DefaultItem) { StartNodeId = currentNodeId, AttributeId = item.AttributeId, DisplayName = node.DisplayName.Text, MonitoringMode = item.MonitoringMode, SamplingInterval = item.SamplingInterval, QueueSize = item.QueueSize, DiscardOldest = item.DiscardOldest }; monitoredItem.Notification += item.Notification; subscription.AddItem(monitoredItem); subscription.ApplyChanges(); item.MonitoredItem = monitoredItem; item.State = MonitoredItemInfo.MonitoredItemState.Monitoreded; item.EndpointUri = EndpointUri; Trace($"Created monitored item for node '{currentNodeId}' on endpoint '{EndpointUri.AbsoluteUri}'"); } catch (Exception e) when(e.GetType() == typeof(ServiceResultException)) { ServiceResultException sre = (ServiceResultException)e; switch ((uint)sre.Result.StatusCode) { case StatusCodes.BadSessionIdInvalid: { Trace($"Session with Id {Session.SessionId} is no longer available on endpoint '{EndpointUri}'. Cleaning up."); // clean up the session _opcSessionSemaphore.Release(); Disconnect(); break; } case StatusCodes.BadNodeIdInvalid: case StatusCodes.BadNodeIdUnknown: { Trace($"Failed to monitor node '{item.StartNodeId.Identifier}' on endpoint '{EndpointUri}'."); Trace($"OPC UA ServiceResultException is '{sre.Result}'. Please check your publisher configuration for this node."); break; } default: { Trace($"Unhandled OPC UA ServiceResultException '{sre.Result}' when monitoring node '{item.StartNodeId.Identifier}' on endpoint '{EndpointUri}'. Continue."); break; } } } catch (Exception e) { Trace(e, $"Failed to monitor node '{item.StartNodeId.Identifier}' on endpoint '{EndpointUri}'"); } } // shutdown unused sessions. var unusedSessions = OpcSessions.Where(s => s.MonitoredItemsInfo.Count == 0); foreach (var unusedSession in unusedSessions) { await unusedSession.Shutdown(); OpcSessions.Remove(unusedSession); } } finally { _opcSessionSemaphore.Release(); } }
/// <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. /// </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}")); } }
/// <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> /// This task is executed regularily and ensures: /// - disconnected sessions are reconnected. /// - monitored nodes are no longer monitored if requested to do so. /// - monitoring for a node starts if it is required. /// - unused subscriptions (without any nodes to monitor) are removed. /// - sessions with out subscriptions are removed. /// </summary> /// <returns></returns> public async Task ConnectAndMonitor() { await _opcSessionSemaphore.WaitAsync(); try { // if the session is disconnected, create one. if (State == SessionState.Disconnected) { Trace($"Connect and monitor session and nodes on endpoint '{EndpointUri.AbsoluteUri}'."); try { // release the session to not block for high network timeouts. _opcSessionSemaphore.Release(); // start connecting EndpointDescription selectedEndpoint = CoreClientUtils.SelectEndpoint(EndpointUri.AbsoluteUri, true); ConfiguredEndpoint configuredEndpoint = new ConfiguredEndpoint(null, selectedEndpoint, EndpointConfiguration.Create(OpcConfiguration)); uint timeout = SessionTimeout * ((UnsuccessfulConnectionCount >= OpcSessionCreationBackoffMax) ? OpcSessionCreationBackoffMax : UnsuccessfulConnectionCount + 1); Trace($"Create session for endpoint URI '{EndpointUri.AbsoluteUri}' with timeout of {timeout} ms."); Session = await Session.Create( OpcConfiguration, configuredEndpoint, true, false, OpcConfiguration.ApplicationName, timeout, new UserIdentity(new AnonymousIdentityToken()), null); if (Session != null) { Trace($"Session successfully created with Id {Session.SessionId}."); if (!selectedEndpoint.EndpointUrl.Equals(configuredEndpoint.EndpointUrl.AbsoluteUri)) { Trace($"the Server has updated the EndpointUrl to '{selectedEndpoint.EndpointUrl}'"); } // init object state and install keep alive UnsuccessfulConnectionCount = 0; Session.KeepAliveInterval = OpcKeepAliveIntervalInSec * 1000; Session.KeepAlive += StandardClient_KeepAlive; // fetch the namespace array and cache it. it will not change as long the session exists. DataValue namespaceArrayNodeValue = Session.ReadValue(VariableIds.Server_NamespaceArray); _namespaceTable.Update(namespaceArrayNodeValue.GetValue <string[]>(null)); // show the available namespaces Trace($"The session to endpoint '{selectedEndpoint.EndpointUrl}' has {_namespaceTable.Count} entries in its namespace array:"); int i = 0; foreach (var ns in _namespaceTable.ToArray()) { Trace($"Namespace index {i++}: {ns}"); } // fetch the minimum supported item sampling interval from the server. DataValue minSupportedSamplingInterval = Session.ReadValue(VariableIds.Server_ServerCapabilities_MinSupportedSampleRate); _minSupportedSamplingInterval = minSupportedSamplingInterval.GetValue(0); Trace($"The server on endpoint '{selectedEndpoint.EndpointUrl}' supports a minimal sampling interval of {_minSupportedSamplingInterval} ms."); } } catch (Exception e) { Trace(e, $"Session creation to endpoint '{EndpointUri.AbsoluteUri}' failed {++UnsuccessfulConnectionCount} time(s). Please verify if server is up and Publisher configuration is correct."); State = SessionState.Disconnected; Session = null; return; } finally { await _opcSessionSemaphore.WaitAsync(); if (Session != null) { State = SessionState.Connected; } } } // stop monitoring of nodes if requested and remove them from the monitored items list. foreach (var opcSubscription in OpcSubscriptions) { var itemsToRemove = opcSubscription.OpcMonitoredItems.Where(i => i.State == OpcMonitoredItem.OpcMonitoredItemState.StopMonitoring); if (itemsToRemove.Any()) { Trace($"Remove nodes in subscription with id {opcSubscription.Subscription.Id} on endpoint '{EndpointUri.AbsoluteUri}'"); opcSubscription.Subscription.RemoveItems(itemsToRemove.Select(i => i.MonitoredItem)); } } // ensure all nodes in all subscriptions of this session are monitored. foreach (var opcSubscription in OpcSubscriptions) { // create the subscription, if it is not yet there. if (opcSubscription.Subscription == null) { int revisedPublishingInterval; opcSubscription.Subscription = CreateSubscription(opcSubscription.RequestedPublishingInterval, out revisedPublishingInterval); opcSubscription.PublishingInterval = revisedPublishingInterval; Trace($"Create subscription on endpoint '{EndpointUri.AbsoluteUri}' requested OPC publishing interval is {opcSubscription.RequestedPublishingInterval} ms. (revised: {revisedPublishingInterval} ms)"); } // process all unmonitored items. var unmonitoredItems = opcSubscription.OpcMonitoredItems.Where(i => i.State == OpcMonitoredItem.OpcMonitoredItemState.Unmonitored); foreach (var item in unmonitoredItems) { // if the session is disconnected, we stop trying and wait for the next cycle if (State == SessionState.Disconnected) { break; } Trace($"Start monitoring nodes on endpoint '{EndpointUri.AbsoluteUri}'."); NodeId currentNodeId; try { // lookup namespace index if ExpandedNodeId format has been used and build NodeId identifier. if (!string.IsNullOrEmpty(item.StartNodeId.NamespaceUri)) { currentNodeId = NodeId.Create(item.StartNodeId.Identifier, item.StartNodeId.NamespaceUri, _namespaceTable); } else { currentNodeId = new NodeId((NodeId)item.StartNodeId); } // get the DisplayName for the node, otherwise use the nodeId Node node = Session.ReadNode(currentNodeId); item.DisplayName = node.DisplayName.Text ?? currentNodeId.ToString(); // add the new monitored item. MonitoredItem monitoredItem = new MonitoredItem() { StartNodeId = currentNodeId, AttributeId = item.AttributeId, DisplayName = node.DisplayName.Text, MonitoringMode = item.MonitoringMode, SamplingInterval = item.RequestedSamplingInterval, QueueSize = item.QueueSize, DiscardOldest = item.DiscardOldest }; monitoredItem.Notification += item.Notification; opcSubscription.Subscription.AddItem(monitoredItem); opcSubscription.Subscription.SetPublishingMode(true); opcSubscription.Subscription.ApplyChanges(); item.MonitoredItem = monitoredItem; item.State = OpcMonitoredItem.OpcMonitoredItemState.Monitoreded; item.EndpointUri = EndpointUri; Trace($"Created monitored item for node '{currentNodeId}' in subscription with id {opcSubscription.Subscription.Id} on endpoint '{EndpointUri.AbsoluteUri}'"); if (item.RequestedSamplingInterval != monitoredItem.SamplingInterval) { Trace($"Sampling interval: requested: {item.RequestedSamplingInterval}; revised: {monitoredItem.SamplingInterval}"); item.SamplingInterval = monitoredItem.SamplingInterval; } } catch (Exception e) when(e.GetType() == typeof(ServiceResultException)) { ServiceResultException sre = (ServiceResultException)e; switch ((uint)sre.Result.StatusCode) { case StatusCodes.BadSessionIdInvalid: { Trace($"Session with Id {Session.SessionId} is no longer available on endpoint '{EndpointUri}'. Cleaning up."); // clean up the session _opcSessionSemaphore.Release(); await Disconnect(); break; } case StatusCodes.BadNodeIdInvalid: case StatusCodes.BadNodeIdUnknown: { Trace($"Failed to monitor node '{item.StartNodeId.Identifier}' on endpoint '{EndpointUri}'."); Trace($"OPC UA ServiceResultException is '{sre.Result}'. Please check your publisher configuration for this node."); break; } default: { Trace($"Unhandled OPC UA ServiceResultException '{sre.Result}' when monitoring node '{item.StartNodeId.Identifier}' on endpoint '{EndpointUri}'. Continue."); break; } } } catch (Exception e) { Trace(e, $"Failed to monitor node '{item.StartNodeId.Identifier}' on endpoint '{EndpointUri}'"); } } } // remove unused subscriptions. foreach (var opcSubscription in OpcSubscriptions) { if (opcSubscription.OpcMonitoredItems.Count == 0) { Trace($"Subscription with id {opcSubscription.Subscription.Id} on endpoint '{EndpointUri}' is not used and will be deleted."); Session.RemoveSubscription(opcSubscription.Subscription); opcSubscription.Subscription = null; } } // shutdown unused sessions. try { await OpcSessionsSemaphore.WaitAsync(); var unusedSessions = OpcSessions.Where(s => s.OpcSubscriptions.Count == 0); foreach (var unusedSession in unusedSessions) { OpcSessions.Remove(unusedSession); await unusedSession.Shutdown(); } // Shutdown everything on shutdown. if (PublisherShutdownInProgress == true) { var allSessions = OpcSessions; foreach (var session in allSessions) { OpcSessions.Remove(session); await session.Shutdown(); } } } finally { OpcSessionsSemaphore.Release(); } } catch (Exception e) { Trace(e, "Error during ConnectAndMonitor."); } finally { _opcSessionSemaphore.Release(); } }