Files
UA-.NETStandard/SampleApplications/Samples/NetCoreComplexClient/Program.cs
T
Martin Regen 1ff1ae7e52 IOP 2020: Workshop fixes (#916)
- fix to validate structuredefinition if servers do not fill out the DataTypeDefinition correctly
- The GDS creates the CRL cert with its own CA serial number, invalidating the CA by default on some stacks
- complex structuretype must be virtual, non reversible union encoding could not be detected 
- fix binary schema validation, allow the \" character in a qualified name
- support use of simple subtypes in a complex type. For custom and standard subtypes.
- only support structure objects with valuerank scalar or onedimension
2020-03-18 18:49:53 +01:00

696 lines
28 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2019 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.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Mono.Options;
using Newtonsoft.Json;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Client.ComplexTypes;
using Opc.Ua.Configuration;
namespace NetCoreConsoleClient
{
public enum ExitCode : int
{
Ok = 0,
ErrorCreateApplication = 0x11,
ErrorDiscoverEndpoints = 0x12,
ErrorCreateSession = 0x13,
ErrorBrowseNamespace = 0x14,
ErrorCreateSubscription = 0x15,
ErrorMonitoredItem = 0x16,
ErrorAddSubscription = 0x17,
ErrorRunning = 0x18,
ErrorLoadTypeDictionary = 0x19,
ErrorReadComplexTypes = 0x1a,
ErrorJSONDecode = 0x1b,
ErrorNoKeepAlive = 0x30,
ErrorInvalidCommandLine = 0x100
};
public class Program
{
public static int Main(string[] args)
{
Console.WriteLine(".Net Core OPC UA Complex Types Client sample");
// command line options
bool showHelp = false;
int stopTimeout = Timeout.Infinite;
bool autoAccept = false;
bool writeComplexInt = false;
bool noTypes = false;
bool noBrowse = false;
bool verbose = false;
bool json = false;
bool jsonReversible = false;
string username = null;
string pw = null;
Mono.Options.OptionSet options = new Mono.Options.OptionSet {
{ "h|help", "show this message and exit", h => showHelp = h != null },
{ "a|autoaccept", "auto accept certificates (for testing only)", a => autoAccept = a != null },
{ "t|timeout=", "the number of seconds until the client stops.", (int t) => stopTimeout = t },
{ "w|writeint", "Read and increment all complex types with an Int32.", w => writeComplexInt = w != null},
{ "n|notypes", "Do not load the type system dictionary from the server.", n => noTypes = n != null},
{ "b|nobrowse", "Do not browse the address space of the server.", n => noBrowse = n != null},
{ "u|username=", "Username to access server.", (string n) => username = n},
{ "p|password=", "Password to access server.", (string n) => pw = n},
{ "v|verbose", "Verbose output.", v => verbose = v != null},
{ "j|json", "Print custom nodes as Json.", j => json = j != null},
{ "r|reversible", "Use Json reversible encoding.", r => jsonReversible = r != null},
};
IList<string> extraArgs = null;
try
{
extraArgs = options.Parse(args);
if (extraArgs.Count > 1)
{
foreach (string extraArg in extraArgs)
{
Console.WriteLine("Error: Unknown option: {0}", extraArg);
showHelp = true;
}
}
}
catch (OptionException e)
{
Console.WriteLine(e.Message);
showHelp = true;
}
if (showHelp)
{
// show some app description message
Console.WriteLine("Usage: dotnet NetCoreConsoleClient.dll [OPTIONS] [ENDPOINTURL]");
Console.WriteLine();
// output the options
Console.WriteLine("Options:");
options.WriteOptionDescriptions(Console.Out);
return (int)ExitCode.ErrorInvalidCommandLine;
}
string endpointURL;
if (extraArgs.Count == 0)
{
// use OPC UA .Net Sample server
endpointURL = "opc.tcp://localhost:51210/UA/SampleServer";
}
else
{
endpointURL = extraArgs[0];
}
MySampleClient client = new MySampleClient(endpointURL, autoAccept, stopTimeout) {
Verbose = verbose,
LoadTypeSystem = !noTypes,
BrowseAdddressSpace = !noBrowse,
WriteComplexInt = writeComplexInt,
PrintAsJson = json,
JsonReversible = jsonReversible,
Username = username,
Password = pw
};
return (int)client.Run();
}
}
public class MySampleClient
{
public int ReconnectPeriod { get; set; } = 10;
public ExitCode ExitCode { get; set; }
public bool Verbose { get; set; } = false;
public bool WriteComplexInt { get; set; } = false;
public bool PrintAsJson { get; set; } = false;
public bool JsonReversible { get; set; } = false;
public bool LoadTypeSystem { get; set; } = false;
public bool BrowseAdddressSpace { get; set; } = false;
public String Username { get; set; }
public String Password { get; set; }
public MySampleClient(
string endpointURL,
bool autoAccept,
int stopTimeout)
{
m_endpointURL = endpointURL;
m_autoAccept = autoAccept;
m_clientRunTime = stopTimeout <= 0 ? Timeout.Infinite : stopTimeout * 1000;
}
public ExitCode Run()
{
Session session;
try
{
session = ConsoleSampleClient().Result;
}
catch (Exception ex)
{
Utils.Trace("ServiceResultException:" + ex.Message);
Console.WriteLine("Exception: {0}", ex.Message);
return ExitCode;
}
ManualResetEvent quitEvent = new ManualResetEvent(false);
try
{
Console.CancelKeyPress += (sender, eArgs) => {
quitEvent.Set();
eArgs.Cancel = true;
};
}
catch
{
}
// Test the session reconnecthandler
bool eventResult = quitEvent.WaitOne(5000);
if (!eventResult)
{
Console.WriteLine(" --- Start simulated reconnect... --- ");
m_reconnectHandler = new SessionReconnectHandler();
m_reconnectHandler.BeginReconnect(session, 1000, Client_ReconnectComplete);
}
// wait for timeout or Ctrl-C
quitEvent.WaitOne(m_clientRunTime);
// return error conditions
if (session.KeepAliveStopped)
{
ExitCode = ExitCode.ErrorNoKeepAlive;
return ExitCode;
}
ExitCode = ExitCode.Ok;
return ExitCode;
}
private async Task<Session> ConsoleSampleClient()
{
Console.WriteLine("1 - Create an Application Configuration.");
ExitCode = ExitCode.ErrorCreateApplication;
ApplicationInstance application = new ApplicationInstance {
ApplicationName = "UA Core Complex Client",
ApplicationType = ApplicationType.Client,
ConfigSectionName = "Opc.Ua.ComplexClient"
};
// load the application configuration.
ApplicationConfiguration config = await application.LoadApplicationConfiguration(false).ConfigureAwait(false);
// check the application certificate.
bool haveAppCertificate = await application.CheckApplicationInstanceCertificate(false, 0).ConfigureAwait(false);
if (!haveAppCertificate)
{
throw new Exception("Application instance certificate invalid!");
}
if (haveAppCertificate)
{
config.ApplicationUri = Utils.GetApplicationUriFromCertificate(config.SecurityConfiguration.ApplicationCertificate.Certificate);
if (config.SecurityConfiguration.AutoAcceptUntrustedCertificates)
{
m_autoAccept = true;
}
config.CertificateValidator.CertificateValidation += new CertificateValidationEventHandler(CertificateValidator_CertificateValidation);
}
else
{
Console.WriteLine(" WARN: missing application certificate, using unsecure connection.");
}
Console.WriteLine("2 - Discover endpoints of {0}.", m_endpointURL);
ExitCode = ExitCode.ErrorDiscoverEndpoints;
var selectedEndpoint = CoreClientUtils.SelectEndpoint(m_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.");
ExitCode = ExitCode.ErrorCreateSession;
// create the user identity
UserIdentity userIdentity;
if (String.IsNullOrEmpty(Username) && String.IsNullOrEmpty(Password))
{
userIdentity = new UserIdentity(new AnonymousIdentityToken());
}
else
{
userIdentity = new UserIdentity(Username, Password);
}
// create worker session
m_session = await CreateSession(config, selectedEndpoint, userIdentity).ConfigureAwait(false);
// register keep alive handler
m_session.KeepAlive += Client_KeepAlive;
Console.WriteLine("4 - Browse for all custom type variables.");
ExitCode = ExitCode.ErrorReadComplexTypes;
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
var allVariableNodes = BrowseAdddressSpace ? BrowseAllVariables() : new List<INode>();
var allCustomTypeVariables = allVariableNodes.Where(n => ((VariableNode)n).DataType.NamespaceIndex != 0).ToList();
stopWatch.Stop();
Console.WriteLine($" -- Browse all nodes took {stopWatch.ElapsedMilliseconds}ms.");
Console.WriteLine($" -- Browsed {allVariableNodes.Count} nodes, from which {allCustomTypeVariables.Count} are custom type variables.");
stopWatch.Reset();
// for testing clear the nodecache
m_session.NodeCache.Clear();
stopWatch.Start();
if (LoadTypeSystem)
{
Console.WriteLine("5 - Load the server type dictionary.");
ExitCode = ExitCode.ErrorLoadTypeDictionary;
stopWatch.Reset();
stopWatch.Start();
var complexTypeSystem = new ComplexTypeSystem(m_session);
await complexTypeSystem.Load().ConfigureAwait(false);
stopWatch.Stop();
Console.WriteLine($"Load type system took {stopWatch.ElapsedMilliseconds}ms.");
Console.WriteLine($"Custom types defined for this session:");
foreach (var type in complexTypeSystem.GetDefinedTypes())
{
Console.WriteLine($"{type.Namespace}.{type.Name}");
}
Console.WriteLine($"Loaded {m_session.DataTypeSystem.Count} dictionaries:");
foreach (var dictionary in m_session.DataTypeSystem)
{
Console.WriteLine($" + {dictionary.Value.Name}");
foreach (var type in dictionary.Value.DataTypes)
{
Console.WriteLine($" -- {type.Key}:{type.Value}");
}
}
}
else
{
Console.WriteLine("4 - Not loading the server type dictionary.");
}
foreach (VariableNode variableNode in allCustomTypeVariables)
{
try
{
var value = m_session.ReadValue(variableNode.NodeId);
CastInt32ToEnum(variableNode, value);
Console.WriteLine($" -- {variableNode}:{value}");
if (value.Value is ExtensionObject extensionObject)
{
if (extensionObject.Body is BaseComplexType complexType)
{
foreach (var item in complexType.GetPropertyEnumerator())
{
if (Verbose)
{
Console.WriteLine($" -- -- {item.Name}:{complexType[item.Name]}");
}
if (WriteComplexInt && item.PropertyType == typeof(Int32))
{
var data = complexType[item.Name];
if (data != null)
{
complexType[item.Name] = (Int32)data + 1;
}
Console.WriteLine($" -- -- Increment: {item.Name}, {complexType[item.Name]}");
WriteValue(m_session, variableNode.NodeId, value);
}
}
}
}
if (PrintAsJson)
{
PrintValueAsJson(variableNode.BrowseName.Name, value);
}
}
catch (ServiceResultException sre)
{
if (sre.StatusCode == StatusCodes.BadUserAccessDenied)
{
Console.WriteLine($" -- {variableNode}: Access denied!");
}
}
}
Console.WriteLine("6 - Create test sessions which load only single types as needed.");
if (LoadTypeSystem)
{
foreach (VariableNode variableNode in allCustomTypeVariables)
{
Session testSession = null;
try
{
Console.WriteLine($"Open session for {variableNode}:");
testSession = await CreateSession(config, selectedEndpoint, userIdentity).ConfigureAwait(false);
var complexTypeSystem = new ComplexTypeSystem(testSession);
NodeId dataType = variableNode.DataType;
Type nullType = testSession.Factory.GetSystemType(dataType);
var valueBefore = testSession.ReadValue(variableNode.NodeId);
Console.WriteLine($" -- {valueBefore}");
Type systemType = await complexTypeSystem.LoadType(dataType).ConfigureAwait(false);
var valueAfter = testSession.ReadValue(variableNode.NodeId);
Console.WriteLine($" -- {variableNode}: {systemType} {dataType}");
Console.WriteLine($" -- {valueAfter}");
Console.WriteLine($"Custom types defined for {variableNode}:");
foreach (var type in complexTypeSystem.GetDefinedTypes())
{
Console.WriteLine($" -- {type.Namespace}.{type.Name}");
}
}
catch (ServiceResultException sre)
{
if (sre.StatusCode == StatusCodes.BadUserAccessDenied)
{
Console.WriteLine($" -- {variableNode}: Access denied!");
}
}
finally
{
testSession?.Close();
}
}
}
else
{
Console.WriteLine("6 - Not testing to load individual types.");
}
Console.WriteLine("7 - Create a subscription with publishing interval of 1 second.");
ExitCode = ExitCode.ErrorCreateSubscription;
var subscription = new Subscription(m_session.DefaultSubscription) { PublishingInterval = 1000 };
Console.WriteLine("8 - Add all custom values and the server time to the subscription.");
ExitCode = ExitCode.ErrorMonitoredItem;
var list = new List<MonitoredItem> {
new MonitoredItem(subscription.DefaultItem)
{
DisplayName = "ServerStatusCurrentTime", StartNodeId = "i="+Variables.Server_ServerStatus_CurrentTime.ToString()
}
};
list.ForEach(i => i.Notification += OnNotification);
foreach (var customVariable in allCustomTypeVariables)
{
var newItem = new MonitoredItem(subscription.DefaultItem) {
DisplayName = customVariable.DisplayName.Text,
StartNodeId = ExpandedNodeId.ToNodeId(customVariable.NodeId, m_session.NamespaceUris)
};
newItem.Notification += OnComplexTypeNotification;
list.Add(newItem);
}
subscription.AddItems(list);
Console.WriteLine("9 - Add the subscription to the session.");
ExitCode = ExitCode.ErrorAddSubscription;
m_session.AddSubscription(subscription);
subscription.Create();
Console.WriteLine("10 - Running...Press Ctrl-C to exit...");
ExitCode = ExitCode.ErrorRunning;
return m_session;
}
/// <summary>
/// Browse all variables in the objects folder.
/// </summary>
private IList<INode> BrowseAllVariables()
{
var result = new List<INode>();
var nodesToBrowse = new ExpandedNodeIdCollection {
ObjectIds.ObjectsFolder
};
while (nodesToBrowse.Count > 0)
{
var nextNodesToBrowse = new ExpandedNodeIdCollection();
foreach (var node in nodesToBrowse)
{
try
{
var organizers = m_session.NodeCache.FindReferences(
node,
ReferenceTypeIds.Organizes,
false,
false);
var components = m_session.NodeCache.FindReferences(
node,
ReferenceTypeIds.HasComponent,
false,
false);
var properties = m_session.NodeCache.FindReferences(
node,
ReferenceTypeIds.HasProperty,
false,
false);
nextNodesToBrowse.AddRange(organizers
.Where(n => n is ObjectNode)
.Select(n => n.NodeId).ToList());
nextNodesToBrowse.AddRange(components
.Where(n => n is ObjectNode)
.Select(n => n.NodeId).ToList());
result.AddRange(organizers.Where(n => n is VariableNode));
result.AddRange(components.Where(n => n is VariableNode));
result.AddRange(properties.Where(n => n is VariableNode));
}
catch (ServiceResultException sre)
{
if (sre.StatusCode == StatusCodes.BadUserAccessDenied)
{
Console.WriteLine($"Access denied: Skip node {node}.");
}
}
}
nodesToBrowse = nextNodesToBrowse;
}
return result;
}
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 (m_reconnectHandler == null)
{
Console.WriteLine("--- RECONNECTING ---");
m_reconnectHandler = new SessionReconnectHandler();
m_reconnectHandler.BeginReconnect(sender, ReconnectPeriod * 1000, Client_ReconnectComplete);
}
}
}
private void Client_ReconnectComplete(object sender, EventArgs e)
{
// ignore callbacks from discarded objects.
if (!Object.ReferenceEquals(sender, m_reconnectHandler))
{
return;
}
m_session = m_reconnectHandler.Session;
m_reconnectHandler.Dispose();
m_reconnectHandler = null;
Console.WriteLine("--- RECONNECTED ---");
}
/// <summary>
/// Helper to cast a enumeration node value to an enumeration type.
/// </summary>
private void CastInt32ToEnum(VariableNode variableNode, DataValue value)
{
if (value.Value?.GetType() == typeof(Int32))
{
// test if this is an enum datatype?
Type systemType = m_session.Factory.GetSystemType(
NodeId.ToExpandedNodeId(variableNode.DataType, m_session.NamespaceUris)
);
if (systemType != null)
{
value.Value = Enum.ToObject(systemType, value.Value);
}
}
}
private Task<Session> CreateSession(ApplicationConfiguration config, EndpointDescription selectedEndpoint, IUserIdentity userIdentity)
{
var endpointConfiguration = EndpointConfiguration.Create(config);
var endpoint = new ConfiguredEndpoint(null, selectedEndpoint, endpointConfiguration);
return Session.Create(config, endpoint, false, "OPC UA Complex Types Client", 60000, userIdentity, null);
}
private void WriteValue(Session session, NodeId variableId, DataValue value)
{
WriteValue nodeToWrite = new WriteValue {
NodeId = variableId,
AttributeId = Attributes.Value,
Value = new DataValue {
WrappedValue = value.WrappedValue
}
};
WriteValueCollection nodesToWrite = new WriteValueCollection {
nodeToWrite
};
// read the attributes.
StatusCodeCollection results = null;
DiagnosticInfoCollection diagnosticInfos = null;
ResponseHeader responseHeader = session.Write(
null,
nodesToWrite,
out results,
out diagnosticInfos);
ClientBase.ValidateResponse(results, nodesToWrite);
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToWrite);
// check for error.
if (StatusCode.IsBad(results[0]))
{
throw ServiceResultException.Create(results[0], 0, diagnosticInfos, responseHeader.StringTable);
}
}
private void PrintValueAsJson(string name, DataValue value)
{
var jsonEncoder = new JsonEncoder(m_session.MessageContext, JsonReversible);
jsonEncoder.WriteDataValue(name, value);
var textbuffer = jsonEncoder.CloseAndReturnText();
// prettify
using (var stringWriter = new StringWriter())
{
try
{
using (var stringReader = new StringReader(textbuffer))
{
var jsonReader = new JsonTextReader(stringReader);
var jsonWriter = new JsonTextWriter(stringWriter) {
Formatting = Formatting.Indented,
Culture = CultureInfo.InvariantCulture
};
jsonWriter.WriteToken(jsonReader);
Console.WriteLine(stringWriter.ToString());
}
}
catch (Exception ex)
{
Console.WriteLine("Failed to format the JSON output:", ex.Message);
Console.WriteLine(textbuffer);
Console.WriteLine(stringWriter.ToString());
ExitCode = ExitCode.ErrorJSONDecode;
throw ;
}
}
}
private void OnNotification(MonitoredItem item, MonitoredItemNotificationEventArgs e)
{
foreach (var value in item.DequeueValues())
{
Console.WriteLine("{0}: {1}, {2}, {3}", item.DisplayName, value.Value, value.SourceTimestamp, value.StatusCode);
}
}
private void OnComplexTypeNotification(MonitoredItem item, MonitoredItemNotificationEventArgs e)
{
foreach (var value in item.DequeueValues())
{
Console.WriteLine("{0}: {1}, {2}", item.DisplayName, value.SourceTimestamp, value.StatusCode);
if (Verbose)
{
Console.WriteLine(value);
}
else if (PrintAsJson)
{
PrintValueAsJson(item.DisplayName, value);
}
}
}
private void CertificateValidator_CertificateValidation(CertificateValidator validator, CertificateValidationEventArgs e)
{
if (e.Error.StatusCode == StatusCodes.BadCertificateUntrusted)
{
e.Accept = m_autoAccept;
if (m_autoAccept)
{
Console.WriteLine("Accepted Certificate: {0}", e.Certificate.Subject);
}
else
{
Console.WriteLine("Rejected Certificate: {0}", e.Certificate.Subject);
}
}
}
private Session m_session;
private SessionReconnectHandler m_reconnectHandler;
private string m_endpointURL;
private bool m_autoAccept = false;
private int m_clientRunTime = Timeout.Infinite;
}
}