mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-22 09:37:12 +08:00
39dca2c103
Adds a complex type system client library extension for custom enumerations and structure types based on a server dictionary or DataTypeDefinition using emitted classes at runtime.
- Custom types are added to the session factory, not to the global factory. Types are loaded when the session connects by adding
` var complexTypeSystem = new ComplexTypeSystem(session);
await complexTypeSystem.Load();
`
- Custom type properties can be accessed with an accessor [propertyname] or [index]. Direct access through the properties is possible, but some cases do not update the optional or union flags correctly.
- Support includes V1.03 dictionaries and V1.04 DataTypeDefinitions (--> known issues).
- NetCoreComplexClient can be used for quick testing servers. It prints out all custom types with content and subscribes to all. Also includes sample how to write back complex type variables and how to output JSON.
- The UA Sample Client and the Quickstart Data Type client can be used for testing with UI. Many issues were adressed to be able to read and write complex types, however some support is limited, e.g. only editing simple fields is currently working (help wanted).
- Caveat: The complex type libaries for .Net Core must be built for .netcoreapp, so it needs a seperate nuget package for the complex type client extension.
- Support for a factory type builder class to allow to plugin other type builder methods (e.g. Codedom etc...)
- Added JSON test case and include the complex types client in the testing shell script, so the complex types lib is tested on mac/linux too.
- Few fixes in the JSON encoder.
Finding enough servers with custom types is an issue, so if you find servers with unsupported custom types please provide feedback.
Known limitations:
- Only V1.03 OPC binary dictionaries are supported (no XML Schema dictionaries).
- V1.03 dictionary structure definitions are only supported if they can be mapped to a V1.04 DataTypeDefinition. Nested types and cross dictionary refs should work.
- OptionSet is only partially implemented. Only shows the byte arrays, not the flags.
- Opaque Types in dictionaries are ignored.
581 lines
19 KiB
C#
581 lines
19 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.ComponentModel;
|
|
using System.Drawing;
|
|
using System.Windows.Forms;
|
|
using System.Threading.Tasks;
|
|
using Opc.Ua.Client.ComplexTypes;
|
|
|
|
namespace Opc.Ua.Client.Controls
|
|
{
|
|
/// <summary>
|
|
/// A tool bar used to connect to a server.
|
|
/// </summary>
|
|
public partial class ConnectServerCtrl : UserControl
|
|
{
|
|
#region Constructors
|
|
/// <summary>
|
|
/// Initializes the object.
|
|
/// </summary>
|
|
public ConnectServerCtrl()
|
|
{
|
|
InitializeComponent();
|
|
m_CertificateValidation = new CertificateValidationEventHandler(CertificateValidator_CertificateValidation);
|
|
}
|
|
#endregion
|
|
|
|
#region Private Fields
|
|
private ApplicationConfiguration m_configuration;
|
|
private Session m_session;
|
|
private int m_reconnectPeriod = 10;
|
|
private int m_discoverTimeout = 5000;
|
|
private SessionReconnectHandler m_reconnectHandler;
|
|
private CertificateValidationEventHandler m_CertificateValidation;
|
|
private EventHandler m_ReconnectComplete;
|
|
private EventHandler m_ReconnectStarting;
|
|
private EventHandler m_KeepAliveComplete;
|
|
private EventHandler m_ConnectComplete;
|
|
private StatusStrip m_StatusStrip;
|
|
private ToolStripItem m_ServerStatusLB;
|
|
private ToolStripItem m_StatusUpateTimeLB;
|
|
#endregion
|
|
|
|
#region Public Members
|
|
/// <summary>
|
|
/// A strip used to display session status information.
|
|
/// </summary>
|
|
public StatusStrip StatusStrip
|
|
{
|
|
get { return m_StatusStrip; }
|
|
|
|
set
|
|
{
|
|
if (!Object.ReferenceEquals(m_StatusStrip, value))
|
|
{
|
|
m_StatusStrip = value;
|
|
|
|
if (value != null)
|
|
{
|
|
m_ServerStatusLB = new ToolStripStatusLabel();
|
|
m_StatusUpateTimeLB = new ToolStripStatusLabel();
|
|
m_StatusStrip.Items.Add(m_ServerStatusLB);
|
|
m_StatusStrip.Items.Add(m_StatusUpateTimeLB);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A control that contains the last time a keep alive was returned from the server.
|
|
/// </summary>
|
|
public ToolStripItem ServerStatusControl { get { return m_ServerStatusLB; } set { m_ServerStatusLB = value; } }
|
|
|
|
/// <summary>
|
|
/// A control that contains the last time a keep alive was returned from the server.
|
|
/// </summary>
|
|
public ToolStripItem StatusUpateTimeControl { get { return m_StatusUpateTimeLB; } set { m_StatusUpateTimeLB = value; } }
|
|
|
|
/// <summary>
|
|
/// The name of the session to create.
|
|
/// </summary>
|
|
public string SessionName { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a flag indicating that the domain checks should be ignored when connecting.
|
|
/// </summary>
|
|
public bool DisableDomainCheck { get; set; }
|
|
|
|
/// <summary>
|
|
/// The URL displayed in the control.
|
|
/// </summary>
|
|
public string ServerUrl
|
|
{
|
|
get
|
|
{
|
|
if (UrlCB.SelectedIndex >= 0)
|
|
{
|
|
return (string)UrlCB.SelectedItem;
|
|
}
|
|
|
|
return UrlCB.Text;
|
|
}
|
|
|
|
set
|
|
{
|
|
UrlCB.SelectedIndex = -1;
|
|
UrlCB.Text = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether to use security when connecting.
|
|
/// </summary>
|
|
public bool UseSecurity
|
|
{
|
|
get { return UseSecurityCK.Checked; }
|
|
set { UseSecurityCK.Checked = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The locales to use when creating the session.
|
|
/// </summary>
|
|
public string[] PreferredLocales { get; set; }
|
|
|
|
/// <summary>
|
|
/// The user identity to use when creating the session.
|
|
/// </summary>
|
|
public IUserIdentity UserIdentity { get; set; }
|
|
|
|
/// <summary>
|
|
/// The client application configuration.
|
|
/// </summary>
|
|
public ApplicationConfiguration Configuration
|
|
{
|
|
get { return m_configuration; }
|
|
|
|
set
|
|
{
|
|
if (!Object.ReferenceEquals(m_configuration, value))
|
|
{
|
|
if (m_configuration != null)
|
|
{
|
|
m_configuration.CertificateValidator.CertificateValidation -= m_CertificateValidation;
|
|
}
|
|
|
|
m_configuration = value;
|
|
|
|
if (m_configuration != null)
|
|
{
|
|
m_configuration.CertificateValidator.CertificateValidation += m_CertificateValidation;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The currently active session.
|
|
/// </summary>
|
|
public Session Session
|
|
{
|
|
get { return m_session; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The number of seconds between reconnect attempts (0 means reconnect is disabled).
|
|
/// </summary>
|
|
[DefaultValue(10)]
|
|
public int ReconnectPeriod
|
|
{
|
|
get { return m_reconnectPeriod; }
|
|
set { m_reconnectPeriod = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raised when a good keep alive from the server arrives.
|
|
/// </summary>
|
|
public event EventHandler KeepAliveComplete
|
|
{
|
|
add { m_KeepAliveComplete += value; }
|
|
remove { m_KeepAliveComplete -= value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raised when a reconnect operation starts.
|
|
/// </summary>
|
|
public event EventHandler ReconnectStarting
|
|
{
|
|
add { m_ReconnectStarting += value; }
|
|
remove { m_ReconnectStarting -= value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raised when a reconnect operation completes.
|
|
/// </summary>
|
|
public event EventHandler ReconnectComplete
|
|
{
|
|
add { m_ReconnectComplete += value; }
|
|
remove { m_ReconnectComplete -= value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raised after successfully connecting to or disconnecing from a server.
|
|
/// </summary>
|
|
public event EventHandler ConnectComplete
|
|
{
|
|
add { m_ConnectComplete += value; }
|
|
remove { m_ConnectComplete -= value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the URLs shown in the control.
|
|
/// </summary>
|
|
public void SetAvailableUrls(IList<string> urls)
|
|
{
|
|
UrlCB.Items.Clear();
|
|
|
|
if (urls != null)
|
|
{
|
|
foreach (string url in urls)
|
|
{
|
|
int index = url.LastIndexOf("/discovery", StringComparison.InvariantCultureIgnoreCase);
|
|
|
|
if (index != -1)
|
|
{
|
|
UrlCB.Items.Add(url.Substring(0, index));
|
|
continue;
|
|
}
|
|
|
|
UrlCB.Items.Add(url);
|
|
}
|
|
|
|
if (UrlCB.Items.Count > 0)
|
|
{
|
|
UrlCB.SelectedIndex = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new session.
|
|
/// </summary>
|
|
/// <returns>The new session object.</returns>
|
|
public async Task<Session> Connect()
|
|
{
|
|
// disconnect from existing session.
|
|
Disconnect();
|
|
|
|
// determine the URL that was selected.
|
|
string serverUrl = UrlCB.Text;
|
|
|
|
if (UrlCB.SelectedIndex >= 0)
|
|
{
|
|
serverUrl = (string)UrlCB.SelectedItem;
|
|
}
|
|
|
|
if (m_configuration == null)
|
|
{
|
|
throw new ArgumentNullException("m_configuration");
|
|
}
|
|
|
|
// select the best endpoint.
|
|
EndpointDescription endpointDescription = CoreClientUtils.SelectEndpoint(serverUrl, UseSecurityCK.Checked, m_discoverTimeout);
|
|
|
|
EndpointConfiguration endpointConfiguration = EndpointConfiguration.Create(m_configuration);
|
|
ConfiguredEndpoint endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration);
|
|
|
|
m_session = await Session.Create(
|
|
m_configuration,
|
|
endpoint,
|
|
false,
|
|
!DisableDomainCheck,
|
|
(String.IsNullOrEmpty(SessionName)) ? m_configuration.ApplicationName : SessionName,
|
|
60000,
|
|
UserIdentity,
|
|
PreferredLocales);
|
|
|
|
// set up keep alive callback.
|
|
m_session.KeepAlive += new KeepAliveEventHandler(Session_KeepAlive);
|
|
|
|
// raise an event.
|
|
DoConnectComplete(null);
|
|
|
|
var typeSystemLoader = new ComplexTypeSystem(m_session);
|
|
await typeSystemLoader.Load();
|
|
|
|
// return the new session.
|
|
return m_session;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new session.
|
|
/// </summary>
|
|
/// <param name="serverUrl">The URL of a server endpoint.</param>
|
|
/// <param name="useSecurity">Whether to use security.</param>
|
|
/// <returns>The new session object.</returns>
|
|
public async Task<Session> Connect(string serverUrl, bool useSecurity)
|
|
{
|
|
UrlCB.Text = serverUrl;
|
|
UseSecurityCK.Checked = useSecurity;
|
|
return await Connect();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disconnects from the server.
|
|
/// </summary>
|
|
public void Disconnect()
|
|
{
|
|
UpdateStatus(false, DateTime.UtcNow, "Disconnected");
|
|
|
|
// stop any reconnect operation.
|
|
if (m_reconnectHandler != null)
|
|
{
|
|
m_reconnectHandler.Dispose();
|
|
m_reconnectHandler = null;
|
|
}
|
|
|
|
// disconnect any existing session.
|
|
if (m_session != null)
|
|
{
|
|
m_session.Close(10000);
|
|
m_session = null;
|
|
}
|
|
|
|
// raise an event.
|
|
DoConnectComplete(null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prompts the user to choose a server on another host.
|
|
/// </summary>
|
|
public void Discover(string hostName)
|
|
{
|
|
string endpointUrl = new DiscoverServerDlg().ShowDialog(m_configuration, hostName);
|
|
|
|
if (endpointUrl != null)
|
|
{
|
|
ServerUrl = endpointUrl;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Private Methods
|
|
/// <summary>
|
|
/// Raises the connect complete event on the main GUI thread.
|
|
/// </summary>
|
|
private void DoConnectComplete(object state)
|
|
{
|
|
if (m_ConnectComplete != null)
|
|
{
|
|
if (this.InvokeRequired)
|
|
{
|
|
this.BeginInvoke(new System.Threading.WaitCallback(DoConnectComplete), state);
|
|
return;
|
|
}
|
|
|
|
m_ConnectComplete(this, null);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds the endpoint that best matches the current settings.
|
|
/// </summary>
|
|
private EndpointDescription SelectEndpoint()
|
|
{
|
|
try
|
|
{
|
|
Cursor = Cursors.WaitCursor;
|
|
|
|
// determine the URL that was selected.
|
|
string discoveryUrl = UrlCB.Text;
|
|
|
|
if (UrlCB.SelectedIndex >= 0)
|
|
{
|
|
discoveryUrl = (string)UrlCB.SelectedItem;
|
|
}
|
|
|
|
// return the selected endpoint.
|
|
return CoreClientUtils.SelectEndpoint(discoveryUrl, UseSecurityCK.Checked, m_discoverTimeout);
|
|
}
|
|
finally
|
|
{
|
|
Cursor = Cursors.Default;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Event Handlers
|
|
/// <summary>
|
|
/// Updates the status control.
|
|
/// </summary>
|
|
/// <param name="error">Whether the status represents an error.</param>
|
|
/// <param name="time">The time associated with the status.</param>
|
|
/// <param name="status">The status message.</param>
|
|
/// <param name="args">Arguments used to format the status message.</param>
|
|
private void UpdateStatus(bool error, DateTime time, string status, params object[] args)
|
|
{
|
|
if (m_ServerStatusLB != null)
|
|
{
|
|
m_ServerStatusLB.Text = String.Format(status, args);
|
|
m_ServerStatusLB.ForeColor = (error) ? Color.Red : Color.Empty;
|
|
}
|
|
|
|
if (m_StatusUpateTimeLB != null)
|
|
{
|
|
m_StatusUpateTimeLB.Text = time.ToLocalTime().ToString("hh:mm:ss");
|
|
m_StatusUpateTimeLB.ForeColor = (error) ? Color.Red : Color.Empty;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles a keep alive event from a session.
|
|
/// </summary>
|
|
private void Session_KeepAlive(Session session, KeepAliveEventArgs e)
|
|
{
|
|
if (this.InvokeRequired)
|
|
{
|
|
this.BeginInvoke(new KeepAliveEventHandler(Session_KeepAlive), session, e);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
// check for events from discarded sessions.
|
|
if (!Object.ReferenceEquals(session, m_session))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// start reconnect sequence on communication error.
|
|
if (ServiceResult.IsBad(e.Status))
|
|
{
|
|
if (m_reconnectPeriod <= 0)
|
|
{
|
|
UpdateStatus(true, e.CurrentTime, "Communication Error ({0})", e.Status);
|
|
return;
|
|
}
|
|
|
|
UpdateStatus(true, e.CurrentTime, "Reconnecting in {0}s", m_reconnectPeriod);
|
|
|
|
if (m_reconnectHandler == null)
|
|
{
|
|
if (m_ReconnectStarting != null)
|
|
{
|
|
m_ReconnectStarting(this, e);
|
|
}
|
|
|
|
m_reconnectHandler = new SessionReconnectHandler();
|
|
m_reconnectHandler.BeginReconnect(m_session, m_reconnectPeriod * 1000, Server_ReconnectComplete);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// update status.
|
|
UpdateStatus(false, e.CurrentTime, "Connected [{0}]", session.Endpoint.EndpointUrl);
|
|
|
|
// raise any additional notifications.
|
|
if (m_KeepAliveComplete != null)
|
|
{
|
|
m_KeepAliveComplete(this, e);
|
|
}
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
ClientUtils.HandleException(this.Text, exception);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles a click on the connect button.
|
|
/// </summary>
|
|
private async void Server_ConnectMI_Click(object sender, EventArgs e)
|
|
{
|
|
try
|
|
{
|
|
await Connect();
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
ClientUtils.HandleException(this.Text, exception);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles a reconnect event complete from the reconnect handler.
|
|
/// </summary>
|
|
private void Server_ReconnectComplete(object sender, EventArgs e)
|
|
{
|
|
if (this.InvokeRequired)
|
|
{
|
|
this.BeginInvoke(new EventHandler(Server_ReconnectComplete), sender, e);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
// ignore callbacks from discarded objects.
|
|
if (!Object.ReferenceEquals(sender, m_reconnectHandler))
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_session = m_reconnectHandler.Session;
|
|
m_reconnectHandler.Dispose();
|
|
m_reconnectHandler = null;
|
|
|
|
// raise any additional notifications.
|
|
if (m_ReconnectComplete != null)
|
|
{
|
|
m_ReconnectComplete(this, e);
|
|
}
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
ClientUtils.HandleException(this.Text, exception);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles a certificate validation error.
|
|
/// </summary>
|
|
private void CertificateValidator_CertificateValidation(CertificateValidator sender, CertificateValidationEventArgs e)
|
|
{
|
|
if (this.InvokeRequired)
|
|
{
|
|
this.Invoke(new CertificateValidationEventHandler(CertificateValidator_CertificateValidation), sender, e);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
e.Accept = m_configuration.SecurityConfiguration.AutoAcceptUntrustedCertificates;
|
|
|
|
if (!m_configuration.SecurityConfiguration.AutoAcceptUntrustedCertificates)
|
|
{
|
|
DialogResult result = MessageBox.Show(
|
|
e.Certificate.Subject,
|
|
"Untrusted Certificate",
|
|
MessageBoxButtons.YesNo,
|
|
MessageBoxIcon.Warning);
|
|
|
|
e.Accept = (result == DialogResult.Yes);
|
|
}
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
ClientUtils.HandleException(this.Text, exception);
|
|
}
|
|
}
|
|
#endregion
|
|
}
|
|
}
|