Initial commit
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user