diff --git a/MP.MONO.ADAPTER.OPC.sln b/MP.MONO.ADAPTER.OPC.sln new file mode 100644 index 0000000..f328032 --- /dev/null +++ b/MP.MONO.ADAPTER.OPC.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.32510.428 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MP.MONO.ADAPTER.OPC", "MP.MONO.ADAPTER.OPC\MP.MONO.ADAPTER.OPC.csproj", "{458FD824-7804-4CF9-8C43-463C1F5659DA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {458FD824-7804-4CF9-8C43-463C1F5659DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {458FD824-7804-4CF9-8C43-463C1F5659DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {458FD824-7804-4CF9-8C43-463C1F5659DA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {458FD824-7804-4CF9-8C43-463C1F5659DA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3811E8B4-1DD3-4841-9840-1B1A08782CD3} + EndGlobalSection +EndGlobal diff --git a/MP.MONO.ADAPTER.OPC/ConsoleUtils.cs b/MP.MONO.ADAPTER.OPC/ConsoleUtils.cs new file mode 100644 index 0000000..92bcf42 --- /dev/null +++ b/MP.MONO.ADAPTER.OPC/ConsoleUtils.cs @@ -0,0 +1,382 @@ +/* ======================================================================== + * Copyright (c) 2005-2021 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Mono.Options; +using Opc.Ua; +using Opc.Ua.Configuration; +using Serilog; +using Serilog.Events; +using Serilog.Templates; +using static Opc.Ua.Utils; + +namespace MP.MONO.ADAPTER.OPC +{ + /// + /// The log output implementation of a TextWriter. + /// + public class LogWriter : TextWriter + { + private StringBuilder m_builder = new StringBuilder(); + + public override void Write(char value) + { + m_builder.Append(value); + } + + public override void WriteLine(char value) + { + m_builder.Append(value); + LogInfo("{0}", m_builder.ToString()); + m_builder.Clear(); + } + + public override void WriteLine() + { + LogInfo("{0}", m_builder.ToString()); + m_builder.Clear(); + } + + public override void WriteLine(string format, object arg0) + { + m_builder.Append(format); + LogInfo(m_builder.ToString(), arg0); + m_builder.Clear(); + } + + public override void WriteLine(string format, object arg0, object arg1) + { + m_builder.Append(format); + LogInfo(m_builder.ToString(), arg0, arg1); + m_builder.Clear(); + } + + public override void WriteLine(string format, params object[] arg) + { + m_builder.Append(format); + LogInfo(m_builder.ToString(), arg); + m_builder.Clear(); + } + + public override void Write(string value) + { + m_builder.Append(value); + } + + public override void WriteLine(string value) + { + m_builder.Append(value); + LogInfo("{0}", m_builder.ToString()); + m_builder.Clear(); + } + + public override Encoding Encoding + { + get { return Encoding.Default; } + } + } + + /// + /// The error code why the application exit. + /// + public enum ExitCode : int + { + Ok = 0, + ErrorNotStarted = 0x80, + ErrorRunning = 0x81, + ErrorException = 0x82, + ErrorStopping = 0x83, + ErrorCertificate = 0x84, + ErrorInvalidCommandLine = 0x100 + }; + + /// + /// An exception that occured and caused an exit of the application. + /// + public class ErrorExitException : Exception + { + public ExitCode ExitCode { get; } + + public ErrorExitException(ExitCode exitCode) + { + ExitCode = exitCode; + } + + public ErrorExitException() + { + ExitCode = ExitCode.Ok; + } + + public ErrorExitException(string message) : base(message) + { + ExitCode = ExitCode.Ok; + } + + public ErrorExitException(string message, ExitCode exitCode) : base(message) + { + ExitCode = exitCode; + } + + public ErrorExitException(string message, Exception innerException) : base(message, innerException) + { + ExitCode = ExitCode.Ok; + } + + public ErrorExitException(string message, Exception innerException, ExitCode exitCode) : base(message, innerException) + { + ExitCode = exitCode; + } + } + + /// + /// A dialog which asks for user input. + /// + public class ApplicationMessageDlg : IApplicationMessageDlg + { + private TextWriter m_output; + private string m_message = string.Empty; + private bool m_ask; + + public ApplicationMessageDlg(TextWriter output) + { + m_output = output; + } + + public override void Message(string text, bool ask) + { + m_message = text; + m_ask = ask; + } + + public override async Task ShowAsync() + { + if (m_ask) + { + var message = new StringBuilder(m_message); + message.Append(" (y/n, default y): "); + m_output.Write(message.ToString()); + + try + { + ConsoleKeyInfo result = Console.ReadKey(); + m_output.WriteLine(); + return await Task.FromResult((result.KeyChar == 'y') || + (result.KeyChar == 'Y') || (result.KeyChar == '\r')).ConfigureAwait(false); + } + catch + { + // intentionally fall through + } + } + else + { + m_output.WriteLine(m_message); + } + + return await Task.FromResult(true).ConfigureAwait(false); + } + } + + /// + /// Helper functions shared in various console applications. + /// + public static class ConsoleUtils + { + /// + /// Process a command line of the console sample application. + /// + public static string ProcessCommandLine( + TextWriter output, + string[] args, + Mono.Options.OptionSet options, + ref bool showHelp, + bool noExtraArgs = true) + { + IList extraArgs = null; + try + { + extraArgs = options.Parse(args); + if (noExtraArgs) + { + foreach (string extraArg in extraArgs) + { + output.WriteLine("Error: Unknown option: {0}", extraArg); + showHelp = true; + } + } + } + catch (OptionException e) + { + output.WriteLine(e.Message); + showHelp = true; + } + + if (showHelp) + { + options.WriteOptionDescriptions(output); + throw new ErrorExitException("Invalid Commandline or help requested.", ExitCode.ErrorInvalidCommandLine); + } + + return extraArgs.FirstOrDefault(); + } + + /// + /// Configure the logging providers. + /// + /// + /// Replaces the Opc.Ua.Core default ILogger with a + /// Microsoft.Extension.Logger with a Serilog file, debug and console logger. + /// The debug logger is only enabled for debug builds. + /// The console logger is enabled by the logConsole flag at the consoleLogLevel. + /// The file logger uses the setting in the ApplicationConfiguration. + /// The Trace logLevel is chosen if required by the Tracemasks. + /// + /// The application configuration. + /// The context name for the logger. + /// Enable logging to the console. + /// The LogLevel to use for the console/debug.< + /// /param> + public static void ConfigureLogging( + ApplicationConfiguration configuration, + string context, + bool logConsole, + LogLevel consoleLogLevel) + { + var loggerConfiguration = new LoggerConfiguration() + .Enrich.FromLogContext(); + + if (logConsole) + { + loggerConfiguration.WriteTo.Console( + restrictedToMinimumLevel: (LogEventLevel)consoleLogLevel + ); + } +#if DEBUG + else + { + loggerConfiguration + .WriteTo.Debug(restrictedToMinimumLevel: (LogEventLevel)consoleLogLevel); + } +#endif + LogLevel fileLevel = LogLevel.Information; + + // switch for Trace/Verbose output + var traceMasks = configuration.TraceConfiguration.TraceMasks; + if ((traceMasks & ~(TraceMasks.Information | TraceMasks.Error | + TraceMasks.Security | TraceMasks.StartStop | TraceMasks.StackTrace)) != 0) + { + fileLevel = LogLevel.Trace; + } + + // add file logging if configured + var outputFilePath = configuration.TraceConfiguration.OutputFilePath; + if (!string.IsNullOrWhiteSpace(outputFilePath)) + { + loggerConfiguration.WriteTo.File( + new ExpressionTemplate("{UtcDateTime(@t):yyyy-MM-dd HH:mm:ss.fff} [{@l:u3}] {@m}\n{@x}"), + ReplaceSpecialFolderNames(outputFilePath), + restrictedToMinimumLevel: (LogEventLevel)fileLevel, + rollOnFileSizeLimit: true); + } + + // adjust minimum level + if (fileLevel < LogLevel.Information || consoleLogLevel < LogLevel.Information) + { + loggerConfiguration.MinimumLevel.Verbose(); + } + + // create the serilog logger + var serilogger = loggerConfiguration + .CreateLogger(); + + // create the ILogger for Opc.Ua.Core + var logger = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Trace)) + .AddSerilog(serilogger) + .CreateLogger(context); + + // set logger interface, disables TraceEvent + SetLogger(logger); + } + + /// + /// Output log messages. + /// + public static void LogTest() + { + // print legacy logging output, for testing + Trace(TraceMasks.Error, "This is an Error message: {0}", TraceMasks.Error); + Trace(TraceMasks.Information, "This is a Information message: {0}", TraceMasks.Information); + Trace(TraceMasks.StackTrace, "This is a StackTrace message: {0}", TraceMasks.StackTrace); + Trace(TraceMasks.Service, "This is a Service message: {0}", TraceMasks.Service); + Trace(TraceMasks.ServiceDetail, "This is a ServiceDetail message: {0}", TraceMasks.ServiceDetail); + Trace(TraceMasks.Operation, "This is a Operation message: {0}", TraceMasks.Operation); + Trace(TraceMasks.OperationDetail, "This is a OperationDetail message: {0}", TraceMasks.OperationDetail); + Trace(TraceMasks.StartStop, "This is a StartStop message: {0}", TraceMasks.StartStop); + Trace(TraceMasks.ExternalSystem, "This is a ExternalSystem message: {0}", TraceMasks.ExternalSystem); + Trace(TraceMasks.Security, "This is a Security message: {0}", TraceMasks.Security); + + // print ILogger logging output + LogTrace("This is a Trace message: {0}", LogLevel.Trace); + LogDebug("This is a Debug message: {0}", LogLevel.Debug); + LogInfo("This is a Info message: {0}", LogLevel.Information); + LogWarning("This is a Warning message: {0}", LogLevel.Warning); + LogError("This is a Error message: {0}", LogLevel.Error); + LogCritical("This is a Critical message: {0}", LogLevel.Critical); + } + + /// + /// Create an event which is set if a user + /// enters the Ctrl-C key combination. + /// + public static ManualResetEvent CtrlCHandler() + { + var quitEvent = new ManualResetEvent(false); + try + { + Console.CancelKeyPress += (_, eArgs) => { + quitEvent.Set(); + eArgs.Cancel = true; + }; + } + catch + { + // intentionally left blank + } + return quitEvent; + } + } +} + diff --git a/MP.MONO.ADAPTER.OPC/MP.MONO.ADAPTER.OPC.csproj b/MP.MONO.ADAPTER.OPC/MP.MONO.ADAPTER.OPC.csproj new file mode 100644 index 0000000..b4a2e95 --- /dev/null +++ b/MP.MONO.ADAPTER.OPC/MP.MONO.ADAPTER.OPC.csproj @@ -0,0 +1,33 @@ + + + + net6.0 + MP.MONO.ADAPTER.OPC + Exe + MP.MONO.ADAPTER.OPC + EgalWare + OPC UA Console Client + Copyright © 2020+ EgalWare + MP.MONO.ADAPTER.OPC + + + + + + + + + + + + + + + + + + + PreserveNewest + + + diff --git a/MP.MONO.ADAPTER/Program.cs b/MP.MONO.ADAPTER.OPC/Program.cs similarity index 99% rename from MP.MONO.ADAPTER/Program.cs rename to MP.MONO.ADAPTER.OPC/Program.cs index 50961cb..a949e0d 100644 --- a/MP.MONO.ADAPTER/Program.cs +++ b/MP.MONO.ADAPTER.OPC/Program.cs @@ -36,7 +36,7 @@ using Microsoft.Extensions.Logging; using Opc.Ua; using Opc.Ua.Configuration; -namespace Quickstarts.ConsoleReferenceClient +namespace MP.MONO.ADAPTER.OPC { /// /// The program. diff --git a/MP.MONO.ADAPTER.OPC/Quickstarts.ReferenceClient.Config.xml b/MP.MONO.ADAPTER.OPC/Quickstarts.ReferenceClient.Config.xml new file mode 100644 index 0000000..d9f67d5 --- /dev/null +++ b/MP.MONO.ADAPTER.OPC/Quickstarts.ReferenceClient.Config.xml @@ -0,0 +1,89 @@ + + + Egalware Console Adapter OPC Client + urn:localhost:UA:Egalware:MP:MONO:ADAPTER + uri:egalware.com:MP:MONO:ADAPTER + Client_1 + + + + + + Directory + %LocalApplicationData%/EgalWare/pki/own + CN=EgalWare Adapter Client, C=IT, S=Bergamo, O=EgalWare, DC=localhost + + + + + Directory + %LocalApplicationData%/EgalWare/pki/issuer + + + + + Directory + %LocalApplicationData%/EgalWare/pki/trusted + + + + + Directory + %LocalApplicationData%/EgalWare/pki/rejected + + + + false + + + + + + + 600000 + 1048576 + 1048576 + 65535 + 4194304 + 65535 + 300000 + 3600000 + + + + 60000 + + opc.tcp://{0}:4840 + http://{0}:52601/UADiscovery + http://{0}/UADiscovery/Default.svc + + + 10000 + + + + + + + %LocalApplicationData%/EgalWare/Logs/MP.MONO.ADAPTER.OPC.log.txt + true + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MP.MONO.ADAPTER/UAClient.cs b/MP.MONO.ADAPTER.OPC/UAClient.cs similarity index 99% rename from MP.MONO.ADAPTER/UAClient.cs rename to MP.MONO.ADAPTER.OPC/UAClient.cs index 66290eb..3b815f5 100644 --- a/MP.MONO.ADAPTER/UAClient.cs +++ b/MP.MONO.ADAPTER.OPC/UAClient.cs @@ -35,7 +35,7 @@ using System.Threading.Tasks; using Opc.Ua; using Opc.Ua.Client; -namespace Quickstarts +namespace MP.MONO.ADAPTER.OPC { /// /// OPC UA Client with examples of basic functionality. diff --git a/MP.MONO.ADAPTER/dotnettrace.cmd b/MP.MONO.ADAPTER.OPC/dotnettrace.cmd similarity index 100% rename from MP.MONO.ADAPTER/dotnettrace.cmd rename to MP.MONO.ADAPTER.OPC/dotnettrace.cmd diff --git a/MP.MONO.ADAPTER/MP.MONO.ADAPTER.csproj b/MP.MONO.ADAPTER.OPC2/MP - Backup.MONO.ADAPTER.OPC.csproj similarity index 78% rename from MP.MONO.ADAPTER/MP.MONO.ADAPTER.csproj rename to MP.MONO.ADAPTER.OPC2/MP - Backup.MONO.ADAPTER.OPC.csproj index 65bfedf..a47c3ec 100644 --- a/MP.MONO.ADAPTER/MP.MONO.ADAPTER.csproj +++ b/MP.MONO.ADAPTER.OPC2/MP - Backup.MONO.ADAPTER.OPC.csproj @@ -2,13 +2,13 @@ $(AppTargetFrameWorks) - ConsoleReferenceClient + MP.MONO.ADAPTER.OPC Exe - ConsoleReferenceClient - OPC Foundation - .NET Console Reference Client - Copyright © 2004-2022 OPC Foundation, Inc - Quickstarts.ConsoleReferenceClient + MP.MONO.ADAPTER.OPC + EgalWare + OPC UA Console Client + Copyright © 2020+ EgalWare + MP.MONO.ADAPTER.OPC @@ -37,7 +37,7 @@ - + PreserveNewest diff --git a/MP.MONO.ADAPTER/Quickstarts.ReferenceClient.Config.xml b/MP.MONO.ADAPTER.OPC2/MP.MONO.ADAPTER.OPC.Config.xml similarity index 100% rename from MP.MONO.ADAPTER/Quickstarts.ReferenceClient.Config.xml rename to MP.MONO.ADAPTER.OPC2/MP.MONO.ADAPTER.OPC.Config.xml diff --git a/MP.MONO.ADAPTER.OPC2/MP.MONO.ADAPTER.OPC.csproj b/MP.MONO.ADAPTER.OPC2/MP.MONO.ADAPTER.OPC.csproj new file mode 100644 index 0000000..a47c3ec --- /dev/null +++ b/MP.MONO.ADAPTER.OPC2/MP.MONO.ADAPTER.OPC.csproj @@ -0,0 +1,45 @@ + + + + $(AppTargetFrameWorks) + MP.MONO.ADAPTER.OPC + Exe + MP.MONO.ADAPTER.OPC + EgalWare + OPC UA Console Client + Copyright © 2020+ EgalWare + MP.MONO.ADAPTER.OPC + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/MP.MONO.ADAPTER.OPC2/Program.cs b/MP.MONO.ADAPTER.OPC2/Program.cs new file mode 100644 index 0000000..a949e0d --- /dev/null +++ b/MP.MONO.ADAPTER.OPC2/Program.cs @@ -0,0 +1,186 @@ +/* ======================================================================== + * Copyright (c) 2005-2021 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.Globalization; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Opc.Ua; +using Opc.Ua.Configuration; + +namespace MP.MONO.ADAPTER.OPC +{ + /// + /// The program. + /// + public static class Program + { + /// + /// Main entry point. + /// + public static async Task Main(string[] args) + { + TextWriter output = Console.Out; + output.WriteLine("OPC UA Console Reference Client"); + + output.WriteLine("OPC UA library: {0} @ {1} -- {2}", + Utils.GetAssemblyBuildNumber(), + Utils.GetAssemblyTimestamp().ToString("G", CultureInfo.InvariantCulture), + Utils.GetAssemblySoftwareVersion()); + + // The application name and config file names + var applicationName = "ConsoleReferenceClient"; + var configSectionName = "Quickstarts.ReferenceClient"; + var usage = $"Usage: dotnet {applicationName}.dll [OPTIONS]"; + + // command line options + bool showHelp = false; + bool autoAccept = false; + bool logConsole = false; + bool appLog = false; + bool renewCertificate = false; + string password = null; + int timeout = Timeout.Infinite; + + Mono.Options.OptionSet options = new Mono.Options.OptionSet { + usage, + { "h|help", "show this message and exit", h => showHelp = h != null }, + { "a|autoaccept", "auto accept certificates (for testing only)", a => autoAccept = a != null }, + { "c|console", "log to console", c => logConsole = c != null }, + { "l|log", "log app output", c => appLog = c != null }, + { "p|password=", "optional password for private key", (string p) => password = p }, + { "r|renew", "renew application certificate", r => renewCertificate = r != null }, + { "t|timeout=", "timeout in seconds to exit application", (int t) => timeout = t * 1000 }, + }; + + try + { + // parse command line and set options + var extraArg = ConsoleUtils.ProcessCommandLine(output, args, options, ref showHelp, false); + + // connect Url? + Uri serverUrl = new Uri("opc.tcp://localhost:62541/Quickstarts/ReferenceServer"); + if (!string.IsNullOrEmpty(extraArg)) + { + serverUrl = new Uri(extraArg); + } + + // log console output to logger + if (logConsole && appLog) + { + output = new LogWriter(); + } + + // Define the UA Client application + ApplicationInstance.MessageDlg = new ApplicationMessageDlg(output); + CertificatePasswordProvider PasswordProvider = new CertificatePasswordProvider(password); + ApplicationInstance application = new ApplicationInstance { + ApplicationName = applicationName, + ApplicationType = ApplicationType.Client, + ConfigSectionName = configSectionName, + CertificatePasswordProvider = PasswordProvider + }; + + // load the application configuration. + var config = await application.LoadApplicationConfiguration(silent: false); + + // setup the logging + ConsoleUtils.ConfigureLogging(config, applicationName, logConsole, LogLevel.Information); + + // delete old certificate + if (renewCertificate) + { + await application.DeleteApplicationInstanceCertificate().ConfigureAwait(false); + } + + // check the application certificate. + bool haveAppCertificate = await application.CheckApplicationInstanceCertificate(false, minimumKeySize: 0).ConfigureAwait(false); + if (!haveAppCertificate) + { + throw new ErrorExitException("Application instance certificate invalid!", ExitCode.ErrorCertificate); + } + + // wait for timeout or Ctrl-C + var quitEvent = ConsoleUtils.CtrlCHandler(); + + // connect to a server until application stopped + bool quit = false; + DateTime start = DateTime.UtcNow; + int waitTime = int.MaxValue; + do + { + if (timeout > 0) + { + waitTime = timeout - (int)DateTime.UtcNow.Subtract(start).TotalMilliseconds; + if (waitTime <= 0) + { + break; + } + } + + // create the UA Client object and connect to configured server. + UAClient uaClient = new UAClient(application.ApplicationConfiguration, output, ClientBase.ValidateResponse) { + AutoAccept = autoAccept + }; + + bool connected = await uaClient.ConnectAsync(serverUrl.ToString()); + if (connected) + { + // Run tests for available methods. + uaClient.ReadNodes(); + uaClient.WriteNodes(); + uaClient.Browse(); + uaClient.CallMethod(); + + uaClient.SubscribeToDataChanges(); + + // Wait for some DataChange notifications from MonitoredItems + quit = quitEvent.WaitOne(Math.Min(30_000, waitTime)); + + uaClient.Disconnect(); + } + else + { + output.WriteLine("Could not connect to server! Retry in 10 seconds or Ctrl-C to quit."); + quit = quitEvent.WaitOne(Math.Min(10_000, waitTime)); + } + + } while (!quit); + + output.WriteLine("\nClient stopped."); + } + catch (Exception ex) + { + output.WriteLine(ex.Message); + } + } + } +} diff --git a/MP.MONO.ADAPTER.OPC2/UAClient.cs b/MP.MONO.ADAPTER.OPC2/UAClient.cs new file mode 100644 index 0000000..3b815f5 --- /dev/null +++ b/MP.MONO.ADAPTER.OPC2/UAClient.cs @@ -0,0 +1,501 @@ +/* ======================================================================== + * Copyright (c) 2005-2020 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Opc.Ua; +using Opc.Ua.Client; + +namespace MP.MONO.ADAPTER.OPC +{ + /// + /// OPC UA Client with examples of basic functionality. + /// + class UAClient + { + #region Constructors + /// + /// Initializes a new instance of the UAClient class. + /// + public UAClient(ApplicationConfiguration configuration, TextWriter writer, Action validateResponse) + { + m_validateResponse = validateResponse; + m_output = writer; + m_configuration = configuration; + m_configuration.CertificateValidator.CertificateValidation += CertificateValidation; + } + #endregion + + #region Public Properties + /// + /// Gets the client session. + /// + public Session Session => m_session; + + /// + /// Auto accept untrusted certificates. + /// + public bool AutoAccept { get; set; } = false; + #endregion + + #region Public Methods + /// + /// Creates a session with the UA server + /// + public async Task ConnectAsync(string serverUrl) + { + if (serverUrl == null) throw new ArgumentNullException(nameof(serverUrl)); + + try + { + if (m_session != null && m_session.Connected == true) + { + m_output.WriteLine("Session already connected!"); + } + else + { + m_output.WriteLine("Connecting to... {0}", serverUrl); + + // Get the endpoint by connecting to server's discovery endpoint. + // Try to find the first endopint with security. + EndpointDescription endpointDescription = CoreClientUtils.SelectEndpoint(m_configuration, serverUrl, true); + EndpointConfiguration endpointConfiguration = EndpointConfiguration.Create(m_configuration); + ConfiguredEndpoint endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration); + + // Create the session + Session session = await Session.Create( + m_configuration, + endpoint, + false, + false, + m_configuration.ApplicationName, + 30 * 60 * 1000, + new UserIdentity(), + null + ); + + // Assign the created session + if (session != null && session.Connected) + { + m_session = session; + } + + // Session created successfully. + m_output.WriteLine("New Session Created with SessionName = {0}", m_session.SessionName); + } + + return true; + } + catch (Exception ex) + { + // Log Error + m_output.WriteLine("Create Session Error : {0}", ex.Message); + return false; + } + } + + /// + /// Disconnects the session. + /// + public void Disconnect() + { + try + { + if (m_session != null) + { + m_output.WriteLine("Disconnecting..."); + + m_session.Close(); + m_session.Dispose(); + m_session = null; + + // Log Session Disconnected event + m_output.WriteLine("Session Disconnected."); + } + else + { + m_output.WriteLine("Session not created!"); + } + } + catch (Exception ex) + { + // Log Error + m_output.WriteLine($"Disconnect Error : {ex.Message}"); + } + } + + /// + /// Read a list of nodes from Server + /// + public void ReadNodes() + { + if (m_session == null || m_session.Connected == false) + { + m_output.WriteLine("Session not connected!"); + return; + } + + try + { + #region Read a node by calling the Read Service + + // build a list of nodes to be read + ReadValueIdCollection nodesToRead = new ReadValueIdCollection() + { + // Value of ServerStatus + new ReadValueId() { NodeId = Variables.Server_ServerStatus, AttributeId = Attributes.Value }, + // BrowseName of ServerStatus_StartTime + new ReadValueId() { NodeId = Variables.Server_ServerStatus_StartTime, AttributeId = Attributes.BrowseName }, + // Value of ServerStatus_StartTime + new ReadValueId() { NodeId = Variables.Server_ServerStatus_StartTime, AttributeId = Attributes.Value } + }; + + // Read the node attributes + m_output.WriteLine("Reading nodes..."); + + // Call Read Service + m_session.Read( + null, + 0, + TimestampsToReturn.Both, + nodesToRead, + out DataValueCollection resultsValues, + out DiagnosticInfoCollection diagnosticInfos); + + // Validate the results + m_validateResponse(resultsValues, nodesToRead); + + // Display the results. + foreach (DataValue result in resultsValues) + { + m_output.WriteLine("Read Value = {0} , StatusCode = {1}", result.Value, result.StatusCode); + } + #endregion + + #region Read the Value attribute of a node by calling the Session.ReadValue method + // Read Server NamespaceArray + m_output.WriteLine("Reading Value of NamespaceArray node..."); + DataValue namespaceArray = m_session.ReadValue(Variables.Server_NamespaceArray); + // Display the result + m_output.WriteLine($"NamespaceArray Value = {namespaceArray}"); + #endregion + } + catch (Exception ex) + { + // Log Error + m_output.WriteLine($"Read Nodes Error : {ex.Message}."); + } + } + + /// + /// Write a list of nodes to the Server + /// + public void WriteNodes() + { + if (m_session == null || m_session.Connected == false) + { + m_output.WriteLine("Session not connected!"); + return; + } + + try + { + // Write the configured nodes + WriteValueCollection nodesToWrite = new WriteValueCollection(); + + // Int32 Node - Objects\CTT\Scalar\Scalar_Static\Int32 + WriteValue intWriteVal = new WriteValue(); + intWriteVal.NodeId = new NodeId("ns=2;s=Scalar_Static_Int32"); + intWriteVal.AttributeId = Attributes.Value; + intWriteVal.Value = new DataValue(); + intWriteVal.Value.Value = (int)100; + nodesToWrite.Add(intWriteVal); + + // Float Node - Objects\CTT\Scalar\Scalar_Static\Float + WriteValue floatWriteVal = new WriteValue(); + floatWriteVal.NodeId = new NodeId("ns=2;s=Scalar_Static_Float"); + floatWriteVal.AttributeId = Attributes.Value; + floatWriteVal.Value = new DataValue(); + floatWriteVal.Value.Value = (float)100.5; + nodesToWrite.Add(floatWriteVal); + + // String Node - Objects\CTT\Scalar\Scalar_Static\String + WriteValue stringWriteVal = new WriteValue(); + stringWriteVal.NodeId = new NodeId("ns=2;s=Scalar_Static_String"); + stringWriteVal.AttributeId = Attributes.Value; + stringWriteVal.Value = new DataValue(); + stringWriteVal.Value.Value = "String Test"; + nodesToWrite.Add(stringWriteVal); + + // Write the node attributes + StatusCodeCollection results = null; + DiagnosticInfoCollection diagnosticInfos; + m_output.WriteLine("Writing nodes..."); + + // Call Write Service + m_session.Write(null, + nodesToWrite, + out results, + out diagnosticInfos); + + // Validate the response + m_validateResponse(results, nodesToWrite); + + // Display the results. + m_output.WriteLine("Write Results :"); + + foreach (StatusCode writeResult in results) + { + m_output.WriteLine(" {0}", writeResult); + } + } + catch (Exception ex) + { + // Log Error + m_output.WriteLine($"Write Nodes Error : {ex.Message}."); + } + } + + /// + /// Browse Server nodes + /// + public void Browse() + { + if (m_session == null || m_session.Connected == false) + { + m_output.WriteLine("Session not connected!"); + return; + } + + try + { + // Create a Browser object + Browser browser = new Browser(m_session); + + // Set browse parameters + browser.BrowseDirection = BrowseDirection.Forward; + browser.NodeClassMask = (int)NodeClass.Object | (int)NodeClass.Variable; + browser.ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences; + + NodeId nodeToBrowse = ObjectIds.Server; + + // Call Browse service + m_output.WriteLine("Browsing {0} node...", nodeToBrowse); + ReferenceDescriptionCollection browseResults = browser.Browse(nodeToBrowse); + + // Display the results + m_output.WriteLine("Browse returned {0} results:", browseResults.Count); + + foreach (ReferenceDescription result in browseResults) + { + m_output.WriteLine(" DisplayName = {0}, NodeClass = {1}", result.DisplayName.Text, result.NodeClass); + } + } + catch (Exception ex) + { + // Log Error + m_output.WriteLine($"Browse Error : {ex.Message}."); + } + } + + /// + /// Call UA method + /// + public void CallMethod() + { + if (m_session == null || m_session.Connected == false) + { + m_output.WriteLine("Session not connected!"); + return; + } + + try + { + // Define the UA Method to call + // Parent node - Objects\CTT\Methods + // Method node - Objects\CTT\Methods\Add + NodeId objectId = new NodeId("ns=2;s=Methods"); + NodeId methodId = new NodeId("ns=2;s=Methods_Add"); + + // Define the method parameters + // Input argument requires a Float and an UInt32 value + object[] inputArguments = new object[] { (float)10.5, (uint)10 }; + IList outputArguments = null; + + // Invoke Call service + m_output.WriteLine("Calling UAMethod for node {0} ...", methodId); + outputArguments = m_session.Call(objectId, methodId, inputArguments); + + // Display results + m_output.WriteLine("Method call returned {0} output argument(s):", outputArguments.Count); + + foreach (var outputArgument in outputArguments) + { + m_output.WriteLine(" OutputValue = {0}", outputArgument.ToString()); + } + } + catch (Exception ex) + { + m_output.WriteLine("Method call error: {0}", ex.Message); + } + } + + /// + /// Create Subscription and MonitoredItems for DataChanges + /// + public void SubscribeToDataChanges() + { + if (m_session == null || m_session.Connected == false) + { + m_output.WriteLine("Session not connected!"); + return; + } + + try + { + // Create a subscription for receiving data change notifications + + // Define Subscription parameters + Subscription subscription = new Subscription(m_session.DefaultSubscription); + + subscription.DisplayName = "Console ReferenceClient Subscription"; + subscription.PublishingEnabled = true; + subscription.PublishingInterval = 1000; + + m_session.AddSubscription(subscription); + + // Create the subscription on Server side + subscription.Create(); + m_output.WriteLine("New Subscription created with SubscriptionId = {0}.", subscription.Id); + + // Create MonitoredItems for data changes (Reference Server) + + MonitoredItem intMonitoredItem = new MonitoredItem(subscription.DefaultItem); + // Int32 Node - Objects\CTT\Scalar\Simulation\Int32 + intMonitoredItem.StartNodeId = new NodeId("ns=2;s=Scalar_Simulation_Int32"); + intMonitoredItem.AttributeId = Attributes.Value; + intMonitoredItem.DisplayName = "Int32 Variable"; + intMonitoredItem.SamplingInterval = 1000; + intMonitoredItem.Notification += OnMonitoredItemNotification; + + subscription.AddItem(intMonitoredItem); + + MonitoredItem floatMonitoredItem = new MonitoredItem(subscription.DefaultItem); + // Float Node - Objects\CTT\Scalar\Simulation\Float + floatMonitoredItem.StartNodeId = new NodeId("ns=2;s=Scalar_Simulation_Float"); + floatMonitoredItem.AttributeId = Attributes.Value; + floatMonitoredItem.DisplayName = "Float Variable"; + floatMonitoredItem.SamplingInterval = 1000; + floatMonitoredItem.Notification += OnMonitoredItemNotification; + + subscription.AddItem(floatMonitoredItem); + + MonitoredItem stringMonitoredItem = new MonitoredItem(subscription.DefaultItem); + // String Node - Objects\CTT\Scalar\Simulation\String + stringMonitoredItem.StartNodeId = new NodeId("ns=2;s=Scalar_Simulation_String"); + stringMonitoredItem.AttributeId = Attributes.Value; + stringMonitoredItem.DisplayName = "String Variable"; + stringMonitoredItem.SamplingInterval = 1000; + stringMonitoredItem.Notification += OnMonitoredItemNotification; + + subscription.AddItem(stringMonitoredItem); + + // Create the monitored items on Server side + subscription.ApplyChanges(); + m_output.WriteLine("MonitoredItems created for SubscriptionId = {0}.", subscription.Id); + } + catch (Exception ex) + { + m_output.WriteLine("Subscribe error: {0}", ex.Message); + } + } + #endregion + + #region Private Methods + /// + /// Handle DataChange notifications from Server + /// + private void OnMonitoredItemNotification(MonitoredItem monitoredItem, MonitoredItemNotificationEventArgs e) + { + try + { + // Log MonitoredItem Notification event + MonitoredItemNotification notification = e.NotificationValue as MonitoredItemNotification; + m_output.WriteLine("Notification Received for Variable \"{0}\" and Value = {1}.", monitoredItem.DisplayName, notification.Value); + } + catch (Exception ex) + { + m_output.WriteLine("OnMonitoredItemNotification error: {0}", ex.Message); + } + } + + /// + /// Handles the certificate validation event. + /// This event is triggered every time an untrusted certificate is received from the server. + /// + private void CertificateValidation(CertificateValidator sender, CertificateValidationEventArgs e) + { + bool certificateAccepted = false; + + // **** + // Implement a custom logic to decide if the certificate should be + // accepted or not and set certificateAccepted flag accordingly. + // The certificate can be retrieved from the e.Certificate field + // *** + + ServiceResult error = e.Error; + m_output.WriteLine(error); + if (error.StatusCode == StatusCodes.BadCertificateUntrusted && AutoAccept) + { + certificateAccepted = true; + } + + if (certificateAccepted) + { + m_output.WriteLine("Untrusted Certificate accepted. Subject = {0}", e.Certificate.Subject); + e.Accept = true; + } + else + { + m_output.WriteLine("Untrusted Certificate rejected. Subject = {0}", e.Certificate.Subject); + } + } + #endregion + + #region Private Fields + private ApplicationConfiguration m_configuration; + private Session m_session; + private readonly TextWriter m_output; + private readonly Action m_validateResponse; + #endregion + } +} diff --git a/MP.MONO.ADAPTER.OPC2/dotnettrace.cmd b/MP.MONO.ADAPTER.OPC2/dotnettrace.cmd new file mode 100644 index 0000000..2d31776 --- /dev/null +++ b/MP.MONO.ADAPTER.OPC2/dotnettrace.cmd @@ -0,0 +1,3 @@ +REM collect a trace using the EventSource provider OPC-UA-Core +dotnet tool install --global dotnet-trace +dotnet-trace collect --name consolereferenceclient --providers OPC-UA-Core,OPC-UA-Client diff --git a/MP.MONO.ADAPTER.OPC2/targets.props b/MP.MONO.ADAPTER.OPC2/targets.props new file mode 100644 index 0000000..258a154 --- /dev/null +++ b/MP.MONO.ADAPTER.OPC2/targets.props @@ -0,0 +1,44 @@ + + + + + + + + + net6.0;netcoreapp3.1;net462 + net6.0 + net462;netcoreapp3.1;net6.0 + net462;netstandard2.0;netstandard2.1;net6.0 + net462;netstandard2.1;net6.0 + net462;netstandard2.0;netcoreapp3.1;net6.0 + + + + + + netcoreapp3.1;net462 + netcoreapp3.1 + net462;netcoreapp3.1 + net462;netstandard2.0;netstandard2.1 + net462;netstandard2.1 + net462;netstandard2.0;netcoreapp3.1 + + + + + + net462 + net462 + net462 + net462 + net462 + net462 + + + + diff --git a/MP.MONO.ALL.sln b/MP.MONO.ALL.sln index 3a89628..f19dea8 100644 --- a/MP.MONO.ALL.sln +++ b/MP.MONO.ALL.sln @@ -15,8 +15,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MP.MONO.DECODER", "MP.MONO. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MP.MONO.ANALYZER", "MP.MONO.ANALYZER\MP.MONO.ANALYZER.csproj", "{4C9BEAED-1A33-41A7-B9D6-7C173A43FDB8}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MP.MONO.ADAPTER", "MP.MONO.ADAPTER\MP.MONO.ADAPTER.csproj", "{873736BA-CDB6-4CE5-A340-6D904C11C07C}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -99,18 +97,6 @@ Global {4C9BEAED-1A33-41A7-B9D6-7C173A43FDB8}.Release|x64.Build.0 = Release|x64 {4C9BEAED-1A33-41A7-B9D6-7C173A43FDB8}.Release|x86.ActiveCfg = Release|x86 {4C9BEAED-1A33-41A7-B9D6-7C173A43FDB8}.Release|x86.Build.0 = Release|x86 - {873736BA-CDB6-4CE5-A340-6D904C11C07C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {873736BA-CDB6-4CE5-A340-6D904C11C07C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {873736BA-CDB6-4CE5-A340-6D904C11C07C}.Debug|x64.ActiveCfg = Debug|Any CPU - {873736BA-CDB6-4CE5-A340-6D904C11C07C}.Debug|x64.Build.0 = Debug|Any CPU - {873736BA-CDB6-4CE5-A340-6D904C11C07C}.Debug|x86.ActiveCfg = Debug|Any CPU - {873736BA-CDB6-4CE5-A340-6D904C11C07C}.Debug|x86.Build.0 = Debug|Any CPU - {873736BA-CDB6-4CE5-A340-6D904C11C07C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {873736BA-CDB6-4CE5-A340-6D904C11C07C}.Release|Any CPU.Build.0 = Release|Any CPU - {873736BA-CDB6-4CE5-A340-6D904C11C07C}.Release|x64.ActiveCfg = Release|Any CPU - {873736BA-CDB6-4CE5-A340-6D904C11C07C}.Release|x64.Build.0 = Release|Any CPU - {873736BA-CDB6-4CE5-A340-6D904C11C07C}.Release|x86.ActiveCfg = Release|Any CPU - {873736BA-CDB6-4CE5-A340-6D904C11C07C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/MP.MONO.UI/MP.MONO.UI.csproj b/MP.MONO.UI/MP.MONO.UI.csproj index a70cbff..ad0b53e 100644 --- a/MP.MONO.UI/MP.MONO.UI.csproj +++ b/MP.MONO.UI/MP.MONO.UI.csproj @@ -5,7 +5,7 @@ enable enable AnyCPU;x86;x64 - 1.12206.1319 + 1.12206.1411 diff --git a/MP.MONO.UI/Resources/ChangeLog.html b/MP.MONO.UI/Resources/ChangeLog.html index a7c264c..e755785 100644 --- a/MP.MONO.UI/Resources/ChangeLog.html +++ b/MP.MONO.UI/Resources/ChangeLog.html @@ -1,6 +1,6 @@ MAPO-MONO -

Version: 1.12206.1319

+

Version: 1.12206.1411


Release Note:
  • diff --git a/MP.MONO.UI/Resources/VersNum.txt b/MP.MONO.UI/Resources/VersNum.txt index 4ebdb53..b7e1283 100644 --- a/MP.MONO.UI/Resources/VersNum.txt +++ b/MP.MONO.UI/Resources/VersNum.txt @@ -1 +1 @@ -1.12206.1319 +1.12206.1411 diff --git a/MP.MONO.UI/Resources/manifest.xml b/MP.MONO.UI/Resources/manifest.xml index 3645212..d55cab2 100644 --- a/MP.MONO.UI/Resources/manifest.xml +++ b/MP.MONO.UI/Resources/manifest.xml @@ -1,6 +1,6 @@ - 1.12206.1319 + 1.12206.1411 http://nexus.steamware.net/repository/SWS/MP.MONO.UI/stable/LAST/MP.Mon.zip http://nexus.steamware.net/repository/SWS/MP.MONO.UI/stable/LAST/ChangeLog.html false