diff --git a/src/AasxPluginAssetInterfaceDesc/AasOpcUaClient.cs b/src/AasxPluginAssetInterfaceDesc/AasOpcUaClient.cs new file mode 100644 index 00000000..4772f6d5 --- /dev/null +++ b/src/AasxPluginAssetInterfaceDesc/AasOpcUaClient.cs @@ -0,0 +1,324 @@ +/* +Copyright (c) 2018-2023 Festo SE & Co. KG +Author: Michael Hoffmeister + +This source code is licensed under the Apache License 2.0 (see LICENSE.txt). + +This source code may use other Open Source software components (see LICENSE.txt). +*/ + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Opc.Ua; +using Opc.Ua.Client; +using Opc.Ua.Configuration; + +// Note: this is a DUPLICATE from WpfMtpControl + +namespace AasxPluginAssetInterfaceDescription +{ + public enum AasOpcUaClientStatus + { + ErrorCreateApplication = 0x11, + ErrorDiscoverEndpoints = 0x12, + ErrorCreateSession = 0x13, + ErrorBrowseNamespace = 0x14, + ErrorCreateSubscription = 0x15, + ErrorMonitoredItem = 0x16, + ErrorAddSubscription = 0x17, + ErrorRunning = 0x18, + ErrorReadConfigFile = 0x19, + ErrorNoKeepAlive = 0x30, + ErrorInvalidCommandLine = 0x100, + Running = 0x1000, + Quitting = 0x8000, + Quitted = 0x8001 + }; + + public class AasOpcUaClient + { + const int ReconnectPeriod = 10; + Session session; + SessionReconnectHandler reconnectHandler; + string endpointURL; + static bool autoAccept = true; + static AasOpcUaClientStatus ClientStatus; + string userName; + string password; + + public AasOpcUaClient(string _endpointURL, bool _autoAccept, + string _userName, string _password) + { + endpointURL = _endpointURL; + autoAccept = _autoAccept; + userName = _userName; + password = _password; + } + + private BackgroundWorker worker = null; + + public void Run() + { + // start server as a worker (will start in the background) + // ReSharper disable once LocalVariableHidesMember + var worker = new BackgroundWorker(); + worker.WorkerSupportsCancellation = true; + worker.DoWork += (s1, e1) => + { + try + { + while (true) + { + StartClientAsync().Wait(); + + // keep running + if (ClientStatus == AasOpcUaClientStatus.Running) + while (true) + Thread.Sleep(200); + + // restart + Thread.Sleep(200); + } + } + catch (Exception ex) + { + AdminShellNS.LogInternally.That.SilentlyIgnoredError(ex); + } + }; + worker.RunWorkerCompleted += (s1, e1) => + { + ; + }; + worker.RunWorkerAsync(); + } + + public void Cancel() + { + if (worker != null && worker.IsBusy) + try + { + worker.CancelAsync(); + worker.Dispose(); + } + catch (Exception ex) + { + AdminShellNS.LogInternally.That.SilentlyIgnoredError(ex); + } + } + + public void Close() + { + if (session == null) + return; + session.Close(1); + session = null; + } + + public AasOpcUaClientStatus StatusCode { get => ClientStatus; } + + public async Task StartClientAsync() + { + Console.WriteLine("1 - Create an Application Configuration."); + ClientStatus = AasOpcUaClientStatus.ErrorCreateApplication; + + ApplicationInstance application = new ApplicationInstance + { + ApplicationName = "UA Core Sample Client", + ApplicationType = ApplicationType.Client, + ConfigSectionName = Utils.IsRunningOnMono() ? "Opc.Ua.MonoSampleClient" : "Opc.Ua.SampleClient" + }; + + // load the application configuration. + ApplicationConfiguration config = null; + try + { + config = await application.LoadApplicationConfiguration(false); + } + catch (Exception ex) + { + AdminShellNS.LogInternally.That.Error(ex, "Error reading the config file"); + ClientStatus = AasOpcUaClientStatus.ErrorReadConfigFile; + return; + } + + // check the application certificate. + bool haveAppCertificate = await application.CheckApplicationInstanceCertificate(false, 0); + if (!haveAppCertificate) + { + throw new Exception("Application instance certificate invalid!"); + } + + // ReSharper disable HeuristicUnreachableCode + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (haveAppCertificate) + { + config.ApplicationUri = X509Utils.GetApplicationUriFromCertificate( + config.SecurityConfiguration.ApplicationCertificate.Certificate); + + if (config.SecurityConfiguration.AutoAcceptUntrustedCertificates) + { + autoAccept = true; + } + // ReSharper disable once RedundantDelegateCreation + config.CertificateValidator.CertificateValidation += new CertificateValidationEventHandler( + CertificateValidator_CertificateValidation); + } + else + { + Console.WriteLine(" WARN: missing application certificate, using unsecure connection."); + } + // ReSharper enable HeuristicUnreachableCode + + Console.WriteLine("2 - Discover endpoints of {0}.", endpointURL); + ClientStatus = AasOpcUaClientStatus.ErrorDiscoverEndpoints; + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + var selectedEndpoint = CoreClientUtils.SelectEndpoint(endpointURL, haveAppCertificate, 15000); + Console.WriteLine(" Selected endpoint uses: {0}", + selectedEndpoint.SecurityPolicyUri.Substring(selectedEndpoint.SecurityPolicyUri.LastIndexOf('#') + 1)); + + Console.WriteLine("3 - Create a session with OPC UA server."); + ClientStatus = AasOpcUaClientStatus.ErrorCreateSession; + var endpointConfiguration = EndpointConfiguration.Create(config); + var endpoint = new ConfiguredEndpoint(null, selectedEndpoint, endpointConfiguration); + + session = await Session.Create(config, endpoint, false, "OPC UA Console Client", 60000, + new UserIdentity(userName, password), null); + + // register keep alive handler + session.KeepAlive += Client_KeepAlive; + + // ok + ClientStatus = AasOpcUaClientStatus.Running; + } + + private void Client_KeepAlive(Session sender, KeepAliveEventArgs e) + { + if (e.Status != null && ServiceResult.IsNotGood(e.Status)) + { + Console.WriteLine("{0} {1}/{2}", e.Status, sender.OutstandingRequestCount, sender.DefunctRequestCount); + + if (reconnectHandler == null) + { + Console.WriteLine("--- RECONNECTING ---"); + reconnectHandler = new SessionReconnectHandler(); + reconnectHandler.BeginReconnect(sender, ReconnectPeriod * 1000, Client_ReconnectComplete); + } + } + } + + private void Client_ReconnectComplete(object sender, EventArgs e) + { + // ignore callbacks from discarded objects. + if (!Object.ReferenceEquals(sender, reconnectHandler)) + { + return; + } + + if (reconnectHandler != null) + { + session = reconnectHandler.Session; + reconnectHandler.Dispose(); + } + + reconnectHandler = null; + + Console.WriteLine("--- RECONNECTED ---"); + } + + private static void OnNotification(MonitoredItem item, MonitoredItemNotificationEventArgs e) + { + // ReSharper disable once UnusedVariable + foreach (var value in item.DequeueValues()) + { + //// Console.WriteLine("{0}: {1}, {2}, {3}", item.DisplayName, value.Value, + //// value.SourceTimestamp, value.StatusCode); + } + } + + private static void CertificateValidator_CertificateValidation( + CertificateValidator validator, CertificateValidationEventArgs e) + { + if (e.Error.StatusCode == StatusCodes.BadCertificateUntrusted) + { + e.Accept = autoAccept; + if (autoAccept) + { + Console.WriteLine("Accepted Certificate: {0}", e.Certificate.Subject); + } + else + { + Console.WriteLine("Rejected Certificate: {0}", e.Certificate.Subject); + } + } + } + + public NodeId CreateNodeId(string nodeName, int index) + { + return new NodeId(nodeName, (ushort)index); + } + + private Dictionary nsDict = null; + + public NodeId CreateNodeId(string nodeName, string ns) + { + if (session == null || session.NamespaceUris == null) + return null; + + // build up? + if (nsDict == null) + { + nsDict = new Dictionary(); + for (ushort i = 0; i < session.NamespaceUris.Count; i++) + nsDict.Add(session.NamespaceUris.GetString(i), i); + } + + // find? + if (nsDict == null || !nsDict.ContainsKey(ns)) + return null; + + return new NodeId(nodeName, nsDict[ns]); + } + + public string ReadSubmodelElementValueAsString(string nodeName, int index) + { + if (session == null) + return ""; + + NodeId node = new NodeId(nodeName, (ushort)index); + return (session.ReadValue(node).ToString()); + } + + public DataValue ReadNodeId(NodeId nid) + { + if (session == null || nid == null || !session.Connected) + return null; + return (session.ReadValue(nid)); + } + + public void SubscribeNodeIds(NodeId[] nids, MonitoredItemNotificationEventHandler handler, + int publishingInteral = 1000) + { + if (session == null || nids == null || !session.Connected || handler == null) + return; + + var subscription = new Subscription(session.DefaultSubscription) + { PublishingInterval = publishingInteral }; + + foreach (var nid in nids) + { + var mi = new MonitoredItem(subscription.DefaultItem); + mi.StartNodeId = nid; + mi.Notification += handler; + subscription.AddItem(mi); + } + + session.AddSubscription(subscription); + subscription.Create(); + } + } +} diff --git a/src/AasxPluginAssetInterfaceDesc/AasxPluginAssetInterfaceDesc.csproj b/src/AasxPluginAssetInterfaceDesc/AasxPluginAssetInterfaceDesc.csproj index b72c7a71..edfa5bd1 100644 --- a/src/AasxPluginAssetInterfaceDesc/AasxPluginAssetInterfaceDesc.csproj +++ b/src/AasxPluginAssetInterfaceDesc/AasxPluginAssetInterfaceDesc.csproj @@ -14,6 +14,7 @@ + @@ -31,6 +32,7 @@ + @@ -48,5 +50,6 @@ + diff --git a/src/AasxPluginAssetInterfaceDesc/AidInterfaceStatus.cs b/src/AasxPluginAssetInterfaceDesc/AidInterfaceStatus.cs index d4142373..3ad973f4 100644 --- a/src/AasxPluginAssetInterfaceDesc/AidInterfaceStatus.cs +++ b/src/AasxPluginAssetInterfaceDesc/AidInterfaceStatus.cs @@ -69,7 +69,7 @@ public class AidIfxItemStatus public AnyUiUIElement RenderedUiElement = null; } - public enum AidInterfaceTechnology { HTTP, Modbus, MQTT } + public enum AidInterfaceTechnology { HTTP, Modbus, MQTT, OPCUA } public class AidInterfaceStatus { @@ -177,6 +177,16 @@ public class AidBaseConnection { public Uri TargetUri; + /// + /// For initiating the connection. Right now, not foreseen/ encouraged by the SMT. + /// + public string User = null; + + /// + /// For initiating the connection. Right now, not foreseen/ encouraged by the SMT. + /// + public string Password = null; + public DateTime LastActive = default(DateTime); public Action MessageReceived = null; @@ -246,7 +256,7 @@ public T GetOrCreate(string target) /// public class AidAllInterfaceStatus { - public bool[] UseTech = { true, false, false }; + public bool[] UseTech = { false, false, false, true }; /// /// Will hold connections steady and continously update values, either by @@ -265,6 +275,9 @@ public class AidAllInterfaceStatus public AidGenericConnections MqttConnections = new AidGenericConnections(); + public AidGenericConnections OpcUaConnections = + new AidGenericConnections(); + protected AidBaseConnection GetOrCreate(AidInterfaceTechnology tech, string endpointBase) { // find connection by factory @@ -282,6 +295,10 @@ protected AidBaseConnection GetOrCreate(AidInterfaceTechnology tech, string endp case AidInterfaceTechnology.MQTT: conn = MqttConnections.GetOrCreate(endpointBase); break; + + case AidInterfaceTechnology.OPCUA: + conn = OpcUaConnections.GetOrCreate(endpointBase); + break; } return conn; } diff --git a/src/AasxPluginAssetInterfaceDesc/AidOpcUaConnection.cs b/src/AasxPluginAssetInterfaceDesc/AidOpcUaConnection.cs new file mode 100644 index 00000000..8ffdb2df --- /dev/null +++ b/src/AasxPluginAssetInterfaceDesc/AidOpcUaConnection.cs @@ -0,0 +1,181 @@ +/* +Copyright (c) 2018-2023 Festo SE & Co. KG +Author: Michael Hoffmeister + +This source code is licensed under the Apache License 2.0 (see LICENSE.txt). + +This source code may use other Open Source software components (see LICENSE.txt). +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AasxPredefinedConcepts; +using Aas = AasCore.Aas3_0; +using AdminShellNS; +using Extensions; +using WpfMtpControl; +using AasxIntegrationBase; +using AasxPredefinedConcepts.AssetInterfacesDescription; +using FluentModbus; +using System.Net; +using System.Text.RegularExpressions; +using System.Globalization; +using System.Net.Http; +using MQTTnet; +using MQTTnet.Client; +using System.Web.Services.Description; +using Opc.Ua; + +namespace AasxPluginAssetInterfaceDescription +{ + public class AidOpcUaConnection : AidBaseConnection + { + public AasOpcUaClient Client; + + // protected Dictionary _subscribedTopics = new Dictionary(); + + override public bool Open() + { + try + { + // make client + // use the full target uri as endpoint (first) + Client = new AasOpcUaClient( + TargetUri.ToString(), + _autoAccept: true, + _userName: this.User, + _password: this.Password); + Client.Run(); + + // ok + return IsConnected(); + } + catch (Exception ex) + { + Client = null; + // _subscribedTopics.Clear(); + return false; + } + } + + override public bool IsConnected() + { + // simple + return Client != null && Client.StatusCode == AasOpcUaClientStatus.Running; + } + + override public void Close() + { + if (IsConnected()) + { + try + { + Client.Cancel(); + Client.Close(); + } catch (Exception ex) + { + ; + } + // _subscribedTopics.Clear(); + } + } + + protected NodeId ParseAndCreateNodeId (string input) + { + if (input?.HasContent() != true) + return null; + + { + var match = Regex.Match(input, @"^\s*ns\s*=\s*(\d+)\s*;\s*i\s*=\s*(\d+)\s*$"); + if (match.Success + && ushort.TryParse(match.Groups[1].ToString(), out var ns) + && uint.TryParse(match.Groups[2].ToString(), out var i)) + return new NodeId(i, ns); + } + { + var match = Regex.Match(input, @"^\s*NS\s*(\d+)\s*\|\s*Numeric\s*\|\s*(\d+)\s*$"); + if (match.Success + && ushort.TryParse(match.Groups[1].ToString(), out var ns) + && uint.TryParse(match.Groups[2].ToString(), out var i)) + return new NodeId(i, ns); + } + { + var match = Regex.Match(input, @"^\s*ns\s*=\s*(\d+)\s*;\s*s\s*=\s*(.*)$"); + if (match.Success + && ushort.TryParse(match.Groups[1].ToString(), out var ns)) + return new NodeId("" + match.Groups[2].ToString(), ns); + } + { + var match = Regex.Match(input, @"^\s*NS\s*(\d+)\s*\|\s*Alphanumeric\s*\|\s*(.+)$"); + if (match.Success + && ushort.TryParse(match.Groups[1].ToString(), out var ns)) + return new NodeId("" + match.Groups[2].ToString(), ns); + } + + // no + return null; + } + + override public int UpdateItemValue(AidIfxItemStatus item) + { + // access + if (!IsConnected()) + return 0; + + // careful + try + { + // get an node id? + var nid = ParseAndCreateNodeId(item?.FormData?.Href); + + // direct read possible? + var dv = Client.ReadNodeId(nid); + item.Value = "" + dv?.Value; + LastActive = DateTime.Now; + } + catch (Exception ex) + { + ; + } + + + return 0; + } + + //override public void PrepareContinousRun(IEnumerable items) + //{ + // // access + // if (!IsConnected() || items == null) + // return; + + // foreach (var item in items) + // { + // // valid topic? + // var topic = "" + item.FormData?.Href; + // if (topic.StartsWith("/")) + // topic = topic.Remove(0, 1); + // if (!topic.HasContent()) + // continue; + + // // need only "subscribe" + // if (item.FormData?.Mqv_controlPacket?.HasContent() != true) + // continue; + // if (item.FormData.Mqv_controlPacket.Trim().ToLower() != "subscribe") + // continue; + + // // is topic already subscribed? + // if (_subscribedTopics.ContainsKey(topic)) + // continue; + + // // ok, subscribe + // var task = Task.Run(() => Client.SubscribeAsync(topic)); + // task.Wait(); + // _subscribedTopics.Add(topic, topic); + // } + //} + + } +} diff --git a/src/AasxPluginAssetInterfaceDesc/AssetInterfaceAnyUiControl.cs b/src/AasxPluginAssetInterfaceDesc/AssetInterfaceAnyUiControl.cs index 36a2f312..d016d953 100644 --- a/src/AasxPluginAssetInterfaceDesc/AssetInterfaceAnyUiControl.cs +++ b/src/AasxPluginAssetInterfaceDesc/AssetInterfaceAnyUiControl.cs @@ -120,6 +120,10 @@ public void Start( AnyUiGdiHelper.CreateAnyUiBitmapFromResource( "AasxPluginAssetInterfaceDesc.Resources.logo-mqtt.png", assembly: Assembly.GetExecutingAssembly())); + _dictTechnologyToBitmap.Add(AidInterfaceTechnology.OPCUA, + AnyUiGdiHelper.CreateAnyUiBitmapFromResource( + "AasxPluginAssetInterfaceDesc.Resources.logo-opc-ua.png", + assembly: Assembly.GetExecutingAssembly())); } // fill given panel @@ -454,6 +458,7 @@ protected List PrepareAidInformation(Aas.Submodel sm) var ifxs = data?.InterfaceHTTP; if (tech == AidInterfaceTechnology.Modbus) ifxs = data?.InterfaceMODBUS; if (tech == AidInterfaceTechnology.MQTT) ifxs = data?.InterfaceMQTT; + if (tech == AidInterfaceTechnology.OPCUA) ifxs = data?.InterfaceOPCUA; if (ifxs == null || ifxs.Count < 1) continue; foreach (var ifx in ifxs) diff --git a/src/AasxPluginAssetInterfaceDesc/Resources/logo-opc-ua.png b/src/AasxPluginAssetInterfaceDesc/Resources/logo-opc-ua.png new file mode 100644 index 00000000..4adf42ca Binary files /dev/null and b/src/AasxPluginAssetInterfaceDesc/Resources/logo-opc-ua.png differ diff --git a/src/AasxPredefinedConcepts/DefinitionsAssetInterfacesDescription.cs b/src/AasxPredefinedConcepts/DefinitionsAssetInterfacesDescription.cs index a318cc88..5084549d 100644 --- a/src/AasxPredefinedConcepts/DefinitionsAssetInterfacesDescription.cs +++ b/src/AasxPredefinedConcepts/DefinitionsAssetInterfacesDescription.cs @@ -516,6 +516,10 @@ public class CD_AssetInterfacesDescription SupplSemId = "http://www.w3.org/2011/mqtt")] public List InterfaceMQTT = new List(); + [AasConcept(Cd = "https://admin-shell.io/idta/AssetInterfacesDescription/1/0/Interface", Card = AasxPredefinedCardinality.ZeroToMany, + SupplSemId = "http://www.w3.org/2011/opc-ua")] + public List InterfaceOPCUA = new List(); + // auto-generated informations public AasClassMapperInfo __Info__ = null; }