/// <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> /// Method to remove the node from the subscription and stop publishing telemetry to IoTHub. /// </summary> private ServiceResult OnUnpublishNodeCall(ISystemContext context, MethodState method, IList <object> inputArguments, IList <object> outputArguments) { if (inputArguments[0] == null || inputArguments[1] == null) { Trace("UnpublishNode: Invalid arguments!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide all arguments!")); } NodeId nodeId = null; Uri endpointUri = null; try { if (string.IsNullOrEmpty(inputArguments[0] as string) || string.IsNullOrEmpty(inputArguments[1] as string)) { Trace($"UnpublishNode: Arguments (0 (nodeId), 1 (endpointUrl)) are not valid strings!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide all arguments as strings!")); } nodeId = inputArguments[0] as string; endpointUri = new Uri(inputArguments[1] as string); } catch (UriFormatException) { Trace($"UnpublishNode: The endpointUrl is invalid '{inputArguments[1] as string}'!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide a valid OPC UA endpoint URL as second argument!")); } // find the session and stop monitoring the node. try { if (PublisherShutdownInProgress) { return(ServiceResult.Create(StatusCodes.BadUnexpectedError, $"Publisher shutdown in progress.")); } // find the session we need to monitor the node OpcSession opcSession = null; try { OpcSessionsSemaphore.Wait(); opcSession = OpcSessions.FirstOrDefault(s => s.EndpointUri == endpointUri); } catch { opcSession = null; } finally { OpcSessionsSemaphore.Release(); } if (opcSession == null) { // do nothing if there is no session for this endpoint. Trace($"UnpublishNode: Session for endpoint '{endpointUri.OriginalString}' not found."); return(ServiceResult.Create(StatusCodes.BadSessionIdInvalid, "Session for endpoint of published node not found!")); } else { Trace($"UnpublishNode: Session found for endpoint '{endpointUri.OriginalString}'"); } // remove the node from the sessions monitored items list. opcSession.TagNodeForMonitoringStop(nodeId); Trace("UnpublishNode: Requested to stop monitoring of node."); // remove node from persisted config file try { PublishDataSemaphore.Wait(); var entryToRemove = PublisherConfigFileEntries.Find(l => l.NodeId == nodeId && l.EndpointUri == endpointUri); PublisherConfigFileEntries.Remove(entryToRemove); File.WriteAllText(NodesToPublishAbsFilename, JsonConvert.SerializeObject(PublisherConfigFileEntries)); } finally { PublishDataSemaphore.Release(); } } catch (Exception e) { Trace(e, $"UnpublishNode: Exception while trying to configure publishing node '{nodeId.ToString()}'"); return(ServiceResult.Create(e, StatusCodes.BadUnexpectedError, $"Unexpected error publishing node: {e.Message}")); } return(ServiceResult.Good); }
/// <summary> /// Method to start monitoring a node and publish the data to IoTHub. /// </summary> private ServiceResult OnPublishNodeCall(ISystemContext context, MethodState method, IList <object> inputArguments, IList <object> outputArguments) { if (inputArguments[0] == null || inputArguments[1] == null) { Trace("PublishNode: Invalid Arguments when trying to publish a node."); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide all arguments!")); } NodeToPublishConfig nodeToPublish; NodeId nodeId = null; Uri endpointUri = null; try { if (string.IsNullOrEmpty(inputArguments[0] as string) || string.IsNullOrEmpty(inputArguments[1] as string)) { Trace($"PublishNode: Arguments (0 (nodeId), 1 (endpointUrl)) are not valid strings!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide all arguments as strings!")); } nodeId = NodeId.Parse(inputArguments[0] as string); endpointUri = new Uri(inputArguments[1] as string); nodeToPublish = new NodeToPublishConfig(nodeId, endpointUri, OpcSamplingInterval, OpcPublishingInterval); } catch (UriFormatException) { Trace($"PublishNode: The EndpointUri has an invalid format '{inputArguments[1] as string}'!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide a valid OPC UA endpoint URL as second argument!")); } catch (Exception e) { Trace(e, $"PublishNode: The NodeId has an invalid format '{inputArguments[0] as string}'!"); return(ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide a valid OPC UA NodeId in 'ns=' syntax as first argument!")); } // find/create a session to the endpoint URL and start monitoring the node. try { if (PublisherShutdownInProgress) { return(ServiceResult.Create(StatusCodes.BadUnexpectedError, $"Publisher shutdown in progress.")); } // find the session we need to monitor the node OpcSession opcSession = null; try { OpcSessionsSemaphore.Wait(); opcSession = OpcSessions.FirstOrDefault(s => s.EndpointUri == nodeToPublish.EndpointUri); // add a new session. if (opcSession == null) { // create new session info. opcSession = new OpcSession(nodeToPublish.EndpointUri, OpcSessionCreationTimeout); OpcSessions.Add(opcSession); Trace($"PublishNode: No matching session found for endpoint '{nodeToPublish.EndpointUri.OriginalString}'. Requested to create a new one."); } else { Trace($"PublishNode: Session found for endpoint '{nodeToPublish.EndpointUri.OriginalString}'"); } // add the node info to the subscription with the default publishing interval opcSession.AddNodeForMonitoring(OpcPublishingInterval, OpcSamplingInterval, nodeToPublish.NodeId); Trace($"PublishNode: Requested to monitor item with NodeId '{nodeToPublish.NodeId.ToString()}' (PublishingInterval: {OpcPublishingInterval}, SamplingInterval: {OpcSamplingInterval})"); } finally { OpcSessionsSemaphore.Release(); } // update our data try { PublishDataSemaphore.Wait(); PublishConfig.Add(nodeToPublish); // add it also to the publish file var publisherConfigFileEntry = new PublisherConfigFileEntry() { EndpointUri = endpointUri, NodeId = nodeId }; PublisherConfigFileEntries.Add(publisherConfigFileEntry); File.WriteAllText(NodesToPublishAbsFilename, JsonConvert.SerializeObject(PublisherConfigFileEntries)); } finally { PublishDataSemaphore.Release(); } return(ServiceResult.Good); } catch (Exception e) { Trace(e, $"PublishNode: Exception while trying to configure publishing node '{nodeToPublish.NodeId.ToString()}'"); return(ServiceResult.Create(e, StatusCodes.BadUnexpectedError, $"Unexpected error publishing node: {e.Message}")); } }
/// <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(); } }