313 lines
11 KiB
C#
313 lines
11 KiB
C#
using System.Diagnostics;
|
|
using Microsoft.Extensions.Logging;
|
|
using Opc.Ua;
|
|
using Opc.Ua.Client;
|
|
using RobotNet10.Script.IO;
|
|
|
|
namespace RobotNet10.ScriptEngine.IO;
|
|
|
|
/// <summary>
|
|
/// Implementation of OPC UA connection using OPCFoundation.NetStandard.Opc.Ua library.
|
|
/// </summary>
|
|
public class OpcUaConnection : IOpcUaConnection
|
|
{
|
|
private Session? _session;
|
|
private SessionReconnectHandler? _reconnectHandler;
|
|
private bool _disposed;
|
|
|
|
public string EndpointUrl { get; }
|
|
public bool IsConnected { get; private set; }
|
|
|
|
public OpcUaConnection(string endpointUrl)
|
|
{
|
|
EndpointUrl = endpointUrl;
|
|
IsConnected = false;
|
|
}
|
|
|
|
public async Task ConnectAsync()
|
|
{
|
|
await ConnectAsync(string.Empty, string.Empty);
|
|
}
|
|
|
|
public async Task ConnectAsync(string username, string password)
|
|
{
|
|
if (IsConnected)
|
|
return;
|
|
|
|
try
|
|
{
|
|
var applicationConfiguration = new ApplicationConfiguration
|
|
{
|
|
ApplicationName = "RobotNet10 ScriptEngine",
|
|
ApplicationUri = Utils.Format(@"urn:{0}:ScriptEngine", System.Net.Dns.GetHostName()),
|
|
ApplicationType = ApplicationType.Client,
|
|
SecurityConfiguration = new SecurityConfiguration
|
|
{
|
|
ApplicationCertificate = new CertificateIdentifier { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\MachineDefault" },
|
|
TrustedIssuerCertificates = new CertificateTrustList { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\UA Certificate Authorities" },
|
|
TrustedPeerCertificates = new CertificateTrustList { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\UA Applications" },
|
|
RejectedCertificateStore = new CertificateTrustList { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\RejectedCertificates" },
|
|
AutoAcceptUntrustedCertificates = true,
|
|
RejectSHA1SignedCertificates = false
|
|
},
|
|
TransportConfigurations = new TransportConfigurationCollection(),
|
|
ClientConfiguration = new ClientConfiguration
|
|
{
|
|
DefaultSessionTimeout = 60000
|
|
}
|
|
};
|
|
|
|
await applicationConfiguration.ValidateAsync(ApplicationType.Client);
|
|
|
|
// Discover endpoints first using new API
|
|
var endpointUrl = new Uri(EndpointUrl);
|
|
var telemetryContext = new SimpleTelemetryContext();
|
|
var discoveryClient = await DiscoveryClient.CreateAsync(applicationConfiguration, endpointUrl, DiagnosticsMasks.All, CancellationToken.None);
|
|
var endpoints = await discoveryClient.GetEndpointsAsync(null, CancellationToken.None);
|
|
await discoveryClient.CloseAsync(CancellationToken.None);
|
|
|
|
// Select endpoint - use the first available endpoint or find best match
|
|
if (endpoints == null || endpoints.Count == 0)
|
|
{
|
|
throw new InvalidOperationException($"No endpoints found for OPC UA server at {EndpointUrl}");
|
|
}
|
|
|
|
// Try to find a secure endpoint first, otherwise use the first one
|
|
var endpointDescription = endpoints.FirstOrDefault(e => e.SecurityMode != MessageSecurityMode.None) ?? endpoints[0];
|
|
|
|
var endpointConfiguration = EndpointConfiguration.Create(applicationConfiguration);
|
|
var configuredEndpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration);
|
|
|
|
UserIdentity? userIdentity = null;
|
|
if (!string.IsNullOrEmpty(username))
|
|
{
|
|
var passwordBytes = string.IsNullOrEmpty(password) ? Array.Empty<byte>() : System.Text.Encoding.UTF8.GetBytes(password);
|
|
userIdentity = new UserIdentity(username, passwordBytes);
|
|
}
|
|
|
|
// Use ISessionFactory.CreateAsync instead of Session.CreateAsync
|
|
ISessionFactory sessionFactory = new DefaultSessionFactory(telemetryContext);
|
|
var session = await sessionFactory.CreateAsync(
|
|
applicationConfiguration,
|
|
configuredEndpoint,
|
|
updateBeforeConnect: false,
|
|
checkDomain: false,
|
|
"RobotNet10 ScriptEngine Session",
|
|
60000,
|
|
userIdentity,
|
|
preferredLocales: null,
|
|
CancellationToken.None);
|
|
|
|
// Cast ISession to Session
|
|
_session = session as Session ?? throw new InvalidOperationException("Failed to create OPC UA session");
|
|
|
|
_session.KeepAlive += (ISession session, KeepAliveEventArgs e) =>
|
|
{
|
|
if (e.CurrentState != ServerState.Unknown)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_reconnectHandler != null && session is Session sessionImpl)
|
|
{
|
|
_reconnectHandler.BeginReconnect(sessionImpl, 5000, (sender, args) => { });
|
|
}
|
|
};
|
|
_reconnectHandler = new SessionReconnectHandler(telemetryContext, false, 5000);
|
|
_reconnectHandler.BeginReconnect(_session, 5000, (sender, e) => { });
|
|
|
|
IsConnected = true;
|
|
}
|
|
catch
|
|
{
|
|
Disconnect();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
|
|
public async Task DisconnectAsync()
|
|
{
|
|
IsConnected = false;
|
|
_reconnectHandler?.Dispose();
|
|
_reconnectHandler = null;
|
|
if (_session != null)
|
|
{
|
|
try
|
|
{
|
|
await _session.CloseAsync(CancellationToken.None);
|
|
}
|
|
catch
|
|
{
|
|
// Ignore errors during close
|
|
}
|
|
_session.Dispose();
|
|
}
|
|
_session = null;
|
|
}
|
|
|
|
private void Disconnect()
|
|
{
|
|
// Synchronous wrapper for async disconnect
|
|
DisconnectAsync().GetAwaiter().GetResult();
|
|
}
|
|
|
|
public async Task<object?> ReadNodeAsync(string nodeId)
|
|
{
|
|
EnsureConnected();
|
|
var node = new NodeId(nodeId);
|
|
var readValueId = new ReadValueId
|
|
{
|
|
NodeId = node,
|
|
AttributeId = Attributes.Value
|
|
};
|
|
|
|
var readValueIdCollection = new ReadValueIdCollection { readValueId };
|
|
var response = await _session!.ReadAsync(null, 0, TimestampsToReturn.Neither, readValueIdCollection, CancellationToken.None);
|
|
|
|
if (StatusCode.IsGood(response.Results[0].StatusCode))
|
|
{
|
|
return response.Results[0].Value;
|
|
}
|
|
|
|
throw new Exception($"Failed to read node {nodeId}: {response.Results[0].StatusCode}");
|
|
}
|
|
|
|
public async Task<Dictionary<string, object?>> ReadNodesAsync(string[] nodeIds)
|
|
{
|
|
EnsureConnected();
|
|
var readValueIdCollection = new ReadValueIdCollection();
|
|
|
|
foreach (var nodeId in nodeIds)
|
|
{
|
|
readValueIdCollection.Add(new ReadValueId
|
|
{
|
|
NodeId = new NodeId(nodeId),
|
|
AttributeId = Attributes.Value
|
|
});
|
|
}
|
|
|
|
var response = await _session!.ReadAsync(null, 0, TimestampsToReturn.Neither, readValueIdCollection, CancellationToken.None);
|
|
var result = new Dictionary<string, object?>();
|
|
|
|
for (int i = 0; i < nodeIds.Length; i++)
|
|
{
|
|
if (StatusCode.IsGood(response.Results[i].StatusCode))
|
|
{
|
|
result[nodeIds[i]] = response.Results[i].Value;
|
|
}
|
|
else
|
|
{
|
|
result[nodeIds[i]] = null;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public async Task WriteNodeAsync(string nodeId, object value)
|
|
{
|
|
EnsureConnected();
|
|
var writeValue = new WriteValue
|
|
{
|
|
NodeId = new NodeId(nodeId),
|
|
AttributeId = Attributes.Value,
|
|
Value = new DataValue(new Variant(value))
|
|
};
|
|
|
|
var writeValueCollection = new WriteValueCollection { writeValue };
|
|
var response = await _session!.WriteAsync(null, writeValueCollection, CancellationToken.None);
|
|
|
|
if (!StatusCode.IsGood(response.Results[0]))
|
|
{
|
|
throw new Exception($"Failed to write node {nodeId}: {response.Results[0]}");
|
|
}
|
|
}
|
|
|
|
public async Task WriteNodesAsync(Dictionary<string, object> values)
|
|
{
|
|
EnsureConnected();
|
|
var writeValueCollection = new WriteValueCollection();
|
|
|
|
foreach (var kvp in values)
|
|
{
|
|
writeValueCollection.Add(new WriteValue
|
|
{
|
|
NodeId = new NodeId(kvp.Key),
|
|
AttributeId = Attributes.Value,
|
|
Value = new DataValue(new Variant(kvp.Value))
|
|
});
|
|
}
|
|
|
|
var response = await _session!.WriteAsync(null, writeValueCollection, CancellationToken.None);
|
|
|
|
for (int i = 0; i < response.Results.Count; i++)
|
|
{
|
|
if (!StatusCode.IsGood(response.Results[i]))
|
|
{
|
|
var nodeId = values.Keys.ElementAt(i);
|
|
throw new Exception($"Failed to write node {nodeId}: {response.Results[i]}");
|
|
}
|
|
}
|
|
}
|
|
|
|
public async Task<string[]> BrowseNodesAsync(string? nodeId = null)
|
|
{
|
|
EnsureConnected();
|
|
var node = nodeId == null ? ObjectIds.ObjectsFolder : new NodeId(nodeId);
|
|
var nodesToBrowse = new BrowseDescriptionCollection
|
|
{
|
|
new BrowseDescription
|
|
{
|
|
NodeId = node,
|
|
BrowseDirection = BrowseDirection.Forward,
|
|
ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
|
|
IncludeSubtypes = true,
|
|
NodeClassMask = 0,
|
|
ResultMask = (uint)BrowseResultMask.All
|
|
}
|
|
};
|
|
|
|
var response = await _session!.BrowseAsync(null, null, 0, nodesToBrowse, CancellationToken.None);
|
|
|
|
if (response.Results != null && response.Results.Count > 0 && response.Results[0].References != null)
|
|
{
|
|
return response.Results[0].References.Select(rd => rd.NodeId.ToString()).ToArray();
|
|
}
|
|
|
|
return Array.Empty<string>();
|
|
}
|
|
|
|
private void EnsureConnected()
|
|
{
|
|
if (!IsConnected || _session == null)
|
|
{
|
|
throw new InvalidOperationException("OPC UA connection is not connected. Call ConnectAsync() first.");
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (!_disposed)
|
|
{
|
|
Disconnect();
|
|
_disposed = true;
|
|
}
|
|
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Simple telemetry context implementation for OPC UA
|
|
/// </summary>
|
|
internal class SimpleTelemetryContext : ITelemetryContext
|
|
{
|
|
public ILoggerFactory LoggerFactory => Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory.Instance;
|
|
|
|
public ActivitySource ActivitySource => new ActivitySource("RobotNet10.ScriptEngine.OpcUa");
|
|
|
|
public System.Diagnostics.Metrics.Meter CreateMeter() => new System.Diagnostics.Metrics.Meter("RobotNet10.ScriptEngine.OpcUa");
|
|
}
|
|
|